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) {