From 45604e5c66ac8b2fc0b8f6d4706c3f38a20ad68a Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 11 Oct 2019 12:11:07 -0400 Subject: [PATCH 1/6] add keybinding package --- runtime/run.go | 7 +- runtime/ui/controller.go | 20 ++ runtime/ui/controller_collection.go | 52 +++++ runtime/ui/details_controller.go | 72 ++++--- runtime/ui/filetree_controller.go | 271 +++++++++++-------------- runtime/ui/filetree_viewmodel.go | 51 ++--- runtime/ui/filetree_viewmodel_test.go | 9 +- runtime/ui/filter_controller.go | 41 ++-- runtime/ui/format/format.go | 35 ++++ runtime/ui/key/binding.go | 117 +++++++++++ runtime/ui/layer_controller.go | 171 ++++++++-------- runtime/ui/layout_manager.go | 11 + runtime/ui/status_controller.go | 53 +++-- runtime/ui/ui.go | 277 ++++++++++++-------------- 14 files changed, 692 insertions(+), 495 deletions(-) create mode 100644 runtime/ui/controller.go create mode 100644 runtime/ui/controller_collection.go create mode 100644 runtime/ui/format/format.go create mode 100644 runtime/ui/key/binding.go create mode 100644 runtime/ui/layout_manager.go 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 + 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 + 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 { - return err + 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, + }, } - 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 - } + helpKeys, err := key.GenerateBindings(controller.gui, controller.name, infos) + if 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 performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer. -func (vm *FileTreeViewModel) CursorUp() bool { +func (vm *fileTreeViewModel) CursorUp() bool { if vm.TreeIndex <= 0 { return false } @@ -140,7 +141,7 @@ func (vm *FileTreeViewModel) CursorUp() bool { } // doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer. -func (vm *FileTreeViewModel) CursorDown() bool { +func (vm *fileTreeViewModel) CursorDown() bool { if vm.TreeIndex >= vm.ModelTree.VisibleSize() { return false } @@ -156,7 +157,7 @@ func (vm *FileTreeViewModel) CursorDown() bool { } // CursorLeft moves the cursor up until we reach the Parent Node or top of the tree -func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error { +func (vm *fileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error { var visitor func(*filetree.FileNode) error var evaluator func(*filetree.FileNode) bool var dfsCounter, newIndex int @@ -207,7 +208,7 @@ func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error { } // CursorRight descends into directory expanding it if needed -func (vm *FileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error { +func (vm *fileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error { node := vm.getAbsPositionNode(filterRegex) if node == nil { return nil @@ -239,7 +240,7 @@ func (vm *FileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error { } // PageDown moves to next page putting the cursor on top -func (vm *FileTreeViewModel) PageDown() error { +func (vm *fileTreeViewModel) PageDown() error { nextBufferIndexLowerBound := vm.bufferIndexLowerBound + vm.height() nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height() @@ -265,7 +266,7 @@ func (vm *FileTreeViewModel) PageDown() error { } // PageUp moves to previous page putting the cursor on top -func (vm *FileTreeViewModel) PageUp() error { +func (vm *fileTreeViewModel) PageUp() error { nextBufferIndexLowerBound := vm.bufferIndexLowerBound - vm.height() nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height() @@ -290,7 +291,7 @@ func (vm *FileTreeViewModel) PageUp() error { } // getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode. -func (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) { +func (vm *fileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) { var visitor func(*filetree.FileNode) error var evaluator func(*filetree.FileNode) bool var dfsCounter int @@ -321,7 +322,7 @@ func (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (nod } // toggleCollapse will collapse/expand the selected FileNode. -func (vm *FileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error { +func (vm *fileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error { node := vm.getAbsPositionNode(filterRegex) if node != nil && node.Data.FileInfo.IsDir { node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed @@ -330,7 +331,7 @@ func (vm *FileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error { } // toggleCollapseAll will collapse/expand the all directories. -func (vm *FileTreeViewModel) toggleCollapseAll() error { +func (vm *fileTreeViewModel) toggleCollapseAll() error { vm.CollapseAll = !vm.CollapseAll visitor := func(curNode *filetree.FileNode) error { @@ -351,18 +352,18 @@ func (vm *FileTreeViewModel) toggleCollapseAll() error { } // toggleCollapse will collapse/expand the selected FileNode. -func (vm *FileTreeViewModel) toggleAttributes() error { +func (vm *fileTreeViewModel) toggleAttributes() error { vm.ShowAttributes = !vm.ShowAttributes return nil } // toggleShowDiffType will show/hide the selected DiffType in the filetree pane. -func (vm *FileTreeViewModel) toggleShowDiffType(diffType filetree.DiffType) { +func (vm *fileTreeViewModel) toggleShowDiffType(diffType filetree.DiffType) { vm.HiddenDiffTypes[diffType] = !vm.HiddenDiffTypes[diffType] } // Update refreshes the state objects for future rendering. -func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error { +func (vm *fileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error { vm.refWidth = width vm.refHeight = height @@ -410,7 +411,7 @@ func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height in } // Render flushes the state objects (file tree) to the pane. -func (vm *FileTreeViewModel) Render() error { +func (vm *fileTreeViewModel) Render() error { treeString := vm.ViewTree.StringBetween(vm.bufferIndexLowerBound, vm.bufferIndexUpperBound(), vm.ShowAttributes) lines := strings.Split(treeString, "\n") @@ -418,7 +419,7 @@ func (vm *FileTreeViewModel) Render() error { vm.mainBuf.Reset() for idx, line := range lines { if idx == vm.bufferIndex { - _, err := fmt.Fprintln(&vm.mainBuf, Formatting.Selected(vtclean.Clean(line, false))) + _, err := fmt.Fprintln(&vm.mainBuf, format.Selected(vtclean.Clean(line, false))) if err != nil { logrus.Debug("unable to write to buffer: ", err) return err diff --git a/runtime/ui/filetree_viewmodel_test.go b/runtime/ui/filetree_viewmodel_test.go index df0e0ca..fde2587 100644 --- a/runtime/ui/filetree_viewmodel_test.go +++ b/runtime/ui/filetree_viewmodel_test.go @@ -3,6 +3,7 @@ package ui import ( "bytes" "github.com/wagoodman/dive/dive/image/docker" + "github.com/wagoodman/dive/runtime/ui/format" "io/ioutil" "os" "path/filepath" @@ -72,7 +73,7 @@ func assertTestData(t *testing.T, actualBytes []byte) { helperCheckDiff(t, expectedBytes, actualBytes) } -func initializeTestViewModel(t *testing.T) *FileTreeViewModel { +func initializeTestViewModel(t *testing.T) *fileTreeViewModel { result := docker.TestAnalysisFromArchive(t, "../../.data/test-docker-image.tar") cache := filetree.NewFileTreeCache(result.RefTrees) @@ -81,20 +82,20 @@ func initializeTestViewModel(t *testing.T) *FileTreeViewModel { t.Fatalf("%s: unable to build cache: %+v", t.Name(), err) } - Formatting.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc() + format.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc() treeStack, err := filetree.StackTreeRange(result.RefTrees, 0, 0) if err != nil { t.Fatalf("%s: unable to stack trees: %v", t.Name(), err) } - vm, err := NewFileTreeViewModel(treeStack, result.RefTrees, cache) + vm, err := newFileTreeViewModel(treeStack, result.RefTrees, cache) if err != nil { t.Fatalf("%s: unable to create tree ViewModel: %+v", t.Name(), err) } return vm } -func runTestCase(t *testing.T, vm *FileTreeViewModel, width, height int, filterRegex *regexp.Regexp) { +func runTestCase(t *testing.T, vm *fileTreeViewModel, width, height int, filterRegex *regexp.Regexp) { err := vm.Update(filterRegex, width, height) if err != nil { t.Errorf("failed to update viewmodel: %v", err) diff --git a/runtime/ui/filter_controller.go b/runtime/ui/filter_controller.go index bd740b3..e01cead 100644 --- a/runtime/ui/filter_controller.go +++ b/runtime/ui/filter_controller.go @@ -4,12 +4,13 @@ import ( "fmt" "github.com/jroimartin/gocui" "github.com/sirupsen/logrus" + "github.com/wagoodman/dive/runtime/ui/format" ) -// FilterController holds the UI objects and data models for populating the bottom row. Specifically the pane that +// filterController holds the UI objects and data models for populating the bottom row. Specifically the pane that // allows the user to filter the file tree by path. -type FilterController struct { - Name string +type filterController struct { + name string gui *gocui.Gui view *gocui.View header *gocui.View @@ -18,12 +19,12 @@ type FilterController struct { hidden bool } -// NewFilterController creates a new view object attached the the global [gocui] screen object. -func NewFilterController(name string, gui *gocui.Gui) (controller *FilterController) { - controller = new(FilterController) +// newFilterController creates a new view object attached the the global [gocui] screen object. +func newFilterController(name string, gui *gocui.Gui) (controller *filterController) { + controller = new(filterController) // populate main fields - controller.Name = name + controller.name = name controller.gui = gui controller.headerStr = "Path Filter: " controller.hidden = true @@ -32,7 +33,7 @@ func NewFilterController(name string, gui *gocui.Gui) (controller *FilterControl } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (controller *FilterController) Setup(v *gocui.View, header *gocui.View) error { +func (controller *filterController) Setup(v *gocui.View, header *gocui.View) error { // set controller options controller.view = v @@ -52,7 +53,7 @@ func (controller *FilterController) Setup(v *gocui.View, header *gocui.View) err } // IsVisible indicates if the filter view pane is currently initialized -func (controller *FilterController) IsVisible() bool { +func (controller *filterController) IsVisible() bool { if controller == nil { return false } @@ -60,17 +61,17 @@ func (controller *FilterController) IsVisible() bool { } // CursorDown moves the cursor down in the filter pane (currently indicates nothing). -func (controller *FilterController) CursorDown() error { +func (controller *filterController) CursorDown() error { return nil } // CursorUp moves the cursor up in the filter pane (currently indicates nothing). -func (controller *FilterController) CursorUp() error { +func (controller *filterController) CursorUp() error { return nil } // Edit intercepts the key press events in the filer view to update the file view in real time. -func (controller *FilterController) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { +func (controller *filterController) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { if !controller.IsVisible() { return } @@ -86,21 +87,21 @@ func (controller *FilterController) Edit(v *gocui.View, key gocui.Key, ch rune, case key == gocui.KeyBackspace || key == gocui.KeyBackspace2: v.EditDelete(true) } - if Controllers.Tree != nil { - _ = Controllers.Tree.Update() - _ = Controllers.Tree.Render() + if controllers.Tree != nil { + _ = controllers.Tree.Update() + _ = controllers.Tree.Render() } } // Update refreshes the state objects for future rendering (currently does nothing). -func (controller *FilterController) Update() error { +func (controller *filterController) Update() error { return nil } // Render flushes the state objects to the screen. Currently this is the users path filter input. -func (controller *FilterController) Render() error { +func (controller *filterController) Render() error { controller.gui.Update(func(g *gocui.Gui) error { - _, err := fmt.Fprintln(controller.header, Formatting.Header(controller.headerStr)) + _, err := fmt.Fprintln(controller.header, format.Header(controller.headerStr)) if err != nil { logrus.Error("unable to write to buffer: ", err) } @@ -110,6 +111,6 @@ func (controller *FilterController) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (controller *FilterController) KeyHelp() string { - return Formatting.StatusControlNormal("▏Type to filter the file tree ") +func (controller *filterController) KeyHelp() string { + return format.StatusControlNormal("▏Type to filter the file tree ") } diff --git a/runtime/ui/format/format.go b/runtime/ui/format/format.go new file mode 100644 index 0000000..b3e891e --- /dev/null +++ b/runtime/ui/format/format.go @@ -0,0 +1,35 @@ +package format + +import ( + "github.com/fatih/color" +) + +var ( + Header func(...interface{}) string + Selected func(...interface{}) string + StatusSelected func(...interface{}) string + StatusNormal func(...interface{}) string + StatusControlSelected func(...interface{}) string + StatusControlNormal func(...interface{}) string + CompareTop func(...interface{}) string + CompareBottom func(...interface{}) string +) + +func init() { + Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc() + Header = color.New(color.Bold).SprintFunc() + StatusSelected = color.New(color.BgMagenta, color.FgWhite).SprintFunc() + StatusNormal = color.New(color.ReverseVideo).SprintFunc() + StatusControlSelected = color.New(color.BgMagenta, color.FgWhite, color.Bold).SprintFunc() + StatusControlNormal = color.New(color.ReverseVideo, color.Bold).SprintFunc() + CompareTop = color.New(color.BgMagenta).SprintFunc() + CompareBottom = color.New(color.BgGreen).SprintFunc() +} + +func RenderHelpKey(control, title string, selected bool) string { + if selected { + return StatusSelected("▏") + StatusControlSelected(control) + StatusSelected(" "+title+" ") + } else { + return StatusNormal("▏") + StatusControlNormal(control) + StatusNormal(" "+title+" ") + } +} diff --git a/runtime/ui/key/binding.go b/runtime/ui/key/binding.go new file mode 100644 index 0000000..d7a75af --- /dev/null +++ b/runtime/ui/key/binding.go @@ -0,0 +1,117 @@ +package key + +import ( + "fmt" + "github.com/jroimartin/gocui" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "github.com/wagoodman/dive/runtime/ui/format" + "github.com/wagoodman/keybinding" +) + +type BindingInfo struct { + Key gocui.Key + Modifier gocui.Modifier + ConfigKeys []string + OnAction func() error + IsSelected func() bool + Display string +} + +type Binding struct { + key []keybinding.Key + displayName string + selectedFn func() bool + actionFn func() error +} + +func GenerateBindings(gui *gocui.Gui, influence string, infos []BindingInfo) ([]*Binding, error) { + var result = make([]*Binding, 0) + for _, info := range infos { + var err error + var binding *Binding + + if info.ConfigKeys != nil && len(info.ConfigKeys) > 0 { + binding, err = NewBindingFromConfig(gui, influence, info.ConfigKeys, info.Display, info.OnAction) + } else { + binding, err = NewBinding(gui, influence, info.Key, info.Modifier, info.Display, info.OnAction) + } + + if err != nil { + return nil, err + } + + if info.IsSelected != nil { + binding.RegisterSelectionFn(info.IsSelected) + } + if len(info.Display) > 0 { + result = append(result, binding) + } + } + return result, nil +} + +func NewBinding(gui *gocui.Gui, influence string, key gocui.Key, mod gocui.Modifier, displayName string, actionFn func() error) (*Binding, error) { + return newBinding(gui, influence, []keybinding.Key{{Value: key, Modifier: mod}}, displayName, actionFn) +} + +func NewBindingFromConfig(gui *gocui.Gui, influence string, configKeys []string, displayName string, actionFn func() error) (*Binding, error) { + var parsedKeys []keybinding.Key + for _, configKey := range configKeys { + bindStr := viper.GetString(configKey) + logrus.Debugf("parsing keybinding '%s' --> '%s'", configKey, bindStr) + + keys, err := keybinding.ParseAll(bindStr) + if err == nil && keys != nil && len(keys) > 0 { + parsedKeys = keys + break + } + } + + if parsedKeys == nil { + return nil, fmt.Errorf("could not find configured keybindings for: %+v", configKeys) + } + + return newBinding(gui, influence, parsedKeys, displayName, actionFn) +} + +func newBinding(gui *gocui.Gui, influence string, keys []keybinding.Key, displayName string, actionFn func() error) (*Binding, error) { + binding := &Binding{ + key: keys, + displayName: displayName, + actionFn: actionFn, + } + + for _, key := range keys { + logrus.Debugf("registering %d %d (%+v)", key.Value, key.Modifier, key.Tokens) + if err := gui.SetKeybinding(influence, key.Value, key.Modifier, binding.onAction); err != nil { + return nil, err + } + } + + return binding, nil +} + +func (binding *Binding) RegisterSelectionFn(selectedFn func() bool) { + binding.selectedFn = selectedFn +} + +func (binding *Binding) onAction(*gocui.Gui, *gocui.View) error { + logrus.Debugf("keybinding invoked: %+v", binding) + if binding.actionFn == nil { + return fmt.Errorf("no action configured for '%+v'", binding) + } + return binding.actionFn() +} + +func (binding *Binding) isSelected() bool { + if binding.selectedFn == nil { + return false + } + + return binding.selectedFn() +} + +func (binding *Binding) RenderKeyHelp() string { + return format.RenderHelpKey(binding.key[0].String(), binding.displayName, binding.isSelected()) +} diff --git a/runtime/ui/layer_controller.go b/runtime/ui/layer_controller.go index 5695bb5..a14897f 100644 --- a/runtime/ui/layer_controller.go +++ b/runtime/ui/layer_controller.go @@ -3,19 +3,20 @@ package ui import ( "fmt" "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/runtime/ui/format" + "github.com/wagoodman/dive/runtime/ui/key" "strings" "github.com/jroimartin/gocui" "github.com/lunixbochs/vtclean" "github.com/sirupsen/logrus" "github.com/spf13/viper" - "github.com/wagoodman/keybinding" ) -// LayerController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that +// layerController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that // shows the image layers and layer selector. -type LayerController struct { - Name string +type layerController struct { + name string gui *gocui.Gui view *gocui.View header *gocui.View @@ -25,18 +26,15 @@ type LayerController struct { CompareStartIndex int ImageSize uint64 - keybindingCompareAll []keybinding.Key - keybindingCompareLayer []keybinding.Key - keybindingPageDown []keybinding.Key - keybindingPageUp []keybinding.Key + helpKeys []*key.Binding } -// NewLayerController creates a new view object attached the the global [gocui] screen object. -func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (controller *LayerController, err error) { - controller = new(LayerController) +// newLayerController creates a new view object attached the the global [gocui] screen object. +func newLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (controller *layerController, err error) { + controller = new(layerController) // populate main fields - controller.Name = name + controller.name = name controller.gui = gui controller.Layers = layers @@ -49,31 +47,11 @@ func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (con return nil, fmt.Errorf("unknown layer.show-aggregated-changes value: %v", mode) } - controller.keybindingCompareAll, err = keybinding.ParseAll(viper.GetString("keybinding.compare-all")) - if err != nil { - logrus.Error(err) - } - - controller.keybindingCompareLayer, err = keybinding.ParseAll(viper.GetString("keybinding.compare-layer")) - 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 *LayerController) Setup(v *gocui.View, header *gocui.View) error { +func (controller *layerController) Setup(v *gocui.View, header *gocui.View) error { // set controller options controller.view = v @@ -86,59 +64,73 @@ func (controller *LayerController) Setup(v *gocui.View, header *gocui.View) erro 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.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil { - return err - } - if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil { - return err + + var infos = []key.BindingInfo{ + { + ConfigKeys: []string{"keybinding.compare-layer"}, + OnAction: func() error { return controller.setCompareMode(CompareLayer) }, + IsSelected: func() bool { return controller.CompareMode == CompareLayer }, + Display: "Show layer changes", + }, + { + ConfigKeys: []string{"keybinding.compare-all"}, + OnAction: func() error { return controller.setCompareMode(CompareAll) }, + IsSelected: func() bool { return controller.CompareMode == CompareAll }, + Display: "Show aggregated changes", + }, + { + 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.CursorUp, + }, + { + Key: gocui.KeyArrowRight, + Modifier: gocui.ModNone, + OnAction: controller.CursorDown, + }, + { + ConfigKeys: []string{"keybinding.page-up"}, + OnAction: controller.PageUp, + }, + { + ConfigKeys: []string{"keybinding.page-down"}, + OnAction: controller.PageDown, + }, } - 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 - } + helpKeys, err := key.GenerateBindings(controller.gui, controller.name, infos) + if err != nil { + return err } + controller.helpKeys = helpKeys - for _, key := range controller.keybindingCompareLayer { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.setCompareMode(CompareLayer) }); err != nil { - return err - } - } - - for _, key := range controller.keybindingCompareAll { - if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.setCompareMode(CompareAll) }); err != nil { - return err - } - } return controller.Render() } // height obtains the height of the current pane (taking into account the lost space due to the header). -func (controller *LayerController) height() uint { +func (controller *layerController) height() uint { _, height := controller.view.Size() return uint(height - 1) } // IsVisible indicates if the layer view pane is currently initialized. -func (controller *LayerController) IsVisible() bool { +func (controller *layerController) IsVisible() bool { return controller != nil } // PageDown moves to next page putting the cursor on top -func (controller *LayerController) PageDown() error { +func (controller *layerController) PageDown() error { step := int(controller.height()) + 1 targetLayerIndex := controller.LayerIndex + step @@ -156,7 +148,7 @@ func (controller *LayerController) PageDown() error { } // PageUp moves to previous page putting the cursor on top -func (controller *LayerController) PageUp() error { +func (controller *layerController) PageUp() error { step := int(controller.height()) + 1 targetLayerIndex := controller.LayerIndex - step @@ -174,7 +166,7 @@ func (controller *LayerController) PageUp() error { } // CursorDown moves the cursor down in the layer pane (selecting a higher layer). -func (controller *LayerController) CursorDown() error { +func (controller *layerController) CursorDown() error { if controller.LayerIndex < len(controller.Layers) { err := CursorDown(controller.gui, controller.view) if err == nil { @@ -185,7 +177,7 @@ func (controller *LayerController) CursorDown() error { } // CursorUp moves the cursor up in the layer pane (selecting a lower layer). -func (controller *LayerController) CursorUp() error { +func (controller *layerController) CursorUp() error { if controller.LayerIndex > 0 { err := CursorUp(controller.gui, controller.view) if err == nil { @@ -196,36 +188,36 @@ func (controller *LayerController) CursorUp() error { } // SetCursor resets the cursor and orients the file tree view based on the given layer index. -func (controller *LayerController) SetCursor(layer int) error { +func (controller *layerController) SetCursor(layer int) error { controller.LayerIndex = layer - err := Controllers.Tree.setTreeByLayer(controller.getCompareIndexes()) + err := controllers.Tree.setTreeByLayer(controller.getCompareIndexes()) if err != nil { return err } - _ = Controllers.Details.Render() + _ = controllers.Details.Render() return controller.Render() } // currentLayer returns the Layer object currently selected. -func (controller *LayerController) currentLayer() *image.Layer { +func (controller *layerController) currentLayer() *image.Layer { return controller.Layers[controller.LayerIndex] } // setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison. -func (controller *LayerController) setCompareMode(compareMode CompareType) error { +func (controller *layerController) setCompareMode(compareMode CompareType) error { controller.CompareMode = compareMode err := UpdateAndRender() if err != nil { logrus.Errorf("unable to set compare mode: %+v", err) return err } - return Controllers.Tree.setTreeByLayer(controller.getCompareIndexes()) + return controllers.Tree.setTreeByLayer(controller.getCompareIndexes()) } // getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode) -func (controller *LayerController) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { +func (controller *layerController) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { bottomTreeStart = controller.CompareStartIndex topTreeStop = controller.LayerIndex @@ -244,22 +236,22 @@ func (controller *LayerController) getCompareIndexes() (bottomTreeStart, bottomT } // renderCompareBar returns the formatted string for the given layer. -func (controller *LayerController) renderCompareBar(layerIdx int) string { +func (controller *layerController) renderCompareBar(layerIdx int) string { bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := controller.getCompareIndexes() result := " " if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop { - result = Formatting.CompareBottom(" ") + result = format.CompareBottom(" ") } if layerIdx >= topTreeStart && layerIdx <= topTreeStop { - result = Formatting.CompareTop(" ") + result = format.CompareTop(" ") } return result } // Update refreshes the state objects for future rendering (currently does nothing). -func (controller *LayerController) Update() error { +func (controller *layerController) Update() error { controller.ImageSize = 0 for idx := 0; idx < len(controller.Layers); idx++ { controller.ImageSize += controller.Layers[idx].Size @@ -270,7 +262,7 @@ func (controller *LayerController) Update() error { // Render flushes the state objects to the screen. The layers pane reports: // 1. the layers of the image + metadata // 2. the current selected image -func (controller *LayerController) Render() error { +func (controller *layerController) Render() error { // indicate when selected title := "Layers" @@ -284,7 +276,7 @@ func (controller *LayerController) Render() error { width, _ := g.Size() headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2)) headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command") - _, err := fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false))) + _, err := fmt.Fprintln(controller.header, format.Header(vtclean.Clean(headerStr, false))) if err != nil { return err } @@ -297,7 +289,7 @@ func (controller *LayerController) Render() error { compareBar := controller.renderCompareBar(idx) if idx == controller.LayerIndex { - _, err = fmt.Fprintln(controller.view, compareBar+" "+Formatting.Selected(layerStr)) + _, err = fmt.Fprintln(controller.view, compareBar+" "+format.Selected(layerStr)) } else { _, err = fmt.Fprintln(controller.view, compareBar+" "+layerStr) } @@ -314,7 +306,10 @@ func (controller *LayerController) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (controller *LayerController) KeyHelp() string { - return renderStatusOption(controller.keybindingCompareLayer[0].String(), "Show layer changes", controller.CompareMode == CompareLayer) + - renderStatusOption(controller.keybindingCompareAll[0].String(), "Show aggregated changes", controller.CompareMode == CompareAll) +func (controller *layerController) KeyHelp() string { + var help string + for _, binding := range controller.helpKeys { + help += binding.RenderKeyHelp() + } + return help } diff --git a/runtime/ui/layout_manager.go b/runtime/ui/layout_manager.go new file mode 100644 index 0000000..4672ac5 --- /dev/null +++ b/runtime/ui/layout_manager.go @@ -0,0 +1,11 @@ +package ui + +type layoutManager struct { + fileTreeSplitRatio float64 +} + +func newLayoutManager(fileTreeSplitRatio float64) *layoutManager { + return &layoutManager{ + fileTreeSplitRatio: fileTreeSplitRatio, + } +} diff --git a/runtime/ui/status_controller.go b/runtime/ui/status_controller.go index 8e590df..964332d 100644 --- a/runtime/ui/status_controller.go +++ b/runtime/ui/status_controller.go @@ -3,33 +3,43 @@ package ui import ( "fmt" "github.com/sirupsen/logrus" - + "github.com/wagoodman/dive/runtime/ui/format" + "github.com/wagoodman/dive/runtime/ui/key" "strings" "github.com/jroimartin/gocui" ) -// StatusController holds the UI objects and data models for populating the bottom-most pane. Specifically the panel +// statusController holds the UI objects and data models for populating the bottom-most pane. Specifically the panel // shows the user a set of possible actions to take in the window and currently selected pane. -type StatusController struct { - Name string +type statusController struct { + name string gui *gocui.Gui view *gocui.View + + helpKeys []*key.Binding } -// NewStatusController creates a new view object attached the the global [gocui] screen object. -func NewStatusController(name string, gui *gocui.Gui) (controller *StatusController) { - controller = new(StatusController) +// newStatusController creates a new view object attached the the global [gocui] screen object. +func newStatusController(name string, gui *gocui.Gui) (controller *statusController) { + controller = new(statusController) // populate main fields - controller.Name = name + controller.name = name controller.gui = gui + controller.helpKeys = make([]*key.Binding, 0) return controller } +func (controller *statusController) AddHelpKeys(keys ...*key.Binding) { + for _, k := range keys { + controller.helpKeys = append(controller.helpKeys, k) + } +} + // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (controller *StatusController) Setup(v *gocui.View, header *gocui.View) error { +func (controller *statusController) Setup(v *gocui.View, header *gocui.View) error { // set controller options controller.view = v @@ -39,30 +49,30 @@ func (controller *StatusController) Setup(v *gocui.View, header *gocui.View) err } // IsVisible indicates if the status view pane is currently initialized. -func (controller *StatusController) IsVisible() bool { +func (controller *statusController) IsVisible() bool { return controller != nil } // CursorDown moves the cursor down in the details pane (currently indicates nothing). -func (controller *StatusController) CursorDown() error { +func (controller *statusController) CursorDown() error { return nil } // CursorUp moves the cursor up in the details pane (currently indicates nothing). -func (controller *StatusController) CursorUp() error { +func (controller *statusController) CursorUp() error { return nil } // Update refreshes the state objects for future rendering (currently does nothing). -func (controller *StatusController) Update() error { +func (controller *statusController) Update() error { return nil } // Render flushes the state objects to the screen. -func (controller *StatusController) Render() error { +func (controller *statusController) Render() error { controller.gui.Update(func(g *gocui.Gui) error { controller.view.Clear() - _, err := fmt.Fprintln(controller.view, controller.KeyHelp()+Controllers.lookup[controller.gui.CurrentView().Name()].KeyHelp()+Formatting.StatusNormal("▏"+strings.Repeat(" ", 1000))) + _, err := fmt.Fprintln(controller.view, controller.KeyHelp()+controllers.lookup[controller.gui.CurrentView().Name()].KeyHelp()+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) if err != nil { logrus.Debug("unable to write to buffer: ", err) } @@ -73,8 +83,13 @@ func (controller *StatusController) Render() error { } // KeyHelp indicates all the possible global actions a user can take when any pane is selected. -func (controller *StatusController) KeyHelp() string { - return renderStatusOption(GlobalKeybindings.quit[0].String(), "Quit", false) + - renderStatusOption(GlobalKeybindings.toggleView[0].String(), "Switch view", false) + - renderStatusOption(GlobalKeybindings.filterView[0].String(), "Filter", Controllers.Filter.IsVisible()) +func (controller *statusController) KeyHelp() string { + // return renderStatusOption(globalKeybinding.quit[0].String(), "Quit", false) + + // renderStatusOption(globalKeybinding.toggleView[0].String(), "Switch view", false) + + // renderStatusOption(globalKeybinding.filterView[0].String(), "Filter", controllers.Filter.IsVisible()) + var help string + for _, binding := range controller.helpKeys { + help += binding.RenderKeyHelp() + } + return help } diff --git a/runtime/ui/ui.go b/runtime/ui/ui.go index 3b2075a..93f4118 100644 --- a/runtime/ui/ui.go +++ b/runtime/ui/ui.go @@ -3,24 +3,82 @@ package ui import ( "errors" "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/runtime/ui/format" + "github.com/wagoodman/dive/runtime/ui/key" + "sync" - "github.com/fatih/color" "github.com/jroimartin/gocui" "github.com/sirupsen/logrus" "github.com/spf13/viper" "github.com/wagoodman/dive/dive/filetree" - "github.com/wagoodman/keybinding" ) const debug = false +// type global +type Ui struct { + controllers *controllerCollection +} + +var ( + once sync.Once + uiSingleton *Ui +) + +func NewUi(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*Ui, error) { + var err error + once.Do(func() { + var theControls *controllerCollection + var globalHelpKeys []*key.Binding + + theControls, err = newControllerCollection(g, analysis, cache) + if err != nil { + return + } + + var infos = []key.BindingInfo{ + { + ConfigKeys: []string{"keybinding.quit"}, + OnAction: quit, + Display: "Removed", + }, + { + ConfigKeys: []string{"keybinding.toggle-view"}, + OnAction: quit, + // OnAction: toggleView, + Display: "Modified", + }, + { + ConfigKeys: []string{"keybinding.filter-files"}, + OnAction: quit, + // OnAction: toggleFilterView, + IsSelected: controllers.Filter.IsVisible, + Display: "Unmodified", + }, + } + + globalHelpKeys, err = key.GenerateBindings(g, "", infos) + if err != nil { + return + } + + theControls.Status.AddHelpKeys(globalHelpKeys...) + + uiSingleton = &Ui{ + controllers: theControls, + } + }) + + return uiSingleton, err +} + // var profileObj = profile.Start(profile.MemProfile, profile.ProfilePath("."), profile.NoShutdownHook) // var onExit func() // debugPrint writes the given string to the debug pane (if the debug pane is enabled) // func debugPrint(s string) { -// if Controllers.Tree != nil && Controllers.Tree.gui != nil { -// v, _ := Controllers.Tree.gui.View("debug") +// if controllers.Tree != nil && controllers.Tree.gui != nil { +// v, _ := controllers.Tree.gui.View("debug") // if v != nil { // if len(v.BufferLines()) > 20 { // v.Clear() @@ -30,47 +88,8 @@ const debug = false // } // } -// Formatting defines standard functions for formatting UI sections. -var Formatting struct { - Header func(...interface{}) string - Selected func(...interface{}) string - StatusSelected func(...interface{}) string - StatusNormal func(...interface{}) string - StatusControlSelected func(...interface{}) string - StatusControlNormal func(...interface{}) string - CompareTop func(...interface{}) string - CompareBottom func(...interface{}) string -} - -// Controllers contains all rendered UI panes. -var Controllers struct { - Tree *FileTreeController - Layer *LayerController - Status *StatusController - Filter *FilterController - Details *DetailsController - lookup map[string]View -} - -var GlobalKeybindings struct { - quit []keybinding.Key - toggleView []keybinding.Key - filterView []keybinding.Key -} - var lastX, lastY int -// View defines the a renderable terminal screen pane. -type View interface { - Setup(*gocui.View, *gocui.View) error - CursorDown() error - CursorUp() error - Render() error - Update() error - KeyHelp() string - IsVisible() bool -} - func UpdateAndRender() error { err := Update() if err != nil { @@ -88,11 +107,12 @@ func UpdateAndRender() error { } // toggleView switches between the file view and the layer view and re-renders the screen. -func toggleView(g *gocui.Gui, v *gocui.View) (err error) { - if v == nil || v.Name() == Controllers.Layer.Name { - _, err = g.SetCurrentView(Controllers.Tree.Name) +func toggleView(g *gocui.Gui) (err error) { + v := g.CurrentView() + if v == nil || v.Name() == controllers.Layer.name { + _, err = g.SetCurrentView(controllers.Tree.name) } else { - _, err = g.SetCurrentView(Controllers.Layer.Name) + _, err = g.SetCurrentView(controllers.Layer.name) } if err != nil { @@ -104,15 +124,15 @@ func toggleView(g *gocui.Gui, v *gocui.View) (err error) { } // toggleFilterView shows/hides the file tree filter pane. -func toggleFilterView(g *gocui.Gui, v *gocui.View) error { +func toggleFilterView(g *gocui.Gui) error { // delete all user input from the tree view - Controllers.Filter.view.Clear() + controllers.Filter.view.Clear() // toggle hiding - Controllers.Filter.hidden = !Controllers.Filter.hidden + controllers.Filter.hidden = !controllers.Filter.hidden - if !Controllers.Filter.hidden { - _, err := g.SetCurrentView(Controllers.Filter.Name) + if !controllers.Filter.hidden { + _, err := g.SetCurrentView(controllers.Filter.name) if err != nil { logrus.Error("unable to toggle filter view: ", err) return err @@ -120,13 +140,13 @@ func toggleFilterView(g *gocui.Gui, v *gocui.View) error { return UpdateAndRender() } - err := toggleView(g, v) + err := toggleView(g) if err != nil { logrus.Error("unable to toggle filter view (back): ", err) return err } - err = Controllers.Filter.view.SetCursor(0, 0) + err = controllers.Filter.view.SetCursor(0, 0) if err != nil { return err } @@ -166,7 +186,7 @@ func CursorStep(g *gocui.Gui, v *gocui.View, step int) error { } // quit is the gocui callback invoked when the user hits Ctrl+C -func quit(g *gocui.Gui, v *gocui.View) error { +func quit() error { // profileObj.Stop() // onExit() @@ -174,29 +194,6 @@ func quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit } -// keyBindings registers global key press actions, valid when in any pane. -func keyBindings(g *gocui.Gui) error { - for _, key := range GlobalKeybindings.quit { - if err := g.SetKeybinding("", key.Value, key.Modifier, quit); err != nil { - return err - } - } - - for _, key := range GlobalKeybindings.toggleView { - if err := g.SetKeybinding("", key.Value, key.Modifier, toggleView); err != nil { - return err - } - } - - for _, key := range GlobalKeybindings.filterView { - if err := g.SetKeybinding("", key.Value, key.Modifier, toggleFilterView); err != nil { - return err - } - } - - return nil -} - // isNewView determines if a view has already been created based on the set of errors given (a bit hokie) func isNewView(errs ...error) bool { for _, err := range errs { @@ -244,7 +241,7 @@ func layout(g *gocui.Gui) error { statusBarIndex := 1 filterBarIndex := 2 - layersHeight := len(Controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row + layersHeight := len(controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row maxLayerHeight := int(0.75 * float64(maxY)) if layersHeight > maxLayerHeight { layersHeight = maxLayerHeight @@ -253,7 +250,7 @@ func layout(g *gocui.Gui) error { var view, header *gocui.View var viewErr, headerErr, err error - if Controllers.Filter.hidden { + if controllers.Filter.hidden { bottomRows-- filterBarHeight = 0 } @@ -268,21 +265,21 @@ func layout(g *gocui.Gui) error { } // Layers - view, viewErr = g.SetView(Controllers.Layer.Name, -1, -1+headerRows, splitCols, layersHeight) - header, headerErr = g.SetView(Controllers.Layer.Name+"header", -1, -1, splitCols, headerRows) + view, viewErr = g.SetView(controllers.Layer.name, -1, -1+headerRows, splitCols, layersHeight) + header, headerErr = g.SetView(controllers.Layer.name+"header", -1, -1, splitCols, headerRows) if isNewView(viewErr, headerErr) { - err = Controllers.Layer.Setup(view, header) + err = controllers.Layer.Setup(view, header) if err != nil { logrus.Error("unable to setup layer controller", err) return err } - if _, err = g.SetCurrentView(Controllers.Layer.Name); err != nil { + if _, err = g.SetCurrentView(controllers.Layer.name); err != nil { logrus.Error("unable to set view to layer", err) return err } // since we are selecting the view, we should rerender to indicate it is selected - err = Controllers.Layer.Render() + err = controllers.Layer.Render() if err != nil { logrus.Error("unable to render layer view", err) return err @@ -290,10 +287,10 @@ func layout(g *gocui.Gui) error { } // Details - view, viewErr = g.SetView(Controllers.Details.Name, -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows) - header, headerErr = g.SetView(Controllers.Details.Name+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows) + view, viewErr = g.SetView(controllers.Details.name, -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows) + header, headerErr = g.SetView(controllers.Details.name+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows) if isNewView(viewErr, headerErr) { - err = Controllers.Details.Setup(view, header) + err = controllers.Details.Setup(view, header) if err != nil { return err } @@ -301,28 +298,28 @@ func layout(g *gocui.Gui) error { // Filetree offset := 0 - if !Controllers.Tree.vm.ShowAttributes { + if !controllers.Tree.vm.ShowAttributes { offset = 1 } - view, viewErr = g.SetView(Controllers.Tree.Name, splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows) - header, headerErr = g.SetView(Controllers.Tree.Name+"header", splitCols, -1, debugCols, headerRows-offset) + view, viewErr = g.SetView(controllers.Tree.name, splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows) + header, headerErr = g.SetView(controllers.Tree.name+"header", splitCols, -1, debugCols, headerRows-offset) if isNewView(viewErr, headerErr) { - err = Controllers.Tree.Setup(view, header) + err = controllers.Tree.Setup(view, header) if err != nil { logrus.Error("unable to setup tree controller", err) return err } } - err = Controllers.Tree.onLayoutChange(resized) + err = controllers.Tree.onLayoutChange(resized) if err != nil { logrus.Error("unable to setup layer controller onLayoutChange", err) return err } // Status Bar - view, viewErr = g.SetView(Controllers.Status.Name, -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1)) + view, viewErr = g.SetView(controllers.Status.name, -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1)) if isNewView(viewErr, headerErr) { - err = Controllers.Status.Setup(view, nil) + err = controllers.Status.Setup(view, nil) if err != nil { logrus.Error("unable to setup status controller", err) return err @@ -330,10 +327,10 @@ func layout(g *gocui.Gui) error { } // Filter Bar - view, viewErr = g.SetView(Controllers.Filter.Name, len(Controllers.Filter.headerStr)-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1)) - header, headerErr = g.SetView(Controllers.Filter.Name+"header", -1, maxY-filterBarHeight-filterBarIndex, len(Controllers.Filter.headerStr), maxY-(filterBarIndex-1)) + view, viewErr = g.SetView(controllers.Filter.name, len(controllers.Filter.headerStr)-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1)) + header, headerErr = g.SetView(controllers.Filter.name+"header", -1, maxY-filterBarHeight-filterBarIndex, len(controllers.Filter.headerStr), maxY-(filterBarIndex-1)) if isNewView(viewErr, headerErr) { - err = Controllers.Filter.Setup(view, header) + err = controllers.Filter.Setup(view, header) if err != nil { logrus.Error("unable to setup filter controller", err) return err @@ -345,7 +342,7 @@ func layout(g *gocui.Gui) error { // Update refreshes the state objects for future rendering. func Update() error { - for _, controller := range Controllers.lookup { + for _, controller := range controllers.lookup { err := controller.Update() if err != nil { logrus.Debug("unable to update controller: ") @@ -357,7 +354,7 @@ func Update() error { // Render flushes the state objects to the screen. func Render() error { - for _, controller := range Controllers.lookup { + for _, controller := range controllers.lookup { if controller.IsVisible() { err := controller.Render() if err != nil { @@ -371,37 +368,15 @@ func Render() error { // renderStatusOption formats key help bindings-to-title pairs. func renderStatusOption(control, title string, selected bool) string { if selected { - return Formatting.StatusSelected("▏") + Formatting.StatusControlSelected(control) + Formatting.StatusSelected(" "+title+" ") + return format.StatusSelected("▏") + format.StatusControlSelected(control) + format.StatusSelected(" "+title+" ") } else { - return Formatting.StatusNormal("▏") + Formatting.StatusControlNormal(control) + Formatting.StatusNormal(" "+title+" ") + return format.StatusNormal("▏") + format.StatusControlNormal(control) + format.StatusNormal(" "+title+" ") } } // Run is the UI entrypoint. func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error { - - Formatting.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc() - Formatting.Header = color.New(color.Bold).SprintFunc() - Formatting.StatusSelected = color.New(color.BgMagenta, color.FgWhite).SprintFunc() - Formatting.StatusNormal = color.New(color.ReverseVideo).SprintFunc() - Formatting.StatusControlSelected = color.New(color.BgMagenta, color.FgWhite, color.Bold).SprintFunc() - Formatting.StatusControlNormal = color.New(color.ReverseVideo, color.Bold).SprintFunc() - Formatting.CompareTop = color.New(color.BgMagenta).SprintFunc() - Formatting.CompareBottom = color.New(color.BgGreen).SprintFunc() - var err error - GlobalKeybindings.quit, err = keybinding.ParseAll(viper.GetString("keybinding.quit")) - if err != nil { - return err - } - GlobalKeybindings.toggleView, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-view")) - if err != nil { - return err - } - GlobalKeybindings.filterView, err = keybinding.ParseAll(viper.GetString("keybinding.filter-files")) - if err != nil { - return err - } g, err := gocui.NewGui(gocui.OutputNormal) if err != nil { @@ -409,32 +384,10 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error { } defer g.Close() - Controllers.lookup = make(map[string]View) - - Controllers.Layer, err = NewLayerController("side", g, analysis.Layers) + _, err = newControllerCollection(g, analysis, cache) if err != nil { return err } - Controllers.lookup[Controllers.Layer.Name] = Controllers.Layer - - treeStack, err := filetree.StackTreeRange(analysis.RefTrees, 0, 0) - if err != nil { - return err - } - Controllers.Tree, err = NewFileTreeController("main", g, treeStack, analysis.RefTrees, cache) - if err != nil { - return err - } - Controllers.lookup[Controllers.Tree.Name] = Controllers.Tree - - Controllers.Status = NewStatusController("status", g) - Controllers.lookup[Controllers.Status.Name] = Controllers.Status - - Controllers.Filter = NewFilterController("command", g) - Controllers.lookup[Controllers.Filter.Name] = Controllers.Filter - - Controllers.Details = NewDetailsController("details", g, analysis.Efficiency, analysis.Inefficiencies) - Controllers.lookup[Controllers.Details.Name] = Controllers.Details g.Cursor = false //g.Mouse = true @@ -446,13 +399,37 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error { // profileObj.Stop() // } - // perform the first update and render now that all resources have been loaded - err = UpdateAndRender() + + var infos = []key.BindingInfo{ + { + ConfigKeys: []string{"keybinding.quit"}, + OnAction: quit, + Display: "Removed", + }, + { + ConfigKeys: []string{"keybinding.toggle-view"}, + OnAction: quit, + // OnAction: toggleView, + Display: "Modified", + }, + { + ConfigKeys: []string{"keybinding.filter-files"}, + OnAction: quit, + // OnAction: toggleFilterView, + // IsSelected: controllers.Filter.IsVisible, + Display: "Unmodified", + }, + } + + globalHelpKeys, err := key.GenerateBindings(g, "", infos) if err != nil { return err } + controllers.Status.AddHelpKeys(globalHelpKeys...) - if err := keyBindings(g); err != nil { + // perform the first update and render now that all resources have been loaded + err = UpdateAndRender() + if err != nil { return err } From 34d8cbcef56e3b4ee1542066128090c9eaebd273 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 11 Oct 2019 12:25:48 -0400 Subject: [PATCH 2/6] move to app object --- runtime/ui/status_controller.go | 3 - runtime/ui/ui.go | 122 ++++++++++++-------------------- 2 files changed, 44 insertions(+), 81 deletions(-) diff --git a/runtime/ui/status_controller.go b/runtime/ui/status_controller.go index 964332d..31f7ce9 100644 --- a/runtime/ui/status_controller.go +++ b/runtime/ui/status_controller.go @@ -84,9 +84,6 @@ func (controller *statusController) Render() error { // KeyHelp indicates all the possible global actions a user can take when any pane is selected. func (controller *statusController) KeyHelp() string { - // return renderStatusOption(globalKeybinding.quit[0].String(), "Quit", false) + - // renderStatusOption(globalKeybinding.toggleView[0].String(), "Switch view", false) + - // renderStatusOption(globalKeybinding.filterView[0].String(), "Filter", controllers.Filter.IsVisible()) var help string for _, binding := range controller.helpKeys { help += binding.RenderKeyHelp() diff --git a/runtime/ui/ui.go b/runtime/ui/ui.go index 93f4118..3859c35 100644 --- a/runtime/ui/ui.go +++ b/runtime/ui/ui.go @@ -3,7 +3,6 @@ package ui import ( "errors" "github.com/wagoodman/dive/dive/image" - "github.com/wagoodman/dive/runtime/ui/format" "github.com/wagoodman/dive/runtime/ui/key" "sync" @@ -16,60 +15,80 @@ import ( const debug = false // type global -type Ui struct { +type app struct { + gui *gocui.Gui controllers *controllerCollection } var ( - once sync.Once - uiSingleton *Ui + once sync.Once + appSingleton *app ) -func NewUi(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*Ui, error) { +func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*app, error) { var err error once.Do(func() { var theControls *controllerCollection var globalHelpKeys []*key.Binding - theControls, err = newControllerCollection(g, analysis, cache) + theControls, err = newControllerCollection(gui, analysis, cache) if err != nil { return } + gui.Cursor = false + //g.Mouse = true + gui.SetManagerFunc(layout) + + // var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook) + // + // onExit = func() { + // profileObj.Stop() + // } + + + appSingleton = &app{ + gui: gui, + controllers: theControls, + } + var infos = []key.BindingInfo{ { ConfigKeys: []string{"keybinding.quit"}, OnAction: quit, - Display: "Removed", + Display: "Quit", }, { ConfigKeys: []string{"keybinding.toggle-view"}, - OnAction: quit, - // OnAction: toggleView, - Display: "Modified", + OnAction: appSingleton.toggleView, + Display: "Switch view", }, { ConfigKeys: []string{"keybinding.filter-files"}, - OnAction: quit, - // OnAction: toggleFilterView, + OnAction: appSingleton.toggleFilterView, IsSelected: controllers.Filter.IsVisible, - Display: "Unmodified", + Display: "Filter", }, } - globalHelpKeys, err = key.GenerateBindings(g, "", infos) + globalHelpKeys, err = key.GenerateBindings(gui, "", infos) if err != nil { return } theControls.Status.AddHelpKeys(globalHelpKeys...) - uiSingleton = &Ui{ - controllers: theControls, + + // perform the first update and render now that all resources have been loaded + err = UpdateAndRender() + if err != nil { + return } + + }) - return uiSingleton, err + return appSingleton, err } // var profileObj = profile.Start(profile.MemProfile, profile.ProfilePath("."), profile.NoShutdownHook) @@ -107,12 +126,12 @@ func UpdateAndRender() error { } // toggleView switches between the file view and the layer view and re-renders the screen. -func toggleView(g *gocui.Gui) (err error) { - v := g.CurrentView() +func (ui *app) toggleView() (err error) { + v := ui.gui.CurrentView() if v == nil || v.Name() == controllers.Layer.name { - _, err = g.SetCurrentView(controllers.Tree.name) + _, err = ui.gui.SetCurrentView(controllers.Tree.name) } else { - _, err = g.SetCurrentView(controllers.Layer.name) + _, err = ui.gui.SetCurrentView(controllers.Layer.name) } if err != nil { @@ -124,7 +143,7 @@ func toggleView(g *gocui.Gui) (err error) { } // toggleFilterView shows/hides the file tree filter pane. -func toggleFilterView(g *gocui.Gui) error { +func (ui *app) toggleFilterView() error { // delete all user input from the tree view controllers.Filter.view.Clear() @@ -132,7 +151,7 @@ func toggleFilterView(g *gocui.Gui) error { controllers.Filter.hidden = !controllers.Filter.hidden if !controllers.Filter.hidden { - _, err := g.SetCurrentView(controllers.Filter.name) + _, err := ui.gui.SetCurrentView(controllers.Filter.name) if err != nil { logrus.Error("unable to toggle filter view: ", err) return err @@ -140,7 +159,7 @@ func toggleFilterView(g *gocui.Gui) error { return UpdateAndRender() } - err := toggleView(g) + err := ui.toggleView() if err != nil { logrus.Error("unable to toggle filter view (back): ", err) return err @@ -365,15 +384,6 @@ func Render() error { return nil } -// renderStatusOption formats key help bindings-to-title pairs. -func renderStatusOption(control, title string, selected bool) string { - if selected { - return format.StatusSelected("▏") + format.StatusControlSelected(control) + format.StatusSelected(" "+title+" ") - } else { - return format.StatusNormal("▏") + format.StatusControlNormal(control) + format.StatusNormal(" "+title+" ") - } -} - // Run is the UI entrypoint. func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error { var err error @@ -384,51 +394,7 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error { } defer g.Close() - _, err = newControllerCollection(g, analysis, cache) - if err != nil { - return err - } - - g.Cursor = false - //g.Mouse = true - g.SetManagerFunc(layout) - - // var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook) - // - // onExit = func() { - // profileObj.Stop() - // } - - - var infos = []key.BindingInfo{ - { - ConfigKeys: []string{"keybinding.quit"}, - OnAction: quit, - Display: "Removed", - }, - { - ConfigKeys: []string{"keybinding.toggle-view"}, - OnAction: quit, - // OnAction: toggleView, - Display: "Modified", - }, - { - ConfigKeys: []string{"keybinding.filter-files"}, - OnAction: quit, - // OnAction: toggleFilterView, - // IsSelected: controllers.Filter.IsVisible, - Display: "Unmodified", - }, - } - - globalHelpKeys, err := key.GenerateBindings(g, "", infos) - if err != nil { - return err - } - controllers.Status.AddHelpKeys(globalHelpKeys...) - - // perform the first update and render now that all resources have been loaded - err = UpdateAndRender() + _, err = newApp(g, analysis, cache) if err != nil { return err } From 2069a3fede176e349a8a555ea8a582cdbb86e220 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sun, 13 Oct 2019 11:42:13 -0400 Subject: [PATCH 3/6] first step at ui obj refactor --- Makefile | 2 +- runtime/ui/{ => controller}/controller.go | 10 +- .../ui/controller/controller_collection.go | 166 ++++++++++ .../ui/{ => controller}/details_controller.go | 34 +- .../{ => controller}/filetree_controller.go | 98 +++--- .../ui/{ => controller}/filter_controller.go | 60 +++- .../ui/{ => controller}/layer_controller.go | 62 ++-- .../ui/{ => controller}/status_controller.go | 38 +-- runtime/ui/controller_collection.go | 52 --- runtime/ui/key/binding.go | 1 - runtime/ui/layout_manager.go | 160 +++++++++- runtime/ui/ui.go | 298 +----------------- .../ui/{ => viewmodel}/filetree_viewmodel.go | 72 ++--- .../filetree_viewmodel_test.go | 62 ++-- .../testdata/TestFileShowAggregateChanges.txt | 0 .../testdata/TestFileTreeDirCollapse.txt | 0 .../testdata/TestFileTreeDirCollapseAll.txt | 0 .../testdata/TestFileTreeDirCursorRight.txt | 0 .../testdata/TestFileTreeFilterTree.txt | 0 .../testdata/TestFileTreeGoCase.txt | 0 .../TestFileTreeHideAddedRemovedModified.txt | 0 .../TestFileTreeHideTypeWithFilter.txt | 0 .../testdata/TestFileTreeHideUnmodified.txt | 0 .../testdata/TestFileTreeNoAttributes.txt | 0 .../testdata/TestFileTreePageDown.txt | 0 .../testdata/TestFileTreePageUp.txt | 0 .../testdata/TestFileTreeRestrictedHeight.txt | 0 .../testdata/TestFileTreeSelectLayer.txt | 0 28 files changed, 582 insertions(+), 533 deletions(-) rename runtime/ui/{ => controller}/controller.go (83%) create mode 100644 runtime/ui/controller/controller_collection.go rename runtime/ui/{ => controller}/details_controller.go (84%) rename runtime/ui/{ => controller}/filetree_controller.go (75%) rename runtime/ui/{ => controller}/filter_controller.go (61%) rename runtime/ui/{ => controller}/layer_controller.go (81%) rename runtime/ui/{ => controller}/status_controller.go (62%) delete mode 100644 runtime/ui/controller_collection.go rename runtime/ui/{ => viewmodel}/filetree_viewmodel.go (83%) rename runtime/ui/{ => viewmodel}/filetree_viewmodel_test.go (86%) rename runtime/ui/{ => viewmodel}/testdata/TestFileShowAggregateChanges.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeDirCollapse.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeDirCollapseAll.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeDirCursorRight.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeFilterTree.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeGoCase.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeHideAddedRemovedModified.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeHideTypeWithFilter.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeHideUnmodified.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeNoAttributes.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreePageDown.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreePageUp.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeRestrictedHeight.txt (100%) rename runtime/ui/{ => viewmodel}/testdata/TestFileTreeSelectLayer.txt (100%) diff --git a/Makefile b/Makefile index ac291e5..18d3786 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ test-coverage: build ./.scripts/test-coverage.sh validate: - grep -R 'const allowTestDataCapture = false' runtime/ui/ + grep -R 'const allowTestDataCapture = false' runtime/ui/viewmodel go vet ./... @! gofmt -s -l . 2>&1 | grep -vE '^\.git/' | grep -vE '^\.cache/' golangci-lint run diff --git a/runtime/ui/controller.go b/runtime/ui/controller/controller.go similarity index 83% rename from runtime/ui/controller.go rename to runtime/ui/controller/controller.go index fb1e500..861b43f 100644 --- a/runtime/ui/controller.go +++ b/runtime/ui/controller/controller.go @@ -1,17 +1,13 @@ -package ui +package controller import ( "github.com/jroimartin/gocui" ) -type Renderable interface { - Update() error - Render() error -} - // Controller defines the a renderable terminal screen pane. type Controller interface { - Renderable + Update() error + Render() error Setup(*gocui.View, *gocui.View) error CursorDown() error CursorUp() error diff --git a/runtime/ui/controller/controller_collection.go b/runtime/ui/controller/controller_collection.go new file mode 100644 index 0000000..f0b7c7b --- /dev/null +++ b/runtime/ui/controller/controller_collection.go @@ -0,0 +1,166 @@ +package controller + +import ( + "errors" + "github.com/jroimartin/gocui" + "github.com/sirupsen/logrus" + "github.com/wagoodman/dive/dive/filetree" + "github.com/wagoodman/dive/dive/image" +) + +// var ccOnce sync.Once +var controllers *ControllerCollection + +type ControllerCollection struct { + gui *gocui.Gui + 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{ + gui: g, + } + 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 +} + +func (c *ControllerCollection) UpdateAndRender() error { + err := c.Update() + if err != nil { + logrus.Debug("failed update: ", err) + return err + } + + err = c.Render() + if err != nil { + logrus.Debug("failed render: ", err) + return err + } + + return nil +} + +// Update refreshes the state objects for future rendering. +func (c *ControllerCollection) Update() error { + for _, controller := range c.lookup { + err := controller.Update() + if err != nil { + logrus.Debug("unable to update controller: ") + return err + } + } + return nil +} + +// Render flushes the state objects to the screen. +func (c *ControllerCollection) Render() error { + for _, controller := range c.lookup { + if controller.IsVisible() { + err := controller.Render() + if err != nil { + return err + } + } + } + return nil +} + +// ToggleView switches between the file view and the layer view and re-renders the screen. +func (c *ControllerCollection) ToggleView() (err error) { + v := c.gui.CurrentView() + if v == nil || v.Name() == c.Layer.Name() { + _, err = c.gui.SetCurrentView(c.Tree.Name()) + } else { + _, err = c.gui.SetCurrentView(c.Layer.Name()) + } + + if err != nil { + logrus.Error("unable to toggle view: ", err) + return err + } + + return c.UpdateAndRender() +} + +func (c *ControllerCollection) ToggleFilterView() error { + // delete all user input from the tree view + err := c.Filter.ToggleVisible() + if err != nil { + logrus.Error("unable to toggle filter visibility: ", err) + return err + } + + // we have just hidden the filter view, adjust focus to a valid (visible) view + if !c.Filter.IsVisible() { + err = c.ToggleView() + if err != nil { + logrus.Error("unable to toggle filter view (back): ", err) + return err + } + } + + return c.UpdateAndRender() +} + +// CursorDown moves the cursor down in the currently selected gocui pane, scrolling the screen as needed. +func (c *ControllerCollection) CursorDown(g *gocui.Gui, v *gocui.View) error { + return c.CursorStep(g, v, 1) +} + +// CursorUp moves the cursor up in the currently selected gocui pane, scrolling the screen as needed. +func (c *ControllerCollection) CursorUp(g *gocui.Gui, v *gocui.View) error { + return c.CursorStep(g, v, -1) +} + +// Moves the cursor the given step distance, setting the origin to the new cursor line +func (c *ControllerCollection) CursorStep(g *gocui.Gui, v *gocui.View, step int) error { + cx, cy := v.Cursor() + + // if there isn't a next line + line, err := v.Line(cy + step) + if err != nil { + return err + } + if len(line) == 0 { + return errors.New("unable to move the cursor, empty line") + } + if err := v.SetCursor(cx, cy+step); err != nil { + ox, oy := v.Origin() + if err := v.SetOrigin(ox, oy+step); err != nil { + return err + } + } + return nil +} diff --git a/runtime/ui/details_controller.go b/runtime/ui/controller/details_controller.go similarity index 84% rename from runtime/ui/details_controller.go rename to runtime/ui/controller/details_controller.go index ed01836..d3eccbc 100644 --- a/runtime/ui/details_controller.go +++ b/runtime/ui/controller/details_controller.go @@ -1,4 +1,4 @@ -package ui +package controller import ( "fmt" @@ -14,9 +14,9 @@ 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 { +type DetailsController struct { name string gui *gocui.Gui view *gocui.View @@ -25,9 +25,9 @@ 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 @@ -38,8 +38,12 @@ func newDetailsController(name string, gui *gocui.Gui, efficiency float64, ineff return controller } +func (controller *DetailsController) Name() string { + return controller.name +} + // 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 @@ -75,22 +79,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 { - return CursorDown(controller.gui, controller.view) +func (controller *DetailsController) CursorDown() error { + return controllers.CursorDown(controller.gui, controller.view) } // CursorUp moves the cursor up in the details pane (currently indicates nothing). -func (controller *detailsController) CursorUp() error { - return CursorUp(controller.gui, controller.view) +func (controller *DetailsController) CursorUp() error { + return controllers.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 } @@ -99,7 +103,7 @@ 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 { +func (controller *DetailsController) Render() error { currentLayer := controllers.Layer.currentLayer() var wastedSpace int64 @@ -168,6 +172,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/controller/filetree_controller.go similarity index 75% rename from runtime/ui/filetree_controller.go rename to runtime/ui/controller/filetree_controller.go index 250289a..5dc27db 100644 --- a/runtime/ui/filetree_controller.go +++ b/runtime/ui/controller/filetree_controller.go @@ -1,9 +1,10 @@ -package ui +package controller import ( "fmt" "github.com/wagoodman/dive/runtime/ui/format" "github.com/wagoodman/dive/runtime/ui/key" + "github.com/wagoodman/dive/runtime/ui/viewmodel" "regexp" "strings" @@ -19,36 +20,43 @@ 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 { +type FileTreeController struct { name string gui *gocui.Gui view *gocui.View header *gocui.View - vm *fileTreeViewModel + vm *viewmodel.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.gui = gui - controller.vm, err = newFileTreeViewModel(tree, refTrees, cache) + controller.vm, err = viewmodel.NewFileTreeViewModel(tree, refTrees, cache) if err != nil { return nil, err } - return controller, err } +func (controller *FileTreeController) Name() string { + return controller.name +} + +func (controller *FileTreeController) AreAttributesVisible() bool { + return controller.vm.ShowAttributes +} + // 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 @@ -147,23 +155,23 @@ 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() { +// ResetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer. +func (controller *FileTreeController) resetCursor() { _ = controller.view.SetCursor(0, 0) - controller.vm.resetCursor() + 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 { - err := controller.vm.setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop) +// SetTreeByLayer populates the view model by stacking the indicated image layer file trees. +func (controller *FileTreeController) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error { + err := controller.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop) if err != nil { return err } - // controller.resetCursor() + // controller.ResetCursor() _ = controller.Update() return controller.Render() @@ -173,7 +181,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() } @@ -184,7 +192,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() } @@ -192,7 +200,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 @@ -202,7 +210,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 @@ -212,7 +220,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 @@ -221,7 +229,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 @@ -230,13 +238,13 @@ 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 { - err := controller.vm.toggleCollapse(filterRegex()) +// ToggleCollapse will collapse/expand the selected FileNode. +func (controller *FileTreeController) toggleCollapse() error { + err := controller.vm.ToggleCollapse(filterRegex()) if err != nil { return err } @@ -244,9 +252,9 @@ func (controller *fileTreeController) toggleCollapse() error { return controller.Render() } -// toggleCollapseAll will collapse/expand the all directories. -func (controller *fileTreeController) toggleCollapseAll() error { - err := controller.vm.toggleCollapseAll() +// ToggleCollapseAll will collapse/expand the all directories. +func (controller *FileTreeController) toggleCollapseAll() error { + err := controller.vm.ToggleCollapseAll() if err != nil { return err } @@ -257,21 +265,21 @@ func (controller *fileTreeController) toggleCollapseAll() error { return controller.Render() } -// toggleAttributes will show/hide file attributes -func (controller *fileTreeController) toggleAttributes() error { - err := controller.vm.toggleAttributes() +// ToggleAttributes will show/hide file attributes +func (controller *FileTreeController) toggleAttributes() error { + err := controller.vm.ToggleAttributes() if err != nil { return err } // we need to render the changes to the status pane as well (not just this contoller/view) - return UpdateAndRender() + return controllers.UpdateAndRender() } -// toggleShowDiffType will show/hide the selected DiffType in the filetree pane. -func (controller *fileTreeController) toggleShowDiffType(diffType filetree.DiffType) error { - controller.vm.toggleShowDiffType(diffType) +// ToggleShowDiffType will show/hide the selected DiffType in the filetree pane. +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() + return controllers.UpdateAndRender() } // filterRegex will return a regular expression object to match the user's filter input. @@ -292,8 +300,8 @@ func filterRegex() *regexp.Regexp { return regex } -// onLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions -func (controller *fileTreeController) onLayoutChange(resized bool) error { +// OnLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions +func (controller *FileTreeController) OnLayoutChange(resized bool) error { _ = controller.Update() if resized { return controller.Render() @@ -302,7 +310,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 { @@ -316,7 +324,7 @@ 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 { title = "Aggregated Layer Contents" @@ -344,7 +352,7 @@ func (controller *fileTreeController) Render() error { if err != nil { return err } - _, err = fmt.Fprint(controller.view, controller.vm.mainBuf.String()) + _, err = fmt.Fprint(controller.view, controller.vm.Buffer.String()) return err }) @@ -352,7 +360,7 @@ 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 { +func (controller *FileTreeController) KeyHelp() string { var help string for _, binding := range controller.helpKeys { help += binding.RenderKeyHelp() diff --git a/runtime/ui/filter_controller.go b/runtime/ui/controller/filter_controller.go similarity index 61% rename from runtime/ui/filter_controller.go rename to runtime/ui/controller/filter_controller.go index e01cead..2ff718d 100644 --- a/runtime/ui/filter_controller.go +++ b/runtime/ui/controller/filter_controller.go @@ -1,4 +1,4 @@ -package ui +package controller import ( "fmt" @@ -7,9 +7,9 @@ import ( "github.com/wagoodman/dive/runtime/ui/format" ) -// filterController holds the UI objects and data models for populating the bottom row. Specifically the pane that +// FilterController holds the UI objects and data models for populating the bottom row. Specifically the pane that // allows the user to filter the file tree by path. -type filterController struct { +type FilterController struct { name string gui *gocui.Gui view *gocui.View @@ -19,9 +19,9 @@ type filterController struct { hidden bool } -// newFilterController creates a new view object attached the the global [gocui] screen object. -func newFilterController(name string, gui *gocui.Gui) (controller *filterController) { - controller = new(filterController) +// NewFilterController creates a new view object attached the the global [gocui] screen object. +func NewFilterController(name string, gui *gocui.Gui) (controller *FilterController) { + controller = new(FilterController) // populate main fields controller.name = name @@ -32,8 +32,12 @@ func newFilterController(name string, gui *gocui.Gui) (controller *filterControl return controller } +func (controller *FilterController) Name() string { + return controller.name +} + // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (controller *filterController) Setup(v *gocui.View, header *gocui.View) error { +func (controller *FilterController) Setup(v *gocui.View, header *gocui.View) error { // set controller options controller.view = v @@ -52,8 +56,36 @@ func (controller *filterController) Setup(v *gocui.View, header *gocui.View) err return controller.Render() } +// ToggleFilterView shows/hides the file tree filter pane. +func (controller *FilterController) ToggleVisible() error { + // delete all user input from the tree view + controller.view.Clear() + + // toggle hiding + controller.hidden = !controller.hidden + + if !controller.hidden { + _, err := controller.gui.SetCurrentView(controller.name) + if err != nil { + logrus.Error("unable to toggle filter view: ", err) + return err + } + return controllers.UpdateAndRender() + } + + // reset the cursor for the next time it is visible + // Note: there is a subtle gocui behavior here where this cannot be called when the view + // is newly visible. Is this a problem with dive or gocui? + return controller.view.SetCursor(0, 0) +} + +// todo: remove the need for this +func (controller *FilterController) HeaderStr() string { + return controller.headerStr +} + // IsVisible indicates if the filter view pane is currently initialized -func (controller *filterController) IsVisible() bool { +func (controller *FilterController) IsVisible() bool { if controller == nil { return false } @@ -61,17 +93,17 @@ func (controller *filterController) IsVisible() bool { } // CursorDown moves the cursor down in the filter pane (currently indicates nothing). -func (controller *filterController) CursorDown() error { +func (controller *FilterController) CursorDown() error { return nil } // CursorUp moves the cursor up in the filter pane (currently indicates nothing). -func (controller *filterController) CursorUp() error { +func (controller *FilterController) CursorUp() error { return nil } // Edit intercepts the key press events in the filer view to update the file view in real time. -func (controller *filterController) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { +func (controller *FilterController) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { if !controller.IsVisible() { return } @@ -94,12 +126,12 @@ func (controller *filterController) Edit(v *gocui.View, key gocui.Key, ch rune, } // Update refreshes the state objects for future rendering (currently does nothing). -func (controller *filterController) Update() error { +func (controller *FilterController) Update() error { return nil } // Render flushes the state objects to the screen. Currently this is the users path filter input. -func (controller *filterController) Render() error { +func (controller *FilterController) Render() error { controller.gui.Update(func(g *gocui.Gui) error { _, err := fmt.Fprintln(controller.header, format.Header(controller.headerStr)) if err != nil { @@ -111,6 +143,6 @@ func (controller *filterController) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (controller *filterController) KeyHelp() string { +func (controller *FilterController) KeyHelp() string { return format.StatusControlNormal("▏Type to filter the file tree ") } diff --git a/runtime/ui/layer_controller.go b/runtime/ui/controller/layer_controller.go similarity index 81% rename from runtime/ui/layer_controller.go rename to runtime/ui/controller/layer_controller.go index a14897f..a0bf0c0 100644 --- a/runtime/ui/layer_controller.go +++ b/runtime/ui/controller/layer_controller.go @@ -1,4 +1,4 @@ -package ui +package controller import ( "fmt" @@ -13,9 +13,9 @@ import ( "github.com/spf13/viper" ) -// layerController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that +// LayerController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that // shows the image layers and layer selector. -type layerController struct { +type LayerController struct { name string gui *gocui.Gui view *gocui.View @@ -29,9 +29,9 @@ type layerController struct { helpKeys []*key.Binding } -// newLayerController creates a new view object attached the the global [gocui] screen object. -func newLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (controller *layerController, err error) { - controller = new(layerController) +// NewLayerController creates a new view object attached the the global [gocui] screen object. +func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (controller *LayerController, err error) { + controller = new(LayerController) // populate main fields controller.name = name @@ -50,8 +50,12 @@ func newLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (con return controller, err } +func (controller *LayerController) Name() string { + return controller.name +} + // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (controller *layerController) Setup(v *gocui.View, header *gocui.View) error { +func (controller *LayerController) Setup(v *gocui.View, header *gocui.View) error { // set controller options controller.view = v @@ -64,17 +68,16 @@ func (controller *layerController) Setup(v *gocui.View, header *gocui.View) erro controller.header.Wrap = false controller.header.Frame = false - var infos = []key.BindingInfo{ { ConfigKeys: []string{"keybinding.compare-layer"}, - OnAction: func() error { return controller.setCompareMode(CompareLayer) }, + OnAction: func() error { return controller.setCompareMode(CompareLayer) }, IsSelected: func() bool { return controller.CompareMode == CompareLayer }, Display: "Show layer changes", }, { ConfigKeys: []string{"keybinding.compare-all"}, - OnAction: func() error { return controller.setCompareMode(CompareAll) }, + OnAction: func() error { return controller.setCompareMode(CompareAll) }, IsSelected: func() bool { return controller.CompareMode == CompareAll }, Display: "Show aggregated changes", }, @@ -114,23 +117,22 @@ func (controller *layerController) Setup(v *gocui.View, header *gocui.View) erro } controller.helpKeys = helpKeys - return controller.Render() } // height obtains the height of the current pane (taking into account the lost space due to the header). -func (controller *layerController) height() uint { +func (controller *LayerController) height() uint { _, height := controller.view.Size() return uint(height - 1) } // IsVisible indicates if the layer view pane is currently initialized. -func (controller *layerController) IsVisible() bool { +func (controller *LayerController) IsVisible() bool { return controller != nil } // PageDown moves to next page putting the cursor on top -func (controller *layerController) PageDown() error { +func (controller *LayerController) PageDown() error { step := int(controller.height()) + 1 targetLayerIndex := controller.LayerIndex + step @@ -139,7 +141,7 @@ func (controller *layerController) PageDown() error { } if step > 0 { - err := CursorStep(controller.gui, controller.view, step) + err := controllers.CursorStep(controller.gui, controller.view, step) if err == nil { return controller.SetCursor(controller.LayerIndex + step) } @@ -148,7 +150,7 @@ func (controller *layerController) PageDown() error { } // PageUp moves to previous page putting the cursor on top -func (controller *layerController) PageUp() error { +func (controller *LayerController) PageUp() error { step := int(controller.height()) + 1 targetLayerIndex := controller.LayerIndex - step @@ -157,7 +159,7 @@ func (controller *layerController) PageUp() error { } if step > 0 { - err := CursorStep(controller.gui, controller.view, -step) + err := controllers.CursorStep(controller.gui, controller.view, -step) if err == nil { return controller.SetCursor(controller.LayerIndex - step) } @@ -166,9 +168,9 @@ func (controller *layerController) PageUp() error { } // CursorDown moves the cursor down in the layer pane (selecting a higher layer). -func (controller *layerController) CursorDown() error { +func (controller *LayerController) CursorDown() error { if controller.LayerIndex < len(controller.Layers) { - err := CursorDown(controller.gui, controller.view) + err := controllers.CursorDown(controller.gui, controller.view) if err == nil { return controller.SetCursor(controller.LayerIndex + 1) } @@ -177,9 +179,9 @@ func (controller *layerController) CursorDown() error { } // CursorUp moves the cursor up in the layer pane (selecting a lower layer). -func (controller *layerController) CursorUp() error { +func (controller *LayerController) CursorUp() error { if controller.LayerIndex > 0 { - err := CursorUp(controller.gui, controller.view) + err := controllers.CursorUp(controller.gui, controller.view) if err == nil { return controller.SetCursor(controller.LayerIndex - 1) } @@ -188,7 +190,7 @@ func (controller *layerController) CursorUp() error { } // SetCursor resets the cursor and orients the file tree view based on the given layer index. -func (controller *layerController) SetCursor(layer int) error { +func (controller *LayerController) SetCursor(layer int) error { controller.LayerIndex = layer err := controllers.Tree.setTreeByLayer(controller.getCompareIndexes()) if err != nil { @@ -201,14 +203,14 @@ func (controller *layerController) SetCursor(layer int) error { } // currentLayer returns the Layer object currently selected. -func (controller *layerController) currentLayer() *image.Layer { +func (controller *LayerController) currentLayer() *image.Layer { return controller.Layers[controller.LayerIndex] } // setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison. -func (controller *layerController) setCompareMode(compareMode CompareType) error { +func (controller *LayerController) setCompareMode(compareMode CompareType) error { controller.CompareMode = compareMode - err := UpdateAndRender() + err := controllers.UpdateAndRender() if err != nil { logrus.Errorf("unable to set compare mode: %+v", err) return err @@ -217,7 +219,7 @@ func (controller *layerController) setCompareMode(compareMode CompareType) error } // getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode) -func (controller *layerController) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { +func (controller *LayerController) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { bottomTreeStart = controller.CompareStartIndex topTreeStop = controller.LayerIndex @@ -236,7 +238,7 @@ func (controller *layerController) getCompareIndexes() (bottomTreeStart, bottomT } // renderCompareBar returns the formatted string for the given layer. -func (controller *layerController) renderCompareBar(layerIdx int) string { +func (controller *LayerController) renderCompareBar(layerIdx int) string { bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := controller.getCompareIndexes() result := " " @@ -251,7 +253,7 @@ func (controller *layerController) renderCompareBar(layerIdx int) string { } // Update refreshes the state objects for future rendering (currently does nothing). -func (controller *layerController) Update() error { +func (controller *LayerController) Update() error { controller.ImageSize = 0 for idx := 0; idx < len(controller.Layers); idx++ { controller.ImageSize += controller.Layers[idx].Size @@ -262,7 +264,7 @@ func (controller *layerController) Update() error { // Render flushes the state objects to the screen. The layers pane reports: // 1. the layers of the image + metadata // 2. the current selected image -func (controller *layerController) Render() error { +func (controller *LayerController) Render() error { // indicate when selected title := "Layers" @@ -306,7 +308,7 @@ func (controller *layerController) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (controller *layerController) KeyHelp() string { +func (controller *LayerController) KeyHelp() string { var help string for _, binding := range controller.helpKeys { help += binding.RenderKeyHelp() diff --git a/runtime/ui/status_controller.go b/runtime/ui/controller/status_controller.go similarity index 62% rename from runtime/ui/status_controller.go rename to runtime/ui/controller/status_controller.go index 31f7ce9..7de9a30 100644 --- a/runtime/ui/status_controller.go +++ b/runtime/ui/controller/status_controller.go @@ -1,4 +1,4 @@ -package ui +package controller import ( "fmt" @@ -10,9 +10,9 @@ import ( "github.com/jroimartin/gocui" ) -// statusController holds the UI objects and data models for populating the bottom-most pane. Specifically the panel +// StatusController holds the UI objects and data models for populating the bottom-most pane. Specifically the panel // shows the user a set of possible actions to take in the window and currently selected pane. -type statusController struct { +type StatusController struct { name string gui *gocui.Gui view *gocui.View @@ -20,9 +20,9 @@ type statusController struct { helpKeys []*key.Binding } -// newStatusController creates a new view object attached the the global [gocui] screen object. -func newStatusController(name string, gui *gocui.Gui) (controller *statusController) { - controller = new(statusController) +// NewStatusController creates a new view object attached the the global [gocui] screen object. +func NewStatusController(name string, gui *gocui.Gui) (controller *StatusController) { + controller = new(StatusController) // populate main fields controller.name = name @@ -32,14 +32,16 @@ func newStatusController(name string, gui *gocui.Gui) (controller *statusControl return controller } -func (controller *statusController) AddHelpKeys(keys ...*key.Binding) { - for _, k := range keys { - controller.helpKeys = append(controller.helpKeys, k) - } +func (controller *StatusController) Name() string { + return controller.name +} + +func (controller *StatusController) AddHelpKeys(keys ...*key.Binding) { + controller.helpKeys = append(controller.helpKeys, keys...) } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (controller *statusController) Setup(v *gocui.View, header *gocui.View) error { +func (controller *StatusController) Setup(v *gocui.View, header *gocui.View) error { // set controller options controller.view = v @@ -49,30 +51,30 @@ func (controller *statusController) Setup(v *gocui.View, header *gocui.View) err } // IsVisible indicates if the status view pane is currently initialized. -func (controller *statusController) IsVisible() bool { +func (controller *StatusController) IsVisible() bool { return controller != nil } // CursorDown moves the cursor down in the details pane (currently indicates nothing). -func (controller *statusController) CursorDown() error { +func (controller *StatusController) CursorDown() error { return nil } // CursorUp moves the cursor up in the details pane (currently indicates nothing). -func (controller *statusController) CursorUp() error { +func (controller *StatusController) CursorUp() error { return nil } // Update refreshes the state objects for future rendering (currently does nothing). -func (controller *statusController) Update() error { +func (controller *StatusController) Update() error { return nil } // Render flushes the state objects to the screen. -func (controller *statusController) Render() error { +func (controller *StatusController) Render() error { controller.gui.Update(func(g *gocui.Gui) error { controller.view.Clear() - _, err := fmt.Fprintln(controller.view, controller.KeyHelp()+controllers.lookup[controller.gui.CurrentView().Name()].KeyHelp()+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) + _, err := fmt.Fprintln(controller.view, controller.KeyHelp()+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) if err != nil { logrus.Debug("unable to write to buffer: ", err) } @@ -83,7 +85,7 @@ func (controller *statusController) Render() error { } // KeyHelp indicates all the possible global actions a user can take when any pane is selected. -func (controller *statusController) KeyHelp() string { +func (controller *StatusController) KeyHelp() string { var help string for _, binding := range controller.helpKeys { help += binding.RenderKeyHelp() diff --git a/runtime/ui/controller_collection.go b/runtime/ui/controller_collection.go deleted file mode 100644 index 47be653..0000000 --- a/runtime/ui/controller_collection.go +++ /dev/null @@ -1,52 +0,0 @@ -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/key/binding.go b/runtime/ui/key/binding.go index d7a75af..777c047 100644 --- a/runtime/ui/key/binding.go +++ b/runtime/ui/key/binding.go @@ -97,7 +97,6 @@ func (binding *Binding) RegisterSelectionFn(selectedFn func() bool) { } func (binding *Binding) onAction(*gocui.Gui, *gocui.View) error { - logrus.Debugf("keybinding invoked: %+v", binding) if binding.actionFn == nil { return fmt.Errorf("no action configured for '%+v'", binding) } diff --git a/runtime/ui/layout_manager.go b/runtime/ui/layout_manager.go index 4672ac5..561f683 100644 --- a/runtime/ui/layout_manager.go +++ b/runtime/ui/layout_manager.go @@ -1,11 +1,169 @@ package ui +import ( + "github.com/jroimartin/gocui" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "github.com/wagoodman/dive/runtime/ui/controller" +) + type layoutManager struct { fileTreeSplitRatio float64 + controllers *controller.ControllerCollection } -func newLayoutManager(fileTreeSplitRatio float64) *layoutManager { +func newLayoutManager(c *controller.ControllerCollection) *layoutManager { + + fileTreeSplitRatio := viper.GetFloat64("filetree.pane-width") + if fileTreeSplitRatio >= 1 || fileTreeSplitRatio <= 0 { + logrus.Errorf("invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'", fileTreeSplitRatio) + fileTreeSplitRatio = 0.5 + } + return &layoutManager{ fileTreeSplitRatio: fileTreeSplitRatio, + controllers: c, } } + +// IsNewView determines if a view has already been created based on the set of errors given (a bit hokie) +func IsNewView(errs ...error) bool { + for _, err := range errs { + if err == nil { + return false + } + if err != gocui.ErrUnknownView { + return false + } + } + return true +} + +// layout defines the definition of the window pane size and placement relations to one another. This +// is invoked at application start and whenever the screen dimensions change. +func (lm *layoutManager) layout(g *gocui.Gui) error { + // TODO: this logic should be refactored into an abstraction that takes care of the math for us + + maxX, maxY := g.Size() + var resized bool + if maxX != lastX { + resized = true + } + if maxY != lastY { + resized = true + } + lastX, lastY = maxX, maxY + + splitCols := int(float64(maxX) * (1.0 - lm.fileTreeSplitRatio)) + debugWidth := 0 + if debug { + debugWidth = maxX / 4 + } + debugCols := maxX - debugWidth + bottomRows := 1 + headerRows := 2 + + filterBarHeight := 1 + statusBarHeight := 1 + + statusBarIndex := 1 + filterBarIndex := 2 + + layersHeight := len(lm.controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row + maxLayerHeight := int(0.75 * float64(maxY)) + if layersHeight > maxLayerHeight { + layersHeight = maxLayerHeight + } + + var view, header *gocui.View + var viewErr, headerErr, err error + + if !lm.controllers.Filter.IsVisible() { + bottomRows-- + filterBarHeight = 0 + } + + // Debug pane + if debug { + if _, err := g.SetView("debug", debugCols, -1, maxX, maxY-bottomRows); err != nil { + if err != gocui.ErrUnknownView { + return err + } + } + } + + // Layers + view, viewErr = g.SetView(lm.controllers.Layer.Name(), -1, -1+headerRows, splitCols, layersHeight) + header, headerErr = g.SetView(lm.controllers.Layer.Name()+"header", -1, -1, splitCols, headerRows) + if IsNewView(viewErr, headerErr) { + err = lm.controllers.Layer.Setup(view, header) + if err != nil { + logrus.Error("unable to setup layer controller", err) + return err + } + + if _, err = g.SetCurrentView(lm.controllers.Layer.Name()); err != nil { + logrus.Error("unable to set view to layer", err) + return err + } + // since we are selecting the view, we should rerender to indicate it is selected + err = lm.controllers.Layer.Render() + if err != nil { + logrus.Error("unable to render layer view", err) + return err + } + } + + // Details + view, viewErr = g.SetView(lm.controllers.Details.Name(), -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows) + header, headerErr = g.SetView(lm.controllers.Details.Name()+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows) + if IsNewView(viewErr, headerErr) { + err = lm.controllers.Details.Setup(view, header) + if err != nil { + return err + } + } + + // Filetree + offset := 0 + if !lm.controllers.Tree.AreAttributesVisible() { + offset = 1 + } + view, viewErr = g.SetView(lm.controllers.Tree.Name(), splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows) + header, headerErr = g.SetView(lm.controllers.Tree.Name()+"header", splitCols, -1, debugCols, headerRows-offset) + if IsNewView(viewErr, headerErr) { + err = lm.controllers.Tree.Setup(view, header) + if err != nil { + logrus.Error("unable to setup tree controller", err) + return err + } + } + err = lm.controllers.Tree.OnLayoutChange(resized) + if err != nil { + logrus.Error("unable to setup layer controller onLayoutChange", err) + return err + } + + // Status Bar + view, viewErr = g.SetView(lm.controllers.Status.Name(), -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1)) + if IsNewView(viewErr, headerErr) { + err = lm.controllers.Status.Setup(view, nil) + if err != nil { + logrus.Error("unable to setup status controller", err) + return err + } + } + + // Filter Bar + view, viewErr = g.SetView(lm.controllers.Filter.Name(), len(lm.controllers.Filter.HeaderStr())-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1)) + header, headerErr = g.SetView(lm.controllers.Filter.Name()+"header", -1, maxY-filterBarHeight-filterBarIndex, len(lm.controllers.Filter.HeaderStr()), maxY-(filterBarIndex-1)) + if IsNewView(viewErr, headerErr) { + err = lm.controllers.Filter.Setup(view, header) + if err != nil { + logrus.Error("unable to setup filter controller", err) + return err + } + } + + return nil +} diff --git a/runtime/ui/ui.go b/runtime/ui/ui.go index 3859c35..b4fa834 100644 --- a/runtime/ui/ui.go +++ b/runtime/ui/ui.go @@ -1,14 +1,13 @@ package ui import ( - "errors" "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/runtime/ui/controller" "github.com/wagoodman/dive/runtime/ui/key" "sync" "github.com/jroimartin/gocui" "github.com/sirupsen/logrus" - "github.com/spf13/viper" "github.com/wagoodman/dive/dive/filetree" ) @@ -16,8 +15,9 @@ const debug = false // type global type app struct { - gui *gocui.Gui - controllers *controllerCollection + gui *gocui.Gui + controllers *controller.ControllerCollection + layout *layoutManager } var ( @@ -28,17 +28,19 @@ var ( func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*app, error) { var err error once.Do(func() { - var theControls *controllerCollection + var theControls *controller.ControllerCollection var globalHelpKeys []*key.Binding - theControls, err = newControllerCollection(gui, analysis, cache) + theControls, err = controller.NewControllerCollection(gui, analysis, cache) if err != nil { return } + lm := newLayoutManager(theControls) + gui.Cursor = false //g.Mouse = true - gui.SetManagerFunc(layout) + gui.SetManagerFunc(lm.layout) // var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook) // @@ -46,27 +48,27 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC // profileObj.Stop() // } - appSingleton = &app{ gui: gui, controllers: theControls, + layout: lm, } var infos = []key.BindingInfo{ { ConfigKeys: []string{"keybinding.quit"}, - OnAction: quit, + OnAction: appSingleton.quit, Display: "Quit", }, { ConfigKeys: []string{"keybinding.toggle-view"}, - OnAction: appSingleton.toggleView, + OnAction: theControls.ToggleView, Display: "Switch view", }, { ConfigKeys: []string{"keybinding.filter-files"}, - OnAction: appSingleton.toggleFilterView, - IsSelected: controllers.Filter.IsVisible, + OnAction: theControls.ToggleFilterView, + IsSelected: theControls.Filter.IsVisible, Display: "Filter", }, } @@ -78,14 +80,12 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC theControls.Status.AddHelpKeys(globalHelpKeys...) - // perform the first update and render now that all resources have been loaded - err = UpdateAndRender() + err = theControls.UpdateAndRender() if err != nil { return } - }) return appSingleton, err @@ -109,103 +109,8 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC var lastX, lastY int -func UpdateAndRender() error { - err := Update() - if err != nil { - logrus.Debug("failed update: ", err) - return err - } - - err = Render() - if err != nil { - logrus.Debug("failed render: ", err) - return err - } - - return nil -} - -// toggleView switches between the file view and the layer view and re-renders the screen. -func (ui *app) toggleView() (err error) { - v := ui.gui.CurrentView() - if v == nil || v.Name() == controllers.Layer.name { - _, err = ui.gui.SetCurrentView(controllers.Tree.name) - } else { - _, err = ui.gui.SetCurrentView(controllers.Layer.name) - } - - if err != nil { - logrus.Error("unable to toggle view: ", err) - return err - } - - return UpdateAndRender() -} - -// toggleFilterView shows/hides the file tree filter pane. -func (ui *app) toggleFilterView() error { - // delete all user input from the tree view - controllers.Filter.view.Clear() - - // toggle hiding - controllers.Filter.hidden = !controllers.Filter.hidden - - if !controllers.Filter.hidden { - _, err := ui.gui.SetCurrentView(controllers.Filter.name) - if err != nil { - logrus.Error("unable to toggle filter view: ", err) - return err - } - return UpdateAndRender() - } - - err := ui.toggleView() - if err != nil { - logrus.Error("unable to toggle filter view (back): ", err) - return err - } - - err = controllers.Filter.view.SetCursor(0, 0) - if err != nil { - return err - } - - return nil -} - -// CursorDown moves the cursor down in the currently selected gocui pane, scrolling the screen as needed. -func CursorDown(g *gocui.Gui, v *gocui.View) error { - return CursorStep(g, v, 1) -} - -// CursorUp moves the cursor up in the currently selected gocui pane, scrolling the screen as needed. -func CursorUp(g *gocui.Gui, v *gocui.View) error { - return CursorStep(g, v, -1) -} - -// Moves the cursor the given step distance, setting the origin to the new cursor line -func CursorStep(g *gocui.Gui, v *gocui.View, step int) error { - cx, cy := v.Cursor() - - // if there isn't a next line - line, err := v.Line(cy + step) - if err != nil { - return err - } - if len(line) == 0 { - return errors.New("unable to move the cursor, empty line") - } - if err := v.SetCursor(cx, cy+step); err != nil { - ox, oy := v.Origin() - if err := v.SetOrigin(ox, oy+step); err != nil { - return err - } - } - return nil -} - // quit is the gocui callback invoked when the user hits Ctrl+C -func quit() error { +func (ui *app) quit() error { // profileObj.Stop() // onExit() @@ -213,177 +118,6 @@ func quit() error { return gocui.ErrQuit } -// isNewView determines if a view has already been created based on the set of errors given (a bit hokie) -func isNewView(errs ...error) bool { - for _, err := range errs { - if err == nil { - return false - } - if err != gocui.ErrUnknownView { - return false - } - } - return true -} - -// layout defines the definition of the window pane size and placement relations to one another. This -// is invoked at application start and whenever the screen dimensions change. -func layout(g *gocui.Gui) error { - // TODO: this logic should be refactored into an abstraction that takes care of the math for us - - maxX, maxY := g.Size() - var resized bool - if maxX != lastX { - resized = true - } - if maxY != lastY { - resized = true - } - lastX, lastY = maxX, maxY - fileTreeSplitRatio := viper.GetFloat64("filetree.pane-width") - if fileTreeSplitRatio >= 1 || fileTreeSplitRatio <= 0 { - logrus.Errorf("invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'", fileTreeSplitRatio) - fileTreeSplitRatio = 0.5 - } - splitCols := int(float64(maxX) * (1.0 - fileTreeSplitRatio)) - debugWidth := 0 - if debug { - debugWidth = maxX / 4 - } - debugCols := maxX - debugWidth - bottomRows := 1 - headerRows := 2 - - filterBarHeight := 1 - statusBarHeight := 1 - - statusBarIndex := 1 - filterBarIndex := 2 - - layersHeight := len(controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row - maxLayerHeight := int(0.75 * float64(maxY)) - if layersHeight > maxLayerHeight { - layersHeight = maxLayerHeight - } - - var view, header *gocui.View - var viewErr, headerErr, err error - - if controllers.Filter.hidden { - bottomRows-- - filterBarHeight = 0 - } - - // Debug pane - if debug { - if _, err := g.SetView("debug", debugCols, -1, maxX, maxY-bottomRows); err != nil { - if err != gocui.ErrUnknownView { - return err - } - } - } - - // Layers - view, viewErr = g.SetView(controllers.Layer.name, -1, -1+headerRows, splitCols, layersHeight) - header, headerErr = g.SetView(controllers.Layer.name+"header", -1, -1, splitCols, headerRows) - if isNewView(viewErr, headerErr) { - err = controllers.Layer.Setup(view, header) - if err != nil { - logrus.Error("unable to setup layer controller", err) - return err - } - - if _, err = g.SetCurrentView(controllers.Layer.name); err != nil { - logrus.Error("unable to set view to layer", err) - return err - } - // since we are selecting the view, we should rerender to indicate it is selected - err = controllers.Layer.Render() - if err != nil { - logrus.Error("unable to render layer view", err) - return err - } - } - - // Details - view, viewErr = g.SetView(controllers.Details.name, -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows) - header, headerErr = g.SetView(controllers.Details.name+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows) - if isNewView(viewErr, headerErr) { - err = controllers.Details.Setup(view, header) - if err != nil { - return err - } - } - - // Filetree - offset := 0 - if !controllers.Tree.vm.ShowAttributes { - offset = 1 - } - view, viewErr = g.SetView(controllers.Tree.name, splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows) - header, headerErr = g.SetView(controllers.Tree.name+"header", splitCols, -1, debugCols, headerRows-offset) - if isNewView(viewErr, headerErr) { - err = controllers.Tree.Setup(view, header) - if err != nil { - logrus.Error("unable to setup tree controller", err) - return err - } - } - err = controllers.Tree.onLayoutChange(resized) - if err != nil { - logrus.Error("unable to setup layer controller onLayoutChange", err) - return err - } - - // Status Bar - view, viewErr = g.SetView(controllers.Status.name, -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1)) - if isNewView(viewErr, headerErr) { - err = controllers.Status.Setup(view, nil) - if err != nil { - logrus.Error("unable to setup status controller", err) - return err - } - } - - // Filter Bar - view, viewErr = g.SetView(controllers.Filter.name, len(controllers.Filter.headerStr)-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1)) - header, headerErr = g.SetView(controllers.Filter.name+"header", -1, maxY-filterBarHeight-filterBarIndex, len(controllers.Filter.headerStr), maxY-(filterBarIndex-1)) - if isNewView(viewErr, headerErr) { - err = controllers.Filter.Setup(view, header) - if err != nil { - logrus.Error("unable to setup filter controller", err) - return err - } - } - - return nil -} - -// Update refreshes the state objects for future rendering. -func Update() error { - for _, controller := range controllers.lookup { - err := controller.Update() - if err != nil { - logrus.Debug("unable to update controller: ") - return err - } - } - return nil -} - -// Render flushes the state objects to the screen. -func Render() error { - for _, controller := range controllers.lookup { - if controller.IsVisible() { - err := controller.Render() - if err != nil { - return err - } - } - } - return nil -} - // Run is the UI entrypoint. func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error { var err error diff --git a/runtime/ui/filetree_viewmodel.go b/runtime/ui/viewmodel/filetree_viewmodel.go similarity index 83% rename from runtime/ui/filetree_viewmodel.go rename to runtime/ui/viewmodel/filetree_viewmodel.go index 5a640e8..cf467e6 100644 --- a/runtime/ui/filetree_viewmodel.go +++ b/runtime/ui/viewmodel/filetree_viewmodel.go @@ -1,4 +1,4 @@ -package ui +package viewmodel import ( "bytes" @@ -13,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 @@ -31,12 +31,12 @@ type fileTreeViewModel struct { refHeight int refWidth int - mainBuf bytes.Buffer + Buffer bytes.Buffer } -// 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) +// 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") @@ -66,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 } @@ -80,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() { +// ResetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer. +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 { +// SetTreeByLayer populates the view model by stacking the indicated image layer file trees. +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) } @@ -126,7 +126,7 @@ func (vm *fileTreeViewModel) setTreeByLayer(bottomTreeStart, bottomTreeStop, top } // doCursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer. -func (vm *fileTreeViewModel) CursorUp() bool { +func (vm *FileTreeViewModel) CursorUp() bool { if vm.TreeIndex <= 0 { return false } @@ -141,7 +141,7 @@ func (vm *fileTreeViewModel) CursorUp() bool { } // doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer. -func (vm *fileTreeViewModel) CursorDown() bool { +func (vm *FileTreeViewModel) CursorDown() bool { if vm.TreeIndex >= vm.ModelTree.VisibleSize() { return false } @@ -157,7 +157,7 @@ func (vm *fileTreeViewModel) CursorDown() bool { } // CursorLeft moves the cursor up until we reach the Parent Node or top of the tree -func (vm *fileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error { +func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error { var visitor func(*filetree.FileNode) error var evaluator func(*filetree.FileNode) bool var dfsCounter, newIndex int @@ -208,7 +208,7 @@ func (vm *fileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error { } // CursorRight descends into directory expanding it if needed -func (vm *fileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error { +func (vm *FileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error { node := vm.getAbsPositionNode(filterRegex) if node == nil { return nil @@ -240,7 +240,7 @@ func (vm *fileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error { } // PageDown moves to next page putting the cursor on top -func (vm *fileTreeViewModel) PageDown() error { +func (vm *FileTreeViewModel) PageDown() error { nextBufferIndexLowerBound := vm.bufferIndexLowerBound + vm.height() nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height() @@ -266,7 +266,7 @@ func (vm *fileTreeViewModel) PageDown() error { } // PageUp moves to previous page putting the cursor on top -func (vm *fileTreeViewModel) PageUp() error { +func (vm *FileTreeViewModel) PageUp() error { nextBufferIndexLowerBound := vm.bufferIndexLowerBound - vm.height() nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height() @@ -291,7 +291,7 @@ func (vm *fileTreeViewModel) PageUp() error { } // getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode. -func (vm *fileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) { +func (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) { var visitor func(*filetree.FileNode) error var evaluator func(*filetree.FileNode) bool var dfsCounter int @@ -321,8 +321,8 @@ func (vm *fileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (nod return node } -// toggleCollapse will collapse/expand the selected FileNode. -func (vm *fileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error { +// ToggleCollapse will collapse/expand the selected FileNode. +func (vm *FileTreeViewModel) ToggleCollapse(filterRegex *regexp.Regexp) error { node := vm.getAbsPositionNode(filterRegex) if node != nil && node.Data.FileInfo.IsDir { node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed @@ -330,8 +330,8 @@ func (vm *fileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error { return nil } -// toggleCollapseAll will collapse/expand the all directories. -func (vm *fileTreeViewModel) toggleCollapseAll() error { +// ToggleCollapseAll will collapse/expand the all directories. +func (vm *FileTreeViewModel) ToggleCollapseAll() error { vm.CollapseAll = !vm.CollapseAll visitor := func(curNode *filetree.FileNode) error { @@ -345,25 +345,25 @@ func (vm *fileTreeViewModel) toggleCollapseAll() error { err := vm.ModelTree.VisitDepthChildFirst(visitor, evaluator) if err != nil { - logrus.Errorf("unable to propagate tree on toggleCollapseAll: %+v", err) + logrus.Errorf("unable to propagate tree on ToggleCollapseAll: %+v", err) } return nil } -// toggleCollapse will collapse/expand the selected FileNode. -func (vm *fileTreeViewModel) toggleAttributes() error { +// ToggleCollapse will collapse/expand the selected FileNode. +func (vm *FileTreeViewModel) ToggleAttributes() error { vm.ShowAttributes = !vm.ShowAttributes return nil } -// toggleShowDiffType will show/hide the selected DiffType in the filetree pane. -func (vm *fileTreeViewModel) toggleShowDiffType(diffType filetree.DiffType) { +// ToggleShowDiffType will show/hide the selected DiffType in the filetree pane. +func (vm *FileTreeViewModel) ToggleShowDiffType(diffType filetree.DiffType) { vm.HiddenDiffTypes[diffType] = !vm.HiddenDiffTypes[diffType] } // Update refreshes the state objects for future rendering. -func (vm *fileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error { +func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error { vm.refWidth = width vm.refHeight = height @@ -411,21 +411,21 @@ func (vm *fileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height in } // Render flushes the state objects (file tree) to the pane. -func (vm *fileTreeViewModel) Render() error { +func (vm *FileTreeViewModel) Render() error { treeString := vm.ViewTree.StringBetween(vm.bufferIndexLowerBound, vm.bufferIndexUpperBound(), vm.ShowAttributes) lines := strings.Split(treeString, "\n") // update the contents - vm.mainBuf.Reset() + vm.Buffer.Reset() for idx, line := range lines { if idx == vm.bufferIndex { - _, err := fmt.Fprintln(&vm.mainBuf, format.Selected(vtclean.Clean(line, false))) + _, err := fmt.Fprintln(&vm.Buffer, format.Selected(vtclean.Clean(line, false))) if err != nil { logrus.Debug("unable to write to buffer: ", err) return err } } else { - _, err := fmt.Fprintln(&vm.mainBuf, line) + _, err := fmt.Fprintln(&vm.Buffer, line) if err != nil { logrus.Debug("unable to write to buffer: ", err) return err diff --git a/runtime/ui/filetree_viewmodel_test.go b/runtime/ui/viewmodel/filetree_viewmodel_test.go similarity index 86% rename from runtime/ui/filetree_viewmodel_test.go rename to runtime/ui/viewmodel/filetree_viewmodel_test.go index fde2587..3d92a67 100644 --- a/runtime/ui/filetree_viewmodel_test.go +++ b/runtime/ui/viewmodel/filetree_viewmodel_test.go @@ -1,4 +1,4 @@ -package ui +package viewmodel import ( "bytes" @@ -73,8 +73,8 @@ func assertTestData(t *testing.T, actualBytes []byte) { helperCheckDiff(t, expectedBytes, actualBytes) } -func initializeTestViewModel(t *testing.T) *fileTreeViewModel { - result := docker.TestAnalysisFromArchive(t, "../../.data/test-docker-image.tar") +func initializeTestViewModel(t *testing.T) *FileTreeViewModel { + result := docker.TestAnalysisFromArchive(t, "../../../.data/test-docker-image.tar") cache := filetree.NewFileTreeCache(result.RefTrees) err := cache.Build() @@ -88,14 +88,14 @@ func initializeTestViewModel(t *testing.T) *fileTreeViewModel { if err != nil { t.Fatalf("%s: unable to stack trees: %v", t.Name(), err) } - vm, err := newFileTreeViewModel(treeStack, result.RefTrees, cache) + vm, err := NewFileTreeViewModel(treeStack, result.RefTrees, cache) if err != nil { t.Fatalf("%s: unable to create tree ViewModel: %+v", t.Name(), err) } return vm } -func runTestCase(t *testing.T, vm *fileTreeViewModel, width, height int, filterRegex *regexp.Regexp) { +func runTestCase(t *testing.T, vm *FileTreeViewModel, width, height int, filterRegex *regexp.Regexp) { err := vm.Update(filterRegex, width, height) if err != nil { t.Errorf("failed to update viewmodel: %v", err) @@ -106,7 +106,7 @@ func runTestCase(t *testing.T, vm *fileTreeViewModel, width, height int, filterR t.Errorf("failed to render viewmodel: %v", err) } - assertTestData(t, vm.mainBuf.Bytes()) + assertTestData(t, vm.Buffer.Bytes()) } func checkError(t *testing.T, err error, message string) { @@ -153,7 +153,7 @@ func TestFileTreeDirCollapse(t *testing.T) { vm.ShowAttributes = true // collapse /bin - err := vm.toggleCollapse(nil) + err := vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /bin") moved := vm.CursorDown() @@ -167,7 +167,7 @@ func TestFileTreeDirCollapse(t *testing.T) { } // collapse /etc - err = vm.toggleCollapse(nil) + err = vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /etc") runTestCase(t, vm, width, height, nil) @@ -180,7 +180,7 @@ func TestFileTreeDirCollapseAll(t *testing.T) { vm.Setup(0, height) vm.ShowAttributes = true - err := vm.toggleCollapseAll() + err := vm.ToggleCollapseAll() checkError(t, err, "unable to collapse all dir") runTestCase(t, vm, width, height, nil) @@ -194,13 +194,13 @@ func TestFileTreeSelectLayer(t *testing.T) { vm.ShowAttributes = true // collapse /bin - err := vm.toggleCollapse(nil) + err := vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /bin") // select the next layer, compareMode = layer - err = vm.setTreeByLayer(0, 0, 1, 1) + err = vm.SetTreeByLayer(0, 0, 1, 1) if err != nil { - t.Errorf("unable to setTreeByLayer: %v", err) + t.Errorf("unable to SetTreeByLayer: %v", err) } runTestCase(t, vm, width, height, nil) } @@ -213,12 +213,12 @@ func TestFileShowAggregateChanges(t *testing.T) { vm.ShowAttributes = true // collapse /bin - err := vm.toggleCollapse(nil) + err := vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /bin") // select the next layer, compareMode = layer - err = vm.setTreeByLayer(0, 0, 1, 13) - checkError(t, err, "unable to setTreeByLayer") + err = vm.SetTreeByLayer(0, 0, 1, 13) + checkError(t, err, "unable to SetTreeByLayer") runTestCase(t, vm, width, height, nil) } @@ -275,7 +275,7 @@ func TestFileTreeDirCursorRight(t *testing.T) { vm.ShowAttributes = true // collapse /bin - err := vm.toggleCollapse(nil) + err := vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /bin") moved := vm.CursorDown() @@ -289,7 +289,7 @@ func TestFileTreeDirCursorRight(t *testing.T) { } // collapse /etc - err = vm.toggleCollapse(nil) + err = vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /etc") // expand /etc @@ -322,23 +322,23 @@ func TestFileTreeHideAddedRemovedModified(t *testing.T) { vm.ShowAttributes = true // collapse /bin - err := vm.toggleCollapse(nil) + err := vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /bin") // select the 7th layer, compareMode = layer - err = vm.setTreeByLayer(0, 0, 1, 7) + err = vm.SetTreeByLayer(0, 0, 1, 7) if err != nil { - t.Errorf("unable to setTreeByLayer: %v", err) + t.Errorf("unable to SetTreeByLayer: %v", err) } // hide added files - vm.toggleShowDiffType(filetree.Added) + vm.ToggleShowDiffType(filetree.Added) // hide modified files - vm.toggleShowDiffType(filetree.Modified) + vm.ToggleShowDiffType(filetree.Modified) // hide removed files - vm.toggleShowDiffType(filetree.Removed) + vm.ToggleShowDiffType(filetree.Removed) runTestCase(t, vm, width, height, nil) } @@ -351,17 +351,17 @@ func TestFileTreeHideUnmodified(t *testing.T) { vm.ShowAttributes = true // collapse /bin - err := vm.toggleCollapse(nil) + err := vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /bin") // select the 7th layer, compareMode = layer - err = vm.setTreeByLayer(0, 0, 1, 7) + err = vm.SetTreeByLayer(0, 0, 1, 7) if err != nil { - t.Errorf("unable to setTreeByLayer: %v", err) + t.Errorf("unable to SetTreeByLayer: %v", err) } // hide unmodified files - vm.toggleShowDiffType(filetree.Unmodified) + vm.ToggleShowDiffType(filetree.Unmodified) runTestCase(t, vm, width, height, nil) } @@ -374,17 +374,17 @@ func TestFileTreeHideTypeWithFilter(t *testing.T) { vm.ShowAttributes = true // collapse /bin - err := vm.toggleCollapse(nil) + err := vm.ToggleCollapse(nil) checkError(t, err, "unable to collapse /bin") // select the 7th layer, compareMode = layer - err = vm.setTreeByLayer(0, 0, 1, 7) + err = vm.SetTreeByLayer(0, 0, 1, 7) if err != nil { - t.Errorf("unable to setTreeByLayer: %v", err) + t.Errorf("unable to SetTreeByLayer: %v", err) } // hide added files - vm.toggleShowDiffType(filetree.Added) + vm.ToggleShowDiffType(filetree.Added) regex, err := regexp.Compile("saved") if err != nil { diff --git a/runtime/ui/testdata/TestFileShowAggregateChanges.txt b/runtime/ui/viewmodel/testdata/TestFileShowAggregateChanges.txt similarity index 100% rename from runtime/ui/testdata/TestFileShowAggregateChanges.txt rename to runtime/ui/viewmodel/testdata/TestFileShowAggregateChanges.txt diff --git a/runtime/ui/testdata/TestFileTreeDirCollapse.txt b/runtime/ui/viewmodel/testdata/TestFileTreeDirCollapse.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeDirCollapse.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeDirCollapse.txt diff --git a/runtime/ui/testdata/TestFileTreeDirCollapseAll.txt b/runtime/ui/viewmodel/testdata/TestFileTreeDirCollapseAll.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeDirCollapseAll.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeDirCollapseAll.txt diff --git a/runtime/ui/testdata/TestFileTreeDirCursorRight.txt b/runtime/ui/viewmodel/testdata/TestFileTreeDirCursorRight.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeDirCursorRight.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeDirCursorRight.txt diff --git a/runtime/ui/testdata/TestFileTreeFilterTree.txt b/runtime/ui/viewmodel/testdata/TestFileTreeFilterTree.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeFilterTree.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeFilterTree.txt diff --git a/runtime/ui/testdata/TestFileTreeGoCase.txt b/runtime/ui/viewmodel/testdata/TestFileTreeGoCase.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeGoCase.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeGoCase.txt diff --git a/runtime/ui/testdata/TestFileTreeHideAddedRemovedModified.txt b/runtime/ui/viewmodel/testdata/TestFileTreeHideAddedRemovedModified.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeHideAddedRemovedModified.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeHideAddedRemovedModified.txt diff --git a/runtime/ui/testdata/TestFileTreeHideTypeWithFilter.txt b/runtime/ui/viewmodel/testdata/TestFileTreeHideTypeWithFilter.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeHideTypeWithFilter.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeHideTypeWithFilter.txt diff --git a/runtime/ui/testdata/TestFileTreeHideUnmodified.txt b/runtime/ui/viewmodel/testdata/TestFileTreeHideUnmodified.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeHideUnmodified.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeHideUnmodified.txt diff --git a/runtime/ui/testdata/TestFileTreeNoAttributes.txt b/runtime/ui/viewmodel/testdata/TestFileTreeNoAttributes.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeNoAttributes.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeNoAttributes.txt diff --git a/runtime/ui/testdata/TestFileTreePageDown.txt b/runtime/ui/viewmodel/testdata/TestFileTreePageDown.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreePageDown.txt rename to runtime/ui/viewmodel/testdata/TestFileTreePageDown.txt diff --git a/runtime/ui/testdata/TestFileTreePageUp.txt b/runtime/ui/viewmodel/testdata/TestFileTreePageUp.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreePageUp.txt rename to runtime/ui/viewmodel/testdata/TestFileTreePageUp.txt diff --git a/runtime/ui/testdata/TestFileTreeRestrictedHeight.txt b/runtime/ui/viewmodel/testdata/TestFileTreeRestrictedHeight.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeRestrictedHeight.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeRestrictedHeight.txt diff --git a/runtime/ui/testdata/TestFileTreeSelectLayer.txt b/runtime/ui/viewmodel/testdata/TestFileTreeSelectLayer.txt similarity index 100% rename from runtime/ui/testdata/TestFileTreeSelectLayer.txt rename to runtime/ui/viewmodel/testdata/TestFileTreeSelectLayer.txt From 939741625d9212e62e3de84dc149f711befba6cf Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sun, 13 Oct 2019 11:49:18 -0400 Subject: [PATCH 4/6] drop package names from object names --- ...controller_collection.go => collection.go} | 34 +-- .../{details_controller.go => details.go} | 80 +++---- .../{filetree_controller.go => filetree.go} | 210 +++++++++--------- .../{filter_controller.go => filter.go} | 80 +++---- .../{layer_controller.go => layer.go} | 182 +++++++-------- .../{status_controller.go => status.go} | 46 ++-- runtime/ui/layout_manager.go | 4 +- runtime/ui/ui.go | 6 +- .../{filetree_viewmodel.go => filetree.go} | 44 ++-- ...ree_viewmodel_test.go => filetree_test.go} | 4 +- 10 files changed, 345 insertions(+), 345 deletions(-) rename runtime/ui/controller/{controller_collection.go => collection.go} (79%) rename runtime/ui/controller/{details_controller.go => details.go} (67%) rename runtime/ui/controller/{filetree_controller.go => filetree.go} (55%) rename runtime/ui/controller/{filter_controller.go => filter.go} (58%) rename runtime/ui/controller/{layer_controller.go => layer.go} (50%) rename runtime/ui/controller/{status_controller.go => status.go} (56%) rename runtime/ui/viewmodel/{filetree_viewmodel.go => filetree.go} (89%) rename runtime/ui/viewmodel/{filetree_viewmodel_test.go => filetree_test.go} (98%) diff --git a/runtime/ui/controller/controller_collection.go b/runtime/ui/controller/collection.go similarity index 79% rename from runtime/ui/controller/controller_collection.go rename to runtime/ui/controller/collection.go index f0b7c7b..a7c6124 100644 --- a/runtime/ui/controller/controller_collection.go +++ b/runtime/ui/controller/collection.go @@ -9,22 +9,22 @@ import ( ) // var ccOnce sync.Once -var controllers *ControllerCollection +var controllers *Collection -type ControllerCollection struct { +type Collection struct { gui *gocui.Gui - Tree *FileTreeController - Layer *LayerController - Status *StatusController - Filter *FilterController - Details *DetailsController + Tree *FileTree + Layer *Layer + Status *Status + Filter *Filter + Details *Details lookup map[string]Controller } -func NewControllerCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*ControllerCollection, error) { +func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*Collection, error) { var err error - controllers = &ControllerCollection{ + controllers = &Collection{ gui: g, } controllers.lookup = make(map[string]Controller) @@ -56,7 +56,7 @@ func NewControllerCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache return controllers, nil } -func (c *ControllerCollection) UpdateAndRender() error { +func (c *Collection) UpdateAndRender() error { err := c.Update() if err != nil { logrus.Debug("failed update: ", err) @@ -73,7 +73,7 @@ func (c *ControllerCollection) UpdateAndRender() error { } // Update refreshes the state objects for future rendering. -func (c *ControllerCollection) Update() error { +func (c *Collection) Update() error { for _, controller := range c.lookup { err := controller.Update() if err != nil { @@ -85,7 +85,7 @@ func (c *ControllerCollection) Update() error { } // Render flushes the state objects to the screen. -func (c *ControllerCollection) Render() error { +func (c *Collection) Render() error { for _, controller := range c.lookup { if controller.IsVisible() { err := controller.Render() @@ -98,7 +98,7 @@ func (c *ControllerCollection) Render() error { } // ToggleView switches between the file view and the layer view and re-renders the screen. -func (c *ControllerCollection) ToggleView() (err error) { +func (c *Collection) ToggleView() (err error) { v := c.gui.CurrentView() if v == nil || v.Name() == c.Layer.Name() { _, err = c.gui.SetCurrentView(c.Tree.Name()) @@ -114,7 +114,7 @@ func (c *ControllerCollection) ToggleView() (err error) { return c.UpdateAndRender() } -func (c *ControllerCollection) ToggleFilterView() error { +func (c *Collection) ToggleFilterView() error { // delete all user input from the tree view err := c.Filter.ToggleVisible() if err != nil { @@ -135,17 +135,17 @@ func (c *ControllerCollection) ToggleFilterView() error { } // CursorDown moves the cursor down in the currently selected gocui pane, scrolling the screen as needed. -func (c *ControllerCollection) CursorDown(g *gocui.Gui, v *gocui.View) error { +func (c *Collection) CursorDown(g *gocui.Gui, v *gocui.View) error { return c.CursorStep(g, v, 1) } // CursorUp moves the cursor up in the currently selected gocui pane, scrolling the screen as needed. -func (c *ControllerCollection) CursorUp(g *gocui.Gui, v *gocui.View) error { +func (c *Collection) CursorUp(g *gocui.Gui, v *gocui.View) error { return c.CursorStep(g, v, -1) } // Moves the cursor the given step distance, setting the origin to the new cursor line -func (c *ControllerCollection) CursorStep(g *gocui.Gui, v *gocui.View, step int) error { +func (c *Collection) CursorStep(g *gocui.Gui, v *gocui.View, step int) error { cx, cy := v.Cursor() // if there isn't a next line diff --git a/runtime/ui/controller/details_controller.go b/runtime/ui/controller/details.go similarity index 67% rename from runtime/ui/controller/details_controller.go rename to runtime/ui/controller/details.go index d3eccbc..60336e1 100644 --- a/runtime/ui/controller/details_controller.go +++ b/runtime/ui/controller/details.go @@ -14,9 +14,9 @@ import ( "github.com/lunixbochs/vtclean" ) -// DetailsController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that +// Details 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 { +type Details struct { name string gui *gocui.Gui view *gocui.View @@ -26,8 +26,8 @@ type DetailsController struct { } // 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) +func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice) (controller *Details) { + controller = new(Details) // populate main fields controller.name = name @@ -38,63 +38,63 @@ func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, ineff return controller } -func (controller *DetailsController) Name() string { - return controller.name +func (c *Details) Name() string { + return c.name } // 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 (c *Details) Setup(v *gocui.View, header *gocui.View) error { // set controller options - controller.view = v - controller.view.Editable = false - controller.view.Wrap = true - controller.view.Highlight = false - controller.view.Frame = false + c.view = v + c.view.Editable = false + c.view.Wrap = true + c.view.Highlight = false + c.view.Frame = false - controller.header = header - controller.header.Editable = false - controller.header.Wrap = false - controller.header.Frame = false + c.header = header + c.header.Editable = false + c.header.Wrap = false + c.header.Frame = false var infos = []key.BindingInfo{ { Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, - OnAction: controller.CursorDown, + OnAction: c.CursorDown, }, { Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, - OnAction: controller.CursorUp, + OnAction: c.CursorUp, }, } - _, err := key.GenerateBindings(controller.gui, controller.name, infos) + _, err := key.GenerateBindings(c.gui, c.name, infos) if err != nil { return err } - return controller.Render() + return c.Render() } // IsVisible indicates if the details view pane is currently initialized. -func (controller *DetailsController) IsVisible() bool { - return controller != nil +func (c *Details) IsVisible() bool { + return c != nil } // CursorDown moves the cursor down in the details pane (currently indicates nothing). -func (controller *DetailsController) CursorDown() error { - return controllers.CursorDown(controller.gui, controller.view) +func (c *Details) CursorDown() error { + return controllers.CursorDown(c.gui, c.view) } // CursorUp moves the cursor up in the details pane (currently indicates nothing). -func (controller *DetailsController) CursorUp() error { - return controllers.CursorUp(controller.gui, controller.view) +func (c *Details) CursorUp() error { + return controllers.CursorUp(c.gui, c.view) } // Update refreshes the state objects for future rendering. -func (controller *DetailsController) Update() error { +func (c *Details) Update() error { return nil } @@ -103,7 +103,7 @@ 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 { +func (c *Details) Render() error { currentLayer := controllers.Layer.currentLayer() var wastedSpace int64 @@ -112,12 +112,12 @@ func (controller *DetailsController) Render() error { inefficiencyReport := fmt.Sprintf(format.Header(template), "Count", "Total Space", "Path") height := 100 - if controller.view != nil { - _, height = controller.view.Size() + if c.view != nil { + _, height = c.view.Size() } - for idx := 0; idx < len(controller.inefficiencies); idx++ { - data := controller.inefficiencies[len(controller.inefficiencies)-1-idx] + for idx := 0; idx < len(c.inefficiencies); idx++ { + data := c.inefficiencies[len(c.inefficiencies)-1-idx] wastedSpace += data.CumulativeSize // todo: make this report scrollable @@ -127,24 +127,24 @@ func (controller *DetailsController) Render() error { } 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)) + effStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*c.efficiency)) wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace))) - controller.gui.Update(func(g *gocui.Gui) error { + c.gui.Update(func(g *gocui.Gui) error { // update header - controller.header.Clear() - width, _ := controller.view.Size() + c.header.Clear() + width, _ := c.view.Size() 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, format.Header(vtclean.Clean(layerHeaderStr, false))) + _, err := fmt.Fprintln(c.header, format.Header(vtclean.Clean(layerHeaderStr, false))) if err != nil { return err } // update contents - controller.view.Clear() + c.view.Clear() var lines = make([]string, 0) if currentLayer.Names != nil && len(currentLayer.Names) > 0 { @@ -162,7 +162,7 @@ func (controller *DetailsController) Render() error { lines = append(lines, effStr+"\n") lines = append(lines, inefficiencyReport) - _, err = fmt.Fprintln(controller.view, strings.Join(lines, "\n")) + _, err = fmt.Fprintln(c.view, strings.Join(lines, "\n")) if err != nil { logrus.Debug("unable to write to buffer: ", err) } @@ -172,6 +172,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 (c *Details) KeyHelp() string { return "TBD" } diff --git a/runtime/ui/controller/filetree_controller.go b/runtime/ui/controller/filetree.go similarity index 55% rename from runtime/ui/controller/filetree_controller.go rename to runtime/ui/controller/filetree.go index 5dc27db..df10a6b 100644 --- a/runtime/ui/controller/filetree_controller.go +++ b/runtime/ui/controller/filetree.go @@ -20,21 +20,21 @@ const ( type CompareType int -// FileTreeController holds the UI objects and data models for populating the right pane. Specifically the pane that +// FileTree 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 { +type FileTree struct { name string gui *gocui.Gui view *gocui.View header *gocui.View - vm *viewmodel.FileTreeViewModel + vm *viewmodel.FileTree 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) +func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *FileTree, err error) { + controller = new(FileTree) // populate main fields controller.name = name @@ -47,143 +47,143 @@ func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, return controller, err } -func (controller *FileTreeController) Name() string { - return controller.name +func (c *FileTree) Name() string { + return c.name } -func (controller *FileTreeController) AreAttributesVisible() bool { - return controller.vm.ShowAttributes +func (c *FileTree) AreAttributesVisible() bool { + return c.vm.ShowAttributes } // 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 (c *FileTree) Setup(v *gocui.View, header *gocui.View) error { // set controller options - controller.view = v - controller.view.Editable = false - controller.view.Wrap = false - controller.view.Frame = false + c.view = v + c.view.Editable = false + c.view.Wrap = false + c.view.Frame = false - controller.header = header - controller.header.Editable = false - controller.header.Wrap = false - controller.header.Frame = false + c.header = header + c.header.Editable = false + c.header.Wrap = false + c.header.Frame = false var infos = []key.BindingInfo{ { ConfigKeys: []string{"keybinding.toggle-collapse-dir"}, - OnAction: controller.toggleCollapse, + OnAction: c.toggleCollapse, Display: "Collapse dir", }, { ConfigKeys: []string{"keybinding.toggle-collapse-all-dir"}, - OnAction: controller.toggleCollapseAll, + OnAction: c.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] }, + OnAction: func() error { return c.toggleShowDiffType(filetree.Added) }, + IsSelected: func() bool { return !c.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] }, + OnAction: func() error { return c.toggleShowDiffType(filetree.Removed) }, + IsSelected: func() bool { return !c.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] }, + OnAction: func() error { return c.toggleShowDiffType(filetree.Modified) }, + IsSelected: func() bool { return !c.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] }, + OnAction: func() error { return c.toggleShowDiffType(filetree.Unmodified) }, + IsSelected: func() bool { return !c.vm.HiddenDiffTypes[filetree.Unmodified] }, Display: "Unmodified", }, { ConfigKeys: []string{"keybinding.toggle-filetree-attributes"}, - OnAction: controller.toggleAttributes, - IsSelected: func() bool { return controller.vm.ShowAttributes }, + OnAction: c.toggleAttributes, + IsSelected: func() bool { return c.vm.ShowAttributes }, Display: "Attributes", }, { ConfigKeys: []string{"keybinding.page-up"}, - OnAction: controller.PageUp, + OnAction: c.PageUp, }, { ConfigKeys: []string{"keybinding.page-down"}, - OnAction: controller.PageDown, + OnAction: c.PageDown, }, { Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, - OnAction: controller.CursorDown, + OnAction: c.CursorDown, }, { Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, - OnAction: controller.CursorUp, + OnAction: c.CursorUp, }, { Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, - OnAction: controller.CursorLeft, + OnAction: c.CursorLeft, }, { Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, - OnAction: controller.CursorRight, + OnAction: c.CursorRight, }, } - helpKeys, err := key.GenerateBindings(controller.gui, controller.name, infos) + helpKeys, err := key.GenerateBindings(c.gui, c.name, infos) if err != nil { return err } - controller.helpKeys = helpKeys + c.helpKeys = helpKeys - _, height := controller.view.Size() - controller.vm.Setup(0, height) - _ = controller.Update() - _ = controller.Render() + _, height := c.view.Size() + c.vm.Setup(0, height) + _ = c.Update() + _ = c.Render() return nil } // IsVisible indicates if the file tree view pane is currently initialized -func (controller *FileTreeController) IsVisible() bool { - return controller != nil +func (c *FileTree) IsVisible() bool { + return c != nil } // ResetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer. -func (controller *FileTreeController) resetCursor() { - _ = controller.view.SetCursor(0, 0) - controller.vm.ResetCursor() +func (c *FileTree) resetCursor() { + _ = c.view.SetCursor(0, 0) + c.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 { - err := controller.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop) +func (c *FileTree) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error { + err := c.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop) if err != nil { return err } // controller.ResetCursor() - _ = controller.Update() - return controller.Render() + _ = c.Update() + return c.Render() } // CursorDown moves the cursor down and renders the view. // 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 { - if controller.vm.CursorDown() { - return controller.Render() +func (c *FileTree) CursorDown() error { + if c.vm.CursorDown() { + return c.Render() } return nil } @@ -192,82 +192,82 @@ 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 { - if controller.vm.CursorUp() { - return controller.Render() +func (c *FileTree) CursorUp() error { + if c.vm.CursorUp() { + return c.Render() } return nil } // CursorLeft moves the cursor up until we reach the Parent Node or top of the tree -func (controller *FileTreeController) CursorLeft() error { - err := controller.vm.CursorLeft(filterRegex()) +func (c *FileTree) CursorLeft() error { + err := c.vm.CursorLeft(filterRegex()) if err != nil { return err } - _ = controller.Update() - return controller.Render() + _ = c.Update() + return c.Render() } // CursorRight descends into directory expanding it if needed -func (controller *FileTreeController) CursorRight() error { - err := controller.vm.CursorRight(filterRegex()) +func (c *FileTree) CursorRight() error { + err := c.vm.CursorRight(filterRegex()) if err != nil { return err } - _ = controller.Update() - return controller.Render() + _ = c.Update() + return c.Render() } // PageDown moves to next page putting the cursor on top -func (controller *FileTreeController) PageDown() error { - err := controller.vm.PageDown() +func (c *FileTree) PageDown() error { + err := c.vm.PageDown() if err != nil { return err } - return controller.Render() + return c.Render() } // PageUp moves to previous page putting the cursor on top -func (controller *FileTreeController) PageUp() error { - err := controller.vm.PageUp() +func (c *FileTree) PageUp() error { + err := c.vm.PageUp() if err != nil { return err } - return controller.Render() + return c.Render() } // 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 *FileTree) getAbsPositionNode() (node *filetree.FileNode) { // return controller.vm.getAbsPositionNode(filterRegex()) // } // ToggleCollapse will collapse/expand the selected FileNode. -func (controller *FileTreeController) toggleCollapse() error { - err := controller.vm.ToggleCollapse(filterRegex()) +func (c *FileTree) toggleCollapse() error { + err := c.vm.ToggleCollapse(filterRegex()) if err != nil { return err } - _ = controller.Update() - return controller.Render() + _ = c.Update() + return c.Render() } // ToggleCollapseAll will collapse/expand the all directories. -func (controller *FileTreeController) toggleCollapseAll() error { - err := controller.vm.ToggleCollapseAll() +func (c *FileTree) toggleCollapseAll() error { + err := c.vm.ToggleCollapseAll() if err != nil { return err } - if controller.vm.CollapseAll { - controller.resetCursor() + if c.vm.CollapseAll { + c.resetCursor() } - _ = controller.Update() - return controller.Render() + _ = c.Update() + return c.Render() } // ToggleAttributes will show/hide file attributes -func (controller *FileTreeController) toggleAttributes() error { - err := controller.vm.ToggleAttributes() +func (c *FileTree) toggleAttributes() error { + err := c.vm.ToggleAttributes() if err != nil { return err } @@ -276,8 +276,8 @@ func (controller *FileTreeController) toggleAttributes() error { } // ToggleShowDiffType will show/hide the selected DiffType in the filetree pane. -func (controller *FileTreeController) toggleShowDiffType(diffType filetree.DiffType) error { - controller.vm.ToggleShowDiffType(diffType) +func (c *FileTree) toggleShowDiffType(diffType filetree.DiffType) error { + c.vm.ToggleShowDiffType(diffType) // we need to render the changes to the status pane as well (not just this contoller/view) return controllers.UpdateAndRender() } @@ -301,58 +301,58 @@ 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 { - _ = controller.Update() +func (c *FileTree) OnLayoutChange(resized bool) error { + _ = c.Update() if resized { - return controller.Render() + return c.Render() } return nil } // Update refreshes the state objects for future rendering. -func (controller *FileTreeController) Update() error { +func (c *FileTree) Update() error { var width, height int - if controller.view != nil { - width, height = controller.view.Size() + if c.view != nil { + width, height = c.view.Size() } else { // before the TUI is setup there may not be a controller to reference. Use the entire screen as reference. - width, height = controller.gui.Size() + width, height = c.gui.Size() } // height should account for the header - return controller.vm.Update(filterRegex(), width, height-1) + return c.vm.Update(filterRegex(), width, height-1) } // Render flushes the state objects (file tree) to the pane. -func (controller *FileTreeController) Render() error { +func (c *FileTree) Render() error { title := "Current Layer Contents" if controllers.Layer.CompareMode == CompareAll { title = "Aggregated Layer Contents" } // indicate when selected - if controller.gui.CurrentView() == controller.view { + if c.gui.CurrentView() == c.view { title = "● " + title } - controller.gui.Update(func(g *gocui.Gui) error { + c.gui.Update(func(g *gocui.Gui) error { // update the header - controller.header.Clear() + c.header.Clear() width, _ := g.Size() headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2)) - if controller.vm.ShowAttributes { + if c.vm.ShowAttributes { headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree") } - _, _ = fmt.Fprintln(controller.header, format.Header(vtclean.Clean(headerStr, false))) + _, _ = fmt.Fprintln(c.header, format.Header(vtclean.Clean(headerStr, false))) // update the contents - controller.view.Clear() - err := controller.vm.Render() + c.view.Clear() + err := c.vm.Render() if err != nil { return err } - _, err = fmt.Fprint(controller.view, controller.vm.Buffer.String()) + _, err = fmt.Fprint(c.view, c.vm.Buffer.String()) return err }) @@ -360,9 +360,9 @@ 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 { +func (c *FileTree) KeyHelp() string { var help string - for _, binding := range controller.helpKeys { + for _, binding := range c.helpKeys { help += binding.RenderKeyHelp() } return help diff --git a/runtime/ui/controller/filter_controller.go b/runtime/ui/controller/filter.go similarity index 58% rename from runtime/ui/controller/filter_controller.go rename to runtime/ui/controller/filter.go index 2ff718d..51c0e3b 100644 --- a/runtime/ui/controller/filter_controller.go +++ b/runtime/ui/controller/filter.go @@ -7,9 +7,9 @@ import ( "github.com/wagoodman/dive/runtime/ui/format" ) -// FilterController holds the UI objects and data models for populating the bottom row. Specifically the pane that +// Filter holds the UI objects and data models for populating the bottom row. Specifically the pane that // allows the user to filter the file tree by path. -type FilterController struct { +type Filter struct { name string gui *gocui.Gui view *gocui.View @@ -20,8 +20,8 @@ type FilterController struct { } // NewFilterController creates a new view object attached the the global [gocui] screen object. -func NewFilterController(name string, gui *gocui.Gui) (controller *FilterController) { - controller = new(FilterController) +func NewFilterController(name string, gui *gocui.Gui) (controller *Filter) { + controller = new(Filter) // populate main fields controller.name = name @@ -32,40 +32,40 @@ func NewFilterController(name string, gui *gocui.Gui) (controller *FilterControl return controller } -func (controller *FilterController) Name() string { - return controller.name +func (c *Filter) Name() string { + return c.name } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (controller *FilterController) Setup(v *gocui.View, header *gocui.View) error { +func (c *Filter) Setup(v *gocui.View, header *gocui.View) error { // set controller options - controller.view = v - controller.maxLength = 200 - controller.view.Frame = false - controller.view.BgColor = gocui.AttrReverse - controller.view.Editable = true - controller.view.Editor = controller + c.view = v + c.maxLength = 200 + c.view.Frame = false + c.view.BgColor = gocui.AttrReverse + c.view.Editable = true + c.view.Editor = c - controller.header = header - controller.header.BgColor = gocui.AttrReverse - controller.header.Editable = false - controller.header.Wrap = false - controller.header.Frame = false + c.header = header + c.header.BgColor = gocui.AttrReverse + c.header.Editable = false + c.header.Wrap = false + c.header.Frame = false - return controller.Render() + return c.Render() } // ToggleFilterView shows/hides the file tree filter pane. -func (controller *FilterController) ToggleVisible() error { +func (c *Filter) ToggleVisible() error { // delete all user input from the tree view - controller.view.Clear() + c.view.Clear() // toggle hiding - controller.hidden = !controller.hidden + c.hidden = !c.hidden - if !controller.hidden { - _, err := controller.gui.SetCurrentView(controller.name) + if !c.hidden { + _, err := c.gui.SetCurrentView(c.name) if err != nil { logrus.Error("unable to toggle filter view: ", err) return err @@ -76,41 +76,41 @@ func (controller *FilterController) ToggleVisible() error { // reset the cursor for the next time it is visible // Note: there is a subtle gocui behavior here where this cannot be called when the view // is newly visible. Is this a problem with dive or gocui? - return controller.view.SetCursor(0, 0) + return c.view.SetCursor(0, 0) } // todo: remove the need for this -func (controller *FilterController) HeaderStr() string { - return controller.headerStr +func (c *Filter) HeaderStr() string { + return c.headerStr } // IsVisible indicates if the filter view pane is currently initialized -func (controller *FilterController) IsVisible() bool { - if controller == nil { +func (c *Filter) IsVisible() bool { + if c == nil { return false } - return !controller.hidden + return !c.hidden } // CursorDown moves the cursor down in the filter pane (currently indicates nothing). -func (controller *FilterController) CursorDown() error { +func (c *Filter) CursorDown() error { return nil } // CursorUp moves the cursor up in the filter pane (currently indicates nothing). -func (controller *FilterController) CursorUp() error { +func (c *Filter) CursorUp() error { return nil } // Edit intercepts the key press events in the filer view to update the file view in real time. -func (controller *FilterController) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { - if !controller.IsVisible() { +func (c *Filter) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { + if !c.IsVisible() { return } cx, _ := v.Cursor() ox, _ := v.Origin() - limit := ox+cx+1 > controller.maxLength + limit := ox+cx+1 > c.maxLength switch { case ch != 0 && mod == 0 && !limit: v.EditWrite(ch) @@ -126,14 +126,14 @@ func (controller *FilterController) Edit(v *gocui.View, key gocui.Key, ch rune, } // Update refreshes the state objects for future rendering (currently does nothing). -func (controller *FilterController) Update() error { +func (c *Filter) Update() error { return nil } // Render flushes the state objects to the screen. Currently this is the users path filter input. -func (controller *FilterController) Render() error { - controller.gui.Update(func(g *gocui.Gui) error { - _, err := fmt.Fprintln(controller.header, format.Header(controller.headerStr)) +func (c *Filter) Render() error { + c.gui.Update(func(g *gocui.Gui) error { + _, err := fmt.Fprintln(c.header, format.Header(c.headerStr)) if err != nil { logrus.Error("unable to write to buffer: ", err) } @@ -143,6 +143,6 @@ func (controller *FilterController) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (controller *FilterController) KeyHelp() string { +func (c *Filter) KeyHelp() string { return format.StatusControlNormal("▏Type to filter the file tree ") } diff --git a/runtime/ui/controller/layer_controller.go b/runtime/ui/controller/layer.go similarity index 50% rename from runtime/ui/controller/layer_controller.go rename to runtime/ui/controller/layer.go index a0bf0c0..948a0b1 100644 --- a/runtime/ui/controller/layer_controller.go +++ b/runtime/ui/controller/layer.go @@ -13,9 +13,9 @@ import ( "github.com/spf13/viper" ) -// LayerController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that +// Layer holds the UI objects and data models for populating the lower-left pane. Specifically the pane that // shows the image layers and layer selector. -type LayerController struct { +type Layer struct { name string gui *gocui.Gui view *gocui.View @@ -30,8 +30,8 @@ type LayerController struct { } // NewLayerController creates a new view object attached the the global [gocui] screen object. -func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (controller *LayerController, err error) { - controller = new(LayerController) +func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) { + controller = new(Layer) // populate main fields controller.name = name @@ -50,196 +50,196 @@ func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (con return controller, err } -func (controller *LayerController) Name() string { - return controller.name +func (c *Layer) Name() string { + return c.name } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (controller *LayerController) Setup(v *gocui.View, header *gocui.View) error { +func (c *Layer) Setup(v *gocui.View, header *gocui.View) error { // set controller options - controller.view = v - controller.view.Editable = false - controller.view.Wrap = false - controller.view.Frame = false + c.view = v + c.view.Editable = false + c.view.Wrap = false + c.view.Frame = false - controller.header = header - controller.header.Editable = false - controller.header.Wrap = false - controller.header.Frame = false + c.header = header + c.header.Editable = false + c.header.Wrap = false + c.header.Frame = false var infos = []key.BindingInfo{ { ConfigKeys: []string{"keybinding.compare-layer"}, - OnAction: func() error { return controller.setCompareMode(CompareLayer) }, - IsSelected: func() bool { return controller.CompareMode == CompareLayer }, + OnAction: func() error { return c.setCompareMode(CompareLayer) }, + IsSelected: func() bool { return c.CompareMode == CompareLayer }, Display: "Show layer changes", }, { ConfigKeys: []string{"keybinding.compare-all"}, - OnAction: func() error { return controller.setCompareMode(CompareAll) }, - IsSelected: func() bool { return controller.CompareMode == CompareAll }, + OnAction: func() error { return c.setCompareMode(CompareAll) }, + IsSelected: func() bool { return c.CompareMode == CompareAll }, Display: "Show aggregated changes", }, { Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, - OnAction: controller.CursorDown, + OnAction: c.CursorDown, }, { Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, - OnAction: controller.CursorUp, + OnAction: c.CursorUp, }, { Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, - OnAction: controller.CursorUp, + OnAction: c.CursorUp, }, { Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, - OnAction: controller.CursorDown, + OnAction: c.CursorDown, }, { ConfigKeys: []string{"keybinding.page-up"}, - OnAction: controller.PageUp, + OnAction: c.PageUp, }, { ConfigKeys: []string{"keybinding.page-down"}, - OnAction: controller.PageDown, + OnAction: c.PageDown, }, } - helpKeys, err := key.GenerateBindings(controller.gui, controller.name, infos) + helpKeys, err := key.GenerateBindings(c.gui, c.name, infos) if err != nil { return err } - controller.helpKeys = helpKeys + c.helpKeys = helpKeys - return controller.Render() + return c.Render() } // height obtains the height of the current pane (taking into account the lost space due to the header). -func (controller *LayerController) height() uint { - _, height := controller.view.Size() +func (c *Layer) height() uint { + _, height := c.view.Size() return uint(height - 1) } // IsVisible indicates if the layer view pane is currently initialized. -func (controller *LayerController) IsVisible() bool { - return controller != nil +func (c *Layer) IsVisible() bool { + return c != nil } // PageDown moves to next page putting the cursor on top -func (controller *LayerController) PageDown() error { - step := int(controller.height()) + 1 - targetLayerIndex := controller.LayerIndex + step +func (c *Layer) PageDown() error { + step := int(c.height()) + 1 + targetLayerIndex := c.LayerIndex + step - if targetLayerIndex > len(controller.Layers) { - step -= targetLayerIndex - (len(controller.Layers) - 1) + if targetLayerIndex > len(c.Layers) { + step -= targetLayerIndex - (len(c.Layers) - 1) } if step > 0 { - err := controllers.CursorStep(controller.gui, controller.view, step) + err := controllers.CursorStep(c.gui, c.view, step) if err == nil { - return controller.SetCursor(controller.LayerIndex + step) + return c.SetCursor(c.LayerIndex + step) } } return nil } // PageUp moves to previous page putting the cursor on top -func (controller *LayerController) PageUp() error { - step := int(controller.height()) + 1 - targetLayerIndex := controller.LayerIndex - step +func (c *Layer) PageUp() error { + step := int(c.height()) + 1 + targetLayerIndex := c.LayerIndex - step if targetLayerIndex < 0 { step += targetLayerIndex } if step > 0 { - err := controllers.CursorStep(controller.gui, controller.view, -step) + err := controllers.CursorStep(c.gui, c.view, -step) if err == nil { - return controller.SetCursor(controller.LayerIndex - step) + return c.SetCursor(c.LayerIndex - step) } } return nil } // CursorDown moves the cursor down in the layer pane (selecting a higher layer). -func (controller *LayerController) CursorDown() error { - if controller.LayerIndex < len(controller.Layers) { - err := controllers.CursorDown(controller.gui, controller.view) +func (c *Layer) CursorDown() error { + if c.LayerIndex < len(c.Layers) { + err := controllers.CursorDown(c.gui, c.view) if err == nil { - return controller.SetCursor(controller.LayerIndex + 1) + return c.SetCursor(c.LayerIndex + 1) } } return nil } // CursorUp moves the cursor up in the layer pane (selecting a lower layer). -func (controller *LayerController) CursorUp() error { - if controller.LayerIndex > 0 { - err := controllers.CursorUp(controller.gui, controller.view) +func (c *Layer) CursorUp() error { + if c.LayerIndex > 0 { + err := controllers.CursorUp(c.gui, c.view) if err == nil { - return controller.SetCursor(controller.LayerIndex - 1) + return c.SetCursor(c.LayerIndex - 1) } } return nil } // SetCursor resets the cursor and orients the file tree view based on the given layer index. -func (controller *LayerController) SetCursor(layer int) error { - controller.LayerIndex = layer - err := controllers.Tree.setTreeByLayer(controller.getCompareIndexes()) +func (c *Layer) SetCursor(layer int) error { + c.LayerIndex = layer + err := controllers.Tree.setTreeByLayer(c.getCompareIndexes()) if err != nil { return err } _ = controllers.Details.Render() - return controller.Render() + return c.Render() } // currentLayer returns the Layer object currently selected. -func (controller *LayerController) currentLayer() *image.Layer { - return controller.Layers[controller.LayerIndex] +func (c *Layer) currentLayer() *image.Layer { + return c.Layers[c.LayerIndex] } // setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison. -func (controller *LayerController) setCompareMode(compareMode CompareType) error { - controller.CompareMode = compareMode +func (c *Layer) setCompareMode(compareMode CompareType) error { + c.CompareMode = compareMode err := controllers.UpdateAndRender() if err != nil { logrus.Errorf("unable to set compare mode: %+v", err) return err } - return controllers.Tree.setTreeByLayer(controller.getCompareIndexes()) + return controllers.Tree.setTreeByLayer(c.getCompareIndexes()) } // getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode) -func (controller *LayerController) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { - bottomTreeStart = controller.CompareStartIndex - topTreeStop = controller.LayerIndex +func (c *Layer) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) { + bottomTreeStart = c.CompareStartIndex + topTreeStop = c.LayerIndex - if controller.LayerIndex == controller.CompareStartIndex { - bottomTreeStop = controller.LayerIndex - topTreeStart = controller.LayerIndex - } else if controller.CompareMode == CompareLayer { - bottomTreeStop = controller.LayerIndex - 1 - topTreeStart = controller.LayerIndex + if c.LayerIndex == c.CompareStartIndex { + bottomTreeStop = c.LayerIndex + topTreeStart = c.LayerIndex + } else if c.CompareMode == CompareLayer { + bottomTreeStop = c.LayerIndex - 1 + topTreeStart = c.LayerIndex } else { - bottomTreeStop = controller.CompareStartIndex - topTreeStart = controller.CompareStartIndex + 1 + bottomTreeStop = c.CompareStartIndex + topTreeStart = c.CompareStartIndex + 1 } return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop } // renderCompareBar returns the formatted string for the given layer. -func (controller *LayerController) renderCompareBar(layerIdx int) string { - bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := controller.getCompareIndexes() +func (c *Layer) renderCompareBar(layerIdx int) string { + bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := c.getCompareIndexes() result := " " if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop { @@ -253,10 +253,10 @@ func (controller *LayerController) renderCompareBar(layerIdx int) string { } // Update refreshes the state objects for future rendering (currently does nothing). -func (controller *LayerController) Update() error { - controller.ImageSize = 0 - for idx := 0; idx < len(controller.Layers); idx++ { - controller.ImageSize += controller.Layers[idx].Size +func (c *Layer) Update() error { + c.ImageSize = 0 + for idx := 0; idx < len(c.Layers); idx++ { + c.ImageSize += c.Layers[idx].Size } return nil } @@ -264,36 +264,36 @@ func (controller *LayerController) Update() error { // Render flushes the state objects to the screen. The layers pane reports: // 1. the layers of the image + metadata // 2. the current selected image -func (controller *LayerController) Render() error { +func (c *Layer) Render() error { // indicate when selected title := "Layers" - if controller.gui.CurrentView() == controller.view { + if c.gui.CurrentView() == c.view { title = "● " + title } - controller.gui.Update(func(g *gocui.Gui) error { + c.gui.Update(func(g *gocui.Gui) error { // update header - controller.header.Clear() + c.header.Clear() width, _ := g.Size() headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2)) headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command") - _, err := fmt.Fprintln(controller.header, format.Header(vtclean.Clean(headerStr, false))) + _, err := fmt.Fprintln(c.header, format.Header(vtclean.Clean(headerStr, false))) if err != nil { return err } // update contents - controller.view.Clear() - for idx, layer := range controller.Layers { + c.view.Clear() + for idx, layer := range c.Layers { layerStr := layer.String() - compareBar := controller.renderCompareBar(idx) + compareBar := c.renderCompareBar(idx) - if idx == controller.LayerIndex { - _, err = fmt.Fprintln(controller.view, compareBar+" "+format.Selected(layerStr)) + if idx == c.LayerIndex { + _, err = fmt.Fprintln(c.view, compareBar+" "+format.Selected(layerStr)) } else { - _, err = fmt.Fprintln(controller.view, compareBar+" "+layerStr) + _, err = fmt.Fprintln(c.view, compareBar+" "+layerStr) } if err != nil { @@ -308,9 +308,9 @@ func (controller *LayerController) Render() error { } // KeyHelp indicates all the possible actions a user can take while the current pane is selected. -func (controller *LayerController) KeyHelp() string { +func (c *Layer) KeyHelp() string { var help string - for _, binding := range controller.helpKeys { + for _, binding := range c.helpKeys { help += binding.RenderKeyHelp() } return help diff --git a/runtime/ui/controller/status_controller.go b/runtime/ui/controller/status.go similarity index 56% rename from runtime/ui/controller/status_controller.go rename to runtime/ui/controller/status.go index 7de9a30..9a3e612 100644 --- a/runtime/ui/controller/status_controller.go +++ b/runtime/ui/controller/status.go @@ -10,9 +10,9 @@ import ( "github.com/jroimartin/gocui" ) -// StatusController holds the UI objects and data models for populating the bottom-most pane. Specifically the panel +// Status holds the UI objects and data models for populating the bottom-most pane. Specifically the panel // shows the user a set of possible actions to take in the window and currently selected pane. -type StatusController struct { +type Status struct { name string gui *gocui.Gui view *gocui.View @@ -21,8 +21,8 @@ type StatusController struct { } // NewStatusController creates a new view object attached the the global [gocui] screen object. -func NewStatusController(name string, gui *gocui.Gui) (controller *StatusController) { - controller = new(StatusController) +func NewStatusController(name string, gui *gocui.Gui) (controller *Status) { + controller = new(Status) // populate main fields controller.name = name @@ -32,49 +32,49 @@ func NewStatusController(name string, gui *gocui.Gui) (controller *StatusControl return controller } -func (controller *StatusController) Name() string { - return controller.name +func (c *Status) Name() string { + return c.name } -func (controller *StatusController) AddHelpKeys(keys ...*key.Binding) { - controller.helpKeys = append(controller.helpKeys, keys...) +func (c *Status) AddHelpKeys(keys ...*key.Binding) { + c.helpKeys = append(c.helpKeys, keys...) } // Setup initializes the UI concerns within the context of a global [gocui] view object. -func (controller *StatusController) Setup(v *gocui.View, header *gocui.View) error { +func (c *Status) Setup(v *gocui.View, header *gocui.View) error { // set controller options - controller.view = v - controller.view.Frame = false + c.view = v + c.view.Frame = false - return controller.Render() + return c.Render() } // IsVisible indicates if the status view pane is currently initialized. -func (controller *StatusController) IsVisible() bool { - return controller != nil +func (c *Status) IsVisible() bool { + return c != nil } // CursorDown moves the cursor down in the details pane (currently indicates nothing). -func (controller *StatusController) CursorDown() error { +func (c *Status) CursorDown() error { return nil } // CursorUp moves the cursor up in the details pane (currently indicates nothing). -func (controller *StatusController) CursorUp() error { +func (c *Status) CursorUp() error { return nil } // Update refreshes the state objects for future rendering (currently does nothing). -func (controller *StatusController) Update() error { +func (c *Status) Update() error { return nil } // Render flushes the state objects to the screen. -func (controller *StatusController) Render() error { - controller.gui.Update(func(g *gocui.Gui) error { - controller.view.Clear() - _, err := fmt.Fprintln(controller.view, controller.KeyHelp()+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) +func (c *Status) Render() error { + c.gui.Update(func(g *gocui.Gui) error { + c.view.Clear() + _, err := fmt.Fprintln(c.view, c.KeyHelp()+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) if err != nil { logrus.Debug("unable to write to buffer: ", err) } @@ -85,9 +85,9 @@ func (controller *StatusController) Render() error { } // KeyHelp indicates all the possible global actions a user can take when any pane is selected. -func (controller *StatusController) KeyHelp() string { +func (c *Status) KeyHelp() string { var help string - for _, binding := range controller.helpKeys { + for _, binding := range c.helpKeys { help += binding.RenderKeyHelp() } return help diff --git a/runtime/ui/layout_manager.go b/runtime/ui/layout_manager.go index 561f683..fb59d54 100644 --- a/runtime/ui/layout_manager.go +++ b/runtime/ui/layout_manager.go @@ -9,10 +9,10 @@ import ( type layoutManager struct { fileTreeSplitRatio float64 - controllers *controller.ControllerCollection + controllers *controller.Collection } -func newLayoutManager(c *controller.ControllerCollection) *layoutManager { +func newLayoutManager(c *controller.Collection) *layoutManager { fileTreeSplitRatio := viper.GetFloat64("filetree.pane-width") if fileTreeSplitRatio >= 1 || fileTreeSplitRatio <= 0 { diff --git a/runtime/ui/ui.go b/runtime/ui/ui.go index b4fa834..e0cced8 100644 --- a/runtime/ui/ui.go +++ b/runtime/ui/ui.go @@ -16,7 +16,7 @@ const debug = false // type global type app struct { gui *gocui.Gui - controllers *controller.ControllerCollection + controllers *controller.Collection layout *layoutManager } @@ -28,10 +28,10 @@ var ( func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*app, error) { var err error once.Do(func() { - var theControls *controller.ControllerCollection + var theControls *controller.Collection var globalHelpKeys []*key.Binding - theControls, err = controller.NewControllerCollection(gui, analysis, cache) + theControls, err = controller.NewCollection(gui, analysis, cache) if err != nil { return } diff --git a/runtime/ui/viewmodel/filetree_viewmodel.go b/runtime/ui/viewmodel/filetree.go similarity index 89% rename from runtime/ui/viewmodel/filetree_viewmodel.go rename to runtime/ui/viewmodel/filetree.go index cf467e6..75cb469 100644 --- a/runtime/ui/viewmodel/filetree_viewmodel.go +++ b/runtime/ui/viewmodel/filetree.go @@ -15,7 +15,7 @@ import ( // 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 FileTree struct { ModelTree *filetree.FileTree ViewTree *filetree.FileTree RefTrees []*filetree.FileTree @@ -35,8 +35,8 @@ type FileTreeViewModel struct { } // 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) +func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeViewModel *FileTree, err error) { + treeViewModel = new(FileTree) // populate main fields treeViewModel.ShowAttributes = viper.GetBool("filetree.show-attributes") @@ -66,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 *FileTree) 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 *FileTree) height() int { if vm.ShowAttributes { return vm.refHeight - 1 } @@ -80,24 +80,24 @@ func (vm *FileTreeViewModel) height() int { } // bufferIndexUpperBound returns the current upper bounds for the view -func (vm *FileTreeViewModel) bufferIndexUpperBound() int { +func (vm *FileTree) 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 *FileTree) 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 *FileTree) 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 *FileTree) 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) } @@ -126,7 +126,7 @@ func (vm *FileTreeViewModel) SetTreeByLayer(bottomTreeStart, bottomTreeStop, top } // doCursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer. -func (vm *FileTreeViewModel) CursorUp() bool { +func (vm *FileTree) CursorUp() bool { if vm.TreeIndex <= 0 { return false } @@ -141,7 +141,7 @@ func (vm *FileTreeViewModel) CursorUp() bool { } // doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer. -func (vm *FileTreeViewModel) CursorDown() bool { +func (vm *FileTree) CursorDown() bool { if vm.TreeIndex >= vm.ModelTree.VisibleSize() { return false } @@ -157,7 +157,7 @@ func (vm *FileTreeViewModel) CursorDown() bool { } // CursorLeft moves the cursor up until we reach the Parent Node or top of the tree -func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error { +func (vm *FileTree) CursorLeft(filterRegex *regexp.Regexp) error { var visitor func(*filetree.FileNode) error var evaluator func(*filetree.FileNode) bool var dfsCounter, newIndex int @@ -208,7 +208,7 @@ func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error { } // CursorRight descends into directory expanding it if needed -func (vm *FileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error { +func (vm *FileTree) CursorRight(filterRegex *regexp.Regexp) error { node := vm.getAbsPositionNode(filterRegex) if node == nil { return nil @@ -240,7 +240,7 @@ func (vm *FileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error { } // PageDown moves to next page putting the cursor on top -func (vm *FileTreeViewModel) PageDown() error { +func (vm *FileTree) PageDown() error { nextBufferIndexLowerBound := vm.bufferIndexLowerBound + vm.height() nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height() @@ -266,7 +266,7 @@ func (vm *FileTreeViewModel) PageDown() error { } // PageUp moves to previous page putting the cursor on top -func (vm *FileTreeViewModel) PageUp() error { +func (vm *FileTree) PageUp() error { nextBufferIndexLowerBound := vm.bufferIndexLowerBound - vm.height() nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height() @@ -291,7 +291,7 @@ func (vm *FileTreeViewModel) PageUp() error { } // getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode. -func (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) { +func (vm *FileTree) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) { var visitor func(*filetree.FileNode) error var evaluator func(*filetree.FileNode) bool var dfsCounter int @@ -322,7 +322,7 @@ func (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (nod } // ToggleCollapse will collapse/expand the selected FileNode. -func (vm *FileTreeViewModel) ToggleCollapse(filterRegex *regexp.Regexp) error { +func (vm *FileTree) ToggleCollapse(filterRegex *regexp.Regexp) error { node := vm.getAbsPositionNode(filterRegex) if node != nil && node.Data.FileInfo.IsDir { node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed @@ -331,7 +331,7 @@ func (vm *FileTreeViewModel) ToggleCollapse(filterRegex *regexp.Regexp) error { } // ToggleCollapseAll will collapse/expand the all directories. -func (vm *FileTreeViewModel) ToggleCollapseAll() error { +func (vm *FileTree) ToggleCollapseAll() error { vm.CollapseAll = !vm.CollapseAll visitor := func(curNode *filetree.FileNode) error { @@ -352,18 +352,18 @@ func (vm *FileTreeViewModel) ToggleCollapseAll() error { } // ToggleCollapse will collapse/expand the selected FileNode. -func (vm *FileTreeViewModel) ToggleAttributes() error { +func (vm *FileTree) ToggleAttributes() error { vm.ShowAttributes = !vm.ShowAttributes return nil } // ToggleShowDiffType will show/hide the selected DiffType in the filetree pane. -func (vm *FileTreeViewModel) ToggleShowDiffType(diffType filetree.DiffType) { +func (vm *FileTree) ToggleShowDiffType(diffType filetree.DiffType) { vm.HiddenDiffTypes[diffType] = !vm.HiddenDiffTypes[diffType] } // Update refreshes the state objects for future rendering. -func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error { +func (vm *FileTree) Update(filterRegex *regexp.Regexp, width, height int) error { vm.refWidth = width vm.refHeight = height @@ -411,7 +411,7 @@ func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height in } // Render flushes the state objects (file tree) to the pane. -func (vm *FileTreeViewModel) Render() error { +func (vm *FileTree) Render() error { treeString := vm.ViewTree.StringBetween(vm.bufferIndexLowerBound, vm.bufferIndexUpperBound(), vm.ShowAttributes) lines := strings.Split(treeString, "\n") diff --git a/runtime/ui/viewmodel/filetree_viewmodel_test.go b/runtime/ui/viewmodel/filetree_test.go similarity index 98% rename from runtime/ui/viewmodel/filetree_viewmodel_test.go rename to runtime/ui/viewmodel/filetree_test.go index 3d92a67..c90f0d7 100644 --- a/runtime/ui/viewmodel/filetree_viewmodel_test.go +++ b/runtime/ui/viewmodel/filetree_test.go @@ -73,7 +73,7 @@ func assertTestData(t *testing.T, actualBytes []byte) { helperCheckDiff(t, expectedBytes, actualBytes) } -func initializeTestViewModel(t *testing.T) *FileTreeViewModel { +func initializeTestViewModel(t *testing.T) *FileTree { result := docker.TestAnalysisFromArchive(t, "../../../.data/test-docker-image.tar") cache := filetree.NewFileTreeCache(result.RefTrees) @@ -95,7 +95,7 @@ func initializeTestViewModel(t *testing.T) *FileTreeViewModel { return vm } -func runTestCase(t *testing.T, vm *FileTreeViewModel, width, height int, filterRegex *regexp.Regexp) { +func runTestCase(t *testing.T, vm *FileTree, width, height int, filterRegex *regexp.Regexp) { err := vm.Update(filterRegex, width, height) if err != nil { t.Errorf("failed to update viewmodel: %v", err) From 74e4fe2560a260079c3803a8b8c13bed3d678378 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sun, 13 Oct 2019 20:56:58 -0400 Subject: [PATCH 5/6] decouple views with listeners --- runtime/ui/{ui.go => app.go} | 9 +- runtime/ui/controller.go | 212 ++++++++++++++++++++ runtime/ui/controller/collection.go | 166 --------------- runtime/ui/controller/controller.go | 16 -- runtime/ui/key/binding.go | 1 - runtime/ui/layout_manager.go | 6 +- runtime/ui/view/cursor.go | 37 ++++ runtime/ui/{controller => view}/details.go | 35 ++-- runtime/ui/{controller => view}/filetree.go | 99 ++++++--- runtime/ui/{controller => view}/filter.go | 35 +++- runtime/ui/{controller => view}/layer.go | 64 +++--- runtime/ui/view/renderer.go | 9 + runtime/ui/{controller => view}/status.go | 20 +- runtime/ui/viewmodel/layer_selection.go | 10 + 14 files changed, 449 insertions(+), 270 deletions(-) rename runtime/ui/{ui.go => app.go} (92%) create mode 100644 runtime/ui/controller.go delete mode 100644 runtime/ui/controller/collection.go delete mode 100644 runtime/ui/controller/controller.go create mode 100644 runtime/ui/view/cursor.go rename runtime/ui/{controller => view}/details.go (82%) rename runtime/ui/{controller => view}/filetree.go (84%) rename runtime/ui/{controller => view}/filter.go (79%) rename runtime/ui/{controller => view}/layer.go (83%) create mode 100644 runtime/ui/view/renderer.go rename runtime/ui/{controller => view}/status.go (81%) create mode 100644 runtime/ui/viewmodel/layer_selection.go diff --git a/runtime/ui/ui.go b/runtime/ui/app.go similarity index 92% rename from runtime/ui/ui.go rename to runtime/ui/app.go index e0cced8..ec7f5ad 100644 --- a/runtime/ui/ui.go +++ b/runtime/ui/app.go @@ -2,7 +2,6 @@ package ui import ( "github.com/wagoodman/dive/dive/image" - "github.com/wagoodman/dive/runtime/ui/controller" "github.com/wagoodman/dive/runtime/ui/key" "sync" @@ -16,7 +15,7 @@ const debug = false // type global type app struct { gui *gocui.Gui - controllers *controller.Collection + controllers *Controller layout *layoutManager } @@ -28,10 +27,10 @@ var ( func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*app, error) { var err error once.Do(func() { - var theControls *controller.Collection + var theControls *Controller var globalHelpKeys []*key.Binding - theControls, err = controller.NewCollection(gui, analysis, cache) + theControls, err = NewCollection(gui, analysis, cache) if err != nil { return } @@ -110,7 +109,7 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC var lastX, lastY int // quit is the gocui callback invoked when the user hits Ctrl+C -func (ui *app) quit() error { +func (a *app) quit() error { // profileObj.Stop() // onExit() diff --git a/runtime/ui/controller.go b/runtime/ui/controller.go new file mode 100644 index 0000000..0eef2c2 --- /dev/null +++ b/runtime/ui/controller.go @@ -0,0 +1,212 @@ +package ui + +import ( + "github.com/jroimartin/gocui" + "github.com/sirupsen/logrus" + "github.com/wagoodman/dive/dive/filetree" + "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/runtime/ui/view" + "github.com/wagoodman/dive/runtime/ui/viewmodel" + "regexp" +) + +type Controller struct { + gui *gocui.Gui + Tree *view.FileTree + Layer *view.Layer + Status *view.Status + Filter *view.Filter + Details *view.Details + lookup map[string]view.Renderer +} + +func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*Controller, error) { + var err error + + controller := &Controller{ + gui: g, + } + controller.lookup = make(map[string]view.Renderer) + + controller.Layer, err = view.NewLayerView("layers", g, analysis.Layers) + if err != nil { + return nil, err + } + controller.lookup[controller.Layer.Name()] = controller.Layer + + treeStack, err := filetree.StackTreeRange(analysis.RefTrees, 0, 0) + if err != nil { + return nil, err + } + controller.Tree, err = view.NewFileTreeView("filetree", g, treeStack, analysis.RefTrees, cache) + if err != nil { + return nil, err + } + controller.lookup[controller.Tree.Name()] = controller.Tree + + // layer view cursor down event should trigger an update in the file tree + controller.Layer.AddLayerChangeListener(controller.onLayerChange) + + controller.Status = view.NewStatusView("status", g) + controller.lookup[controller.Status.Name()] = controller.Status + // set the layer view as the first selected view + controller.Status.SetCurrentView(controller.Layer) + + // update the status pane when a filetree option is changed by the user + controller.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange) + + controller.Filter = view.NewFilterView("filter", g) + controller.lookup[controller.Filter.Name()] = controller.Filter + controller.Filter.AddFilterEditListener(controller.onFilterEdit) + + controller.Details = view.NewDetailsView("details", g, analysis.Efficiency, analysis.Inefficiencies, analysis.SizeBytes) + controller.lookup[controller.Details.Name()] = controller.Details + + // propagate initial conditions to necessary views + err = controller.onLayerChange(viewmodel.LayerSelection{ + Layer: controller.Layer.CurrentLayer(), + BottomTreeStart: 0, + BottomTreeStop: 0, + TopTreeStart: 0, + TopTreeStop: 0, + }) + + if err != nil { + return nil, err + } + + return controller, nil +} + +func (c *Controller) onFileTreeViewOptionChange() error { + err := c.Status.Update() + if err != nil { + return err + } + return c.Status.Render() +} + +func (c *Controller) onFilterEdit(filter string) error { + var filterRegex *regexp.Regexp + var err error + + if len(filter) > 0 { + filterRegex, err = regexp.Compile(filter) + if err != nil { + return err + } + } + + c.Tree.SetFilterRegex(filterRegex) + + err = c.Tree.Update() + if err != nil { + return err + } + + return c.Tree.Render() +} + +func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error { + // update the details + c.Details.SetCurrentLayer(selection.Layer) + + // update the filetree + err := c.Tree.SetTree(selection.BottomTreeStart, selection.BottomTreeStop, selection.TopTreeStart, selection.TopTreeStop) + if err != nil { + return err + } + + if c.Layer.CompareMode == view.CompareAll { + c.Tree.SetTitle("Aggregated Layer Contents") + } else { + c.Tree.SetTitle("Current Layer Contents") + } + + // update details and filetree panes + return c.UpdateAndRender() +} + +func (c *Controller) UpdateAndRender() error { + err := c.Update() + if err != nil { + logrus.Debug("failed update: ", err) + return err + } + + err = c.Render() + if err != nil { + logrus.Debug("failed render: ", err) + return err + } + + return nil +} + +// Update refreshes the state objects for future rendering. +func (c *Controller) Update() error { + for _, controller := range c.lookup { + err := controller.Update() + if err != nil { + logrus.Debug("unable to update controller: ") + return err + } + } + return nil +} + +// Render flushes the state objects to the screen. +func (c *Controller) Render() error { + for _, controller := range c.lookup { + if controller.IsVisible() { + err := controller.Render() + if err != nil { + return err + } + } + } + return nil +} + +// ToggleView switches between the file view and the layer view and re-renders the screen. +func (c *Controller) ToggleView() (err error) { + v := c.gui.CurrentView() + if v == nil || v.Name() == c.Layer.Name() { + _, err = c.gui.SetCurrentView(c.Tree.Name()) + c.Status.SetCurrentView(c.Tree) + } else { + _, err = c.gui.SetCurrentView(c.Layer.Name()) + c.Status.SetCurrentView(c.Layer) + } + + if err != nil { + logrus.Error("unable to toggle view: ", err) + return err + } + + return c.UpdateAndRender() +} + +func (c *Controller) ToggleFilterView() error { + // delete all user input from the tree view + err := c.Filter.ToggleVisible() + if err != nil { + logrus.Error("unable to toggle filter visibility: ", err) + return err + } + + // we have just hidden the filter view... + if !c.Filter.IsVisible() { + // ...remove any filter from the tree + c.Tree.SetFilterRegex(nil) + + // ...adjust focus to a valid (visible) view + err = c.ToggleView() + if err != nil { + logrus.Error("unable to toggle filter view (back): ", err) + return err + } + } + + return c.UpdateAndRender() +} diff --git a/runtime/ui/controller/collection.go b/runtime/ui/controller/collection.go deleted file mode 100644 index a7c6124..0000000 --- a/runtime/ui/controller/collection.go +++ /dev/null @@ -1,166 +0,0 @@ -package controller - -import ( - "errors" - "github.com/jroimartin/gocui" - "github.com/sirupsen/logrus" - "github.com/wagoodman/dive/dive/filetree" - "github.com/wagoodman/dive/dive/image" -) - -// var ccOnce sync.Once -var controllers *Collection - -type Collection struct { - gui *gocui.Gui - Tree *FileTree - Layer *Layer - Status *Status - Filter *Filter - Details *Details - lookup map[string]Controller -} - -func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*Collection, error) { - var err error - - controllers = &Collection{ - gui: g, - } - 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 -} - -func (c *Collection) UpdateAndRender() error { - err := c.Update() - if err != nil { - logrus.Debug("failed update: ", err) - return err - } - - err = c.Render() - if err != nil { - logrus.Debug("failed render: ", err) - return err - } - - return nil -} - -// Update refreshes the state objects for future rendering. -func (c *Collection) Update() error { - for _, controller := range c.lookup { - err := controller.Update() - if err != nil { - logrus.Debug("unable to update controller: ") - return err - } - } - return nil -} - -// Render flushes the state objects to the screen. -func (c *Collection) Render() error { - for _, controller := range c.lookup { - if controller.IsVisible() { - err := controller.Render() - if err != nil { - return err - } - } - } - return nil -} - -// ToggleView switches between the file view and the layer view and re-renders the screen. -func (c *Collection) ToggleView() (err error) { - v := c.gui.CurrentView() - if v == nil || v.Name() == c.Layer.Name() { - _, err = c.gui.SetCurrentView(c.Tree.Name()) - } else { - _, err = c.gui.SetCurrentView(c.Layer.Name()) - } - - if err != nil { - logrus.Error("unable to toggle view: ", err) - return err - } - - return c.UpdateAndRender() -} - -func (c *Collection) ToggleFilterView() error { - // delete all user input from the tree view - err := c.Filter.ToggleVisible() - if err != nil { - logrus.Error("unable to toggle filter visibility: ", err) - return err - } - - // we have just hidden the filter view, adjust focus to a valid (visible) view - if !c.Filter.IsVisible() { - err = c.ToggleView() - if err != nil { - logrus.Error("unable to toggle filter view (back): ", err) - return err - } - } - - return c.UpdateAndRender() -} - -// CursorDown moves the cursor down in the currently selected gocui pane, scrolling the screen as needed. -func (c *Collection) CursorDown(g *gocui.Gui, v *gocui.View) error { - return c.CursorStep(g, v, 1) -} - -// CursorUp moves the cursor up in the currently selected gocui pane, scrolling the screen as needed. -func (c *Collection) CursorUp(g *gocui.Gui, v *gocui.View) error { - return c.CursorStep(g, v, -1) -} - -// Moves the cursor the given step distance, setting the origin to the new cursor line -func (c *Collection) CursorStep(g *gocui.Gui, v *gocui.View, step int) error { - cx, cy := v.Cursor() - - // if there isn't a next line - line, err := v.Line(cy + step) - if err != nil { - return err - } - if len(line) == 0 { - return errors.New("unable to move the cursor, empty line") - } - if err := v.SetCursor(cx, cy+step); err != nil { - ox, oy := v.Origin() - if err := v.SetOrigin(ox, oy+step); err != nil { - return err - } - } - return nil -} diff --git a/runtime/ui/controller/controller.go b/runtime/ui/controller/controller.go deleted file mode 100644 index 861b43f..0000000 --- a/runtime/ui/controller/controller.go +++ /dev/null @@ -1,16 +0,0 @@ -package controller - -import ( - "github.com/jroimartin/gocui" -) - -// Controller defines the a renderable terminal screen pane. -type Controller interface { - Update() error - Render() error - Setup(*gocui.View, *gocui.View) error - CursorDown() error - CursorUp() error - KeyHelp() string - IsVisible() bool -} diff --git a/runtime/ui/key/binding.go b/runtime/ui/key/binding.go index 777c047..40533b4 100644 --- a/runtime/ui/key/binding.go +++ b/runtime/ui/key/binding.go @@ -83,7 +83,6 @@ func newBinding(gui *gocui.Gui, influence string, keys []keybinding.Key, display } for _, key := range keys { - logrus.Debugf("registering %d %d (%+v)", key.Value, key.Modifier, key.Tokens) if err := gui.SetKeybinding(influence, key.Value, key.Modifier, binding.onAction); err != nil { return nil, err } diff --git a/runtime/ui/layout_manager.go b/runtime/ui/layout_manager.go index fb59d54..72f8c26 100644 --- a/runtime/ui/layout_manager.go +++ b/runtime/ui/layout_manager.go @@ -4,15 +4,15 @@ import ( "github.com/jroimartin/gocui" "github.com/sirupsen/logrus" "github.com/spf13/viper" - "github.com/wagoodman/dive/runtime/ui/controller" ) type layoutManager struct { fileTreeSplitRatio float64 - controllers *controller.Collection + controllers *Controller } -func newLayoutManager(c *controller.Collection) *layoutManager { +// todo: this needs a major refactor (derive layout from view obj info, which should not live here) +func newLayoutManager(c *Controller) *layoutManager { fileTreeSplitRatio := viper.GetFloat64("filetree.pane-width") if fileTreeSplitRatio >= 1 || fileTreeSplitRatio <= 0 { diff --git a/runtime/ui/view/cursor.go b/runtime/ui/view/cursor.go new file mode 100644 index 0000000..7cd5bcf --- /dev/null +++ b/runtime/ui/view/cursor.go @@ -0,0 +1,37 @@ +package view + +import ( + "errors" + "github.com/jroimartin/gocui" +) + +// CursorDown moves the cursor down in the currently selected gocui pane, scrolling the screen as needed. +func CursorDown(g *gocui.Gui, v *gocui.View) error { + return CursorStep(g, v, 1) +} + +// CursorUp moves the cursor up in the currently selected gocui pane, scrolling the screen as needed. +func CursorUp(g *gocui.Gui, v *gocui.View) error { + return CursorStep(g, v, -1) +} + +// Moves the cursor the given step distance, setting the origin to the new cursor line +func CursorStep(g *gocui.Gui, v *gocui.View, step int) error { + cx, cy := v.Cursor() + + // if there isn't a next line + line, err := v.Line(cy + step) + if err != nil { + return err + } + if len(line) == 0 { + return errors.New("unable to move the cursor, empty line") + } + if err := v.SetCursor(cx, cy+step); err != nil { + ox, oy := v.Origin() + if err := v.SetOrigin(ox, oy+step); err != nil { + return err + } + } + return nil +} diff --git a/runtime/ui/controller/details.go b/runtime/ui/view/details.go similarity index 82% rename from runtime/ui/controller/details.go rename to runtime/ui/view/details.go index 60336e1..9cb5ce0 100644 --- a/runtime/ui/controller/details.go +++ b/runtime/ui/view/details.go @@ -1,9 +1,10 @@ -package controller +package view import ( "fmt" "github.com/sirupsen/logrus" "github.com/wagoodman/dive/dive/filetree" + "github.com/wagoodman/dive/dive/image" "github.com/wagoodman/dive/runtime/ui/format" "github.com/wagoodman/dive/runtime/ui/key" "strconv" @@ -23,10 +24,13 @@ type Details struct { header *gocui.View efficiency float64 inefficiencies filetree.EfficiencySlice + imageSize uint64 + + currentLayer *image.Layer } -// 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 *Details) { +// NewDetailsView creates a new view object attached the the global [gocui] screen object. +func NewDetailsView(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice, imageSize uint64) (controller *Details) { controller = new(Details) // populate main fields @@ -34,6 +38,7 @@ func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, ineff controller.gui = gui controller.efficiency = efficiency controller.inefficiencies = inefficiencies + controller.imageSize = imageSize return controller } @@ -85,12 +90,12 @@ func (c *Details) IsVisible() bool { // CursorDown moves the cursor down in the details pane (currently indicates nothing). func (c *Details) CursorDown() error { - return controllers.CursorDown(c.gui, c.view) + return CursorDown(c.gui, c.view) } // CursorUp moves the cursor up in the details pane (currently indicates nothing). func (c *Details) CursorUp() error { - return controllers.CursorUp(c.gui, c.view) + return CursorUp(c.gui, c.view) } // Update refreshes the state objects for future rendering. @@ -98,13 +103,19 @@ func (c *Details) Update() error { return nil } +func (c *Details) SetCurrentLayer(layer *image.Layer) { + c.currentLayer = layer +} + // Render flushes the state objects to the screen. The details pane reports: // 1. the current selected layer's command string // 2. the image efficiency score // 3. the estimated wasted image space // 4. a list of inefficient file allocations func (c *Details) Render() error { - currentLayer := controllers.Layer.currentLayer() + if c.currentLayer == nil { + return fmt.Errorf("no layer selected") + } var wastedSpace int64 @@ -126,7 +137,7 @@ func (c *Details) Render() error { } } - imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(controllers.Layer.ImageSize)) + imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(c.imageSize)) effStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*c.efficiency)) wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace))) @@ -147,15 +158,15 @@ func (c *Details) Render() error { c.view.Clear() var lines = make([]string, 0) - if currentLayer.Names != nil && len(currentLayer.Names) > 0 { - lines = append(lines, format.Header("Tags: ")+strings.Join(currentLayer.Names, ", ")) + if c.currentLayer.Names != nil && len(c.currentLayer.Names) > 0 { + lines = append(lines, format.Header("Tags: ")+strings.Join(c.currentLayer.Names, ", ")) } else { lines = append(lines, format.Header("Tags: ")+"(none)") } - lines = append(lines, format.Header("Id: ")+currentLayer.Id) - lines = append(lines, format.Header("Digest: ")+currentLayer.Digest) + lines = append(lines, format.Header("Id: ")+c.currentLayer.Id) + lines = append(lines, format.Header("Digest: ")+c.currentLayer.Digest) lines = append(lines, format.Header("Command:")) - lines = append(lines, currentLayer.Command) + lines = append(lines, c.currentLayer.Command) lines = append(lines, "\n"+format.Header(vtclean.Clean(imageHeaderStr, false))) lines = append(lines, imageSizeStr) lines = append(lines, wastedSpaceStr) diff --git a/runtime/ui/controller/filetree.go b/runtime/ui/view/filetree.go similarity index 84% rename from runtime/ui/controller/filetree.go rename to runtime/ui/view/filetree.go index df10a6b..3185be4 100644 --- a/runtime/ui/controller/filetree.go +++ b/runtime/ui/view/filetree.go @@ -1,7 +1,8 @@ -package controller +package view import ( "fmt" + "github.com/sirupsen/logrus" "github.com/wagoodman/dive/runtime/ui/format" "github.com/wagoodman/dive/runtime/ui/key" "github.com/wagoodman/dive/runtime/ui/viewmodel" @@ -20,6 +21,8 @@ const ( type CompareType int +type ViewOptionChangeListener func() error + // FileTree 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 FileTree struct { @@ -28,13 +31,19 @@ type FileTree struct { view *gocui.View header *gocui.View vm *viewmodel.FileTree + title string + + filterRegex *regexp.Regexp + + listeners []ViewOptionChangeListener 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 *FileTree, err error) { +// NewFileTreeView creates a new view object attached the the global [gocui] screen object. +func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *FileTree, err error) { controller = new(FileTree) + controller.listeners = make([]ViewOptionChangeListener, 0) // populate main fields controller.name = name @@ -47,6 +56,18 @@ func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, return controller, err } +func (c *FileTree) AddViewOptionChangeListener(listener ...ViewOptionChangeListener) { + c.listeners = append(c.listeners, listener...) +} + +func (c *FileTree) SetTitle(title string) { + c.title = title +} + +func (c *FileTree) SetFilterRegex(filterRegex *regexp.Regexp) { + c.filterRegex = filterRegex +} + func (c *FileTree) Name() string { return c.name } @@ -166,12 +187,11 @@ func (c *FileTree) resetCursor() { } // SetTreeByLayer populates the view model by stacking the indicated image layer file trees. -func (c *FileTree) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error { +func (c *FileTree) SetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error { err := c.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop) if err != nil { return err } - // controller.ResetCursor() _ = c.Update() return c.Render() @@ -201,7 +221,7 @@ func (c *FileTree) CursorUp() error { // CursorLeft moves the cursor up until we reach the Parent Node or top of the tree func (c *FileTree) CursorLeft() error { - err := c.vm.CursorLeft(filterRegex()) + err := c.vm.CursorLeft(c.filterRegex) if err != nil { return err } @@ -211,7 +231,7 @@ func (c *FileTree) CursorLeft() error { // CursorRight descends into directory expanding it if needed func (c *FileTree) CursorRight() error { - err := c.vm.CursorRight(filterRegex()) + err := c.vm.CursorRight(c.filterRegex) if err != nil { return err } @@ -244,7 +264,7 @@ func (c *FileTree) PageUp() error { // ToggleCollapse will collapse/expand the selected FileNode. func (c *FileTree) toggleCollapse() error { - err := c.vm.ToggleCollapse(filterRegex()) + err := c.vm.ToggleCollapse(c.filterRegex) if err != nil { return err } @@ -265,44 +285,61 @@ func (c *FileTree) toggleCollapseAll() error { return c.Render() } +func (c *FileTree) notifyOnViewOptionChangeListeners() error { + for _, listener := range c.listeners { + err := listener() + if err != nil { + logrus.Errorf("notifyOnViewOptionChangeListeners error: %+v", err) + return err + } + } + return nil +} + // ToggleAttributes will show/hide file attributes func (c *FileTree) toggleAttributes() error { err := c.vm.ToggleAttributes() if err != nil { return err } + + err = c.Update() + if err != nil { + return err + } + err = c.Render() + if err != nil { + return err + } + // we need to render the changes to the status pane as well (not just this contoller/view) - return controllers.UpdateAndRender() + return c.notifyOnViewOptionChangeListeners() } // ToggleShowDiffType will show/hide the selected DiffType in the filetree pane. func (c *FileTree) toggleShowDiffType(diffType filetree.DiffType) error { c.vm.ToggleShowDiffType(diffType) - // we need to render the changes to the status pane as well (not just this contoller/view) - return controllers.UpdateAndRender() -} -// 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 { - return nil - } - filterString := strings.TrimSpace(controllers.Filter.view.Buffer()) - if len(filterString) == 0 { - return nil - } - - regex, err := regexp.Compile(filterString) + err := c.Update() if err != nil { - return nil + return err + } + err = c.Render() + if err != nil { + return err } - return regex + // we need to render the changes to the status pane as well (not just this contoller/view) + return c.notifyOnViewOptionChangeListeners() } // OnLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions func (c *FileTree) OnLayoutChange(resized bool) error { - _ = c.Update() + err := c.Update() + if err != nil { + return err + } + if resized { return c.Render() } @@ -320,19 +357,15 @@ func (c *FileTree) Update() error { width, height = c.gui.Size() } // height should account for the header - return c.vm.Update(filterRegex(), width, height-1) + return c.vm.Update(c.filterRegex, width, height-1) } // Render flushes the state objects (file tree) to the pane. func (c *FileTree) Render() error { - title := "Current Layer Contents" - if controllers.Layer.CompareMode == CompareAll { - title = "Aggregated Layer Contents" - } - + title := c.title // indicate when selected if c.gui.CurrentView() == c.view { - title = "● " + title + title = "● " + c.title } c.gui.Update(func(g *gocui.Gui) error { diff --git a/runtime/ui/controller/filter.go b/runtime/ui/view/filter.go similarity index 79% rename from runtime/ui/controller/filter.go rename to runtime/ui/view/filter.go index 51c0e3b..e32957a 100644 --- a/runtime/ui/controller/filter.go +++ b/runtime/ui/view/filter.go @@ -1,12 +1,15 @@ -package controller +package view import ( "fmt" "github.com/jroimartin/gocui" "github.com/sirupsen/logrus" "github.com/wagoodman/dive/runtime/ui/format" + "strings" ) +type FilterEditListener func(string) error + // Filter holds the UI objects and data models for populating the bottom row. Specifically the pane that // allows the user to filter the file tree by path. type Filter struct { @@ -17,12 +20,16 @@ type Filter struct { headerStr string maxLength int hidden bool + + filterEditListeners []FilterEditListener } -// NewFilterController creates a new view object attached the the global [gocui] screen object. -func NewFilterController(name string, gui *gocui.Gui) (controller *Filter) { +// NewFilterView creates a new view object attached the the global [gocui] screen object. +func NewFilterView(name string, gui *gocui.Gui) (controller *Filter) { controller = new(Filter) + controller.filterEditListeners = make([]FilterEditListener, 0) + // populate main fields controller.name = name controller.gui = gui @@ -32,6 +39,10 @@ func NewFilterController(name string, gui *gocui.Gui) (controller *Filter) { return controller } +func (c *Filter) AddFilterEditListener(listener ...FilterEditListener) { + c.filterEditListeners = append(c.filterEditListeners, listener...) +} + func (c *Filter) Name() string { return c.name } @@ -70,7 +81,7 @@ func (c *Filter) ToggleVisible() error { logrus.Error("unable to toggle filter view: ", err) return err } - return controllers.UpdateAndRender() + return nil } // reset the cursor for the next time it is visible @@ -119,9 +130,19 @@ func (c *Filter) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) case key == gocui.KeyBackspace || key == gocui.KeyBackspace2: v.EditDelete(true) } - if controllers.Tree != nil { - _ = controllers.Tree.Update() - _ = controllers.Tree.Render() + + // notify listeners + c.notifyFilterEditListeners() +} + +func (c *Filter) notifyFilterEditListeners() { + currentValue := strings.TrimSpace(c.view.Buffer()) + for _, listener := range c.filterEditListeners { + err := listener(currentValue) + if err != nil { + // note: cannot propagate error from here since this is from the main gogui thread + logrus.Errorf("notifyFilterEditListeners: %+v", err) + } } } diff --git a/runtime/ui/controller/layer.go b/runtime/ui/view/layer.go similarity index 83% rename from runtime/ui/controller/layer.go rename to runtime/ui/view/layer.go index 948a0b1..5d3c678 100644 --- a/runtime/ui/controller/layer.go +++ b/runtime/ui/view/layer.go @@ -1,10 +1,11 @@ -package controller +package view import ( "fmt" "github.com/wagoodman/dive/dive/image" "github.com/wagoodman/dive/runtime/ui/format" "github.com/wagoodman/dive/runtime/ui/key" + "github.com/wagoodman/dive/runtime/ui/viewmodel" "strings" "github.com/jroimartin/gocui" @@ -13,6 +14,8 @@ import ( "github.com/spf13/viper" ) +type LayerChangeListener func(viewmodel.LayerSelection) error + // Layer holds the UI objects and data models for populating the lower-left pane. Specifically the pane that // shows the image layers and layer selector. type Layer struct { @@ -24,15 +27,18 @@ type Layer struct { Layers []*image.Layer CompareMode CompareType CompareStartIndex int - ImageSize uint64 + + listeners []LayerChangeListener helpKeys []*key.Binding } -// NewLayerController creates a new view object attached the the global [gocui] screen object. -func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) { +// NewLayerView creates a new view object attached the the global [gocui] screen object. +func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) { controller = new(Layer) + controller.listeners = make([]LayerChangeListener, 0) + // populate main fields controller.name = name controller.gui = gui @@ -50,6 +56,29 @@ func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (con return controller, err } +func (c *Layer) AddLayerChangeListener(listener ...LayerChangeListener) { + c.listeners = append(c.listeners, listener...) +} + +func (c *Layer) notifyLayerChangeListeners() error { + bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := c.getCompareIndexes() + selection := viewmodel.LayerSelection{ + Layer: c.CurrentLayer(), + BottomTreeStart: bottomTreeStart, + BottomTreeStop: bottomTreeStop, + TopTreeStart: topTreeStart, + TopTreeStop: topTreeStop, + } + for _, listener := range c.listeners { + err := listener(selection) + if err != nil { + logrus.Errorf("notifyLayerChangeListeners error: %+v", err) + return err + } + } + return nil +} + func (c *Layer) Name() string { return c.name } @@ -141,7 +170,7 @@ func (c *Layer) PageDown() error { } if step > 0 { - err := controllers.CursorStep(c.gui, c.view, step) + err := CursorStep(c.gui, c.view, step) if err == nil { return c.SetCursor(c.LayerIndex + step) } @@ -159,7 +188,7 @@ func (c *Layer) PageUp() error { } if step > 0 { - err := controllers.CursorStep(c.gui, c.view, -step) + err := CursorStep(c.gui, c.view, -step) if err == nil { return c.SetCursor(c.LayerIndex - step) } @@ -170,7 +199,7 @@ func (c *Layer) PageUp() error { // CursorDown moves the cursor down in the layer pane (selecting a higher layer). func (c *Layer) CursorDown() error { if c.LayerIndex < len(c.Layers) { - err := controllers.CursorDown(c.gui, c.view) + err := CursorDown(c.gui, c.view) if err == nil { return c.SetCursor(c.LayerIndex + 1) } @@ -181,7 +210,7 @@ func (c *Layer) CursorDown() error { // CursorUp moves the cursor up in the layer pane (selecting a lower layer). func (c *Layer) CursorUp() error { if c.LayerIndex > 0 { - err := controllers.CursorUp(c.gui, c.view) + err := CursorUp(c.gui, c.view) if err == nil { return c.SetCursor(c.LayerIndex - 1) } @@ -192,30 +221,23 @@ func (c *Layer) CursorUp() error { // SetCursor resets the cursor and orients the file tree view based on the given layer index. func (c *Layer) SetCursor(layer int) error { c.LayerIndex = layer - err := controllers.Tree.setTreeByLayer(c.getCompareIndexes()) + err := c.notifyLayerChangeListeners() if err != nil { return err } - _ = controllers.Details.Render() - return c.Render() } -// currentLayer returns the Layer object currently selected. -func (c *Layer) currentLayer() *image.Layer { +// CurrentLayer returns the Layer object currently selected. +func (c *Layer) CurrentLayer() *image.Layer { return c.Layers[c.LayerIndex] } // setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison. func (c *Layer) setCompareMode(compareMode CompareType) error { c.CompareMode = compareMode - err := controllers.UpdateAndRender() - if err != nil { - logrus.Errorf("unable to set compare mode: %+v", err) - return err - } - return controllers.Tree.setTreeByLayer(c.getCompareIndexes()) + return c.notifyLayerChangeListeners() } // getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode) @@ -254,10 +276,6 @@ func (c *Layer) renderCompareBar(layerIdx int) string { // Update refreshes the state objects for future rendering (currently does nothing). func (c *Layer) Update() error { - c.ImageSize = 0 - for idx := 0; idx < len(c.Layers); idx++ { - c.ImageSize += c.Layers[idx].Size - } return nil } diff --git a/runtime/ui/view/renderer.go b/runtime/ui/view/renderer.go new file mode 100644 index 0000000..c3fadf5 --- /dev/null +++ b/runtime/ui/view/renderer.go @@ -0,0 +1,9 @@ +package view + +// Controller defines the a renderable terminal screen pane. +type Renderer interface { + Update() error + Render() error + IsVisible() bool + KeyHelp() string +} diff --git a/runtime/ui/controller/status.go b/runtime/ui/view/status.go similarity index 81% rename from runtime/ui/controller/status.go rename to runtime/ui/view/status.go index 9a3e612..bdd0363 100644 --- a/runtime/ui/controller/status.go +++ b/runtime/ui/view/status.go @@ -1,4 +1,4 @@ -package controller +package view import ( "fmt" @@ -17,11 +17,13 @@ type Status struct { gui *gocui.Gui view *gocui.View + selectedView Renderer + helpKeys []*key.Binding } -// NewStatusController creates a new view object attached the the global [gocui] screen object. -func NewStatusController(name string, gui *gocui.Gui) (controller *Status) { +// NewStatusView creates a new view object attached the the global [gocui] screen object. +func NewStatusView(name string, gui *gocui.Gui) (controller *Status) { controller = new(Status) // populate main fields @@ -32,6 +34,10 @@ func NewStatusController(name string, gui *gocui.Gui) (controller *Status) { return controller } +func (c *Status) SetCurrentView(r Renderer) { + c.selectedView = r +} + func (c *Status) Name() string { return c.name } @@ -74,7 +80,13 @@ func (c *Status) Update() error { func (c *Status) Render() error { c.gui.Update(func(g *gocui.Gui) error { c.view.Clear() - _, err := fmt.Fprintln(c.view, c.KeyHelp()+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) + + var selectedHelp string + if c.selectedView != nil { + selectedHelp = c.selectedView.KeyHelp() + } + + _, err := fmt.Fprintln(c.view, c.KeyHelp()+selectedHelp+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) if err != nil { logrus.Debug("unable to write to buffer: ", err) } diff --git a/runtime/ui/viewmodel/layer_selection.go b/runtime/ui/viewmodel/layer_selection.go new file mode 100644 index 0000000..47bc5e2 --- /dev/null +++ b/runtime/ui/viewmodel/layer_selection.go @@ -0,0 +1,10 @@ +package viewmodel + +import ( + "github.com/wagoodman/dive/dive/image" +) + +type LayerSelection struct { + Layer *image.Layer + BottomTreeStart, BottomTreeStop, TopTreeStart, TopTreeStop int +} From 6dd7f7af6ec6d98d81df8ab739d311007c7bfdfb Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 18 Oct 2019 17:39:15 -0400 Subject: [PATCH 6/6] fix non-linux build (exclude podman) --- dive/image/podman/resolver_notlinux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dive/image/podman/resolver_notlinux.go b/dive/image/podman/resolver_notlinux.go index 49b959a..8dfdd22 100644 --- a/dive/image/podman/resolver_notlinux.go +++ b/dive/image/podman/resolver_notlinux.go @@ -9,7 +9,7 @@ import ( type resolver struct{} -func NewResolver() *resolver { +func NewResolverFromEngine() *resolver { return &resolver{} }