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 }