diff options
author | Alex Goodman <wagoodman@gmail.com> | 2019-10-11 12:11:07 -0400 |
---|---|---|
committer | Alex Goodman <wagoodman@gmail.com> | 2019-10-11 12:11:07 -0400 |
commit | 45604e5c66ac8b2fc0b8f6d4706c3f38a20ad68a (patch) | |
tree | 33372d63f34ea919f99a4b86f9e878ad28fb3c29 | |
parent | c8ab7098d84bb647d48d2f8b20aa053e2870eeba (diff) |
add keybinding package
-rw-r--r-- | runtime/run.go | 7 | ||||
-rw-r--r-- | runtime/ui/controller.go | 20 | ||||
-rw-r--r-- | runtime/ui/controller_collection.go | 52 | ||||
-rw-r--r-- | runtime/ui/details_controller.go | 72 | ||||
-rw-r--r-- | runtime/ui/filetree_controller.go | 275 | ||||
-rw-r--r-- | runtime/ui/filetree_viewmodel.go | 51 | ||||
-rw-r--r-- | runtime/ui/filetree_viewmodel_test.go | 9 | ||||
-rw-r--r-- | runtime/ui/filter_controller.go | 41 | ||||
-rw-r--r-- | runtime/ui/format/format.go | 35 | ||||
-rw-r--r-- | runtime/ui/key/binding.go | 117 | ||||
-rw-r--r-- | runtime/ui/layer_controller.go | 169 | ||||
-rw-r--r-- | runtime/ui/layout_manager.go | 11 | ||||
-rw-r--r-- | runtime/ui/status_controller.go | 53 | ||||
-rw-r--r-- | runtime/ui/ui.go | 277 |
14 files changed, 693 insertions, 496 deletions
diff --git a/runtime/run.go b/runtime/run.go index 0dbcfa8..3da39ce 100644 --- a/runtime/run.go +++ b/runtime/run.go @@ -103,7 +103,7 @@ func run(enableUi bool, options Options, imageResolver image.Resolver, events ev err = ui.Run(analysis, cache) if err != nil { - events.exitWithErrorMessage("runtime error", err) + events.exitWithError(err) return } } @@ -131,7 +131,6 @@ func Run(options Options) { } if event.stderr != "" { - logrus.Error(event.stderr) _, err := fmt.Fprintln(os.Stderr, event.stderr) if err != nil { fmt.Println("error: could not write to buffer:", err) @@ -140,6 +139,10 @@ func Run(options Options) { if event.err != nil { logrus.Error(event.err) + _, err := fmt.Fprintln(os.Stderr, event.err.Error()) + if err != nil { + fmt.Println("error: could not write to buffer:", err) + } } if event.errorOnExit { diff --git a/runtime/ui/controller.go b/runtime/ui/controller.go new file mode 100644 index 0000000..fb1e500 --- /dev/null +++ b/runtime/ui/controller.go @@ -0,0 +1,20 @@ +package ui + +import ( + "github.com/jroimartin/gocui" +) + +type Renderable interface { + Update() error + Render() error +} + +// Controller defines the a renderable terminal screen pane. +type Controller interface { + Renderable + Setup(*gocui.View, *gocui.View) error + CursorDown() error + CursorUp() error + KeyHelp() string + IsVisible() bool +} diff --git a/runtime/ui/controller_collection.go b/runtime/ui/controller_collection.go new file mode 100644 index 0000000..47be653 --- /dev/null +++ b/runtime/ui/controller_collection.go @@ -0,0 +1,52 @@ +package ui + +import ( + "github.com/jroimartin/gocui" + "github.com/wagoodman/dive/dive/filetree" + "github.com/wagoodman/dive/dive/image" +) + +// var ccOnce sync.Once +var controllers *controllerCollection + +type controllerCollection struct { + Tree *fileTreeController + Layer *layerController + Status *statusController + Filter *filterController + Details *detailsController + lookup map[string]Controller +} + +func newControllerCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*controllerCollection, error) { + var err error + + controllers = &controllerCollection{} + controllers.lookup = make(map[string]Controller) + + controllers.Layer, err = newLayerController("layers", g, analysis.Layers) + if err != nil { + return nil, err + } + controllers.lookup[controllers.Layer.name] = controllers.Layer + + treeStack, err := filetree.StackTreeRange(analysis.RefTrees, 0, 0) + if err != nil { + return nil, err + } + controllers.Tree, err = newFileTreeController("filetree", g, treeStack, analysis.RefTrees, cache) + if err != nil { + return nil, err + } + controllers.lookup[controllers.Tree.name] = controllers.Tree + + controllers.Status = newStatusController("status", g) + controllers.lookup[controllers.Status.name] = controllers.Status + + controllers.Filter = newFilterController("filter", g) + controllers.lookup[controllers.Filter.name] = controllers.Filter + + controllers.Details = newDetailsController("details", g, analysis.Efficiency, analysis.Inefficiencies) + controllers.lookup[controllers.Details.name] = controllers.Details + return controllers, nil +} diff --git a/runtime/ui/details_controller.go b/runtime/ui/details_controller.go index 0b9de63..ed01836 100644 --- a/runtime/ui/details_controller.go +++ b/runtime/ui/details_controller.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/sirupsen/logrus" "github.com/wagoodman/dive/dive/filetree" + "github.com/wagoodman/dive/runtime/ui/format" + "github.com/wagoodman/dive/runtime/ui/key" "strconv" "strings" @@ -12,10 +14,10 @@ import ( "github.com/lunixbochs/vtclean" ) -// DetailsController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that +// detailsController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that // shows the layer details and image statistics. -type DetailsController struct { - Name string +type detailsController struct { + name string gui *gocui.Gui view *gocui.View header *gocui.View @@ -23,12 +25,12 @@ type DetailsController struct { inefficiencies filetree.EfficiencySlice } -// NewDetailsController creates a new view object attached the the global [gocui] screen object. -func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice) (controller *DetailsController) { - controller = new(DetailsController) +// newDetailsController creates a new view object attached the the global [gocui] screen object. +func newDetailsController(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice) (controller *detailsController) { + controller = new(detailsController) // populate main fields - controller.Name = name + controller.name = name controller.gui = gui controller.efficiency = efficiency controller.inefficiencies = inefficiencies @@ -37,7 +39,7 @@ func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, ineff } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (controller *DetailsController) Setup(v *gocui.View, header *gocui.View) error { +func (controller *detailsController) Setup(v *gocui.View, header *gocui.View) error { // set controller options controller.view = v @@ -51,11 +53,21 @@ func (controller *DetailsController) Setup(v *gocui.View, header *gocui.View) er controller.header.Wrap = false controller.header.Frame = false - // set keybindings - if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil { - return err + var infos = []key.BindingInfo{ + { + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + OnAction: controller.CursorDown, + }, + { + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + OnAction: controller.CursorUp, + }, } - if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil { + + _, err := key.GenerateBindings(controller.gui, controller.name, infos) + if err != nil { return err } @@ -63,22 +75,22 @@ func (controller *DetailsController) Setup(v *gocui.View, header *gocui.View) er } // IsVisible indicates if the details view pane is currently initialized. -func (controller *DetailsController) IsVisible() bool { +func (controller *detailsController) IsVisible() bool { return controller != nil } // CursorDown moves the cursor down in the details pane (currently indicates nothing). -func (controller *DetailsController) CursorDown() error { +func (controller *detailsController) CursorDown() error { return CursorDown(controller.gui, controller.view) } // CursorUp moves the cursor up in the details pane (currently indicates nothing). -func (controller *DetailsController) CursorUp() error { +func (controller *detailsController) CursorUp() error { return CursorUp(controller.gui, controller.view) } // Update refreshes the state objects for future rendering. -func (controller *DetailsController) Update() error { +func (controller *detailsController) Update() error { return nil } @@ -87,13 +99,13 @@ func (controller *DetailsController) Update() error { // 2. the image efficiency score // 3. the estimated wasted image space // 4. a list of inefficient file allocations -func (controller *DetailsController) Render() error { - currentLayer := Controllers.Layer.currentLayer() +func (controller *detailsController) Render() error { + currentLayer := controllers.Layer.currentLayer() var wastedSpace int64 template := "%5s %12s %-s\n" - inefficiencyReport := fmt.Sprintf(Formatting.Header(template), "Count", "Total Space", "Path") + inefficiencyReport := fmt.Sprintf(format.Header(template), "Count", "Total Space", "Path") height := 100 if controller.view != nil { @@ -110,9 +122,9 @@ func (controller *DetailsController) Render() error { } } - imageSizeStr := fmt.Sprintf("%s %s", Formatting.Header("Total Image size:"), humanize.Bytes(Controllers.Layer.ImageSize)) - effStr := fmt.Sprintf("%s %d %%", Formatting.Header("Image efficiency score:"), int(100.0*controller.efficiency)) - wastedSpaceStr := fmt.Sprintf("%s %s", Formatting.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace))) + imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(controllers.Layer.ImageSize)) + effStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*controller.efficiency)) + wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace))) controller.gui.Update(func(g *gocui.Gui) error { // update header @@ -122,7 +134,7 @@ func (controller *DetailsController) Render() error { layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15)) imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15)) - _, err := fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(layerHeaderStr, false))) + _, err := fmt.Fprintln(controller.header, format.Header(vtclean.Clean(layerHeaderStr, false))) if err != nil { return err } @@ -132,15 +144,15 @@ func (controller *DetailsController) Render() error { var lines = make([]string, 0) if currentLayer.Names != nil && len(currentLayer.Names) > 0 { - lines = append(lines, Formatting.Header("Tags: ")+strings.Join(currentLayer.Names, ", ")) + lines = append(lines, format.Header("Tags: ")+strings.Join(currentLayer.Names, ", ")) } else { - lines = append(lines, Formatting.Header("Tags: ")+"(none)") + lines = append(lines, format.Header("Tags: ")+"(none)") } - lines = append(lines, Formatting.Header("Id: ")+currentLayer.Id) - lines = append(lines, Formatting.Header("Digest: ")+currentLayer.Digest) - lines = append(lines, Formatting.Header("Command:")) + lines = append(lines, format.Header("Id: ")+currentLayer.Id) + lines = append(lines, format.Header("Digest: ")+currentLayer.Digest) + lines = append(lines, format.Header("Command:")) lines = append(lines, currentLayer.Command) - lines = append(lines, "\n"+Formatting.Header(vtclean.Clean(imageHeaderStr, false))) + lines = append(lines, "\n"+format.Header(vtclean.Clean(imageHeaderStr, false))) lines = append(lines, imageSizeStr) lines = append(lines, wastedSpaceStr) lines = append(lines, effStr+"\n") @@ -156,6 +168,6 @@ func (controller *DetailsController) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing). -func (controller *DetailsController) KeyHelp() string { +func (controller *detailsController) KeyHelp() string { return "TBD" } diff --git a/runtime/ui/filetree_controller.go b/runtime/ui/filetree_controller.go index 16a72b2..250289a 100644 --- a/runtime/ui/filetree_controller.go +++ b/runtime/ui/filetree_controller.go @@ -2,15 +2,13 @@ package ui import ( "fmt" + "github.com/wagoodman/dive/runtime/ui/format" + "github.com/wagoodman/dive/runtime/ui/key" "regexp" "strings" - "github.com/lunixbochs/vtclean" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "github.com/wagoodman/keybinding" - "github.com/jroimartin/gocui" + "github.com/lunixbochs/vtclean" "github.com/wagoodman/dive/dive/filetree" ) @@ -21,92 +19,36 @@ const ( type CompareType int -// FileTreeController holds the UI objects and data models for populating the right pane. Specifically the pane that +// fileTreeController holds the UI objects and data models for populating the right pane. Specifically the pane that // shows selected layer or aggregate file ASCII tree. -type FileTreeController struct { - Name string +type fileTreeController struct { + name string gui *gocui.Gui view *gocui.View header *gocui.View - vm *FileTreeViewModel - - keybindingToggleCollapse []keybinding.Key - keybindingToggleCollapseAll []keybinding.Key - keybindingToggleAttributes []keybinding.Key - keybindingToggleAdded []keybinding.Key - keybindingToggleRemoved []keybinding.Key - keybindingToggleModified []keybinding.Key - keybindingToggleUnmodified []keybinding.Key - keybindingPageDown []keybinding.Key - keybindingPageUp []keybinding.Key + vm *fileTreeViewModel + + helpKeys []*key.Binding } -// NewFileTreeController creates a new view object attached the the global [gocui] screen object. -func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *FileTreeController, err error) { - controller = new(FileTreeController) +// newFileTreeController creates a new view object attached the the global [gocui] screen object. +func newFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *fileTreeController, err error) { + controller = new(fileTreeController) // populate main fields - controller.Name = name + controller.name = name controller.gui = gui - controller.vm, err = NewFileTreeViewModel(tree, refTrees, cache) + controller.vm, err = newFileTreeViewModel(tree, refTrees, cache) if err != nil { return nil, err } - controller.keybindingToggleCollapse, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-dir")) - if err != nil { - logrus.Error(err) - } - - controller.keybindingToggleCollapseAll, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-all-dir")) - if err != nil { - logrus.Error(err) - } - - controller.keybindingToggleAttributes, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-filetree-attributes")) - if err != nil { - logrus.Error(err) - } - - controller.keybindingToggleAdded, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-added-files")) - if err != nil { - logrus.Error(err) - } - - controller.keybindingToggleRemoved, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-removed-files")) - if err != nil { - logrus.Error(err) - } - - controller.keybindingToggleModified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-modified-files")) - if err != nil { - logrus.Error(err) - } - - // support legacy behavior first, then use default behavior - controller.keybindingToggleUnmodified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unchanged-files")) - if err != nil { - controller.keybindingToggleUnmodified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unmodified-files")) - if err != nil { - logrus.Error(err) - } - } - - controller.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up")) - if err != nil { - logrus.Error(err) - } - - controller.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down")) - if err != nil { - logrus.Error(err) - } return controller, err } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (controller *FileTreeController) Setup(v *gocui.View, header *gocui.View) error { +func (controller *fileTreeController) Setup(v *gocui.View, header *gocui.View) error { // set controller options controller.view = v @@ -119,65 +61,82 @@ func (controller *FileTreeController) Setup(v *gocui.View, header *gocui.View) e controller.header.Wrap = false controller.header.Frame = false - // set keybindings - if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil { - return err - } - if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil { - return err - } - if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorLeft() }); err != nil { - return err - } - if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorRight() }); err != nil { + var infos = []key.BindingInfo{ + { + ConfigKeys: []string{"keybinding.toggle-collapse-dir"}, + OnAction: controller.toggleCollapse, + Display: "Collapse dir", + }, + { + ConfigKeys: []string{"keybinding.toggle-collapse-all-dir"}, + OnAction: controller.toggleCollapseAll, + Display: "Collapse all dir", + }, + { + ConfigKeys: []string{"keybinding.toggle-added-files"}, + OnAction: func() error { return controller.toggleShowDiffType(filetree.Added) }, + IsSelected: func() bool { return !controller.vm.HiddenDiffTypes[filetree.Added] }, + Display: "Added", + }, + { + ConfigKeys: []string{"keybinding.toggle-removed-files"}, + OnAction: func() error { return controller.toggleShowDiffType(filetree.Removed) }, + IsSelected: func() bool { return !controller.vm.HiddenDiffTypes[filetree.Removed] }, + Display: "Removed", + }, + { + ConfigKeys: []string{"keybinding.toggle-modified-files"}, + OnAction: func() error { return controller.toggleShowDiffType(filetree.Modified) }, + IsSelected: func() bool { return !controller.vm.HiddenDiffTypes[filetree.Modified] }, + Display: "Modified", + }, + { + ConfigKeys: []string{"keybinding.toggle-unchanged-files", "keybinding.toggle-unmodified-files"}, + OnAction: func() error { return controller.toggleShowDiffType(filetree.Unmodified) }, + IsSelected: func() bool { return !controller.vm.HiddenDiffTypes[filetree.Unmodified] }, + Display: "Unmodified", + }, + { + ConfigKeys: []string{"keybinding.toggle-filetree-attributes"}, + OnAction: controller.toggleAttributes, + IsSelected: func() bool { return controller.vm.ShowAttributes }, + Display: "Attributes", + }, + { + ConfigKeys: []string{"keybinding.page-up"}, + OnAction: controller.PageUp, + }, + { + ConfigKeys: []string{"keybinding.page-down"}, + OnAction: controller.PageDown, + }, + { + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + OnAction: controller.CursorDown, + }, + { + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + OnAction: controller.CursorUp, + }, + { + Key: gocui.KeyArrowLeft, + Modifier: gocui.ModNone, + OnAction: controller.CursorLeft, + }, + { + Key: gocui.KeyArrowRight, + Modifier: gocui.ModNone, + OnAction: controller.CursorRight, + }, + } + + helpKeys, err := key.GenerateBindings(controller.gui, controller.name, infos) + if err != nil { return err } - - for _, key := range controller.keybindingPageUp { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageUp() }); err != nil { - return err - } - } - for _, key := range controller.keybindingPageDown { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageDown() }); err != nil { - return err - } - } - for _, key := range controller.keybindingToggleCollapse { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleCollapse() }); err != nil { - return err - } - } - for _, key := range controller.keybindingToggleCollapseAll { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleCollapseAll() }); err != nil { - return err - } - } - for _, key := range controller.keybindingToggleAttributes { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleAttributes() }); err != nil { - return err - } - } - for _, key := range controller.keybindingToggleAdded { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Added) }); err != nil { - return err - } - } - for _, key := range controller.keybindingToggleRemoved { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Removed) }); err != nil { - return err - } - } - for _, key := range controller.keybindingToggleModified { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Modified) }); err != nil { - return err - } - } - for _, key := range controller.keybindingToggleUnmodified { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Unmodified) }); err != nil { - return err - } - } + controller.helpKeys = helpKeys _, height := controller.view.Size() controller.vm.Setup(0, height) @@ -188,18 +147,18 @@ func (controller *FileTreeController) Setup(v *gocui.View, header *gocui.View) e } // IsVisible indicates if the file tree view pane is currently initialized -func (controller *FileTreeController) IsVisible() bool { +func (controller *fileTreeController) IsVisible() bool { return controller != nil } // resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer. -func (controller *FileTreeController) resetCursor() { +func (controller *fileTreeController) resetCursor() { _ = controller.view.SetCursor(0, 0) controller.vm.resetCursor() } // setTreeByLayer populates the view model by stacking the indicated image layer file trees. -func (controller *FileTreeController) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error { +func (controller *fileTreeController) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error { err := controller.vm.setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop) if err != nil { return err @@ -214,7 +173,7 @@ func (controller *FileTreeController) setTreeByLayer(bottomTreeStart, bottomTree // Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer. // Instead we are keeping an upper and lower bounds of the tree string to render and only flushing // this range into the view buffer. This is much faster when tree sizes are large. -func (controller *FileTreeController) CursorDown() error { +func (controller *fileTreeController) CursorDown() error { if controller.vm.CursorDown() { return controller.Render() } @@ -225,7 +184,7 @@ func (controller *FileTreeController) CursorDown() error { // Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer. // Instead we are keeping an upper and lower bounds of the tree string to render and only flushing // this range into the view buffer. This is much faster when tree sizes are large. -func (controller *FileTreeController) CursorUp() error { +func (controller *fileTreeController) CursorUp() error { if controller.vm.CursorUp() { return controller.Render() } @@ -233,7 +192,7 @@ func (controller *FileTreeController) CursorUp() error { } // CursorLeft moves the cursor up until we reach the Parent Node or top of the tree -func (controller *FileTreeController) CursorLeft() error { +func (controller *fileTreeController) CursorLeft() error { err := controller.vm.CursorLeft(filterRegex()) if err != nil { return err @@ -243,7 +202,7 @@ func (controller *FileTreeController) CursorLeft() error { } // CursorRight descends into directory expanding it if needed -func (controller *FileTreeController) CursorRight() error { +func (controller *fileTreeController) CursorRight() error { err := controller.vm.CursorRight(filterRegex()) if err != nil { return err @@ -253,7 +212,7 @@ func (controller *FileTreeController) CursorRight() error { } // PageDown moves to next page putting the cursor on top -func (controller *FileTreeController) PageDown() error { +func (controller *fileTreeController) PageDown() error { err := controller.vm.PageDown() if err != nil { return err @@ -262,7 +221,7 @@ func (controller *FileTreeController) PageDown() error { } // PageUp moves to previous page putting the cursor on top -func (controller *FileTreeController) PageUp() error { +func (controller *fileTreeController) PageUp() error { err := controller.vm.PageUp() if err != nil { return err @@ -271,12 +230,12 @@ func (controller *FileTreeController) PageUp() error { } // getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode. -// func (controller *FileTreeController) getAbsPositionNode() (node *filetree.FileNode) { +// func (controller *fileTreeController) getAbsPositionNode() (node *filetree.FileNode) { // return controller.vm.getAbsPositionNode(filterRegex()) // } // toggleCollapse will collapse/expand the selected FileNode. -func (controller *FileTreeController) toggleCollapse() error { +func (controller *fileTreeController) toggleCollapse() error { err := controller.vm.toggleCollapse(filterRegex()) if err != nil { return err @@ -286,7 +245,7 @@ func (controller *FileTreeController) toggleCollapse() error { } // toggleCollapseAll will collapse/expand the all directories. -func (controller *FileTreeController) toggleCollapseAll() error { +func (controller *fileTreeController) toggleCollapseAll() error { err := controller.vm.toggleCollapseAll() if err != nil { return err @@ -299,7 +258,7 @@ func (controller *FileTreeController) toggleCollapseAll() error { } // toggleAttributes will show/hide file attributes -func (controller *FileTreeController) toggleAttributes() error { +func (controller *fileTreeController) toggleAttributes() error { err := controller.vm.toggleAttributes() if err != nil { return err @@ -309,7 +268,7 @@ func (controller *FileTreeController) toggleAttributes() error { } // toggleShowDiffType will show/hide the selected DiffType in the filetree pane. -func (controller *FileTreeController) toggleShowDiffType(diffType filetree.DiffType) error { +func (controller *fileTreeController) toggleShowDiffType(diffType filetree.DiffType) error { controller.vm.toggleShowDiffType(diffType) // we need to render the changes to the status pane as well (not just this contoller/view) return UpdateAndRender() @@ -317,10 +276,10 @@ func (controller *FileTreeController) toggleShowDiffType(diffType filetree.DiffT // filterRegex will return a regular expression object to match the user's filter input. func filterRegex() *regexp.Regexp { - if Controllers.Filter == nil || Controllers.Filter.view == nil { + if controllers.Filter == nil || controllers.Filter.view == nil { return nil } - filterString := strings.TrimSpace(Controllers.Filter.view.Buffer()) + filterString := strings.TrimSpace(controllers.Filter.view.Buffer()) if len(filterString) == 0 { return nil } @@ -334,7 +293,7 @@ func filterRegex() *regexp.Regexp { } // onLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions -func (controller *FileTreeController) onLayoutChange(resized bool) error { +func (controller *fileTreeController) onLayoutChange(resized bool) error { _ = controller.Update() if resized { return controller.Render() @@ -343,7 +302,7 @@ func (controller *FileTreeController) onLayoutChange(resized bool) error { } // Update refreshes the state objects for future rendering. -func (controller *FileTreeController) Update() error { +func (controller *fileTreeController) Update() error { var width, height int if controller.view != nil { @@ -357,9 +316,9 @@ func (controller *FileTreeController) Update() error { } // Render flushes the state objects (file tree) to the pane. -func (controller *FileTreeController) Render() error { +func (controller *fileTreeController) Render() error { title := "Current Layer Contents" - if Controllers.Layer.CompareMode == CompareAll { + if controllers.Layer.CompareMode == CompareAll { title = "Aggregated Layer Contents" } @@ -377,7 +336,7 @@ func (controller *FileTreeController) Render() error { headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree") } - _, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false))) + _, _ = fmt.Fprintln(controller.header, format.Header(vtclean.Clean(headerStr, false))) // update the contents controller.view.Clear() @@ -393,12 +352,10 @@ func (controller *FileTreeController) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (controller *FileTreeController) KeyHelp() string { - return renderStatusOption(controller.keybindingToggleCollapse[0].String(), "Collapse dir", false) + - renderStatusOption(controller.keybindingToggleCollapseAll[0].String(), "Collapse all dir", false) + - renderStatusOption(controller.keybindingToggleAdded[0].String(), "Added", !controller.vm.HiddenDiffTypes[filetree.Added]) + - renderStatusOption(controller.keybindingToggleRemoved[0].String(), "Removed", !controller.vm.HiddenDiffTypes[filetree.Removed]) + - renderStatusOption(controller.keybindingToggleModified[0].String(), "Modified", !controller.vm.HiddenDiffTypes[filetree.Modified]) + - renderStatusOption(controller.keybindingToggleUnmodified[0].String(), "Unmodified", !controller.vm.HiddenDiffTypes[filetree.Unmodified]) + - renderStatusOption(controller.keybindingToggleAttributes[0].String(), "Attributes", controller.vm.ShowAttributes) +func (controller *fileTreeController) KeyHelp() string { + var help string + for _, binding := range controller.helpKeys { + help += binding.RenderKeyHelp() + } + return help } diff --git a/runtime/ui/filetree_viewmodel.go b/runtime/ui/filetree_viewmodel.go index 18ebfdd..5a640e8 100644 --- a/runtime/ui/filetree_viewmodel.go +++ b/runtime/ui/filetree_viewmodel.go @@ -3,6 +3,7 @@ package ui import ( "bytes" "fmt" + "github.com/wagoodman/dive/runtime/ui/format" "regexp" "strings" @@ -12,9 +13,9 @@ import ( "github.com/wagoodman/dive/dive/filetree" ) -// FileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically the pane that +// fileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically the pane that // shows selected layer or aggregate file ASCII tree. -type FileTreeViewModel struct { +type fileTreeViewModel struct { ModelTree *filetree.FileTree ViewTree *filetree.FileTree RefTrees []*filetree.FileTree @@ -33,9 +34,9 @@ type FileTreeViewModel struct { mainBuf bytes.Buffer } -// NewFileTreeController creates a new view object attached the the global [gocui] screen object. -func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeViewModel *FileTreeViewModel, err error) { - treeViewModel = new(FileTreeViewModel) +// newFileTreeViewModel creates a new view object attached the the global [gocui] screen object. +func newFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeViewModel *fileTreeViewModel, err error) { + treeViewModel = new(fileTreeViewModel) // populate main fields treeViewModel.ShowAttributes = viper.GetBool("filetree.show-attributes") @@ -65,13 +66,13 @@ func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (vm *FileTreeViewModel) Setup(lowerBound, height int) { +func (vm *fileTreeViewModel) Setup(lowerBound, height int) { vm.bufferIndexLowerBound = lowerBound vm.refHeight = height } // height returns the current height and considers the header -func (vm *FileTreeViewModel) height() int { +func (vm *fileTreeViewModel) height() int { if vm.ShowAttributes { return vm.refHeight - 1 } @@ -79,24 +80,24 @@ func (vm *FileTreeViewModel) height() int { } // bufferIndexUpperBound returns the current upper bounds for the view -func (vm *FileTreeViewModel) bufferIndexUpperBound() int { +func (vm *fileTreeViewModel) bufferIndexUpperBound() int { return vm.bufferIndexLowerBound + vm.height() } // IsVisible indicates if the file tree view pane is currently initialized -func (vm *FileTreeViewModel) IsVisible() bool { +func (vm *fileTreeViewModel) IsVisible() bool { return vm != nil } // resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer. -func (vm *FileTreeViewModel) resetCursor() { +func (vm *fileTreeViewModel) resetCursor() { vm.TreeIndex = 0 vm.bufferIndex = 0 vm.bufferIndexLowerBound = 0 } // setTreeByLayer populates the view model by stacking the indicated image layer file trees. -func (vm *FileTreeViewModel) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error { +func (vm *fileTreeViewModel) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error { if topTreeStop > len(vm.RefTrees)-1 { return fmt.Errorf("invalid layer index given: %d of %d", topTreeStop, len(vm.RefTrees)-1) } @@ -125,7 +126,7 @@ func (vm *FileTreeViewModel) setTreeByLayer(bottomTreeStart, bottomTreeStop, top } // doCursorUp perfo |