summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Goodman <wagoodman@users.noreply.github.com>2023-07-06 10:25:30 -0400
committerGitHub <noreply@github.com>2023-07-06 10:25:30 -0400
commit8bf4341f7058233a586bda97175e02bde981dc48 (patch)
tree7590b5376d06d2453c7cb78c5510057813618dea
parentabbac157bbf52053e603d569483b17b1f1f49293 (diff)
parent2aad87c37e76d7e6e9d76dafa9eda20801b0c1fb (diff)
Merge pull request #399 from mark2185/feature/gui-layout-rework
GUI rework
-rw-r--r--dive/image/layer.go4
-rw-r--r--runtime/ui/app.go10
-rw-r--r--runtime/ui/controller.go50
-rw-r--r--runtime/ui/layout/compound/layer_details_column.go102
-rw-r--r--runtime/ui/view/details.go204
-rw-r--r--runtime/ui/view/filetree.go2
-rw-r--r--runtime/ui/view/filter.go8
-rw-r--r--runtime/ui/view/image_details.go173
-rw-r--r--runtime/ui/view/layer.go50
-rw-r--r--runtime/ui/view/layer_details.go140
-rw-r--r--runtime/ui/view/views.go53
11 files changed, 481 insertions, 315 deletions
diff --git a/dive/image/layer.go b/dive/image/layer.go
index 550eba1..ac96363 100644
--- a/dive/image/layer.go
+++ b/dive/image/layer.go
@@ -2,6 +2,8 @@ package image
import (
"fmt"
+ "strings"
+
"github.com/dustin/go-humanize"
"github.com/wagoodman/dive/dive/filetree"
)
@@ -39,5 +41,5 @@ func (l *Layer) String() string {
}
return fmt.Sprintf(LayerFormat,
humanize.Bytes(l.Size),
- l.Command)
+ strings.Split(l.Command, "\n")[0])
}
diff --git a/runtime/ui/app.go b/runtime/ui/app.go
index 036429f..249a3aa 100644
--- a/runtime/ui/app.go
+++ b/runtime/ui/app.go
@@ -42,7 +42,7 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
lm := layout.NewManager()
lm.Add(controller.views.Status, layout.LocationFooter)
lm.Add(controller.views.Filter, layout.LocationFooter)
- lm.Add(compound.NewLayerDetailsCompoundLayout(controller.views.Layer, controller.views.Details), layout.LocationColumn)
+ lm.Add(compound.NewLayerDetailsCompoundLayout(controller.views.Layer, controller.views.LayerDetails, controller.views.ImageDetails), layout.LocationColumn)
lm.Add(controller.views.Tree, layout.LocationColumn)
// todo: access this more programmatically
@@ -77,6 +77,14 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
Display: "Switch view",
},
{
+ Key: gocui.KeyArrowRight,
+ OnAction: controller.NextPane,
+ },
+ {
+ Key: gocui.KeyArrowLeft,
+ OnAction: controller.PrevPane,
+ },
+ {
ConfigKeys: []string{"keybinding.filter-files"},
OnAction: controller.ToggleFilterView,
IsSelected: controller.views.Filter.IsVisible,
diff --git a/runtime/ui/controller.go b/runtime/ui/controller.go
index 964fb83..bc76423 100644
--- a/runtime/ui/controller.go
+++ b/runtime/ui/controller.go
@@ -82,7 +82,7 @@ func (c *Controller) onFilterEdit(filter string) error {
func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error {
// update the details
- c.views.Details.SetCurrentLayer(selection.Layer)
+ c.views.LayerDetails.CurrentLayer = selection.Layer
// update the filetree
err := c.views.Tree.SetTree(selection.BottomTreeStart, selection.BottomTreeStop, selection.TopTreeStart, selection.TopTreeStop)
@@ -141,6 +141,54 @@ func (c *Controller) Render() error {
return nil
}
+func (c *Controller) NextPane() (err error) {
+ v := c.gui.CurrentView()
+ if v == nil {
+ panic("Current view is nil")
+ }
+ if v.Name() == c.views.Layer.Name() {
+ _, err = c.gui.SetCurrentView(c.views.LayerDetails.Name())
+ c.views.Status.SetCurrentView(c.views.LayerDetails)
+ } else if v.Name() == c.views.LayerDetails.Name() {
+ _, err = c.gui.SetCurrentView(c.views.ImageDetails.Name())
+ c.views.Status.SetCurrentView(c.views.ImageDetails)
+ } else if v.Name() == c.views.ImageDetails.Name() {
+ _, err = c.gui.SetCurrentView(c.views.Layer.Name())
+ c.views.Status.SetCurrentView(c.views.Layer)
+ }
+
+ if err != nil {
+ logrus.Error("unable to toggle view: ", err)
+ return err
+ }
+
+ return c.UpdateAndRender()
+}
+
+func (c *Controller) PrevPane() (err error) {
+ v := c.gui.CurrentView()
+ if v == nil {
+ panic("Current view is nil")
+ }
+ if v.Name() == c.views.Layer.Name() {
+ _, err = c.gui.SetCurrentView(c.views.ImageDetails.Name())
+ c.views.Status.SetCurrentView(c.views.ImageDetails)
+ } else if v.Name() == c.views.LayerDetails.Name() {
+ _, err = c.gui.SetCurrentView(c.views.Layer.Name())
+ c.views.Status.SetCurrentView(c.views.Layer)
+ } else if v.Name() == c.views.ImageDetails.Name() {
+ _, err = c.gui.SetCurrentView(c.views.LayerDetails.Name())
+ c.views.Status.SetCurrentView(c.views.LayerDetails)
+ }
+
+ if err != nil {
+ logrus.Error("unable to toggle view: ", err)
+ return err
+ }
+
+ return c.UpdateAndRender()
+}
+
// ToggleView switches between the file view and the layer view and re-renders the screen.
func (c *Controller) ToggleView() (err error) {
v := c.gui.CurrentView()
diff --git a/runtime/ui/layout/compound/layer_details_column.go b/runtime/ui/layout/compound/layer_details_column.go
index 3981442..8c73b2a 100644
--- a/runtime/ui/layout/compound/layer_details_column.go
+++ b/runtime/ui/layout/compound/layer_details_column.go
@@ -9,14 +9,16 @@ import (
type LayerDetailsCompoundLayout struct {
layer *view.Layer
- details *view.Details
+ layerDetails *view.LayerDetails
+ imageDetails *view.ImageDetails
constrainRealEstate bool
}
-func NewLayerDetailsCompoundLayout(layer *view.Layer, details *view.Details) *LayerDetailsCompoundLayout {
+func NewLayerDetailsCompoundLayout(layer *view.Layer, layerDetails *view.LayerDetails, imageDetails *view.ImageDetails) *LayerDetailsCompoundLayout {
return &LayerDetailsCompoundLayout{
- layer: layer,
- details: details,
+ layer: layer,
+ layerDetails: layerDetails,
+ imageDetails: imageDetails,
}
}
@@ -32,87 +34,65 @@ func (cl *LayerDetailsCompoundLayout) OnLayoutChange() error {
return err
}
- err = cl.details.OnLayoutChange()
+ err = cl.layerDetails.OnLayoutChange()
if err != nil {
- logrus.Error("unable to setup details controller onLayoutChange", err)
+ logrus.Error("unable to setup layer details controller onLayoutChange", err)
+ return err
+ }
+
+ err = cl.imageDetails.OnLayoutChange()
+ if err != nil {
+ logrus.Error("unable to setup image 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
-
+func (cl *LayerDetailsCompoundLayout) layoutRow(g *gocui.Gui, minX, minY, maxX, maxY int, viewName string, setup func(*gocui.View, *gocui.View) error) error {
+ logrus.Tracef("layoutRow(g, minX: %d, minY: %d, maxX: %d, maxY: %d, viewName: %s, <setup func>)", minX, minY, maxX, maxY, viewName)
// header + border
- layerHeaderHeight := 2
-
- layersHeight := cl.layer.LayerCount() + layerHeaderHeight + 1 // layers + header + base image layer row
- maxLayerHeight := int(0.75 * float64(maxY))
- if layersHeight > maxLayerHeight {
- layersHeight = maxLayerHeight
- }
+ headerHeight := 2
+ // TODO: investigate overlap
// note: maxY needs to account for the (invisible) border, thus a +1
- header, headerErr := g.SetView(cl.layer.Name()+"header", minX, minY, maxX, minY+layerHeaderHeight+1, 0)
+ headerView, headerErr := g.SetView(viewName+"Header", minX, minY, maxX, minY+headerHeight+1, 0)
// we are going to overlap the view over the (invisible) border (so minY will be one less than expected)
- main, viewErr := g.SetView(cl.layer.Name(), minX, minY+layerHeaderHeight, maxX, minY+layerHeaderHeight+layersHeight, 0)
+ bodyView, bodyErr := g.SetView(viewName, minX, minY+headerHeight, maxX, maxY, 0)
- if utils.IsNewView(viewErr, headerErr) {
- err := cl.layer.Setup(main, header)
+ if utils.IsNewView(bodyErr, headerErr) {
+ err := setup(bodyView, headerView)
if err != nil {
- logrus.Error("unable to setup layer layout", err)
- return err
- }
-
- if _, err = g.SetCurrentView(cl.layer.Name()); err != nil {
- logrus.Error("unable to set view to layer", err)
+ logrus.Debug("unable to setup row layout for ", viewName, err)
return err
}
}
+ return nil
+}
- ////////////////////////////////////////////////////////////////////////////////////
- // Details
- detailsMinY := minY + layersHeight
-
- // header + border
- detailsHeaderHeight := 2
-
- v, _ := g.View(cl.details.Name())
- if v != nil {
- // the view exists already!
-
- // don't show the details pane when there isn't enough room on the screen
- if cl.constrainRealEstate {
- // take note: deleting a view will invoke layout again, so ensure this call is protected from an infinite loop
- err := g.DeleteView(cl.details.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 = g.DeleteView(cl.details.Name() + "header")
- if err != nil {
- return err
- }
-
- return nil
- }
+func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {
+ logrus.Tracef("LayerDetailsCompountLayout.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, cl.Name())
+ layouts := []view.IView{
+ cl.layer,
+ cl.layerDetails,
+ cl.imageDetails,
}
- header, headerErr = g.SetView(cl.details.Name()+"header", minX, detailsMinY, maxX, detailsMinY+detailsHeaderHeight, 0)
- main, viewErr = g.SetView(cl.details.Name(), minX, detailsMinY+detailsHeaderHeight, maxX, maxY, 0)
-
- if utils.IsNewView(viewErr, headerErr) {
- err := cl.details.Setup(main, header)
- if err != nil {
+ rowHeight := maxY / 3
+ for i := 0; i < 3; i++ {
+ if err := cl.layoutRow(g, minX, i*rowHeight, maxX, (i+1)*rowHeight, layouts[i].Name(), layouts[i].Setup); err != nil {
+ logrus.Debug("Laying out layers view errored!")
return err
}
}
+ if g.CurrentView() == nil {
+ if _, err := g.SetCurrentView(cl.layer.Name()); err != nil {
+ logrus.Error("unable to set view to layer", err)
+ return err
+ }
+ }
return nil
}
diff --git a/runtime/ui/view/details.go b/runtime/ui/view/details.go
deleted file mode 100644
index 43ea329..0000000
--- a/runtime/ui/view/details.go
+++ /dev/null
@@ -1,204 +0,0 @@
-package view
-
-import (
- "fmt"
- "strconv"
- "strings"
-
- "github.com/sirupsen/logrus"
- "github.com/wagoodman/dive/dive/filetree"
- "github.com/wagoodman/dive/dive/image"
- "github.com/wagoodman/dive/runtime/ui/format"
- "github.com/wagoodman/dive/runtime/ui/key"
-
- "github.com/awesome-gocui/gocui"
- "github.com/dustin/go-humanize"
-)
-
-// Details holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
-// shows the layer details and image statistics.
-type Details struct {
- name string
- gui *gocui.Gui
- view *gocui.View
- header *gocui.View
- imageName string
- efficiency float64
- inefficiencies filetree.EfficiencySlice
- imageSize uint64
-
- currentLayer *image.Layer
-}
-
-// newDetailsView creates a new view object attached the the global [gocui] screen object.
-func newDetailsView(gui *gocui.Gui, imageName string, efficiency float64, inefficiencies filetree.EfficiencySlice, imageSize uint64) (controller *Details) {
- controller = new(Details)
-
- // populate main fields
- controller.name = "details"
- controller.gui = gui
- controller.imageName = imageName
- controller.efficiency = efficiency
- controller.inefficiencies = inefficiencies
- controller.imageSize = imageSize
-
- return controller
-}
-
-func (v *Details) Name() string {
- return v.name
-}
-
-// 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.Tracef("view.Setup() %s", v.Name())
-
- // set controller options
- v.view = view
- v.view.Editable = false
- v.view.Wrap = false
- v.view.Highlight = false
- v.view.Frame = false
-
- v.header = header
- v.header.Editable = false
- v.header.Wrap = false
- v.header.Frame = false
-
- var infos = []key.BindingInfo{
- {
- Key: gocui.KeyArrowDown,
- Modifier: gocui.ModNone,
- OnAction: v.CursorDown,
- },
- {
- Key: gocui.KeyArrowUp,
- Modifier: gocui.ModNone,
- OnAction: v.CursorUp,
- },
- }
-
- _, err := key.GenerateBindings(v.gui, v.name, infos)
- if err != nil {
- return err
- }
-
- return v.Render()
-}
-
-// IsVisible indicates if the details view pane is currently initialized.
-func (v *Details) IsVisible() bool {
- return v != nil
-}
-
-// CursorDown moves the cursor down in the details pane (currently indicates nothing).
-func (v *Details) CursorDown() error {
- return CursorDown(v.gui, v.view)
-}
-
-// CursorUp moves the cursor up in the details pane (currently indicates nothing).
-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
-}
-
-func (v *Details) SetCurrentLayer(layer *image.Layer) {
- v.currentLayer = layer
-}
-
-// Render flushes the state objects to the screen. The details pane reports:
-// 1. the current selected layer's command string
-// 2. the image efficiency score
-// 3. the estimated wasted image space
-// 4. a list of inefficient file allocations
-func (v *Details) Render() error {
- logrus.Tracef("view.Render() %s", v.Name())
-
- if v.currentLayer == nil {
- return fmt.Errorf("no layer selected")
- }
-
- var wastedSpace int64
-
- template := "%5s %12s %-s\n"
- inefficiencyReport := fmt.Sprintf(format.Header(template), "Count", "Total Space", "Path")
-
- height := 100
- if v.view != nil {
- _, height = v.view.Size()
- }
-
- for idx := 0; idx < len(v.inefficiencies); idx++ {
- data := v.inefficiencies[len(v.inefficiencies)-1-idx]
- wastedSpace += data.CumulativeSize
-
- // todo: make this report scrollable
- if idx < height {
- inefficiencyReport += fmt.Sprintf(template, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
- }
- }
-
- imageNameStr := fmt.Sprintf("%s %s", format.Header("Image name:"), v.imageName)
- imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(v.imageSize))
- effStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*v.efficiency))
- wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
-
- v.gui.Update(func(g *gocui.Gui) error {
- // update header
- v.header.Clear()
- width, _ := v.view.Size()
-
- layerHeaderStr := format.RenderHeader("Layer Details", width, false)
- imageHeaderStr := format.RenderHeader("Image Details", width, false)
-
- _, err := fmt.Fprintln(v.header, layerHeaderStr)
- if err != nil {
- return err
- }
-
- // update contents
- v.view.Clear()
-
- var lines = make([]string, 0)
- if v.currentLayer.Names != nil && len(v.currentLayer.Names) > 0 {
- lines = append(lines, format.Header("Tags: ")+strings.Join(v.currentLayer.Names, ", "))
- } else {
- lines = append(lines, format.Header("Tags: ")+"(none)")
- }
- lines = append(lines, format.Header("Id: ")+v.currentLayer.Id)
- 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"+imageHeaderStr)
- lines = append(lines, imageNameStr)
- lines = append(lines, imageSizeStr)
- lines = append(lines, wastedSpaceStr)
- lines = append(lines, effStr+"\n")
- lines = append(lines, inefficiencyReport)
-
- _, err = fmt.Fprintln(v.view, strings.Join(lines, "\n"))
- if err != nil {
- logrus.Debug("unable to write to buffer: ", err)
- }
- return err
- })
- return nil
-}
-
-// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
-func (v *Details) KeyHelp() string {
- return "TBD"
-}
diff --git a/runtime/ui/view/filetree.go b/runtime/ui/view/filetree.go
index 5b551c2..e90f0e4 100644
--- a/runtime/ui/view/filetree.go
+++ b/runtime/ui/view/filetree.go
@@ -72,7 +72,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 {
+func (v *FileTree) Setup(view, header *gocui.View) error {
logrus.Tracef("view.Setup() %s", v.Name())
// set controller options
diff --git a/runtime/ui/view/filter.go b/runtime/ui/view/filter.go
index 5fdf915..83978e5 100644
--- a/runtime/ui/view/filter.go
+++ b/runtime/ui/view/filter.go
@@ -15,7 +15,6 @@ type FilterEditListener func(string) error
// Filter holds the UI objects and data models for populating the bottom row. Specifically the pane that
// allows the user to filter the file tree by path.
type Filter struct {
- name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
@@ -34,7 +33,6 @@ func newFilterView(gui *gocui.Gui) (controller *Filter) {
controller.filterEditListeners = make([]FilterEditListener, 0)
// populate main fields
- controller.name = "filter"
controller.gui = gui
controller.labelStr = "Path Filter: "
controller.hidden = true
@@ -49,11 +47,11 @@ func (v *Filter) AddFilterEditListener(listener ...FilterEditListener) {
}
func (v *Filter) Name() string {
- return v.name
+ return "filter"
}
// 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 {
+func (v *Filter) Setup(view, header *gocui.View) error {
logrus.Tracef("view.Setup() %s", v.Name())
// set controller options
@@ -82,7 +80,7 @@ func (v *Filter) ToggleVisible() error {
v.hidden = !v.hidden
if !v.hidden {
- _, err := v.gui.SetCurrentView(v.name)
+ _, err := v.gui.SetCurrentView(v.Name())
if err != nil {
logrus.Error("unable to toggle filter view: ", err)
return err
diff --git a/runtime/ui/view/image_details.go b/runtime/ui/view/image_details.go
new file mode 100644
index 0000000..6a31b55
--- /dev/null
+++ b/runtime/ui/view/image_details.go
@@ -0,0 +1,173 @@
+package view
+
+import (
+ "fmt"
+ "github.com/awesome-gocui/gocui"
+ "github.com/dustin/go-humanize"
+ "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"
+)
+
+type ImageDetails struct {
+ gui *gocui.Gui
+ body *gocui.View
+ header *gocui.View
+ imageName string
+ imageSize uint64
+ efficiency float64
+ inefficiencies filetree.EfficiencySlice
+}
+
+func (v *ImageDetails) Name() string {
+ return "imageDetails"
+}
+
+func (v *ImageDetails) Setup(body, header *gocui.View) error {
+ logrus.Tracef("ImageDetails setup()")
+ v.body = body
+ v.body.Editable = false
+ v.body.Wrap = true
+ v.body.Highlight = true
+ v.body.Frame = false
+
+ v.header = header
+ v.header.Editable = false
+ v.header.Wrap = true
+ v.header.Highlight = false
+ v.header.Frame = false
+
+ var infos = []key.BindingInfo{
+ {
+ Key: gocui.KeyArrowDown,
+ Modifier: gocui.ModNone,
+ OnAction: v.CursorDown,
+ },
+ {
+ Key: gocui.KeyArrowUp,
+ Modifier: gocui.ModNone,
+ OnAction: v.CursorUp,
+ },
+ {
+ ConfigKeys: []string{"keybinding.page-up"},
+ OnAction: v.PageUp,
+ },
+ {
+ ConfigKeys: []string{"keybinding.page-down"},
+ OnAction: v.PageDown,
+ },
+ }
+
+ _, err := key.GenerateBindings(v.gui, v.Name(), infos)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// Render flushes the state objects to the screen. The details pane reports:
+// 1. the image efficiency score
+// 2. the estimated wasted image space
+// 3. a list of inefficient file allocations
+func (v *ImageDetails) Render() error {
+ analysisTemplate := "%5s %12s %-s\n"
+ inefficiencyReport := fmt.Sprintf(format.Header(analysisTemplate), "Count", "Total Space", "Path")
+
+ var wastedSpace int64
+ for idx := 0; idx < len(v.inefficiencies); idx++ {
+ data := v.inefficiencies[len(v.inefficiencies)-1-idx]
+ wastedSpace += data.CumulativeSize
+
+ inefficiencyReport += fmt.Sprintf(analysisTemplate, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
+ }
+
+ imageNameStr := fmt.Sprintf("%s %s", format.Header("Image name:"), v.imageName)
+ imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(v.imageSize))
+ efficiencyStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*v.efficiency))
+ wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
+
+ v.gui.Update(func(g *gocui.Gui) error {
+ width, _ := v.body.Size()
+
+ imageHeaderStr := format.RenderHeader("Image Details", width, v.gui.CurrentView() == v.body)
+
+ v.header.Clear()
+ _, err := fmt.Fprintln(v.header, imageHeaderStr)
+ if err != nil {
+ logrus.Debug("unable to write to buffer: ", err)
+ }
+
+ var lines = []string{
+ imageNameStr,
+ imageSizeStr,
+ wastedSpaceStr,
+ efficiencyStr,
+ " ", // to avoid an empty line so CursorDown can work as expected
+ inefficiencyReport,
+ }
+
+ v.body.Clear()
+ _, err = fmt.Fprintln(v.body, strings.Join(lines, "\n"))
+ if err != nil {
+ logrus.Debug("unable to write to buffer: ", err)
+ }
+ return err
+ })
+
+ return nil
+}
+
+func (v *ImageDetails) OnLayoutChange() error {
+ if err := v.Update(); err != nil {
+ return err
+ }
+ return v.Render()
+}
+
+// IsVisible indicates if the details view pane is currently initialized.
+func (v *ImageDetails) IsVisible() bool {
+ return v.body != nil
+}
+
+func (v *ImageDetails) PageUp() error {
+ _, height := v.body.Size()
+ if err := CursorStep(v.gui, v.body, -height); err != nil {
+ logrus.Debugf("Couldn't move the cursor up by %d steps", height)
+ }
+ return nil
+}
+
+func (v *ImageDetails) PageDown() error {
+ _, height := v.body.Size()
+ if err := CursorStep(v.gui, v.body, height); err != nil {
+ logrus.Debugf("Couldn't move the cursor down by %d steps", height)
+ }
+ return nil
+}
+
+func (v *ImageDetails) CursorUp() error {
+ if err := CursorUp(v.gui, v.body); err != nil {
+ logrus.Debug("Couldn't move the cursor up")
+ }
+ return nil
+}
+
+func (v *ImageDetails) CursorDown() error {
+ if err := CursorDown(v.gui, v.body); err != nil {
+ logrus.Debug("Couldn't move the cursor down")
+ }
+ return nil
+}
+
+// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
+func (v *ImageDetails) KeyHelp() string {
+ return ""
+}
+
+// Update refreshes the state objects for future rendering.
+func (v *ImageDetails) Update() error {
+ return nil
+}
diff --git a/runtime/ui/view/layer.go b/runtime/ui/view/layer.go
index c1f9d0c..d6616d1 100644
--- a/runtime/ui/view/layer.go
+++ b/runtime/ui/view/layer.go
@@ -11,12 +11,12 @@ import (
"github.com/wagoodman/dive/runtime/ui/viewmodel"
)
-// Layer holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
-// shows the image layers and layer selector.
+// Layer holds the UI objects and data models for populating the lower-left pane.
+// Specifically the pane that shows the image layers and layer selector.
type Layer struct {
name string
gui *gocui.Gui
- view *gocui.View
+ body *gocui.View
header *gocui.View
vm *viewmodel.LayerSetState
constrainedRealEstate bool
@@ -72,6 +72,12 @@ func (v *Layer) notifyLayerChangeListeners() error {
return err
}
}
+ // this is hacky, and I do not like it
+ if layerDetails, err := v.gui.View("layerDetails"); err == nil {
+ if err := layerDetails.SetCursor(0, 0); err != nil {
+ logrus.Debug("Couldn't set cursor to 0,0 for layerDetails")
+ }
+ }
return nil
}
@@ -80,14 +86,14 @@ 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 {
+func (v *Layer) Setup(body *gocui.View, header *gocui.View) error {
logrus.Tracef("view.Setup() %s", v.Name())
// set controller options
- v.view = view
- v.view.Editable = false
- v.view.Wrap = false
- v.view.Frame = false
+ v.body = body
+ v.body.Editable = false
+ v.body.Wrap = false
+ v.body.Frame = false
v.header = header
v.header.Editable = false
@@ -118,16 +124,6 @@ func (v *Layer) Setup(view *gocui.View, header *gocui.View) error {
OnAction: v.CursorUp,
},
{
- Key: gocui.KeyArrowLeft,
- Modifier: gocui.ModNone,
- OnAction: v.CursorUp,
- },
- {
- Key: gocui.KeyArrowRight,
- Modifier: gocui.ModNone,
- OnAction: v.CursorDown,
- },
- {
ConfigKeys: []string{"keybinding.page-up"},
OnAction: v.PageUp,
},
@@ -148,7 +144,7 @@ func (v *Layer) Setup(view *gocui.View, header *gocui.View) error {
// height obtains the height of the current pane (taking into account the lost space due to the header).
func (v *Layer) height() uint {
- _, height := v.view.Size()
+ _, height := v.body.Size()
return uint(height - 1)
}
@@ -171,7 +167,7 @@ func (v *Layer) PageDown() error {
}
if step > 0 {
- err := CursorStep(v.gui, v.view, step)
+ err := CursorStep(v.gui, v.body, step)
if err == nil {
return v.SetCursor(v.vm.LayerIndex + step)
}
@@ -189,7 +185,7 @@ func (v *Layer) PageUp() error {
}
if step > 0 {
- err := CursorStep(v.gui, v.view, -step)
+ err := CursorStep(v.gui, v.body, -step)
if err == nil {
return v.SetCursor(v.vm.LayerIndex - step)
}
@@ -200,7 +196,7 @@ func (v *Layer) PageUp() error {
// CursorDown moves the cursor down in the layer pane (selecting a higher layer).
func (v *Layer) CursorDown() error {
if v.vm.LayerIndex < len(v.vm.Layers) {
- err := CursorDown(v.gui, v.view)
+ err := CursorDown(v.gui, v.body)
if err == nil {
return v.SetCursor(v.vm.LayerIndex + 1)
}
@@ -211,7 +207,7 @@ func (v *Layer) CursorDown() error {
// CursorUp moves the cursor up in the layer pane (selecting a lower layer).
func (v *Layer) CursorUp() error {
if v.vm.LayerIndex > 0 {
- err := CursorUp(v.gui, v.view)
+ err := CursorUp(v.gui, v.body)
if err == nil {
return v.SetCursor(v.vm.LayerIndex - 1)
}
@@ -292,7 +288,7 @@ func (v *Layer) Render() error {
// indicate when selected
title := "Layers"
- isSelected := v.gui.CurrentView() == v.view
+ isSelected := v.gui.CurrentView() == v.body
v.gui.Update(func(g *gocui.Gui) error {
var err error
@@ -316,7 +312,7 @@ func (v *Layer) Render() error {
}
// update contents
- v.view.Clear()
+ v.body.Clear()
for idx, layer := range v.vm.Layers {
var layerStr string
@@ -329,9 +325,9 @@ func (v *Layer) Render() error {
compareBar := v.renderCompareBar(idx)
if idx == v.vm.LayerIndex {
- _, err = fmt.Fprintln(v.view, compareBar+" "+format.Selected(layerStr))
+ _, err = fmt.Fprintln(v.body, compareBar+" "+format.Selected(layerStr))
} else {
- _, err = fmt.Fprintln(v.view, compareBar+" "+layerStr)
+ _, err = fmt.Fprintln(v.body, compareBar+" "+layerStr)
}
if err != nil {
diff --git a/runtime/ui/view/layer_details.go b/runtime/ui/view/layer_details.go
new file mode 100644
index 0000000..b4c6f68
--- /dev/null
+++ b/runtime/ui/view/layer_details.go
@@ -0,0 +1,140 @@
+package view
+
+import (
+ "fmt"
+ "github.com/awesome-gocui/gocui"
+ "github.com/sirupsen/logrus"
+ "github.com/wagoodman/dive/dive/image"
+ "github.com/wagoodman/dive/runtime/ui/format"
+ "github.com/wagoodman/dive/runtime/ui/key"
+ "strings"
+)
+
+type LayerDetails struct {
+ gui *gocui.Gui
+ header *gocui.View
+ body *gocui.View
+ CurrentLayer *image.Layer
+}
+
+func (v *LayerDetails) Name() string {
+ return "layerDetails"
+}
+
+func (v *LayerDetails) Setup(body, header *gocui.View) error {
+ logrus.Tracef("LayerDetails setup()")
+ v.body = body
+ v.body.Editable = false
+ v.body.Wrap = true
+ v.body.Highlight = true
+ v.body.Frame = false
+
+ v.header = header
+ v.header.Editable = false
+ v.header.Wrap = true
+ v.header.Highlight = false
+ v.header.Frame = false
+
+ var infos = []key.BindingInfo{
+ {
+ Key: gocui.KeyArrowDown,
+ Modifier: gocui.ModNone,
+ OnAction: v.CursorDown,
+ },
+ {
+ Key: gocui.KeyArrowUp,
+ Modifier: gocui.ModNone,
+ OnAction: v.CursorUp,
+ },
+ }
+
+ _, err := key.GenerateBindings(v.gui, v.Name(), infos)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// Render flushes the state objects to the screen.
+// The details pane reports the currently selected layer's:
+// 1. tags
+// 2. ID
+// 3. digest
+// 4. command
+func (v *LayerDetails) Render() error {
+ v.gui.Update(func(g *gocui.Gui) error {
+ v.header.Clear()
+ width, _ := v.body.Size()
+
+ layerHeaderStr := format.RenderHeader("Layer Details", width, v.gui.CurrentView() == v.body)
+
+ _, err := fmt.Fprintln(v.header, layerHeaderStr)
+ if err != nil {
+ return err
+ }
+
+ // this is for layer details
+ var lines = make([]string, 0)
+
+ tags := "(none)"
+ if v.CurrentLayer.Names != nil && len(v.CurrentLayer.Names) > 0 {
+ tags = strings.Join(v.CurrentLayer.Names, ", ")
+ }
+ lines = append(lines, []string{
+ format.Header("Tags: ") + tags,
+ format.Header("Id: ") + v.CurrentLayer.Id,
+ format.Header("Digest: ") + v.CurrentLayer.Digest,
+ format.Header("Command:"),
+ v.CurrentLayer.Command,
+ }...)
+
+ v.body.Clear()
+ if _, err = fmt.Fprintln(v.body, strings.Join(lines, "\n")); err != nil {
+ logrus.Debug("unable to write to buffer: ", err)
+ }
+ return nil
+ })
+ return nil
+}
+
+func (v *LayerDetails) OnLayoutChange() error {
+ if err := v.Update(); err != nil {
+ return err
+ }
+ return v.Render()
+}
+
+// IsVisible indicates if the details view pane is currently initialized.
+func (v *LayerDetails) IsVisible() bool {
+ return v.body != nil
+}
+
+// CursorUp moves the cursor up in the details pane
+func (v *LayerDetails) CursorUp() error {
+ if err := CursorUp(v.gui, v.body); err != nil {
+ logrus.Debug("Couldn't move the cursor up")
+ }
+ return nil
+}
+
+// CursorDown moves the cursor up in the details pane
+func (v *LayerDetails) CursorDown() error {
+ if err := CursorDown(v.gui, v.body); err != nil {
+ logrus.Debug("Couldn't move the cursor down")
+ }
+ return nil
+}
+
+// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
+func (v *LayerDetails) KeyHelp() string {
+ return ""
+}
+
+// Update refreshes the state objects for future rendering.
+func (v *LayerDetails) Update() error {
+ return nil
+}
+
+func (v *LayerDetails) SetCursor(x, y int) error {
+ return v.body.SetCursor(x, y)
+}
diff --git a/runtime/ui/view/views.go b/runtime/ui/view/views.go
index b1b72b8..0178c36 100644
--- a/runtime/ui/view/views.go
+++ b/runtime/ui/view/views.go
@@ -6,13 +6,29 @@ import (
"github.com/wagoodman/dive/dive/image"