From 91d58ab09da0d062203c10a3aeb839e2a6724797 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sun, 9 Feb 2020 14:33:52 -0500 Subject: basic pane swap working --- cmd/root.go | 1 + runtime/ui/app.go | 66 ++++++++++++--- runtime/ui/controller.go | 3 +- runtime/ui/format/format.go | 10 ++- runtime/ui/layout/manager.go | 17 ++++ runtime/ui/view/debug.go | 30 ++++++- runtime/ui/view/details.go | 7 +- runtime/ui/view/filetree.go | 188 +++++++++++++++++++++++++------------------ runtime/ui/view/filter.go | 13 +++ runtime/ui/view/layer.go | 2 +- runtime/ui/view/status.go | 9 +++ 11 files changed, 247 insertions(+), 99 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index cdbc60f..f354ee3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -73,6 +73,7 @@ func initConfig() { viper.SetDefault("keybinding.quit", "ctrl+c") viper.SetDefault("keybinding.toggle-view", "tab") viper.SetDefault("keybinding.filter-files", "ctrl+f, ctrl+slash") + viper.SetDefault("keybinding.toggle-details", "ctrl+d") // keybindings: layer view viper.SetDefault("keybinding.compare-all", "ctrl+a") viper.SetDefault("keybinding.compare-layer", "ctrl+l") diff --git a/runtime/ui/app.go b/runtime/ui/app.go index c1b48a7..4365cd6 100644 --- a/runtime/ui/app.go +++ b/runtime/ui/app.go @@ -16,9 +16,10 @@ const debug = false // type global type app struct { - gui *gocui.Gui - controllers *Controller - layout *layout.Manager + gui *gocui.Gui + controller *Controller + layout *layout.Manager + detailedMode bool } var ( @@ -32,7 +33,7 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa var controller *Controller var globalHelpKeys []*key.Binding - controller, err = NewCollection(gui, analysis, cache) + controller, err = NewController(gui, analysis, cache) if err != nil { return } @@ -59,9 +60,10 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa // } appSingleton = &app{ - gui: gui, - controllers: controller, - layout: lm, + gui: gui, + controller: controller, + layout: lm, + detailedMode: false, } var infos = []key.BindingInfo{ @@ -90,6 +92,19 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa controller.views.Status.AddHelpKeys(globalHelpKeys...) + // dont show these key bindings on the status pane + quietKeys := []key.BindingInfo{ + { + ConfigKeys: []string{"keybinding.toggle-details"}, + OnAction: appSingleton.toggleDetails, + Display: "", + }, + } + _, err = key.GenerateBindings(gui, "", quietKeys) + if err != nil { + return + } + // perform the first update and render now that all resources have been loaded err = controller.UpdateAndRender() if err != nil { @@ -106,8 +121,8 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa // 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 controller.Tree != nil && controller.Tree.gui != nil { +// v, _ := controller.Tree.gui.View("debug") // if v != nil { // if len(v.BufferLines()) > 20 { // v.Clear() @@ -126,6 +141,39 @@ func (a *app) quit() error { return gocui.ErrQuit } +func (a *app) toggleDetails() error { + if a.detailedMode { + err := a.layout.Remove(a.controller.views.Debug) + if err != nil { + logrus.Errorf("could not remove DEBUG pane") + } + a.layout.Add(a.controller.views.Tree, layout.LocationColumn) + a.controller.views.Debug.ToggleHide() + a.controller.views.Tree.ToggleHide() + + a.controller.views.Status.Retop() + a.controller.views.Filter.Retop() + + a.detailedMode = false + } else { + err := a.layout.Remove(a.controller.views.Tree) + if err != nil { + logrus.Errorf("could not remove TREE pane") + } + a.layout.Add(a.controller.views.Debug, layout.LocationColumn) + a.controller.views.Debug.ToggleHide() + a.controller.views.Tree.ToggleHide() + + a.controller.views.Status.Retop() + a.controller.views.Filter.Retop() + + a.detailedMode = true + } + + return nil +} + + // Run is the UI entrypoint. func Run(analysis *image.AnalysisResult, treeStack filetree.Comparer) error { var err error diff --git a/runtime/ui/controller.go b/runtime/ui/controller.go index d9ca330..59dbdd0 100644 --- a/runtime/ui/controller.go +++ b/runtime/ui/controller.go @@ -13,9 +13,10 @@ import ( type Controller struct { gui *gocui.Gui views *view.Views + } -func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Comparer) (*Controller, error) { +func NewController(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Comparer) (*Controller, error) { views, err := view.NewViews(g, analysis, cache) if err != nil { return nil, err diff --git a/runtime/ui/format/format.go b/runtime/ui/format/format.go index 6a7c671..b0bc97e 100644 --- a/runtime/ui/format/format.go +++ b/runtime/ui/format/format.go @@ -65,7 +65,11 @@ func RenderNoHeader(width int, selected bool) string { return strings.Repeat(fillStr, width) } -func RenderHeader(title string, width int, selected bool) string { +func RenderHeader(title string, width int, selected bool, nl bool) string { + newLine := "\n" + if !nl{ + newLine = "" + } if selected { body := Header(fmt.Sprintf("%s%s ", selectStr, title)) bodyLen := len(vtclean.Clean(body, false)) @@ -73,7 +77,7 @@ func RenderHeader(title string, width int, selected bool) string { if repeatCount < 0 { repeatCount = 0 } - return fmt.Sprintf("%s%s%s%s\n", selectedLeftBracketStr, body, selectedRightBracketStr, strings.Repeat(selectedFillStr, repeatCount)) + return fmt.Sprintf("%s%s%s%s%s", selectedLeftBracketStr, body, selectedRightBracketStr, strings.Repeat(selectedFillStr, repeatCount), newLine) //return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), Selected(strings.Repeat(selectedFillStr, width-bodyLen-2))) //return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), strings.Repeat(selectedFillStr, width-bodyLen-2)) } @@ -83,7 +87,7 @@ func RenderHeader(title string, width int, selected bool) string { if repeatCount < 0 { repeatCount = 0 } - return fmt.Sprintf("%s%s%s%s\n", leftBracketStr, body, rightBracketStr, strings.Repeat(fillStr, repeatCount)) + return fmt.Sprintf("%s%s%s%s%s", leftBracketStr, body, rightBracketStr, strings.Repeat(fillStr, repeatCount), newLine) } func RenderHelpKey(control, title string, selected bool) string { diff --git a/runtime/ui/layout/manager.go b/runtime/ui/layout/manager.go index 460cbea..33e04d5 100644 --- a/runtime/ui/layout/manager.go +++ b/runtime/ui/layout/manager.go @@ -1,6 +1,7 @@ package layout import ( + "fmt" "github.com/jroimartin/gocui" "github.com/sirupsen/logrus" ) @@ -26,6 +27,22 @@ func (lm *Manager) Add(element Layout, location Location) { lm.elements[location] = append(lm.elements[location], element) } +func (lm *Manager) Remove(element Layout) error { + for location, elements := range lm.elements { + idx := -1 + for i, el := range elements { + if el == element { + idx = i + } + } + if idx >= 0 { + lm.elements[location] = append(elements[:idx], elements[idx+1:]...) + return nil + } + } + return fmt.Errorf("could not remove element from layout manager") +} + func (lm *Manager) planAndLayoutHeaders(g *gocui.Gui, area Area) (Area, error) { // layout headers top down if elements, exists := lm.elements[LocationHeader]; exists { diff --git a/runtime/ui/view/debug.go b/runtime/ui/view/debug.go index aab9d06..e78340e 100644 --- a/runtime/ui/view/debug.go +++ b/runtime/ui/view/debug.go @@ -14,6 +14,8 @@ type Debug struct { gui *gocui.Gui view *gocui.View header *gocui.View + //once sync.Once + hidden bool selectedView Helper } @@ -25,6 +27,7 @@ func newDebugView(gui *gocui.Gui) (controller *Debug) { // populate main fields controller.name = "debug" controller.gui = gui + controller.hidden = true return controller } @@ -37,6 +40,29 @@ func (v *Debug) Name() string { return v.name } +func (v *Debug) ToggleHide() error { + v.hidden = !v.hidden + if v.hidden { + logrus.Trace("hiding debug view...") + + // take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop + err := v.gui.DeleteView(v.Name()) + if err != nil { + return err + } + // take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop + err = v.gui.DeleteView(v.Name() + "header") + if err != nil { + return err + } + } + return nil +} + +func (v *Debug) IsHidden() bool { + return v.hidden +} + // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *Debug) Setup(view *gocui.View, header *gocui.View) error { logrus.Tracef("view.Setup() %s", v.Name()) @@ -82,7 +108,7 @@ func (v *Debug) Render() error { // update header... v.header.Clear() width, _ := g.Size() - headerStr := format.RenderHeader("Debug", width, false) + headerStr := format.RenderHeader("Debug", width, false, false) _, _ = fmt.Fprintln(v.header, headerStr) // update view... @@ -98,7 +124,7 @@ func (v *Debug) Render() error { } func (v *Debug) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { - logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) + logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d, hidden: %v) %s", minX, minY, maxX, maxY, v.hidden, v.Name()) // header headerSize := 1 diff --git a/runtime/ui/view/details.go b/runtime/ui/view/details.go index 95ee962..d8a0112 100644 --- a/runtime/ui/view/details.go +++ b/runtime/ui/view/details.go @@ -157,8 +157,8 @@ func (v *Details) Render() error { v.header.Clear() width, _ := v.view.Size() - layerHeaderStr := format.RenderHeader("Layer Details", width, false) - imageHeaderStr := format.RenderHeader("Image Details", width, false) + layerHeaderStr := format.RenderHeader("Layer Details", width, false, false) + imageHeaderStr := format.RenderHeader("Image", width, false, false) _, err := fmt.Fprintln(v.header, layerHeaderStr) if err != nil { @@ -182,7 +182,8 @@ func (v *Details) Render() error { lines = append(lines, imageSizeStr) lines = append(lines, wastedSpaceStr) lines = append(lines, effStr+"\n") - lines = append(lines, inefficiencyReport) + lines = append(lines, format.Header("[^D for detailed image information]")) + //lines = append(lines, inefficiencyReport) _, err = fmt.Fprintln(v.view, strings.Join(lines, "\n")) if err != nil { diff --git a/runtime/ui/view/filetree.go b/runtime/ui/view/filetree.go index 97e6f7b..0eabfd7 100644 --- a/runtime/ui/view/filetree.go +++ b/runtime/ui/view/filetree.go @@ -11,6 +11,7 @@ import ( "github.com/wagoodman/dive/runtime/ui/viewmodel" "github.com/wagoodman/dive/utils" "regexp" + "sync" ) type ViewOptionChangeListener func() error @@ -22,6 +23,8 @@ type FileTree struct { gui *gocui.Gui view *gocui.View header *gocui.View + once sync.Once + hidden bool vm *viewmodel.FileTree title string @@ -73,6 +76,7 @@ func (v *FileTree) Name() string { // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *FileTree) Setup(view *gocui.View, header *gocui.View) error { logrus.Tracef("view.Setup() %s", v.Name()) + var err error // set controller options v.view = view @@ -85,89 +89,90 @@ func (v *FileTree) Setup(view *gocui.View, header *gocui.View) error { v.header.Wrap = false v.header.Frame = false - var infos = []key.BindingInfo{ - { - ConfigKeys: []string{"keybinding.toggle-collapse-dir"}, - OnAction: v.toggleCollapse, - Display: "Collapse dir", - }, - { - ConfigKeys: []string{"keybinding.toggle-collapse-all-dir"}, - OnAction: v.toggleCollapseAll, - Display: "Collapse all dir", - }, - { - ConfigKeys: []string{"keybinding.toggle-added-files"}, - OnAction: func() error { return v.toggleShowDiffType(filetree.Added) }, - IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Added] }, - Display: "Added", - }, - { - ConfigKeys: []string{"keybinding.toggle-removed-files"}, - OnAction: func() error { return v.toggleShowDiffType(filetree.Removed) }, - IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Removed] }, - Display: "Removed", - }, - { - ConfigKeys: []string{"keybinding.toggle-modified-files"}, - OnAction: func() error { return v.toggleShowDiffType(filetree.Modified) }, - IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Modified] }, - Display: "Modified", - }, - { - ConfigKeys: []string{"keybinding.toggle-unchanged-files", "keybinding.toggle-unmodified-files"}, - OnAction: func() error { return v.toggleShowDiffType(filetree.Unmodified) }, - IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Unmodified] }, - Display: "Unmodified", - }, - { - ConfigKeys: []string{"keybinding.toggle-filetree-attributes"}, - OnAction: v.toggleAttributes, - IsSelected: func() bool { return v.vm.ShowAttributes }, - Display: "Attributes", - }, - { - ConfigKeys: []string{"keybinding.page-up"}, - OnAction: v.PageUp, - }, - { - ConfigKeys: []string{"keybinding.page-down"}, - OnAction: v.PageDown, - }, - { - Key: gocui.KeyArrowDown, - Modifier: gocui.ModNone, - OnAction: v.CursorDown, - }, - { - Key: gocui.KeyArrowUp, - Modifier: gocui.ModNone, - OnAction: v.CursorUp, - }, - { - Key: gocui.KeyArrowLeft, - Modifier: gocui.ModNone, - OnAction: v.CursorLeft, - }, - { - Key: gocui.KeyArrowRight, - Modifier: gocui.ModNone, - OnAction: v.CursorRight, - }, - } + v.once.Do(func() { + var infos = []key.BindingInfo{ + { + ConfigKeys: []string{"keybinding.toggle-collapse-dir"}, + OnAction: v.toggleCollapse, + Display: "Collapse dir", + }, + { + ConfigKeys: []string{"keybinding.toggle-collapse-all-dir"}, + OnAction: v.toggleCollapseAll, + Display: "Collapse all dir", + }, + { + ConfigKeys: []string{"keybinding.toggle-added-files"}, + OnAction: func() error { return v.toggleShowDiffType(filetree.Added) }, + IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Added] }, + Display: "Added", + }, + { + ConfigKeys: []string{"keybinding.toggle-removed-files"}, + OnAction: func() error { return v.toggleShowDiffType(filetree.Removed) }, + IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Removed] }, + Display: "Removed", + }, + { + ConfigKeys: []string{"keybinding.toggle-modified-files"}, + OnAction: func() error { return v.toggleShowDiffType(filetree.Modified) }, + IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Modified] }, + Display: "Modified", + }, + { + ConfigKeys: []string{"keybinding.toggle-unchanged-files", "keybinding.toggle-unmodified-files"}, + OnAction: func() error { return v.toggleShowDiffType(filetree.Unmodified) }, + IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Unmodified] }, + Display: "Unmodified", + }, + { + ConfigKeys: []string{"keybinding.toggle-filetree-attributes"}, + OnAction: v.toggleAttributes, + IsSelected: func() bool { return v.vm.ShowAttributes }, + Display: "Attributes", + }, + { + ConfigKeys: []string{"keybinding.page-up"}, + OnAction: v.PageUp, + }, + { + ConfigKeys: []string{"keybinding.page-down"}, + OnAction: v.PageDown, + }, + { + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + OnAction: v.CursorDown, + }, + { + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + OnAction: v.CursorUp, + }, + { + Key: gocui.KeyArrowLeft, + Modifier: gocui.ModNone, + OnAction: v.CursorLeft, + }, + { + Key: gocui.KeyArrowRight, + Modifier: gocui.ModNone, + OnAction: v.CursorRight, + }, + } - helpKeys, err := key.GenerateBindings(v.gui, v.name, infos) - if err != nil { - return err - } - v.helpKeys = helpKeys + helpKeys, err := key.GenerateBindings(v.gui, v.name, infos) + if err != nil { + return + } + v.helpKeys = helpKeys + }) _, height := v.view.Size() v.vm.Setup(0, height) _ = v.Update() _ = v.Render() - - return nil + return err } // IsVisible indicates if the file tree view pane is currently initialized @@ -328,6 +333,30 @@ func (v *FileTree) toggleShowDiffType(diffType filetree.DiffType) error { return v.notifyOnViewOptionChangeListeners() } +func (v *FileTree) ToggleHide() error { + v.hidden = !v.hidden + + if v.hidden { + logrus.Trace("hiding filetree view...") + + // take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop + err := v.gui.DeleteView(v.Name()) + if err != nil { + return err + } + // take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop + err = v.gui.DeleteView(v.Name() + "header") + if err != nil { + return err + } + } + return nil +} + +func (v *FileTree) IsHidden() bool { + return v.hidden +} + // OnLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions func (v *FileTree) OnLayoutChange() error { err := v.Update() @@ -362,7 +391,7 @@ func (v *FileTree) Render() error { // update the header v.header.Clear() width, _ := g.Size() - headerStr := format.RenderHeader(title, width, isSelected) + headerStr := format.RenderHeader(title, width, isSelected, true) if v.vm.ShowAttributes { headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree") } @@ -391,7 +420,7 @@ func (v *FileTree) KeyHelp() string { } func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { - logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) + logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d, hidden: %v) %s", minX, minY, maxX, maxY, v.hidden, v.Name()) attributeRowSize := 0 // make the layout responsive to the available realestate. Make more room for the main content by hiding auxillary @@ -408,7 +437,6 @@ func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { // header + attribute header headerSize := 1 + attributeRowSize - // note: maxY needs to account for the (invisible) border, thus a +1 header, headerErr := g.SetView(v.Name()+"header", minX, minY, maxX, minY+headerSize+1) // we are going to overlap the view over the (invisible) border (so minY will be one less than expected). // additionally, maxY will be bumped by one to include the border diff --git a/runtime/ui/view/filter.go b/runtime/ui/view/filter.go index e8f911b..38759e5 100644 --- a/runtime/ui/view/filter.go +++ b/runtime/ui/view/filter.go @@ -169,6 +169,19 @@ func (v *Filter) OnLayoutChange() error { return v.Render() } +func (v *Filter) Retop() { + logrus.Trace("asserting filter on top...") + // take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop + err := v.gui.DeleteView(v.Name()+"label") + if err != nil { + logrus.Errorf("could not put filter label on top:", err) + } + err = v.gui.DeleteView(v.Name()) + if err != nil { + logrus.Errorf("could not put filter on top:", err) + } +} + func (v *Filter) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) diff --git a/runtime/ui/view/layer.go b/runtime/ui/view/layer.go index 08472fa..08405ab 100644 --- a/runtime/ui/view/layer.go +++ b/runtime/ui/view/layer.go @@ -307,7 +307,7 @@ func (v *Layer) Render() error { return err } } else { - headerStr := format.RenderHeader(title, width, isSelected) + headerStr := format.RenderHeader(title, width, isSelected, true) headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command") _, err := fmt.Fprintln(v.header, headerStr) if err != nil { diff --git a/runtime/ui/view/status.go b/runtime/ui/view/status.go index 87a4b46..59d2af4 100644 --- a/runtime/ui/view/status.go +++ b/runtime/ui/view/status.go @@ -110,6 +110,15 @@ func (v *Status) KeyHelp() string { return help } +func (v *Status) Retop() { + logrus.Trace("asserting status on top...") + // take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop + err := v.gui.DeleteView(v.Name()) + if err != nil { + logrus.Errorf("could not put status on top:", err) + } +} + func (v *Status) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error { logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) -- cgit v1.2.3