From 8a3642a57fa77a98997afd74e0a731b9c28c36af Mon Sep 17 00:00:00 2001 From: Alex Goodman <wagoodman@gmail.com> Date: Sun, 24 Nov 2019 12:23:43 -0500 Subject: [PATCH] central header formatter --- runtime/ui/format/format.go | 45 +++++++++++++++++++ .../layout/compound/layer_details_column.go | 27 +++++++---- runtime/ui/layout/layout.go | 3 +- runtime/ui/layout/manager.go | 18 ++++++-- runtime/ui/view/details.go | 22 ++++++--- runtime/ui/view/filetree.go | 37 +++++---------- runtime/ui/view/filter.go | 17 +++++-- runtime/ui/view/layer.go | 30 +++++++------ runtime/ui/view/status.go | 17 +++++-- 9 files changed, 150 insertions(+), 66 deletions(-) diff --git a/runtime/ui/format/format.go b/runtime/ui/format/format.go index b3e891e..0f54910 100644 --- a/runtime/ui/format/format.go +++ b/runtime/ui/format/format.go @@ -1,7 +1,39 @@ package format import ( + "fmt" "github.com/fatih/color" + "github.com/lunixbochs/vtclean" + "strings" +) + +const ( + //selectedLeftBracketStr = " " + //selectedRightBracketStr = " " + //selectedFillStr = " " + // + //leftBracketStr = "▏" + //rightBracketStr = "▕" + //fillStr = "─" + + //selectedLeftBracketStr = " " + //selectedRightBracketStr = " " + //selectedFillStr = "━" + // + //leftBracketStr = "▏" + //rightBracketStr = "▕" + //fillStr = "─" + + selectedLeftBracketStr = "┃" + selectedRightBracketStr = "┣" + selectedFillStr = "━" + + leftBracketStr = "│" + rightBracketStr = "├" + fillStr = "─" + + selectStr = " ● " + //selectStr = " " ) var ( @@ -26,6 +58,19 @@ func init() { CompareBottom = color.New(color.BgGreen).SprintFunc() } +func RenderHeader(title string, width int, selected bool) string { + if selected { + body := Header(fmt.Sprintf("%s%s ", selectStr, title)) + bodyLen := len(vtclean.Clean(body, false)) + return fmt.Sprintf("%s%s%s%s\n", selectedLeftBracketStr, body, selectedRightBracketStr, strings.Repeat(selectedFillStr, width-bodyLen-2)) + //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)) + } + body := Header(fmt.Sprintf(" %s ", title)) + bodyLen := len(vtclean.Clean(body, false)) + return fmt.Sprintf("%s%s%s%s\n", leftBracketStr, body, rightBracketStr, strings.Repeat(fillStr, width-bodyLen-2)) +} + func RenderHelpKey(control, title string, selected bool) string { if selected { return StatusSelected("▏") + StatusControlSelected(control) + StatusSelected(" "+title+" ") diff --git a/runtime/ui/layout/compound/layer_details_column.go b/runtime/ui/layout/compound/layer_details_column.go index 61cd127..754b6f0 100644 --- a/runtime/ui/layout/compound/layer_details_column.go +++ b/runtime/ui/layout/compound/layer_details_column.go @@ -23,8 +23,24 @@ func (cl *LayerDetailsCompoundLayout) Name() string { return "layer-details-compound-column" } -func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error { - logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, cl.Name()) +// OnLayoutChange is called whenever the screen dimensions are changed +func (cl *LayerDetailsCompoundLayout) OnLayoutChange() error { + err := cl.layer.OnLayoutChange() + if err != nil { + logrus.Error("unable to setup layer controller onLayoutChange", err) + return err + } + + err = cl.details.OnLayoutChange() + if err != nil { + logrus.Error("unable to setup details controller onLayoutChange", err) + return err + } + return nil +} + +func (cl *LayerDetailsCompoundLayout) 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, cl.Name()) //////////////////////////////////////////////////////////////////////////////////// // Layers View @@ -55,12 +71,6 @@ func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, max 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 = cl.layer.Render() - if err != nil { - logrus.Error("unable to render layer view", err) - return err - } } //////////////////////////////////////////////////////////////////////////////////// @@ -83,6 +93,7 @@ func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, max return err } } + return nil } diff --git a/runtime/ui/layout/layout.go b/runtime/ui/layout/layout.go index 52be197..abc0dbb 100644 --- a/runtime/ui/layout/layout.go +++ b/runtime/ui/layout/layout.go @@ -4,7 +4,8 @@ import "github.com/jroimartin/gocui" type Layout interface { Name() string - Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error + Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error RequestedSize(available int) *int IsVisible() bool + OnLayoutChange() error } diff --git a/runtime/ui/layout/manager.go b/runtime/ui/layout/manager.go index cbb25a2..d5693c2 100644 --- a/runtime/ui/layout/manager.go +++ b/runtime/ui/layout/manager.go @@ -57,7 +57,7 @@ func (lm *Manager) Layout(g *gocui.Gui) error { } // layout the header within the allocated space - err := element.Layout(g, minX, minY, maxX, minY+height, hasResized) + err := element.Layout(g, minX, minY, maxX, minY+height) if err != nil { logrus.Errorf("failed to layout '%s' header: %+v", element.Name(), err) } @@ -138,7 +138,7 @@ func (lm *Manager) Layout(g *gocui.Gui) error { } // layout the column within the allocated space - err := element.Layout(g, minX, minY, minX+width, maxY, hasResized) + err := element.Layout(g, minX, minY, minX+width, maxY) if err != nil { logrus.Errorf("failed to layout '%s' column: %+v", element.Name(), err) } @@ -165,12 +165,24 @@ func (lm *Manager) Layout(g *gocui.Gui) error { // layout the footer within the allocated space // note: since the headers and rows are inclusive counting from -1 (to account for a border) we must // do the same vertically, thus a -1 is needed for a starting Y - err := element.Layout(g, footerMinX, topY, footerMaxX, bottomY, hasResized) + err := element.Layout(g, footerMinX, topY, footerMaxX, bottomY) if err != nil { logrus.Errorf("failed to layout '%s' footer: %+v", element.Name(), err) } } } + // notify everyone of a layout change (allow to update and render) + if hasResized { + for _, elements := range lm.elements { + for _, element := range elements { + err := element.OnLayoutChange() + if err != nil { + return err + } + } + } + } + return nil } diff --git a/runtime/ui/view/details.go b/runtime/ui/view/details.go index a82a6a4..4611aa4 100644 --- a/runtime/ui/view/details.go +++ b/runtime/ui/view/details.go @@ -12,7 +12,6 @@ import ( "github.com/dustin/go-humanize" "github.com/jroimartin/gocui" - "github.com/lunixbochs/vtclean" ) // Details holds the UI objects and data models for populating the lower-left pane. Specifically the pane that @@ -49,7 +48,7 @@ func (v *Details) Name() string { // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *Details) Setup(view *gocui.View, header *gocui.View) error { - logrus.Debugf("view.Setup() %s", v.Name()) + logrus.Tracef("view.Setup() %s", v.Name()) // set controller options v.view = view @@ -99,6 +98,15 @@ func (v *Details) CursorUp() error { return CursorUp(v.gui, v.view) } +// OnLayoutChange is called whenever the screen dimensions are changed +func (v *Details) OnLayoutChange() error { + err := v.Update() + if err != nil { + return err + } + return v.Render() +} + // Update refreshes the state objects for future rendering. func (v *Details) Update() error { return nil @@ -114,7 +122,7 @@ func (v *Details) SetCurrentLayer(layer *image.Layer) { // 3. the estimated wasted image space // 4. a list of inefficient file allocations func (v *Details) Render() error { - logrus.Debugf("view.Render() %s", v.Name()) + logrus.Tracef("view.Render() %s", v.Name()) if v.currentLayer == nil { return fmt.Errorf("no layer selected") @@ -149,10 +157,10 @@ func (v *Details) Render() error { v.header.Clear() width, _ := v.view.Size() - layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15)) - imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15)) + layerHeaderStr := format.RenderHeader("Layer Details", width, false) + imageHeaderStr := format.RenderHeader("Image Details", width, false) - _, err := fmt.Fprintln(v.header, format.Header(vtclean.Clean(layerHeaderStr, false))) + _, err := fmt.Fprintln(v.header, layerHeaderStr) if err != nil { return err } @@ -170,7 +178,7 @@ func (v *Details) Render() error { lines = append(lines, format.Header("Digest: ")+v.currentLayer.Digest) lines = append(lines, format.Header("Command:")) lines = append(lines, v.currentLayer.Command) - lines = append(lines, "\n"+format.Header(vtclean.Clean(imageHeaderStr, false))) + lines = append(lines, "\n"+imageHeaderStr) lines = append(lines, imageSizeStr) lines = append(lines, wastedSpaceStr) lines = append(lines, effStr+"\n") diff --git a/runtime/ui/view/filetree.go b/runtime/ui/view/filetree.go index 0cd324d..646973f 100644 --- a/runtime/ui/view/filetree.go +++ b/runtime/ui/view/filetree.go @@ -2,18 +2,15 @@ package view import ( "fmt" + "github.com/jroimartin/gocui" "github.com/sirupsen/logrus" "github.com/spf13/viper" + "github.com/wagoodman/dive/dive/filetree" "github.com/wagoodman/dive/runtime/ui/format" "github.com/wagoodman/dive/runtime/ui/key" "github.com/wagoodman/dive/runtime/ui/viewmodel" "github.com/wagoodman/dive/utils" "regexp" - "strings" - - "github.com/jroimartin/gocui" - "github.com/lunixbochs/vtclean" - "github.com/wagoodman/dive/dive/filetree" ) const ( @@ -86,7 +83,7 @@ func (v *FileTree) areAttributesVisible() bool { // 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.Debugf("view.Setup() %s", v.Name()) + logrus.Tracef("view.Setup() %s", v.Name()) // set controller options v.view = view @@ -343,16 +340,12 @@ func (v *FileTree) toggleShowDiffType(diffType filetree.DiffType) error { } // OnLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions -func (v *FileTree) OnLayoutChange(resized bool) error { +func (v *FileTree) OnLayoutChange() error { err := v.Update() if err != nil { return err } - - if resized { - return v.Render() - } - return nil + return v.Render() } // Update refreshes the state objects for future rendering. @@ -371,24 +364,21 @@ func (v *FileTree) Update() error { // Render flushes the state objects (file tree) to the pane. func (v *FileTree) Render() error { - logrus.Debugf("view.Render() %s", v.Name()) + logrus.Tracef("view.Render() %s", v.Name()) title := v.title - // indicate when selected - if v.gui.CurrentView() == v.view { - title = "● " + v.title - } + isSelected := v.gui.CurrentView() == v.view v.gui.Update(func(g *gocui.Gui) error { // update the header v.header.Clear() width, _ := g.Size() - headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2)) + headerStr := format.RenderHeader(title, width, isSelected) if v.vm.ShowAttributes { headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree") } - _, _ = fmt.Fprintln(v.header, format.Header(vtclean.Clean(headerStr, false))) + _, _ = fmt.Fprintln(v.header, headerStr, false) // update the contents v.view.Clear() @@ -412,8 +402,8 @@ func (v *FileTree) KeyHelp() string { return help } -func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error { - logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) +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()) attributeRowSize := 0 if !v.areAttributesVisible() { attributeRowSize = 1 @@ -432,11 +422,6 @@ func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized b return err } } - err := v.OnLayoutChange(hasResized) - if err != nil { - logrus.Error("unable to setup layer controller onLayoutChange", err) - return err - } return nil } diff --git a/runtime/ui/view/filter.go b/runtime/ui/view/filter.go index 0eeafcd..a4b090f 100644 --- a/runtime/ui/view/filter.go +++ b/runtime/ui/view/filter.go @@ -53,7 +53,7 @@ func (v *Filter) Name() string { // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *Filter) Setup(view *gocui.View, header *gocui.View) error { - logrus.Debugf("view.Setup() %s", v.Name()) + logrus.Tracef("view.Setup() %s", v.Name()) // set controller options v.view = view @@ -153,7 +153,7 @@ func (v *Filter) Update() error { // Render flushes the state objects to the screen. Currently this is the users path filter input. func (v *Filter) Render() error { - logrus.Debugf("view.Render() %s", v.Name()) + logrus.Tracef("view.Render() %s", v.Name()) v.gui.Update(func(g *gocui.Gui) error { _, err := fmt.Fprintln(v.header, format.Header(v.labelStr)) @@ -170,8 +170,17 @@ func (v *Filter) KeyHelp() string { return format.StatusControlNormal("▏Type to filter the file tree ") } -func (v *Filter) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error { - logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) +// OnLayoutChange is called whenever the screen dimensions are changed +func (v *Filter) OnLayoutChange() error { + err := v.Update() + if err != nil { + return err + } + return v.Render() +} + +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()) label, labelErr := g.SetView(v.Name()+"label", minX, minY, len(v.labelStr), maxY) view, viewErr := g.SetView(v.Name(), minX+(len(v.labelStr)-1), minY, maxX, maxY) diff --git a/runtime/ui/view/layer.go b/runtime/ui/view/layer.go index 687eb7b..8f1ec9e 100644 --- a/runtime/ui/view/layer.go +++ b/runtime/ui/view/layer.go @@ -2,16 +2,13 @@ package view import ( "fmt" + "github.com/jroimartin/gocui" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" "github.com/wagoodman/dive/dive/image" "github.com/wagoodman/dive/runtime/ui/format" "github.com/wagoodman/dive/runtime/ui/key" "github.com/wagoodman/dive/runtime/ui/viewmodel" - "strings" - - "github.com/jroimartin/gocui" - "github.com/lunixbochs/vtclean" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" ) type LayerChangeListener func(viewmodel.LayerSelection) error @@ -85,7 +82,7 @@ func (v *Layer) Name() string { // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *Layer) Setup(view *gocui.View, header *gocui.View) error { - logrus.Debugf("view.Setup() %s", v.Name()) + logrus.Tracef("view.Setup() %s", v.Name()) // set controller options v.view = view @@ -275,6 +272,15 @@ func (v *Layer) renderCompareBar(layerIdx int) string { return result } +// OnLayoutChange is called whenever the screen dimensions are changed +func (v *Layer) OnLayoutChange() error { + err := v.Update() + if err != nil { + return err + } + return v.Render() +} + // Update refreshes the state objects for future rendering (currently does nothing). func (v *Layer) Update() error { return nil @@ -284,21 +290,19 @@ func (v *Layer) Update() error { // 1. the layers of the image + metadata // 2. the current selected image func (v *Layer) Render() error { - logrus.Debugf("view.Render() %s", v.Name()) + logrus.Tracef("view.Render() %s", v.Name()) // indicate when selected title := "Layers" - if v.gui.CurrentView() == v.view { - title = "● " + title - } + isSelected := v.gui.CurrentView() == v.view v.gui.Update(func(g *gocui.Gui) error { // update header v.header.Clear() width, _ := g.Size() - headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2)) + headerStr := format.RenderHeader(title, width, isSelected) headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command") - _, err := fmt.Fprintln(v.header, format.Header(vtclean.Clean(headerStr, false))) + _, err := fmt.Fprintln(v.header, headerStr) if err != nil { return err } diff --git a/runtime/ui/view/status.go b/runtime/ui/view/status.go index 0e05da7..3b53adc 100644 --- a/runtime/ui/view/status.go +++ b/runtime/ui/view/status.go @@ -51,7 +51,7 @@ func (v *Status) AddHelpKeys(keys ...*key.Binding) { // Setup initializes the UI concerns within the context of a global [gocui] view object. func (v *Status) Setup(view *gocui.View) error { - logrus.Debugf("view.Setup() %s", v.Name()) + logrus.Tracef("view.Setup() %s", v.Name()) // set controller options v.view = view @@ -80,9 +80,18 @@ func (v *Status) Update() error { return nil } +// OnLayoutChange is called whenever the screen dimensions are changed +func (v *Status) OnLayoutChange() error { + err := v.Update() + if err != nil { + return err + } + return v.Render() +} + // Render flushes the state objects to the screen. func (v *Status) Render() error { - logrus.Debugf("view.Render() %s", v.Name()) + logrus.Tracef("view.Render() %s", v.Name()) v.gui.Update(func(g *gocui.Gui) error { v.view.Clear() @@ -111,8 +120,8 @@ func (v *Status) KeyHelp() string { return help } -func (v *Status) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error { - logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name()) +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()) view, viewErr := g.SetView(v.Name(), minX, minY, maxX, maxY) if utils.IsNewView(viewErr) {