Merge pull request #399 from mark2185/feature/gui-layout-rework
GUI rework
This commit is contained in:
commit
8bf4341f70
@ -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])
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
173
runtime/ui/view/image_details.go
Normal file
173
runtime/ui/view/image_details.go
Normal 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
|
||||
}
|
@ -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 {
|
||||
|
140
runtime/ui/view/layer_details.go
Normal file
140
runtime/ui/view/layer_details.go
Normal 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)
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user