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:
parent
2030e74234
commit
2aad87c37e
@ -42,7 +42,7 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
|
|||||||
lm := layout.NewManager()
|
lm := layout.NewManager()
|
||||||
lm.Add(controller.views.Status, layout.LocationFooter)
|
lm.Add(controller.views.Status, layout.LocationFooter)
|
||||||
lm.Add(controller.views.Filter, 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)
|
lm.Add(controller.views.Tree, layout.LocationColumn)
|
||||||
|
|
||||||
// todo: access this more programmatically
|
// todo: access this more programmatically
|
||||||
@ -76,6 +76,14 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
|
|||||||
OnAction: controller.ToggleView,
|
OnAction: controller.ToggleView,
|
||||||
Display: "Switch view",
|
Display: "Switch view",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: gocui.KeyArrowRight,
|
||||||
|
OnAction: controller.NextPane,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: gocui.KeyArrowLeft,
|
||||||
|
OnAction: controller.PrevPane,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ConfigKeys: []string{"keybinding.filter-files"},
|
ConfigKeys: []string{"keybinding.filter-files"},
|
||||||
OnAction: controller.ToggleFilterView,
|
OnAction: controller.ToggleFilterView,
|
||||||
|
@ -82,7 +82,7 @@ func (c *Controller) onFilterEdit(filter string) error {
|
|||||||
|
|
||||||
func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error {
|
func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error {
|
||||||
// update the details
|
// update the details
|
||||||
c.views.Details.SetCurrentLayer(selection.Layer)
|
c.views.LayerDetails.CurrentLayer = selection.Layer
|
||||||
|
|
||||||
// update the filetree
|
// update the filetree
|
||||||
err := c.views.Tree.SetTree(selection.BottomTreeStart, selection.BottomTreeStop, selection.TopTreeStart, selection.TopTreeStop)
|
err := c.views.Tree.SetTree(selection.BottomTreeStart, selection.BottomTreeStop, selection.TopTreeStart, selection.TopTreeStop)
|
||||||
@ -141,6 +141,54 @@ func (c *Controller) Render() error {
|
|||||||
return nil
|
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.
|
// ToggleView switches between the file view and the layer view and re-renders the screen.
|
||||||
func (c *Controller) ToggleView() (err error) {
|
func (c *Controller) ToggleView() (err error) {
|
||||||
v := c.gui.CurrentView()
|
v := c.gui.CurrentView()
|
||||||
|
@ -9,14 +9,16 @@ import (
|
|||||||
|
|
||||||
type LayerDetailsCompoundLayout struct {
|
type LayerDetailsCompoundLayout struct {
|
||||||
layer *view.Layer
|
layer *view.Layer
|
||||||
details *view.Details
|
layerDetails *view.LayerDetails
|
||||||
|
imageDetails *view.ImageDetails
|
||||||
constrainRealEstate bool
|
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{
|
return &LayerDetailsCompoundLayout{
|
||||||
layer: layer,
|
layer: layer,
|
||||||
details: details,
|
layerDetails: layerDetails,
|
||||||
|
imageDetails: imageDetails,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,87 +34,65 @@ func (cl *LayerDetailsCompoundLayout) OnLayoutChange() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cl.details.OnLayoutChange()
|
err = cl.layerDetails.OnLayoutChange()
|
||||||
if err != nil {
|
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 err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {
|
func (cl *LayerDetailsCompoundLayout) layoutRow(g *gocui.Gui, minX, minY, maxX, maxY int, viewName string, setup func(*gocui.View, *gocui.View) error) error {
|
||||||
logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, cl.Name())
|
logrus.Tracef("layoutRow(g, minX: %d, minY: %d, maxX: %d, maxY: %d, viewName: %s, <setup func>)", minX, minY, maxX, maxY, viewName)
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Layers View
|
|
||||||
|
|
||||||
// header + border
|
// header + border
|
||||||
layerHeaderHeight := 2
|
headerHeight := 2
|
||||||
|
|
||||||
layersHeight := cl.layer.LayerCount() + layerHeaderHeight + 1 // layers + header + base image layer row
|
|
||||||
maxLayerHeight := int(0.75 * float64(maxY))
|
|
||||||
if layersHeight > maxLayerHeight {
|
|
||||||
layersHeight = maxLayerHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// TODO: investigate overlap
|
||||||
// note: maxY needs to account for the (invisible) border, thus a +1
|
// 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)
|
// 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) {
|
if utils.IsNewView(bodyErr, headerErr) {
|
||||||
err := cl.layer.Setup(main, header)
|
err := setup(bodyView, headerView)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Error("unable to setup layer layout", err)
|
logrus.Debug("unable to setup row layout for ", viewName, err)
|
||||||
return 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)
|
logrus.Error("unable to set view to layer", err)
|
||||||
return 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
|
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.
|
// 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())
|
logrus.Tracef("view.Setup() %s", v.Name())
|
||||||
|
|
||||||
// set controller options
|
// 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
|
// 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.
|
// allows the user to filter the file tree by path.
|
||||||
type Filter struct {
|
type Filter struct {
|
||||||
name string
|
|
||||||
gui *gocui.Gui
|
gui *gocui.Gui
|
||||||
view *gocui.View
|
view *gocui.View
|
||||||
header *gocui.View
|
header *gocui.View
|
||||||
@ -34,7 +33,6 @@ func newFilterView(gui *gocui.Gui) (controller *Filter) {
|
|||||||
controller.filterEditListeners = make([]FilterEditListener, 0)
|
controller.filterEditListeners = make([]FilterEditListener, 0)
|
||||||
|
|
||||||
// populate main fields
|
// populate main fields
|
||||||
controller.name = "filter"
|
|
||||||
controller.gui = gui
|
controller.gui = gui
|
||||||
controller.labelStr = "Path Filter: "
|
controller.labelStr = "Path Filter: "
|
||||||
controller.hidden = true
|
controller.hidden = true
|
||||||
@ -49,11 +47,11 @@ func (v *Filter) AddFilterEditListener(listener ...FilterEditListener) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *Filter) Name() string {
|
func (v *Filter) Name() string {
|
||||||
return v.name
|
return "filter"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
// 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())
|
logrus.Tracef("view.Setup() %s", v.Name())
|
||||||
|
|
||||||
// set controller options
|
// set controller options
|
||||||
@ -82,7 +80,7 @@ func (v *Filter) ToggleVisible() error {
|
|||||||
v.hidden = !v.hidden
|
v.hidden = !v.hidden
|
||||||
|
|
||||||
if !v.hidden {
|
if !v.hidden {
|
||||||
_, err := v.gui.SetCurrentView(v.name)
|
_, err := v.gui.SetCurrentView(v.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Error("unable to toggle filter view: ", err)
|
logrus.Error("unable to toggle filter view: ", err)
|
||||||
return 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"
|
"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
|
// Layer holds the UI objects and data models for populating the lower-left pane.
|
||||||
// shows the image layers and layer selector.
|
// Specifically the pane that shows the image layers and layer selector.
|
||||||
type Layer struct {
|
type Layer struct {
|
||||||
name string
|
name string
|
||||||
gui *gocui.Gui
|
gui *gocui.Gui
|
||||||
view *gocui.View
|
body *gocui.View
|
||||||
header *gocui.View
|
header *gocui.View
|
||||||
vm *viewmodel.LayerSetState
|
vm *viewmodel.LayerSetState
|
||||||
constrainedRealEstate bool
|
constrainedRealEstate bool
|
||||||
@ -72,6 +72,12 @@ func (v *Layer) notifyLayerChangeListeners() error {
|
|||||||
return err
|
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
|
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.
|
// 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())
|
logrus.Tracef("view.Setup() %s", v.Name())
|
||||||
|
|
||||||
// set controller options
|
// set controller options
|
||||||
v.view = view
|
v.body = body
|
||||||
v.view.Editable = false
|
v.body.Editable = false
|
||||||
v.view.Wrap = false
|
v.body.Wrap = false
|
||||||
v.view.Frame = false
|
v.body.Frame = false
|
||||||
|
|
||||||
v.header = header
|
v.header = header
|
||||||
v.header.Editable = false
|
v.header.Editable = false
|
||||||
@ -117,16 +123,6 @@ func (v *Layer) Setup(view *gocui.View, header *gocui.View) error {
|
|||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
OnAction: v.CursorUp,
|
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"},
|
ConfigKeys: []string{"keybinding.page-up"},
|
||||||
OnAction: v.PageUp,
|
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).
|
// height obtains the height of the current pane (taking into account the lost space due to the header).
|
||||||
func (v *Layer) height() uint {
|
func (v *Layer) height() uint {
|
||||||
_, height := v.view.Size()
|
_, height := v.body.Size()
|
||||||
return uint(height - 1)
|
return uint(height - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +167,7 @@ func (v *Layer) PageDown() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if step > 0 {
|
if step > 0 {
|
||||||
err := CursorStep(v.gui, v.view, step)
|
err := CursorStep(v.gui, v.body, step)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return v.SetCursor(v.vm.LayerIndex + step)
|
return v.SetCursor(v.vm.LayerIndex + step)
|
||||||
}
|
}
|
||||||
@ -189,7 +185,7 @@ func (v *Layer) PageUp() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if step > 0 {
|
if step > 0 {
|
||||||
err := CursorStep(v.gui, v.view, -step)
|
err := CursorStep(v.gui, v.body, -step)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return v.SetCursor(v.vm.LayerIndex - step)
|
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).
|
// CursorDown moves the cursor down in the layer pane (selecting a higher layer).
|
||||||
func (v *Layer) CursorDown() error {
|
func (v *Layer) CursorDown() error {
|
||||||
if v.vm.LayerIndex < len(v.vm.Layers) {
|
if v.vm.LayerIndex < len(v.vm.Layers) {
|
||||||
err := CursorDown(v.gui, v.view)
|
err := CursorDown(v.gui, v.body)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return v.SetCursor(v.vm.LayerIndex + 1)
|
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).
|
// CursorUp moves the cursor up in the layer pane (selecting a lower layer).
|
||||||
func (v *Layer) CursorUp() error {
|
func (v *Layer) CursorUp() error {
|
||||||
if v.vm.LayerIndex > 0 {
|
if v.vm.LayerIndex > 0 {
|
||||||
err := CursorUp(v.gui, v.view)
|
err := CursorUp(v.gui, v.body)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return v.SetCursor(v.vm.LayerIndex - 1)
|
return v.SetCursor(v.vm.LayerIndex - 1)
|
||||||
}
|
}
|
||||||
@ -292,7 +288,7 @@ func (v *Layer) Render() error {
|
|||||||
|
|
||||||
// indicate when selected
|
// indicate when selected
|
||||||
title := "Layers"
|
title := "Layers"
|
||||||
isSelected := v.gui.CurrentView() == v.view
|
isSelected := v.gui.CurrentView() == v.body
|
||||||
|
|
||||||
v.gui.Update(func(g *gocui.Gui) error {
|
v.gui.Update(func(g *gocui.Gui) error {
|
||||||
var err error
|
var err error
|
||||||
@ -316,7 +312,7 @@ func (v *Layer) Render() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update contents
|
// update contents
|
||||||
v.view.Clear()
|
v.body.Clear()
|
||||||
for idx, layer := range v.vm.Layers {
|
for idx, layer := range v.vm.Layers {
|
||||||
|
|
||||||
var layerStr string
|
var layerStr string
|
||||||
@ -329,9 +325,9 @@ func (v *Layer) Render() error {
|
|||||||
compareBar := v.renderCompareBar(idx)
|
compareBar := v.renderCompareBar(idx)
|
||||||
|
|
||||||
if idx == v.vm.LayerIndex {
|
if idx == v.vm.LayerIndex {
|
||||||
_, err = fmt.Fprintln(v.view, compareBar+" "+format.Selected(layerStr))
|
_, err = fmt.Fprintln(v.body, compareBar+" "+format.Selected(layerStr))
|
||||||
} else {
|
} else {
|
||||||
_, err = fmt.Fprintln(v.view, compareBar+" "+layerStr)
|
_, err = fmt.Fprintln(v.body, compareBar+" "+layerStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
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"
|
"github.com/wagoodman/dive/dive/image"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type IView interface {
|
||||||
|
Setup(*gocui.View, *gocui.View) error
|
||||||
|
Name() string
|
||||||
|
IsVisible() bool
|
||||||
|
}
|
||||||
|
|
||||||
type Views struct {
|
type Views struct {
|
||||||
Tree *FileTree
|
Tree *FileTree
|
||||||
Layer *Layer
|
Layer *Layer
|
||||||
Status *Status
|
Status *Status
|
||||||
Filter *Filter
|
Filter *Filter
|
||||||
Details *Details
|
LayerDetails *LayerDetails
|
||||||
Debug *Debug
|
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) {
|
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)
|
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)
|
Debug := newDebugView(g)
|
||||||
|
|
||||||
return &Views{
|
return &Views{
|
||||||
Tree: Tree,
|
Tree: Tree,
|
||||||
Layer: Layer,
|
Layer: Layer,
|
||||||
Status: Status,
|
Status: Status,
|
||||||
Filter: Filter,
|
Filter: Filter,
|
||||||
Details: Details,
|
ImageDetails: ImageDetails,
|
||||||
Debug: Debug,
|
LayerDetails: LayerDetails,
|
||||||
|
Debug: Debug,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +78,7 @@ func (views *Views) All() []Renderer {
|
|||||||
views.Layer,
|
views.Layer,
|
||||||
views.Status,
|
views.Status,
|
||||||
views.Filter,
|
views.Filter,
|
||||||
views.Details,
|
views.LayerDetails,
|
||||||
|
views.ImageDetails,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user