From 2030e74234f37b30b9b0e7ab10d8ffff76cb8e05 Mon Sep 17 00:00:00 2001 From: lightsnowball Date: Thu, 21 Apr 2022 16:28:57 +0000 Subject: [PATCH 1/2] Fix error on Layer section and reduce information on one line per step. Instead of printing out multiple lines for some steps in Layer section, now its only printing one line while other informations can be found in Layer details. This change also provides fix for index out of bounds error when user scrolls through steps in Layer section and there exists at least one step with multi-line commands. --- dive/image/layer.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dive/image/layer.go b/dive/image/layer.go index 550eba18..ac963639 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]) } From 2aad87c37e76d7e6e9d76dafa9eda20801b0c1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?= Date: Fri, 1 Apr 2022 08:40:02 +0200 Subject: [PATCH 2/2] Refactor the GUI layout The Details struct was split into two, LayerDetails and ImageDetails, Each of the three views (Layer, LayerDetails, ImageDetails) takes up a third of the available height of the screen, and they are all now selectable and scrollable. --- runtime/ui/app.go | 10 +- runtime/ui/controller.go | 50 ++++- .../layout/compound/layer_details_column.go | 102 ++++----- runtime/ui/view/details.go | 204 ------------------ runtime/ui/view/filetree.go | 2 +- runtime/ui/view/filter.go | 8 +- runtime/ui/view/image_details.go | 173 +++++++++++++++ runtime/ui/view/layer.go | 50 ++--- runtime/ui/view/layer_details.go | 140 ++++++++++++ runtime/ui/view/views.go | 53 +++-- 10 files changed, 478 insertions(+), 314 deletions(-) delete mode 100644 runtime/ui/view/details.go create mode 100644 runtime/ui/view/image_details.go create mode 100644 runtime/ui/view/layer_details.go diff --git a/runtime/ui/app.go b/runtime/ui/app.go index 036429f5..249a3aa1 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 @@ -76,6 +76,14 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca OnAction: controller.ToggleView, Display: "Switch view", }, + { + Key: gocui.KeyArrowRight, + OnAction: controller.NextPane, + }, + { + Key: gocui.KeyArrowLeft, + OnAction: controller.PrevPane, + }, { ConfigKeys: []string{"keybinding.filter-files"}, OnAction: controller.ToggleFilterView, diff --git a/runtime/ui/controller.go b/runtime/ui/controller.go index 964fb832..bc764232 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 3981442f..8c73b2ae 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, )", 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 43ea329f..00000000 --- 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 5b551c24..e90f0e40 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 5fdf9151..83978e56 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 00000000..6a31b552 --- /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 c1f9d0c4..d6616d1f 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 @@ -117,16 +123,6 @@ func (v *Layer) Setup(view *gocui.View, header *gocui.View) error { Modifier: gocui.ModNone, 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 00000000..b4c6f688 --- /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 b1b72b85..0178c36c 100644 --- a/runtime/ui/view/views.go +++ b/runtime/ui/view/views.go @@ -6,13 +6,29 @@ import ( "github.com/wagoodman/dive/dive/image" ) +type IView interface { + Setup(*gocui.View, *gocui.View) error + Name() string + IsVisible() bool +} + type Views struct { - Tree *FileTree - Layer *Layer - Status *Status - Filter *Filter - Details *Details - Debug *Debug + Tree *FileTree + Layer *Layer + Status *Status + Filter *Filter + LayerDetails *LayerDetails + ImageDetails *ImageDetails + Debug *Debug +} + +var _ []IView = []IView{ + &FileTree{}, + &Layer{}, + &Filter{}, + &LayerDetails{}, + &ImageDetails{}, + &Debug{}, } func NewViews(g *gocui.Gui, imageName string, analysis *image.AnalysisResult, cache filetree.Comparer) (*Views, error) { @@ -34,17 +50,25 @@ func NewViews(g *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca Filter := newFilterView(g) - Details := newDetailsView(g, imageName, analysis.Efficiency, analysis.Inefficiencies, analysis.SizeBytes) + LayerDetails := &LayerDetails{gui: g} + ImageDetails := &ImageDetails{ + gui: g, + imageName: imageName, + imageSize: analysis.SizeBytes, + efficiency: analysis.Efficiency, + inefficiencies: analysis.Inefficiencies, + } Debug := newDebugView(g) return &Views{ - Tree: Tree, - Layer: Layer, - Status: Status, - Filter: Filter, - Details: Details, - Debug: Debug, + Tree: Tree, + Layer: Layer, + Status: Status, + Filter: Filter, + ImageDetails: ImageDetails, + LayerDetails: LayerDetails, + Debug: Debug, }, nil } @@ -54,6 +78,7 @@ func (views *Views) All() []Renderer { views.Layer, views.Status, views.Filter, - views.Details, + views.LayerDetails, + views.ImageDetails, } }