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, } }