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.
This commit is contained in:
Luka Markušić 2022-04-01 08:40:02 +02:00
parent 2030e74234
commit 2aad87c37e
10 changed files with 482 additions and 318 deletions

View file

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

View file

@ -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()

View file

@ -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)
logrus.Debug("unable to setup row layout for ", viewName, err)
return err
}
}
return nil
}
if _, err = g.SetCurrentView(cl.layer.Name()); err != 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,
}
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
}
}
////////////////////////////////////////////////////////////////////////////////////
// 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
}
}
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 {
return err
}
}
return nil
}

View file

@ -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"
}

View file

@ -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

View file

@ -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

View file

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

View file

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

View file

@ -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)
}

View file

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