added simple layout manager
This commit is contained in:
parent
119040e72c
commit
14706152a1
@ -141,6 +141,10 @@ func initLogging() {
|
||||
|
||||
log.SetLevel(level)
|
||||
log.Debug("Starting Dive...")
|
||||
log.Debugf("config filepath: %s", viper.ConfigFileUsed())
|
||||
for k, v := range viper.AllSettings() {
|
||||
log.Debug("config value: ", k, " : ", v)
|
||||
}
|
||||
}
|
||||
|
||||
// getCfgFile checks for config file in paths from xdg specs
|
||||
|
@ -3,6 +3,8 @@ package ui
|
||||
import (
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"github.com/wagoodman/dive/runtime/ui/key"
|
||||
"github.com/wagoodman/dive/runtime/ui/layout"
|
||||
"github.com/wagoodman/dive/runtime/ui/layout/compound"
|
||||
"sync"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
@ -16,7 +18,7 @@ const debug = false
|
||||
type app struct {
|
||||
gui *gocui.Gui
|
||||
controllers *Controller
|
||||
layout *layoutManager
|
||||
layout *layout.Manager
|
||||
}
|
||||
|
||||
var (
|
||||
@ -27,19 +29,24 @@ var (
|
||||
func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Comparer) (*app, error) {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
var theControls *Controller
|
||||
var controller *Controller
|
||||
var globalHelpKeys []*key.Binding
|
||||
|
||||
theControls, err = NewCollection(gui, analysis, cache)
|
||||
controller, err = NewCollection(gui, analysis, cache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lm := newLayoutManager(theControls)
|
||||
// note: order matters when adding elements to the layout
|
||||
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(controller.views.Tree, layout.LocationColumn)
|
||||
|
||||
gui.Cursor = false
|
||||
//g.Mouse = true
|
||||
gui.SetManagerFunc(lm.layout)
|
||||
gui.SetManagerFunc(lm.Layout)
|
||||
|
||||
// var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook)
|
||||
//
|
||||
@ -49,7 +56,7 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa
|
||||
|
||||
appSingleton = &app{
|
||||
gui: gui,
|
||||
controllers: theControls,
|
||||
controllers: controller,
|
||||
layout: lm,
|
||||
}
|
||||
|
||||
@ -61,13 +68,13 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.toggle-view"},
|
||||
OnAction: theControls.ToggleView,
|
||||
OnAction: controller.ToggleView,
|
||||
Display: "Switch view",
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.filter-files"},
|
||||
OnAction: theControls.ToggleFilterView,
|
||||
IsSelected: theControls.Filter.IsVisible,
|
||||
OnAction: controller.ToggleFilterView,
|
||||
IsSelected: controller.views.Filter.IsVisible,
|
||||
Display: "Filter",
|
||||
},
|
||||
}
|
||||
@ -77,10 +84,10 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa
|
||||
return
|
||||
}
|
||||
|
||||
theControls.Status.AddHelpKeys(globalHelpKeys...)
|
||||
controller.views.Status.AddHelpKeys(globalHelpKeys...)
|
||||
|
||||
// perform the first update and render now that all resources have been loaded
|
||||
err = theControls.UpdateAndRender()
|
||||
err = controller.UpdateAndRender()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -106,8 +113,6 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Compa
|
||||
// }
|
||||
// }
|
||||
|
||||
var lastX, lastY int
|
||||
|
||||
// quit is the gocui callback invoked when the user hits Ctrl+C
|
||||
func (a *app) quit() error {
|
||||
|
||||
|
@ -11,61 +11,33 @@ import (
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
gui *gocui.Gui
|
||||
Tree *view.FileTree
|
||||
Layer *view.Layer
|
||||
Status *view.Status
|
||||
Filter *view.Filter
|
||||
Details *view.Details
|
||||
lookup map[string]view.Renderer
|
||||
gui *gocui.Gui
|
||||
views *view.Views
|
||||
}
|
||||
|
||||
func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Comparer) (*Controller, error) {
|
||||
var err error
|
||||
views, err := view.NewViews(g, analysis, cache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
controller := &Controller{
|
||||
gui: g,
|
||||
gui: g,
|
||||
views: views,
|
||||
}
|
||||
controller.lookup = make(map[string]view.Renderer)
|
||||
|
||||
controller.Layer, err = view.NewLayerView("layers", g, analysis.Layers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
controller.lookup[controller.Layer.Name()] = controller.Layer
|
||||
|
||||
//treeStack, err := filetree.StackTreeRange(analysis.RefTrees, 0, 0)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
treeStack := analysis.RefTrees[0]
|
||||
controller.Tree, err = view.NewFileTreeView("filetree", g, treeStack, analysis.RefTrees, cache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
controller.lookup[controller.Tree.Name()] = controller.Tree
|
||||
|
||||
// layer view cursor down event should trigger an update in the file tree
|
||||
controller.Layer.AddLayerChangeListener(controller.onLayerChange)
|
||||
|
||||
controller.Status = view.NewStatusView("status", g)
|
||||
controller.lookup[controller.Status.Name()] = controller.Status
|
||||
// set the layer view as the first selected view
|
||||
controller.Status.SetCurrentView(controller.Layer)
|
||||
controller.views.Layer.AddLayerChangeListener(controller.onLayerChange)
|
||||
|
||||
// update the status pane when a filetree option is changed by the user
|
||||
controller.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange)
|
||||
controller.views.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange)
|
||||
|
||||
controller.Filter = view.NewFilterView("filter", g)
|
||||
controller.lookup[controller.Filter.Name()] = controller.Filter
|
||||
controller.Filter.AddFilterEditListener(controller.onFilterEdit)
|
||||
|
||||
controller.Details = view.NewDetailsView("details", g, analysis.Efficiency, analysis.Inefficiencies, analysis.SizeBytes)
|
||||
controller.lookup[controller.Details.Name()] = controller.Details
|
||||
// update the tree view while the user types into the filter view
|
||||
controller.views.Filter.AddFilterEditListener(controller.onFilterEdit)
|
||||
|
||||
// propagate initial conditions to necessary views
|
||||
err = controller.onLayerChange(viewmodel.LayerSelection{
|
||||
Layer: controller.Layer.CurrentLayer(),
|
||||
Layer: controller.views.Layer.CurrentLayer(),
|
||||
BottomTreeStart: 0,
|
||||
BottomTreeStop: 0,
|
||||
TopTreeStart: 0,
|
||||
@ -80,11 +52,11 @@ func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.
|
||||
}
|
||||
|
||||
func (c *Controller) onFileTreeViewOptionChange() error {
|
||||
err := c.Status.Update()
|
||||
err := c.views.Status.Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status.Render()
|
||||
return c.views.Status.Render()
|
||||
}
|
||||
|
||||
func (c *Controller) onFilterEdit(filter string) error {
|
||||
@ -98,30 +70,30 @@ func (c *Controller) onFilterEdit(filter string) error {
|
||||
}
|
||||
}
|
||||
|
||||
c.Tree.SetFilterRegex(filterRegex)
|
||||
c.views.Tree.SetFilterRegex(filterRegex)
|
||||
|
||||
err = c.Tree.Update()
|
||||
err = c.views.Tree.Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Tree.Render()
|
||||
return c.views.Tree.Render()
|
||||
}
|
||||
|
||||
func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error {
|
||||
// update the details
|
||||
c.Details.SetCurrentLayer(selection.Layer)
|
||||
c.views.Details.SetCurrentLayer(selection.Layer)
|
||||
|
||||
// update the filetree
|
||||
err := c.Tree.SetTree(selection.BottomTreeStart, selection.BottomTreeStop, selection.TopTreeStart, selection.TopTreeStop)
|
||||
err := c.views.Tree.SetTree(selection.BottomTreeStart, selection.BottomTreeStop, selection.TopTreeStart, selection.TopTreeStop)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Layer.CompareMode == view.CompareAll {
|
||||
c.Tree.SetTitle("Aggregated Layer Contents")
|
||||
if c.views.Layer.CompareMode == view.CompareAll {
|
||||
c.views.Tree.SetTitle("Aggregated Layer Contents")
|
||||
} else {
|
||||
c.Tree.SetTitle("Current Layer Contents")
|
||||
c.views.Tree.SetTitle("Current Layer Contents")
|
||||
}
|
||||
|
||||
// update details and filetree panes
|
||||
@ -146,7 +118,7 @@ func (c *Controller) UpdateAndRender() error {
|
||||
|
||||
// Update refreshes the state objects for future rendering.
|
||||
func (c *Controller) Update() error {
|
||||
for _, controller := range c.lookup {
|
||||
for _, controller := range c.views.All() {
|
||||
err := controller.Update()
|
||||
if err != nil {
|
||||
logrus.Debug("unable to update controller: ")
|
||||
@ -158,7 +130,7 @@ func (c *Controller) Update() error {
|
||||
|
||||
// Render flushes the state objects to the screen.
|
||||
func (c *Controller) Render() error {
|
||||
for _, controller := range c.lookup {
|
||||
for _, controller := range c.views.All() {
|
||||
if controller.IsVisible() {
|
||||
err := controller.Render()
|
||||
if err != nil {
|
||||
@ -172,12 +144,12 @@ func (c *Controller) Render() error {
|
||||
// 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()
|
||||
if v == nil || v.Name() == c.Layer.Name() {
|
||||
_, err = c.gui.SetCurrentView(c.Tree.Name())
|
||||
c.Status.SetCurrentView(c.Tree)
|
||||
if v == nil || v.Name() == c.views.Layer.Name() {
|
||||
_, err = c.gui.SetCurrentView(c.views.Tree.Name())
|
||||
c.views.Status.SetCurrentView(c.views.Tree)
|
||||
} else {
|
||||
_, err = c.gui.SetCurrentView(c.Layer.Name())
|
||||
c.Status.SetCurrentView(c.Layer)
|
||||
_, err = c.gui.SetCurrentView(c.views.Layer.Name())
|
||||
c.views.Status.SetCurrentView(c.views.Layer)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -190,16 +162,16 @@ func (c *Controller) ToggleView() (err error) {
|
||||
|
||||
func (c *Controller) ToggleFilterView() error {
|
||||
// delete all user input from the tree view
|
||||
err := c.Filter.ToggleVisible()
|
||||
err := c.views.Filter.ToggleVisible()
|
||||
if err != nil {
|
||||
logrus.Error("unable to toggle filter visibility: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// we have just hidden the filter view...
|
||||
if !c.Filter.IsVisible() {
|
||||
if !c.views.Filter.IsVisible() {
|
||||
// ...remove any filter from the tree
|
||||
c.Tree.SetFilterRegex(nil)
|
||||
c.views.Tree.SetFilterRegex(nil)
|
||||
|
||||
// ...adjust focus to a valid (visible) view
|
||||
err = c.ToggleView()
|
||||
|
96
runtime/ui/layout/compound/layer_details_column.go
Normal file
96
runtime/ui/layout/compound/layer_details_column.go
Normal file
@ -0,0 +1,96 @@
|
||||
package compound
|
||||
|
||||
import (
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/wagoodman/dive/runtime/ui/view"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
)
|
||||
|
||||
type LayerDetailsCompoundLayout struct {
|
||||
layer *view.Layer
|
||||
details *view.Details
|
||||
}
|
||||
|
||||
func NewLayerDetailsCompoundLayout(layer *view.Layer, details *view.Details) *LayerDetailsCompoundLayout {
|
||||
return &LayerDetailsCompoundLayout{
|
||||
layer: layer,
|
||||
details: details,
|
||||
}
|
||||
}
|
||||
|
||||
func (cl *LayerDetailsCompoundLayout) Name() string {
|
||||
return "layer-details-compound-column"
|
||||
}
|
||||
|
||||
func (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error {
|
||||
logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, cl.Name())
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// Layers View
|
||||
|
||||
// header + border
|
||||
layerHeaderHeight := 2
|
||||
|
||||
layersHeight := len(cl.layer.Layers) + layerHeaderHeight + 1 // layers + header + base image layer row
|
||||
maxLayerHeight := int(0.75 * float64(maxY))
|
||||
if layersHeight > maxLayerHeight {
|
||||
layersHeight = maxLayerHeight
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
|
||||
if utils.IsNewView(viewErr, headerErr) {
|
||||
err := cl.layer.Setup(main, header)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup layer layout", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = g.SetCurrentView(cl.layer.Name()); err != nil {
|
||||
logrus.Error("unable to set view to layer", err)
|
||||
return err
|
||||
}
|
||||
// since we are selecting the view, we should rerender to indicate it is selected
|
||||
err = cl.layer.Render()
|
||||
if err != nil {
|
||||
logrus.Error("unable to render layer view", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// Details
|
||||
detailsMinY := minY + layersHeight
|
||||
|
||||
// header + border
|
||||
detailsHeaderHeight := 2
|
||||
|
||||
// note: maxY needs to account for the (invisible) border, thus a +1
|
||||
header, headerErr = g.SetView(cl.details.Name()+"header", minX, detailsMinY, maxX, detailsMinY+detailsHeaderHeight+1)
|
||||
|
||||
// we are going to overlap the view over the (invisible) border (so minY will be one less than expected)
|
||||
// additionally, maxY will be bumped by one to include the border
|
||||
main, viewErr = g.SetView(cl.details.Name(), minX, detailsMinY+detailsHeaderHeight, maxX, maxY+1)
|
||||
|
||||
if utils.IsNewView(viewErr, headerErr) {
|
||||
err := cl.details.Setup(main, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cl *LayerDetailsCompoundLayout) RequestedSize(available int) *int {
|
||||
return nil
|
||||
}
|
||||
|
||||
// todo: make this variable based on the nested views
|
||||
func (cl *LayerDetailsCompoundLayout) IsVisible() bool {
|
||||
return true
|
||||
}
|
10
runtime/ui/layout/layout.go
Normal file
10
runtime/ui/layout/layout.go
Normal file
@ -0,0 +1,10 @@
|
||||
package layout
|
||||
|
||||
import "github.com/jroimartin/gocui"
|
||||
|
||||
type Layout interface {
|
||||
Name() string
|
||||
Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error
|
||||
RequestedSize(available int) *int
|
||||
IsVisible() bool
|
||||
}
|
9
runtime/ui/layout/location.go
Normal file
9
runtime/ui/layout/location.go
Normal file
@ -0,0 +1,9 @@
|
||||
package layout
|
||||
|
||||
const (
|
||||
LocationFooter Location = iota
|
||||
LocationHeader
|
||||
LocationColumn
|
||||
)
|
||||
|
||||
type Location int
|
176
runtime/ui/layout/manager.go
Normal file
176
runtime/ui/layout/manager.go
Normal file
@ -0,0 +1,176 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
lastX, lastY int
|
||||
elements map[Location][]Layout
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
elements: make(map[Location][]Layout),
|
||||
}
|
||||
}
|
||||
|
||||
func (lm *Manager) Add(element Layout, location Location) {
|
||||
if _, exists := lm.elements[location]; !exists {
|
||||
lm.elements[location] = make([]Layout, 0)
|
||||
}
|
||||
lm.elements[location] = append(lm.elements[location], element)
|
||||
}
|
||||
|
||||
// layout defines the definition of the window pane size and placement relations to one another. This
|
||||
// is invoked at application start and whenever the screen dimensions change.
|
||||
// A few things to note:
|
||||
// 1. gocui has borders around all views (even if Frame=false). This means there are a lot of +1/-1 magic numbers
|
||||
// needed (but there are comments!).
|
||||
// 2. since there are borders, in order for it to appear as if there aren't any spaces for borders, the views must
|
||||
// overlap. To prevent screen artifacts, all elements must be layedout from the top of the screen to the bottom.
|
||||
func (lm *Manager) Layout(g *gocui.Gui) error {
|
||||
|
||||
minX, minY := -1, -1
|
||||
maxX, maxY := g.Size()
|
||||
|
||||
var hasResized bool
|
||||
if maxX != lm.lastX || maxY != lm.lastY {
|
||||
hasResized = true
|
||||
}
|
||||
lm.lastX, lm.lastY = maxX, maxY
|
||||
|
||||
// layout headers top down
|
||||
if elements, exists := lm.elements[LocationHeader]; exists {
|
||||
for _, element := range elements {
|
||||
// a visible header cannot take up the whole screen, default to 1.
|
||||
// this eliminates the need to discover a default size based on all element requests
|
||||
height := 0
|
||||
if element.IsVisible() {
|
||||
requestedHeight := element.RequestedSize(maxY)
|
||||
if requestedHeight != nil {
|
||||
height = *requestedHeight
|
||||
} else {
|
||||
height = 1
|
||||
}
|
||||
}
|
||||
|
||||
// layout the header within the allocated space
|
||||
err := element.Layout(g, minX, minY, maxX, minY+height, hasResized)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to layout '%s' header: %+v", element.Name(), err)
|
||||
}
|
||||
|
||||
// restrict the available screen real estate
|
||||
minY += height
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var footerHeights = make([]int, 0)
|
||||
// we need to keep the current maxY before carving out the space for the body columns
|
||||
var footerMaxY = maxY
|
||||
var footerMinX = minX
|
||||
var footerMaxX = maxX
|
||||
|
||||
// we need to layout the footers last, but account for them when drawing the columns. This block is for planning
|
||||
// out the real estate needed for the footers now (but not laying out yet)
|
||||
if elements, exists := lm.elements[LocationFooter]; exists {
|
||||
footerHeights = make([]int, len(elements))
|
||||
for idx := range footerHeights {
|
||||
footerHeights[idx] = 1
|
||||
}
|
||||
|
||||
for idx, element := range elements {
|
||||
// a visible footer cannot take up the whole screen, default to 1.
|
||||
// this eliminates the need to discover a default size based on all element requests
|
||||
height := 0
|
||||
if element.IsVisible() {
|
||||
requestedHeight := element.RequestedSize(maxY)
|
||||
if requestedHeight != nil {
|
||||
height = *requestedHeight
|
||||
} else {
|
||||
height = 1
|
||||
}
|
||||
}
|
||||
footerHeights[idx] = height
|
||||
}
|
||||
// restrict the available screen real estate
|
||||
for _, height := range footerHeights {
|
||||
maxY -= height
|
||||
}
|
||||
}
|
||||
|
||||
// layout columns left to right
|
||||
if elements, exists := lm.elements[LocationColumn]; exists {
|
||||
widths := make([]int, len(elements))
|
||||
for idx := range widths {
|
||||
widths[idx] = -1
|
||||
}
|
||||
variableColumns := len(elements)
|
||||
availableWidth := maxX
|
||||
|
||||
// first pass: planout the column sizes based on the given requests
|
||||
for idx, element := range elements {
|
||||
if !element.IsVisible() {
|
||||
widths[idx] = 0
|
||||
variableColumns--
|
||||
continue
|
||||
}
|
||||
|
||||
requestedWidth := element.RequestedSize(availableWidth)
|
||||
if requestedWidth != nil {
|
||||
widths[idx] = *requestedWidth
|
||||
variableColumns--
|
||||
availableWidth -= widths[idx]
|
||||
}
|
||||
}
|
||||
|
||||
defaultWidth := int(availableWidth / variableColumns)
|
||||
|
||||
// second pass: layout columns left to right (based off predetermined widths)
|
||||
for idx, element := range elements {
|
||||
// use the requested or default width
|
||||
width := widths[idx]
|
||||
if width == -1 {
|
||||
width = defaultWidth
|
||||
}
|
||||
|
||||
// layout the column within the allocated space
|
||||
err := element.Layout(g, minX, minY, minX+width, maxY, hasResized)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to layout '%s' column: %+v", element.Name(), err)
|
||||
}
|
||||
|
||||
// move left to right, scratching off real estate as it is taken
|
||||
minX += width
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// layout footers top down (which is why the list is reversed). Top down is needed due to border overlap.
|
||||
if elements, exists := lm.elements[LocationFooter]; exists {
|
||||
for idx := len(elements) - 1; idx >= 0; idx-- {
|
||||
element := elements[idx]
|
||||
height := footerHeights[idx]
|
||||
var topY, bottomY, bottomPadding int
|
||||
for oIdx := 0; oIdx <= idx; oIdx++ {
|
||||
bottomPadding += footerHeights[oIdx]
|
||||
}
|
||||
topY = footerMaxY - bottomPadding - height
|
||||
// +1 for border
|
||||
bottomY = topY + height + 1
|
||||
|
||||
// layout the footer within the allocated space
|
||||
// note: since the headers and rows are inclusive counting from -1 (to account for a border) we must
|
||||
// do the same vertically, thus a -1 is needed for a starting Y
|
||||
err := element.Layout(g, footerMinX, topY, footerMaxX, bottomY, hasResized)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to layout '%s' footer: %+v", element.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type layoutManager struct {
|
||||
fileTreeSplitRatio float64
|
||||
controllers *Controller
|
||||
}
|
||||
|
||||
// todo: this needs a major refactor (derive layout from view obj info, which should not live here)
|
||||
func newLayoutManager(c *Controller) *layoutManager {
|
||||
|
||||
fileTreeSplitRatio := viper.GetFloat64("filetree.pane-width")
|
||||
if fileTreeSplitRatio >= 1 || fileTreeSplitRatio <= 0 {
|
||||
logrus.Errorf("invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'", fileTreeSplitRatio)
|
||||
fileTreeSplitRatio = 0.5
|
||||
}
|
||||
|
||||
return &layoutManager{
|
||||
fileTreeSplitRatio: fileTreeSplitRatio,
|
||||
controllers: c,
|
||||
}
|
||||
}
|
||||
|
||||
// IsNewView determines if a view has already been created based on the set of errors given (a bit hokie)
|
||||
func IsNewView(errs ...error) bool {
|
||||
for _, err := range errs {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if err != gocui.ErrUnknownView {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// layout defines the definition of the window pane size and placement relations to one another. This
|
||||
// is invoked at application start and whenever the screen dimensions change.
|
||||
func (lm *layoutManager) layout(g *gocui.Gui) error {
|
||||
// TODO: this logic should be refactored into an abstraction that takes care of the math for us
|
||||
|
||||
maxX, maxY := g.Size()
|
||||
var resized bool
|
||||
if maxX != lastX {
|
||||
resized = true
|
||||
}
|
||||
if maxY != lastY {
|
||||
resized = true
|
||||
}
|
||||
lastX, lastY = maxX, maxY
|
||||
|
||||
splitCols := int(float64(maxX) * (1.0 - lm.fileTreeSplitRatio))
|
||||
debugWidth := 0
|
||||
if debug {
|
||||
debugWidth = maxX / 4
|
||||
}
|
||||
debugCols := maxX - debugWidth
|
||||
bottomRows := 1
|
||||
headerRows := 2
|
||||
|
||||
filterBarHeight := 1
|
||||
statusBarHeight := 1
|
||||
|
||||
statusBarIndex := 1
|
||||
filterBarIndex := 2
|
||||
|
||||
layersHeight := len(lm.controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
|
||||
maxLayerHeight := int(0.75 * float64(maxY))
|
||||
if layersHeight > maxLayerHeight {
|
||||
layersHeight = maxLayerHeight
|
||||
}
|
||||
|
||||
var view, header *gocui.View
|
||||
var viewErr, headerErr, err error
|
||||
|
||||
if !lm.controllers.Filter.IsVisible() {
|
||||
bottomRows--
|
||||
filterBarHeight = 0
|
||||
}
|
||||
|
||||
// Debug pane
|
||||
if debug {
|
||||
if _, err := g.SetView("debug", debugCols, -1, maxX, maxY-bottomRows); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Layers
|
||||
view, viewErr = g.SetView(lm.controllers.Layer.Name(), -1, -1+headerRows, splitCols, layersHeight)
|
||||
header, headerErr = g.SetView(lm.controllers.Layer.Name()+"header", -1, -1, splitCols, headerRows)
|
||||
if IsNewView(viewErr, headerErr) {
|
||||
err = lm.controllers.Layer.Setup(view, header)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup layer controller", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = g.SetCurrentView(lm.controllers.Layer.Name()); err != nil {
|
||||
logrus.Error("unable to set view to layer", err)
|
||||
return err
|
||||
}
|
||||
// since we are selecting the view, we should rerender to indicate it is selected
|
||||
err = lm.controllers.Layer.Render()
|
||||
if err != nil {
|
||||
logrus.Error("unable to render layer view", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Details
|
||||
view, viewErr = g.SetView(lm.controllers.Details.Name(), -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows)
|
||||
header, headerErr = g.SetView(lm.controllers.Details.Name()+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows)
|
||||
if IsNewView(viewErr, headerErr) {
|
||||
err = lm.controllers.Details.Setup(view, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Filetree
|
||||
offset := 0
|
||||
if !lm.controllers.Tree.AreAttributesVisible() {
|
||||
offset = 1
|
||||
}
|
||||
view, viewErr = g.SetView(lm.controllers.Tree.Name(), splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows)
|
||||
header, headerErr = g.SetView(lm.controllers.Tree.Name()+"header", splitCols, -1, debugCols, headerRows-offset)
|
||||
if IsNewView(viewErr, headerErr) {
|
||||
err = lm.controllers.Tree.Setup(view, header)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup tree controller", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = lm.controllers.Tree.OnLayoutChange(resized)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup layer controller onLayoutChange", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Status Bar
|
||||
view, viewErr = g.SetView(lm.controllers.Status.Name(), -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1))
|
||||
if IsNewView(viewErr, headerErr) {
|
||||
err = lm.controllers.Status.Setup(view, nil)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup status controller", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Filter Bar
|
||||
view, viewErr = g.SetView(lm.controllers.Filter.Name(), len(lm.controllers.Filter.HeaderStr())-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1))
|
||||
header, headerErr = g.SetView(lm.controllers.Filter.Name()+"header", -1, maxY-filterBarHeight-filterBarIndex, len(lm.controllers.Filter.HeaderStr()), maxY-(filterBarIndex-1))
|
||||
if IsNewView(viewErr, headerErr) {
|
||||
err = lm.controllers.Filter.Setup(view, header)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup filter controller", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -29,8 +29,8 @@ type Details struct {
|
||||
currentLayer *image.Layer
|
||||
}
|
||||
|
||||
// NewDetailsView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewDetailsView(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice, imageSize uint64) (controller *Details) {
|
||||
// newDetailsView creates a new view object attached the the global [gocui] screen object.
|
||||
func newDetailsView(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice, imageSize uint64) (controller *Details) {
|
||||
controller = new(Details)
|
||||
|
||||
// populate main fields
|
||||
@ -43,68 +43,69 @@ func NewDetailsView(name string, gui *gocui.Gui, efficiency float64, inefficienc
|
||||
return controller
|
||||
}
|
||||
|
||||
func (c *Details) Name() string {
|
||||
return c.name
|
||||
func (v *Details) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (c *Details) Setup(v *gocui.View, header *gocui.View) error {
|
||||
func (v *Details) Setup(view *gocui.View, header *gocui.View) error {
|
||||
logrus.Debugf("view.Setup() %s", v.Name())
|
||||
|
||||
// set controller options
|
||||
c.view = v
|
||||
c.view.Editable = false
|
||||
c.view.Wrap = true
|
||||
c.view.Highlight = false
|
||||
c.view.Frame = false
|
||||
v.view = view
|
||||
v.view.Editable = false
|
||||
v.view.Wrap = true
|
||||
v.view.Highlight = false
|
||||
v.view.Frame = false
|
||||
|
||||
c.header = header
|
||||
c.header.Editable = false
|
||||
c.header.Wrap = false
|
||||
c.header.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: c.CursorDown,
|
||||
OnAction: v.CursorDown,
|
||||
},
|
||||
{
|
||||
Key: gocui.KeyArrowUp,
|
||||
Modifier: gocui.ModNone,
|
||||
OnAction: c.CursorUp,
|
||||
OnAction: v.CursorUp,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := key.GenerateBindings(c.gui, c.name, infos)
|
||||
_, err := key.GenerateBindings(v.gui, v.name, infos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// IsVisible indicates if the details view pane is currently initialized.
|
||||
func (c *Details) IsVisible() bool {
|
||||
return c != nil
|
||||
func (v *Details) IsVisible() bool {
|
||||
return v != nil
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
|
||||
func (c *Details) CursorDown() error {
|
||||
return CursorDown(c.gui, c.view)
|
||||
func (v *Details) CursorDown() error {
|
||||
return CursorDown(v.gui, v.view)
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
|
||||
func (c *Details) CursorUp() error {
|
||||
return CursorUp(c.gui, c.view)
|
||||
func (v *Details) CursorUp() error {
|
||||
return CursorUp(v.gui, v.view)
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering.
|
||||
func (c *Details) Update() error {
|
||||
func (v *Details) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Details) SetCurrentLayer(layer *image.Layer) {
|
||||
c.currentLayer = layer
|
||||
func (v *Details) SetCurrentLayer(layer *image.Layer) {
|
||||
v.currentLayer = layer
|
||||
}
|
||||
|
||||
// Render flushes the state objects to the screen. The details pane reports:
|
||||
@ -112,8 +113,10 @@ func (c *Details) SetCurrentLayer(layer *image.Layer) {
|
||||
// 2. the image efficiency score
|
||||
// 3. the estimated wasted image space
|
||||
// 4. a list of inefficient file allocations
|
||||
func (c *Details) Render() error {
|
||||
if c.currentLayer == nil {
|
||||
func (v *Details) Render() error {
|
||||
logrus.Debugf("view.Render() %s", v.Name())
|
||||
|
||||
if v.currentLayer == nil {
|
||||
return fmt.Errorf("no layer selected")
|
||||
}
|
||||
|
||||
@ -123,12 +126,12 @@ func (c *Details) Render() error {
|
||||
inefficiencyReport := fmt.Sprintf(format.Header(template), "Count", "Total Space", "Path")
|
||||
|
||||
height := 100
|
||||
if c.view != nil {
|
||||
_, height = c.view.Size()
|
||||
if v.view != nil {
|
||||
_, height = v.view.Size()
|
||||
}
|
||||
|
||||
for idx := 0; idx < len(c.inefficiencies); idx++ {
|
||||
data := c.inefficiencies[len(c.inefficiencies)-1-idx]
|
||||
for idx := 0; idx < len(v.inefficiencies); idx++ {
|
||||
data := v.inefficiencies[len(v.inefficiencies)-1-idx]
|
||||
wastedSpace += data.CumulativeSize
|
||||
|
||||
// todo: make this report scrollable
|
||||
@ -137,43 +140,43 @@ func (c *Details) Render() error {
|
||||
}
|
||||
}
|
||||
|
||||
imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(c.imageSize))
|
||||
effStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*c.efficiency))
|
||||
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)))
|
||||
|
||||
c.gui.Update(func(g *gocui.Gui) error {
|
||||
v.gui.Update(func(g *gocui.Gui) error {
|
||||
// update header
|
||||
c.header.Clear()
|
||||
width, _ := c.view.Size()
|
||||
v.header.Clear()
|
||||
width, _ := v.view.Size()
|
||||
|
||||
layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15))
|
||||
imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15))
|
||||
|
||||
_, err := fmt.Fprintln(c.header, format.Header(vtclean.Clean(layerHeaderStr, false)))
|
||||
_, err := fmt.Fprintln(v.header, format.Header(vtclean.Clean(layerHeaderStr, false)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update contents
|
||||
c.view.Clear()
|
||||
v.view.Clear()
|
||||
|
||||
var lines = make([]string, 0)
|
||||
if c.currentLayer.Names != nil && len(c.currentLayer.Names) > 0 {
|
||||
lines = append(lines, format.Header("Tags: ")+strings.Join(c.currentLayer.Names, ", "))
|
||||
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: ")+c.currentLayer.Id)
|
||||
lines = append(lines, format.Header("Digest: ")+c.currentLayer.Digest)
|
||||
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, c.currentLayer.Command)
|
||||
lines = append(lines, v.currentLayer.Command)
|
||||
lines = append(lines, "\n"+format.Header(vtclean.Clean(imageHeaderStr, false)))
|
||||
lines = append(lines, imageSizeStr)
|
||||
lines = append(lines, wastedSpaceStr)
|
||||
lines = append(lines, effStr+"\n")
|
||||
lines = append(lines, inefficiencyReport)
|
||||
|
||||
_, err = fmt.Fprintln(c.view, strings.Join(lines, "\n"))
|
||||
_, err = fmt.Fprintln(v.view, strings.Join(lines, "\n"))
|
||||
if err != nil {
|
||||
logrus.Debug("unable to write to buffer: ", err)
|
||||
}
|
||||
@ -183,6 +186,6 @@ func (c *Details) Render() error {
|
||||
}
|
||||
|
||||
// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
|
||||
func (c *Details) KeyHelp() string {
|
||||
func (v *Details) KeyHelp() string {
|
||||
return "TBD"
|
||||
}
|
||||
|
@ -3,9 +3,11 @@ package view
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"github.com/wagoodman/dive/runtime/ui/key"
|
||||
"github.com/wagoodman/dive/runtime/ui/viewmodel"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@ -33,15 +35,14 @@ type FileTree struct {
|
||||
vm *viewmodel.FileTree
|
||||
title string
|
||||
|
||||
filterRegex *regexp.Regexp
|
||||
|
||||
listeners []ViewOptionChangeListener
|
||||
|
||||
helpKeys []*key.Binding
|
||||
filterRegex *regexp.Regexp
|
||||
listeners []ViewOptionChangeListener
|
||||
helpKeys []*key.Binding
|
||||
requestedWidthRatio float64
|
||||
}
|
||||
|
||||
// NewFileTreeView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.Comparer) (controller *FileTree, err error) {
|
||||
// newFileTreeView creates a new view object attached the the global [gocui] screen object.
|
||||
func newFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.Comparer) (controller *FileTree, err error) {
|
||||
controller = new(FileTree)
|
||||
controller.listeners = make([]ViewOptionChangeListener, 0)
|
||||
|
||||
@ -53,157 +54,165 @@ func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTr
|
||||
return nil, err
|
||||
}
|
||||
|
||||
requestedWidthRatio := viper.GetFloat64("filetree.pane-width")
|
||||
if requestedWidthRatio >= 1 || requestedWidthRatio <= 0 {
|
||||
logrus.Errorf("invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'", requestedWidthRatio)
|
||||
requestedWidthRatio = 0.5
|
||||
}
|
||||
controller.requestedWidthRatio = requestedWidthRatio
|
||||
|
||||
return controller, err
|
||||
}
|
||||
|
||||
func (c *FileTree) AddViewOptionChangeListener(listener ...ViewOptionChangeListener) {
|
||||
c.listeners = append(c.listeners, listener...)
|
||||
func (v *FileTree) AddViewOptionChangeListener(listener ...ViewOptionChangeListener) {
|
||||
v.listeners = append(v.listeners, listener...)
|
||||
}
|
||||
|
||||
func (c *FileTree) SetTitle(title string) {
|
||||
c.title = title
|
||||
func (v *FileTree) SetTitle(title string) {
|
||||
v.title = title
|
||||
}
|
||||
|
||||
func (c *FileTree) SetFilterRegex(filterRegex *regexp.Regexp) {
|
||||
c.filterRegex = filterRegex
|
||||
func (v *FileTree) SetFilterRegex(filterRegex *regexp.Regexp) {
|
||||
v.filterRegex = filterRegex
|
||||
}
|
||||
|
||||
func (c *FileTree) Name() string {
|
||||
return c.name
|
||||
func (v *FileTree) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func (c *FileTree) AreAttributesVisible() bool {
|
||||
return c.vm.ShowAttributes
|
||||
func (v *FileTree) areAttributesVisible() bool {
|
||||
return v.vm.ShowAttributes
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (c *FileTree) Setup(v *gocui.View, header *gocui.View) error {
|
||||
func (v *FileTree) Setup(view *gocui.View, header *gocui.View) error {
|
||||
logrus.Debugf("view.Setup() %s", v.Name())
|
||||
|
||||
// set controller options
|
||||
c.view = v
|
||||
c.view.Editable = false
|
||||
c.view.Wrap = false
|
||||
c.view.Frame = false
|
||||
v.view = view
|
||||
v.view.Editable = false
|
||||
v.view.Wrap = false
|
||||
v.view.Frame = false
|
||||
|
||||
c.header = header
|
||||
c.header.Editable = false
|
||||
c.header.Wrap = false
|
||||
c.header.Frame = false
|
||||
v.header = header
|
||||
v.header.Editable = false
|
||||
v.header.Wrap = false
|
||||
v.header.Frame = false
|
||||
|
||||
var infos = []key.BindingInfo{
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.toggle-collapse-dir"},
|
||||
OnAction: c.toggleCollapse,
|
||||
OnAction: v.toggleCollapse,
|
||||
Display: "Collapse dir",
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.toggle-collapse-all-dir"},
|
||||
OnAction: c.toggleCollapseAll,
|
||||
OnAction: v.toggleCollapseAll,
|
||||
Display: "Collapse all dir",
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.toggle-added-files"},
|
||||
OnAction: func() error { return c.toggleShowDiffType(filetree.Added) },
|
||||
IsSelected: func() bool { return !c.vm.HiddenDiffTypes[filetree.Added] },
|
||||
OnAction: func() error { return v.toggleShowDiffType(filetree.Added) },
|
||||
IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Added] },
|
||||
Display: "Added",
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.toggle-removed-files"},
|
||||
OnAction: func() error { return c.toggleShowDiffType(filetree.Removed) },
|
||||
IsSelected: func() bool { return !c.vm.HiddenDiffTypes[filetree.Removed] },
|
||||
OnAction: func() error { return v.toggleShowDiffType(filetree.Removed) },
|
||||
IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Removed] },
|
||||
Display: "Removed",
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.toggle-modified-files"},
|
||||
OnAction: func() error { return c.toggleShowDiffType(filetree.Modified) },
|
||||
IsSelected: func() bool { return !c.vm.HiddenDiffTypes[filetree.Modified] },
|
||||
OnAction: func() error { return v.toggleShowDiffType(filetree.Modified) },
|
||||
IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Modified] },
|
||||
Display: "Modified",
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.toggle-unchanged-files", "keybinding.toggle-unmodified-files"},
|
||||
OnAction: func() error { return c.toggleShowDiffType(filetree.Unmodified) },
|
||||
IsSelected: func() bool { return !c.vm.HiddenDiffTypes[filetree.Unmodified] },
|
||||
OnAction: func() error { return v.toggleShowDiffType(filetree.Unmodified) },
|
||||
IsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Unmodified] },
|
||||
Display: "Unmodified",
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.toggle-filetree-attributes"},
|
||||
OnAction: c.toggleAttributes,
|
||||
IsSelected: func() bool { return c.vm.ShowAttributes },
|
||||
OnAction: v.toggleAttributes,
|
||||
IsSelected: func() bool { return v.vm.ShowAttributes },
|
||||
Display: "Attributes",
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.page-up"},
|
||||
OnAction: c.PageUp,
|
||||
OnAction: v.PageUp,
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.page-down"},
|
||||
OnAction: c.PageDown,
|
||||
OnAction: v.PageDown,
|
||||
},
|
||||
{
|
||||
Key: gocui.KeyArrowDown,
|
||||
Modifier: gocui.ModNone,
|
||||
OnAction: c.CursorDown,
|
||||
OnAction: v.CursorDown,
|
||||
},
|
||||
{
|
||||
Key: gocui.KeyArrowUp,
|
||||
Modifier: gocui.ModNone,
|
||||
OnAction: c.CursorUp,
|
||||
OnAction: v.CursorUp,
|
||||
},
|
||||
{
|
||||
Key: gocui.KeyArrowLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
OnAction: c.CursorLeft,
|
||||
OnAction: v.CursorLeft,
|
||||
},
|
||||
{
|
||||
Key: gocui.KeyArrowRight,
|
||||
Modifier: gocui.ModNone,
|
||||
OnAction: c.CursorRight,
|
||||
OnAction: v.CursorRight,
|
||||
},
|
||||
}
|
||||
|
||||
helpKeys, err := key.GenerateBindings(c.gui, c.name, infos)
|
||||
helpKeys, err := key.GenerateBindings(v.gui, v.name, infos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.helpKeys = helpKeys
|
||||
v.helpKeys = helpKeys
|
||||
|
||||
_, height := c.view.Size()
|
||||
c.vm.Setup(0, height)
|
||||
_ = c.Update()
|
||||
_ = c.Render()
|
||||
_, height := v.view.Size()
|
||||
v.vm.Setup(0, height)
|
||||
_ = v.Update()
|
||||
_ = v.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsVisible indicates if the file tree view pane is currently initialized
|
||||
func (c *FileTree) IsVisible() bool {
|
||||
return c != nil
|
||||
func (v *FileTree) IsVisible() bool {
|
||||
return v != nil
|
||||
}
|
||||
|
||||
// ResetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
|
||||
func (c *FileTree) resetCursor() {
|
||||
_ = c.view.SetCursor(0, 0)
|
||||
c.vm.ResetCursor()
|
||||
func (v *FileTree) resetCursor() {
|
||||
_ = v.view.SetCursor(0, 0)
|
||||
v.vm.ResetCursor()
|
||||
}
|
||||
|
||||
// SetTreeByLayer populates the view model by stacking the indicated image layer file trees.
|
||||
func (c *FileTree) SetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
|
||||
err := c.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
|
||||
func (v *FileTree) SetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
|
||||
err := v.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = c.Update()
|
||||
return c.Render()
|
||||
_ = v.Update()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down and renders the view.
|
||||
// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
|
||||
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
|
||||
// this range into the view buffer. This is much faster when tree sizes are large.
|
||||
func (c *FileTree) CursorDown() error {
|
||||
if c.vm.CursorDown() {
|
||||
return c.Render()
|
||||
func (v *FileTree) CursorDown() error {
|
||||
if v.vm.CursorDown() {
|
||||
return v.Render()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -212,49 +221,49 @@ func (c *FileTree) CursorDown() error {
|
||||
// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
|
||||
// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
|
||||
// this range into the view buffer. This is much faster when tree sizes are large.
|
||||
func (c *FileTree) CursorUp() error {
|
||||
if c.vm.CursorUp() {
|
||||
return c.Render()
|
||||
func (v *FileTree) CursorUp() error {
|
||||
if v.vm.CursorUp() {
|
||||
return v.Render()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
|
||||
func (c *FileTree) CursorLeft() error {
|
||||
err := c.vm.CursorLeft(c.filterRegex)
|
||||
func (v *FileTree) CursorLeft() error {
|
||||
err := v.vm.CursorLeft(v.filterRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = c.Update()
|
||||
return c.Render()
|
||||
_ = v.Update()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// CursorRight descends into directory expanding it if needed
|
||||
func (c *FileTree) CursorRight() error {
|
||||
err := c.vm.CursorRight(c.filterRegex)
|
||||
func (v *FileTree) CursorRight() error {
|
||||
err := v.vm.CursorRight(v.filterRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = c.Update()
|
||||
return c.Render()
|
||||
_ = v.Update()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// PageDown moves to next page putting the cursor on top
|
||||
func (c *FileTree) PageDown() error {
|
||||
err := c.vm.PageDown()
|
||||
func (v *FileTree) PageDown() error {
|
||||
err := v.vm.PageDown()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Render()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// PageUp moves to previous page putting the cursor on top
|
||||
func (c *FileTree) PageUp() error {
|
||||
err := c.vm.PageUp()
|
||||
func (v *FileTree) PageUp() error {
|
||||
err := v.vm.PageUp()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Render()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
|
||||
@ -263,30 +272,30 @@ func (c *FileTree) PageUp() error {
|
||||
// }
|
||||
|
||||
// ToggleCollapse will collapse/expand the selected FileNode.
|
||||
func (c *FileTree) toggleCollapse() error {
|
||||
err := c.vm.ToggleCollapse(c.filterRegex)
|
||||
func (v *FileTree) toggleCollapse() error {
|
||||
err := v.vm.ToggleCollapse(v.filterRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = c.Update()
|
||||
return c.Render()
|
||||
_ = v.Update()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// ToggleCollapseAll will collapse/expand the all directories.
|
||||
func (c *FileTree) toggleCollapseAll() error {
|
||||
err := c.vm.ToggleCollapseAll()
|
||||
func (v *FileTree) toggleCollapseAll() error {
|
||||
err := v.vm.ToggleCollapseAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.vm.CollapseAll {
|
||||
c.resetCursor()
|
||||
if v.vm.CollapseAll {
|
||||
v.resetCursor()
|
||||
}
|
||||
_ = c.Update()
|
||||
return c.Render()
|
||||
_ = v.Update()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
func (c *FileTree) notifyOnViewOptionChangeListeners() error {
|
||||
for _, listener := range c.listeners {
|
||||
func (v *FileTree) notifyOnViewOptionChangeListeners() error {
|
||||
for _, listener := range v.listeners {
|
||||
err := listener()
|
||||
if err != nil {
|
||||
logrus.Errorf("notifyOnViewOptionChangeListeners error: %+v", err)
|
||||
@ -297,95 +306,97 @@ func (c *FileTree) notifyOnViewOptionChangeListeners() error {
|
||||
}
|
||||
|
||||
// ToggleAttributes will show/hide file attributes
|
||||
func (c *FileTree) toggleAttributes() error {
|
||||
err := c.vm.ToggleAttributes()
|
||||
func (v *FileTree) toggleAttributes() error {
|
||||
err := v.vm.ToggleAttributes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.Update()
|
||||
err = v.Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Render()
|
||||
err = v.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// we need to render the changes to the status pane as well (not just this contoller/view)
|
||||
return c.notifyOnViewOptionChangeListeners()
|
||||
return v.notifyOnViewOptionChangeListeners()
|
||||
}
|
||||
|
||||
// ToggleShowDiffType will show/hide the selected DiffType in the filetree pane.
|
||||
func (c *FileTree) toggleShowDiffType(diffType filetree.DiffType) error {
|
||||
c.vm.ToggleShowDiffType(diffType)
|
||||
func (v *FileTree) toggleShowDiffType(diffType filetree.DiffType) error {
|
||||
v.vm.ToggleShowDiffType(diffType)
|
||||
|
||||
err := c.Update()
|
||||
err := v.Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Render()
|
||||
err = v.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// we need to render the changes to the status pane as well (not just this contoller/view)
|
||||
return c.notifyOnViewOptionChangeListeners()
|
||||
return v.notifyOnViewOptionChangeListeners()
|
||||
}
|
||||
|
||||
// OnLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions
|
||||
func (c *FileTree) OnLayoutChange(resized bool) error {
|
||||
err := c.Update()
|
||||
func (v *FileTree) OnLayoutChange(resized bool) error {
|
||||
err := v.Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resized {
|
||||
return c.Render()
|
||||
return v.Render()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering.
|
||||
func (c *FileTree) Update() error {
|
||||
func (v *FileTree) Update() error {
|
||||
var width, height int
|
||||
|
||||
if c.view != nil {
|
||||
width, height = c.view.Size()
|
||||
if v.view != nil {
|
||||
width, height = v.view.Size()
|
||||
} else {
|
||||
// before the TUI is setup there may not be a controller to reference. Use the entire screen as reference.
|
||||
width, height = c.gui.Size()
|
||||
width, height = v.gui.Size()
|
||||
}
|
||||
// height should account for the header
|
||||
return c.vm.Update(c.filterRegex, width, height-1)
|
||||
return v.vm.Update(v.filterRegex, width, height-1)
|
||||
}
|
||||
|
||||
// Render flushes the state objects (file tree) to the pane.
|
||||
func (c *FileTree) Render() error {
|
||||
title := c.title
|
||||
func (v *FileTree) Render() error {
|
||||
logrus.Debugf("view.Render() %s", v.Name())
|
||||
|
||||
title := v.title
|
||||
// indicate when selected
|
||||
if c.gui.CurrentView() == c.view {
|
||||
title = "● " + c.title
|
||||
if v.gui.CurrentView() == v.view {
|
||||
title = "● " + v.title
|
||||
}
|
||||
|
||||
c.gui.Update(func(g *gocui.Gui) error {
|
||||
v.gui.Update(func(g *gocui.Gui) error {
|
||||
// update the header
|
||||
c.header.Clear()
|
||||
v.header.Clear()
|
||||
width, _ := g.Size()
|
||||
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
|
||||
if c.vm.ShowAttributes {
|
||||
if v.vm.ShowAttributes {
|
||||
headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(c.header, format.Header(vtclean.Clean(headerStr, false)))
|
||||
_, _ = fmt.Fprintln(v.header, format.Header(vtclean.Clean(headerStr, false)))
|
||||
|
||||
// update the contents
|
||||
c.view.Clear()
|
||||
err := c.vm.Render()
|
||||
v.view.Clear()
|
||||
err := v.vm.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprint(c.view, c.vm.Buffer.String())
|
||||
_, err = fmt.Fprint(v.view, v.vm.Buffer.String())
|
||||
|
||||
return err
|
||||
})
|
||||
@ -393,10 +404,43 @@ func (c *FileTree) Render() error {
|
||||
}
|
||||
|
||||
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||
func (c *FileTree) KeyHelp() string {
|
||||
func (v *FileTree) KeyHelp() string {
|
||||
var help string
|
||||
for _, binding := range c.helpKeys {
|
||||
for _, binding := range v.helpKeys {
|
||||
help += binding.RenderKeyHelp()
|
||||
}
|
||||
return help
|
||||
}
|
||||
|
||||
func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error {
|
||||
logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name())
|
||||
attributeRowSize := 0
|
||||
if !v.areAttributesVisible() {
|
||||
attributeRowSize = 1
|
||||
}
|
||||
// header + attribute + border
|
||||
headerSize := 1 + attributeRowSize + 1
|
||||
// note: maxY needs to account for the (invisible) border, thus a +1
|
||||
header, headerErr := g.SetView(v.Name()+"header", minX, minY, maxX, minY+headerSize+1)
|
||||
// we are going to overlap the view over the (invisible) border (so minY will be one less than expected).
|
||||
// additionally, maxY will be bumped by one to include the border
|
||||
view, viewErr := g.SetView(v.Name(), minX, minY+headerSize, maxX, maxY+1)
|
||||
if utils.IsNewView(viewErr, headerErr) {
|
||||
err := v.Setup(view, header)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup tree controller", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := v.OnLayoutChange(hasResized)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup layer controller onLayoutChange", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *FileTree) RequestedSize(available int) *int {
|
||||
var requestedWidth = int(float64(available) * (1.0 - v.requestedWidthRatio))
|
||||
return &requestedWidth
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -13,19 +14,20 @@ 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
|
||||
headerStr string
|
||||
maxLength int
|
||||
hidden bool
|
||||
name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
header *gocui.View
|
||||
labelStr string
|
||||
maxLength int
|
||||
hidden bool
|
||||
requestedHeight int
|
||||
|
||||
filterEditListeners []FilterEditListener
|
||||
}
|
||||
|
||||
// NewFilterView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewFilterView(name string, gui *gocui.Gui) (controller *Filter) {
|
||||
// newFilterView creates a new view object attached the the global [gocui] screen object.
|
||||
func newFilterView(name string, gui *gocui.Gui) (controller *Filter) {
|
||||
controller = new(Filter)
|
||||
|
||||
controller.filterEditListeners = make([]FilterEditListener, 0)
|
||||
@ -33,50 +35,53 @@ func NewFilterView(name string, gui *gocui.Gui) (controller *Filter) {
|
||||
// populate main fields
|
||||
controller.name = name
|
||||
controller.gui = gui
|
||||
controller.headerStr = "Path Filter: "
|
||||
controller.labelStr = "Path Filter: "
|
||||
controller.hidden = true
|
||||
|
||||
controller.requestedHeight = 1
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
func (c *Filter) AddFilterEditListener(listener ...FilterEditListener) {
|
||||
c.filterEditListeners = append(c.filterEditListeners, listener...)
|
||||
func (v *Filter) AddFilterEditListener(listener ...FilterEditListener) {
|
||||
v.filterEditListeners = append(v.filterEditListeners, listener...)
|
||||
}
|
||||
|
||||
func (c *Filter) Name() string {
|
||||
return c.name
|
||||
func (v *Filter) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (c *Filter) Setup(v *gocui.View, header *gocui.View) error {
|
||||
func (v *Filter) Setup(view *gocui.View, header *gocui.View) error {
|
||||
logrus.Debugf("view.Setup() %s", v.Name())
|
||||
|
||||
// set controller options
|
||||
c.view = v
|
||||
c.maxLength = 200
|
||||
c.view.Frame = false
|
||||
c.view.BgColor = gocui.AttrReverse
|
||||
c.view.Editable = true
|
||||
c.view.Editor = c
|
||||
v.view = view
|
||||
v.maxLength = 200
|
||||
v.view.Frame = false
|
||||
v.view.BgColor = gocui.AttrReverse
|
||||
v.view.Editable = true
|
||||
v.view.Editor = v
|
||||
|
||||
c.header = header
|
||||
c.header.BgColor = gocui.AttrReverse
|
||||
c.header.Editable = false
|
||||
c.header.Wrap = false
|
||||
c.header.Frame = false
|
||||
v.header = header
|
||||
v.header.BgColor = gocui.AttrReverse
|
||||
v.header.Editable = false
|
||||
v.header.Wrap = false
|
||||
v.header.Frame = false
|
||||
|
||||
return c.Render()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// ToggleFilterView shows/hides the file tree filter pane.
|
||||
func (c *Filter) ToggleVisible() error {
|
||||
func (v *Filter) ToggleVisible() error {
|
||||
// delete all user input from the tree view
|
||||
c.view.Clear()
|
||||
v.view.Clear()
|
||||
|
||||
// toggle hiding
|
||||
c.hidden = !c.hidden
|
||||
v.hidden = !v.hidden
|
||||
|
||||
if !c.hidden {
|
||||
_, err := c.gui.SetCurrentView(c.name)
|
||||
if !v.hidden {
|
||||
_, err := v.gui.SetCurrentView(v.name)
|
||||
if err != nil {
|
||||
logrus.Error("unable to toggle filter view: ", err)
|
||||
return err
|
||||
@ -87,57 +92,52 @@ func (c *Filter) ToggleVisible() error {
|
||||
// reset the cursor for the next time it is visible
|
||||
// Note: there is a subtle gocui behavior here where this cannot be called when the view
|
||||
// is newly visible. Is this a problem with dive or gocui?
|
||||
return c.view.SetCursor(0, 0)
|
||||
}
|
||||
|
||||
// todo: remove the need for this
|
||||
func (c *Filter) HeaderStr() string {
|
||||
return c.headerStr
|
||||
return v.view.SetCursor(0, 0)
|
||||
}
|
||||
|
||||
// IsVisible indicates if the filter view pane is currently initialized
|
||||
func (c *Filter) IsVisible() bool {
|
||||
if c == nil {
|
||||
func (v *Filter) IsVisible() bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
return !c.hidden
|
||||
return !v.hidden
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the filter pane (currently indicates nothing).
|
||||
func (c *Filter) CursorDown() error {
|
||||
func (v *Filter) CursorDown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the filter pane (currently indicates nothing).
|
||||
func (c *Filter) CursorUp() error {
|
||||
func (v *Filter) CursorUp() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Edit intercepts the key press events in the filer view to update the file view in real time.
|
||||
func (c *Filter) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||
if !c.IsVisible() {
|
||||
func (v *Filter) Edit(view *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||
if !v.IsVisible() {
|
||||
return
|
||||
}
|
||||
|
||||
cx, _ := v.Cursor()
|
||||
ox, _ := v.Origin()
|
||||
limit := ox+cx+1 > c.maxLength
|
||||
cx, _ := view.Cursor()
|
||||
ox, _ := view.Origin()
|
||||
limit := ox+cx+1 > v.maxLength
|
||||
switch {
|
||||
case ch != 0 && mod == 0 && !limit:
|
||||
v.EditWrite(ch)
|
||||
view.EditWrite(ch)
|
||||
case key == gocui.KeySpace && !limit:
|
||||
v.EditWrite(' ')
|
||||
view.EditWrite(' ')
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
view.EditDelete(true)
|
||||
}
|
||||
|
||||
// notify listeners
|
||||
c.notifyFilterEditListeners()
|
||||
v.notifyFilterEditListeners()
|
||||
}
|
||||
|
||||
func (c *Filter) notifyFilterEditListeners() {
|
||||
currentValue := strings.TrimSpace(c.view.Buffer())
|
||||
for _, listener := range c.filterEditListeners {
|
||||
func (v *Filter) notifyFilterEditListeners() {
|
||||
currentValue := strings.TrimSpace(v.view.Buffer())
|
||||
for _, listener := range v.filterEditListeners {
|
||||
err := listener(currentValue)
|
||||
if err != nil {
|
||||
// note: cannot propagate error from here since this is from the main gogui thread
|
||||
@ -147,14 +147,16 @@ func (c *Filter) notifyFilterEditListeners() {
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||
func (c *Filter) Update() error {
|
||||
func (v *Filter) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render flushes the state objects to the screen. Currently this is the users path filter input.
|
||||
func (c *Filter) Render() error {
|
||||
c.gui.Update(func(g *gocui.Gui) error {
|
||||
_, err := fmt.Fprintln(c.header, format.Header(c.headerStr))
|
||||
func (v *Filter) Render() error {
|
||||
logrus.Debugf("view.Render() %s", v.Name())
|
||||
|
||||
v.gui.Update(func(g *gocui.Gui) error {
|
||||
_, err := fmt.Fprintln(v.header, format.Header(v.labelStr))
|
||||
if err != nil {
|
||||
logrus.Error("unable to write to buffer: ", err)
|
||||
}
|
||||
@ -164,6 +166,26 @@ func (c *Filter) Render() error {
|
||||
}
|
||||
|
||||
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||
func (c *Filter) KeyHelp() string {
|
||||
func (v *Filter) KeyHelp() string {
|
||||
return format.StatusControlNormal("▏Type to filter the file tree ")
|
||||
}
|
||||
|
||||
func (v *Filter) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error {
|
||||
logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name())
|
||||
|
||||
label, labelErr := g.SetView(v.Name()+"label", minX, minY, len(v.labelStr), maxY)
|
||||
view, viewErr := g.SetView(v.Name(), minX+(len(v.labelStr)-1), minY, maxX, maxY)
|
||||
|
||||
if utils.IsNewView(viewErr, labelErr) {
|
||||
err := v.Setup(view, label)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup status controller", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Filter) RequestedSize(available int) *int {
|
||||
return &v.requestedHeight
|
||||
}
|
||||
|
@ -33,8 +33,8 @@ type Layer struct {
|
||||
helpKeys []*key.Binding
|
||||
}
|
||||
|
||||
// NewLayerView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) {
|
||||
// newLayerView creates a new view object attached the the global [gocui] screen object.
|
||||
func newLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (controller *Layer, err error) {
|
||||
controller = new(Layer)
|
||||
|
||||
controller.listeners = make([]LayerChangeListener, 0)
|
||||
@ -56,20 +56,20 @@ func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (controlle
|
||||
return controller, err
|
||||
}
|
||||
|
||||
func (c *Layer) AddLayerChangeListener(listener ...LayerChangeListener) {
|
||||
c.listeners = append(c.listeners, listener...)
|
||||
func (v *Layer) AddLayerChangeListener(listener ...LayerChangeListener) {
|
||||
v.listeners = append(v.listeners, listener...)
|
||||
}
|
||||
|
||||
func (c *Layer) notifyLayerChangeListeners() error {
|
||||
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := c.getCompareIndexes()
|
||||
func (v *Layer) notifyLayerChangeListeners() error {
|
||||
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.getCompareIndexes()
|
||||
selection := viewmodel.LayerSelection{
|
||||
Layer: c.CurrentLayer(),
|
||||
Layer: v.CurrentLayer(),
|
||||
BottomTreeStart: bottomTreeStart,
|
||||
BottomTreeStop: bottomTreeStop,
|
||||
TopTreeStart: topTreeStart,
|
||||
TopTreeStop: topTreeStop,
|
||||
}
|
||||
for _, listener := range c.listeners {
|
||||
for _, listener := range v.listeners {
|
||||
err := listener(selection)
|
||||
if err != nil {
|
||||
logrus.Errorf("notifyLayerChangeListeners error: %+v", err)
|
||||
@ -79,189 +79,190 @@ func (c *Layer) notifyLayerChangeListeners() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Layer) Name() string {
|
||||
return c.name
|
||||
func (v *Layer) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (c *Layer) Setup(v *gocui.View, header *gocui.View) error {
|
||||
func (v *Layer) Setup(view *gocui.View, header *gocui.View) error {
|
||||
logrus.Debugf("view.Setup() %s", v.Name())
|
||||
|
||||
// set controller options
|
||||
c.view = v
|
||||
c.view.Editable = false
|
||||
c.view.Wrap = false
|
||||
c.view.Frame = false
|
||||
v.view = view
|
||||
v.view.Editable = false
|
||||
v.view.Wrap = false
|
||||
v.view.Frame = false
|
||||
|
||||
c.header = header
|
||||
c.header.Editable = false
|
||||
c.header.Wrap = false
|
||||
c.header.Frame = false
|
||||
v.header = header
|
||||
v.header.Editable = false
|
||||
v.header.Wrap = false
|
||||
v.header.Frame = false
|
||||
|
||||
var infos = []key.BindingInfo{
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.compare-layer"},
|
||||
OnAction: func() error { return c.setCompareMode(CompareLayer) },
|
||||
IsSelected: func() bool { return c.CompareMode == CompareLayer },
|
||||
OnAction: func() error { return v.setCompareMode(CompareLayer) },
|
||||
IsSelected: func() bool { return v.CompareMode == CompareLayer },
|
||||
Display: "Show layer changes",
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.compare-all"},
|
||||
OnAction: func() error { return c.setCompareMode(CompareAll) },
|
||||
IsSelected: func() bool { return c.CompareMode == CompareAll },
|
||||
OnAction: func() error { return v.setCompareMode(CompareAll) },
|
||||
IsSelected: func() bool { return v.CompareMode == CompareAll },
|
||||
Display: "Show aggregated changes",
|
||||
},
|
||||
{
|
||||
Key: gocui.KeyArrowDown,
|
||||
Modifier: gocui.ModNone,
|
||||
OnAction: c.CursorDown,
|
||||
OnAction: v.CursorDown,
|
||||
},
|
||||
{
|
||||
Key: gocui.KeyArrowUp,
|
||||
Modifier: gocui.ModNone,
|
||||
OnAction: c.CursorUp,
|
||||
OnAction: v.CursorUp,
|
||||
},
|
||||
{
|
||||
Key: gocui.KeyArrowLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
OnAction: c.CursorUp,
|
||||
OnAction: v.CursorUp,
|
||||
},
|
||||
{
|
||||
Key: gocui.KeyArrowRight,
|
||||
Modifier: gocui.ModNone,
|
||||
OnAction: c.CursorDown,
|
||||
OnAction: v.CursorDown,
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.page-up"},
|
||||
OnAction: c.PageUp,
|
||||
OnAction: v.PageUp,
|
||||
},
|
||||
{
|
||||
ConfigKeys: []string{"keybinding.page-down"},
|
||||
OnAction: c.PageDown,
|
||||
OnAction: v.PageDown,
|
||||
},
|
||||
}
|
||||
|
||||
helpKeys, err := key.GenerateBindings(c.gui, c.name, infos)
|
||||
helpKeys, err := key.GenerateBindings(v.gui, v.name, infos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.helpKeys = helpKeys
|
||||
v.helpKeys = helpKeys
|
||||
|
||||
return c.Render()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// height obtains the height of the current pane (taking into account the lost space due to the header).
|
||||
func (c *Layer) height() uint {
|
||||
_, height := c.view.Size()
|
||||
func (v *Layer) height() uint {
|
||||
_, height := v.view.Size()
|
||||
return uint(height - 1)
|
||||
}
|
||||
|
||||
// IsVisible indicates if the layer view pane is currently initialized.
|
||||
func (c *Layer) IsVisible() bool {
|
||||
return c != nil
|
||||
func (v *Layer) IsVisible() bool {
|
||||
return v != nil
|
||||
}
|
||||
|
||||
// PageDown moves to next page putting the cursor on top
|
||||
func (c *Layer) PageDown() error {
|
||||
step := int(c.height()) + 1
|
||||
targetLayerIndex := c.LayerIndex + step
|
||||
func (v *Layer) PageDown() error {
|
||||
step := int(v.height()) + 1
|
||||
targetLayerIndex := v.LayerIndex + step
|
||||
|
||||
if targetLayerIndex > len(c.Layers) {
|
||||
step -= targetLayerIndex - (len(c.Layers) - 1)
|
||||
if targetLayerIndex > len(v.Layers) {
|
||||
step -= targetLayerIndex - (len(v.Layers) - 1)
|
||||
}
|
||||
|
||||
if step > 0 {
|
||||
err := CursorStep(c.gui, c.view, step)
|
||||
err := CursorStep(v.gui, v.view, step)
|
||||
if err == nil {
|
||||
return c.SetCursor(c.LayerIndex + step)
|
||||
return v.SetCursor(v.LayerIndex + step)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PageUp moves to previous page putting the cursor on top
|
||||
func (c *Layer) PageUp() error {
|
||||
step := int(c.height()) + 1
|
||||
targetLayerIndex := c.LayerIndex - step
|
||||
func (v *Layer) PageUp() error {
|
||||
step := int(v.height()) + 1
|
||||
targetLayerIndex := v.LayerIndex - step
|
||||
|
||||
if targetLayerIndex < 0 {
|
||||
step += targetLayerIndex
|
||||
}
|
||||
|
||||
if step > 0 {
|
||||
err := CursorStep(c.gui, c.view, -step)
|
||||
err := CursorStep(v.gui, v.view, -step)
|
||||
if err == nil {
|
||||
return c.SetCursor(c.LayerIndex - step)
|
||||
return v.SetCursor(v.LayerIndex - step)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the layer pane (selecting a higher layer).
|
||||
func (c *Layer) CursorDown() error {
|
||||
if c.LayerIndex < len(c.Layers) {
|
||||
err := CursorDown(c.gui, c.view)
|
||||
func (v *Layer) CursorDown() error {
|
||||
if v.LayerIndex < len(v.Layers) {
|
||||
err := CursorDown(v.gui, v.view)
|
||||
if err == nil {
|
||||
return c.SetCursor(c.LayerIndex + 1)
|
||||
return v.SetCursor(v.LayerIndex + 1)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the layer pane (selecting a lower layer).
|
||||
func (c *Layer) CursorUp() error {
|
||||
if c.LayerIndex > 0 {
|
||||
err := CursorUp(c.gui, c.view)
|
||||
func (v *Layer) CursorUp() error {
|
||||
if v.LayerIndex > 0 {
|
||||
err := CursorUp(v.gui, v.view)
|
||||
if err == nil {
|
||||
return c.SetCursor(c.LayerIndex - 1)
|
||||
return v.SetCursor(v.LayerIndex - 1)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCursor resets the cursor and orients the file tree view based on the given layer index.
|
||||
func (c *Layer) SetCursor(layer int) error {
|
||||
c.LayerIndex = layer
|
||||
err := c.notifyLayerChangeListeners()
|
||||
func (v *Layer) SetCursor(layer int) error {
|
||||
v.LayerIndex = layer
|
||||
err := v.notifyLayerChangeListeners()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// CurrentLayer returns the Layer object currently selected.
|
||||
func (c *Layer) CurrentLayer() *image.Layer {
|
||||
return c.Layers[c.LayerIndex]
|
||||
func (v *Layer) CurrentLayer() *image.Layer {
|
||||
return v.Layers[v.LayerIndex]
|
||||
}
|
||||
|
||||
// setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison.
|
||||
func (c *Layer) setCompareMode(compareMode CompareType) error {
|
||||
c.CompareMode = compareMode
|
||||
return c.notifyLayerChangeListeners()
|
||||
func (v *Layer) setCompareMode(compareMode CompareType) error {
|
||||
v.CompareMode = compareMode
|
||||
return v.notifyLayerChangeListeners()
|
||||
}
|
||||
|
||||
// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode)
|
||||
func (c *Layer) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) {
|
||||
bottomTreeStart = c.CompareStartIndex
|
||||
topTreeStop = c.LayerIndex
|
||||
func (v *Layer) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) {
|
||||
bottomTreeStart = v.CompareStartIndex
|
||||
topTreeStop = v.LayerIndex
|
||||
|
||||
if c.LayerIndex == c.CompareStartIndex {
|
||||
bottomTreeStop = c.LayerIndex
|
||||
topTreeStart = c.LayerIndex
|
||||
} else if c.CompareMode == CompareLayer {
|
||||
bottomTreeStop = c.LayerIndex - 1
|
||||
topTreeStart = c.LayerIndex
|
||||
if v.LayerIndex == v.CompareStartIndex {
|
||||
bottomTreeStop = v.LayerIndex
|
||||
topTreeStart = v.LayerIndex
|
||||
} else if v.CompareMode == CompareLayer {
|
||||
bottomTreeStop = v.LayerIndex - 1
|
||||
topTreeStart = v.LayerIndex
|
||||
} else {
|
||||
bottomTreeStop = c.CompareStartIndex
|
||||
topTreeStart = c.CompareStartIndex + 1
|
||||
bottomTreeStop = v.CompareStartIndex
|
||||
topTreeStart = v.CompareStartIndex + 1
|
||||
}
|
||||
|
||||
return bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop
|
||||
}
|
||||
|
||||
// renderCompareBar returns the formatted string for the given layer.
|
||||
func (c *Layer) renderCompareBar(layerIdx int) string {
|
||||
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := c.getCompareIndexes()
|
||||
func (v *Layer) renderCompareBar(layerIdx int) string {
|
||||
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.getCompareIndexes()
|
||||
result := " "
|
||||
|
||||
if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop {
|
||||
@ -275,43 +276,44 @@ func (c *Layer) renderCompareBar(layerIdx int) string {
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||
func (c *Layer) Update() error {
|
||||
func (v *Layer) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render flushes the state objects to the screen. The layers pane reports:
|
||||
// 1. the layers of the image + metadata
|
||||
// 2. the current selected image
|
||||
func (c *Layer) Render() error {
|
||||
func (v *Layer) Render() error {
|
||||
logrus.Debugf("view.Render() %s", v.Name())
|
||||
|
||||
// indicate when selected
|
||||
title := "Layers"
|
||||
if c.gui.CurrentView() == c.view {
|
||||
if v.gui.CurrentView() == v.view {
|
||||
title = "● " + title
|
||||
}
|
||||
|
||||
c.gui.Update(func(g *gocui.Gui) error {
|
||||
v.gui.Update(func(g *gocui.Gui) error {
|
||||
// update header
|
||||
c.header.Clear()
|
||||
v.header.Clear()
|
||||
width, _ := g.Size()
|
||||
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
|
||||
headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command")
|
||||
_, err := fmt.Fprintln(c.header, format.Header(vtclean.Clean(headerStr, false)))
|
||||
_, err := fmt.Fprintln(v.header, format.Header(vtclean.Clean(headerStr, false)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update contents
|
||||
c.view.Clear()
|
||||
for idx, layer := range c.Layers {
|
||||
v.view.Clear()
|
||||
for idx, layer := range v.Layers {
|
||||
|
||||
layerStr := layer.String()
|
||||
compareBar := c.renderCompareBar(idx)
|
||||
compareBar := v.renderCompareBar(idx)
|
||||
|
||||
if idx == c.LayerIndex {
|
||||
_, err = fmt.Fprintln(c.view, compareBar+" "+format.Selected(layerStr))
|
||||
if idx == v.LayerIndex {
|
||||
_, err = fmt.Fprintln(v.view, compareBar+" "+format.Selected(layerStr))
|
||||
} else {
|
||||
_, err = fmt.Fprintln(c.view, compareBar+" "+layerStr)
|
||||
_, err = fmt.Fprintln(v.view, compareBar+" "+layerStr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -326,9 +328,9 @@ func (c *Layer) Render() error {
|
||||
}
|
||||
|
||||
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||
func (c *Layer) KeyHelp() string {
|
||||
func (v *Layer) KeyHelp() string {
|
||||
var help string
|
||||
for _, binding := range c.helpKeys {
|
||||
for _, binding := range v.helpKeys {
|
||||
help += binding.RenderKeyHelp()
|
||||
}
|
||||
return help
|
||||
|
@ -5,5 +5,8 @@ type Renderer interface {
|
||||
Update() error
|
||||
Render() error
|
||||
IsVisible() bool
|
||||
}
|
||||
|
||||
type Helper interface {
|
||||
KeyHelp() string
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"github.com/wagoodman/dive/runtime/ui/key"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
@ -17,76 +18,81 @@ type Status struct {
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
|
||||
selectedView Renderer
|
||||
selectedView Helper
|
||||
requestedHeight int
|
||||
|
||||
helpKeys []*key.Binding
|
||||
}
|
||||
|
||||
// NewStatusView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewStatusView(name string, gui *gocui.Gui) (controller *Status) {
|
||||
// newStatusView creates a new view object attached the the global [gocui] screen object.
|
||||
func newStatusView(name string, gui *gocui.Gui) (controller *Status) {
|
||||
controller = new(Status)
|
||||
|
||||
// populate main fields
|
||||
controller.name = name
|
||||
controller.gui = gui
|
||||
controller.helpKeys = make([]*key.Binding, 0)
|
||||
controller.requestedHeight = 1
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
func (c *Status) SetCurrentView(r Renderer) {
|
||||
c.selectedView = r
|
||||
func (v *Status) SetCurrentView(r Helper) {
|
||||
v.selectedView = r
|
||||
}
|
||||
|
||||
func (c *Status) Name() string {
|
||||
return c.name
|
||||
func (v *Status) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func (c *Status) AddHelpKeys(keys ...*key.Binding) {
|
||||
c.helpKeys = append(c.helpKeys, keys...)
|
||||
func (v *Status) AddHelpKeys(keys ...*key.Binding) {
|
||||
v.helpKeys = append(v.helpKeys, keys...)
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (c *Status) Setup(v *gocui.View, header *gocui.View) error {
|
||||
func (v *Status) Setup(view *gocui.View) error {
|
||||
logrus.Debugf("view.Setup() %s", v.Name())
|
||||
|
||||
// set controller options
|
||||
c.view = v
|
||||
c.view.Frame = false
|
||||
v.view = view
|
||||
v.view.Frame = false
|
||||
|
||||
return c.Render()
|
||||
return v.Render()
|
||||
}
|
||||
|
||||
// IsVisible indicates if the status view pane is currently initialized.
|
||||
func (c *Status) IsVisible() bool {
|
||||
return c != nil
|
||||
func (v *Status) IsVisible() bool {
|
||||
return v != nil
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
|
||||
func (c *Status) CursorDown() error {
|
||||
func (v *Status) CursorDown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
|
||||
func (c *Status) CursorUp() error {
|
||||
func (v *Status) CursorUp() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||
func (c *Status) Update() error {
|
||||
func (v *Status) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render flushes the state objects to the screen.
|
||||
func (c *Status) Render() error {
|
||||
c.gui.Update(func(g *gocui.Gui) error {
|
||||
c.view.Clear()
|
||||
func (v *Status) Render() error {
|
||||
logrus.Debugf("view.Render() %s", v.Name())
|
||||
|
||||
v.gui.Update(func(g *gocui.Gui) error {
|
||||
v.view.Clear()
|
||||
|
||||
var selectedHelp string
|
||||
if c.selectedView != nil {
|
||||
selectedHelp = c.selectedView.KeyHelp()
|
||||
if v.selectedView != nil {
|
||||
selectedHelp = v.selectedView.KeyHelp()
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintln(c.view, c.KeyHelp()+selectedHelp+format.StatusNormal("▏"+strings.Repeat(" ", 1000)))
|
||||
_, err := fmt.Fprintln(v.view, v.KeyHelp()+selectedHelp+format.StatusNormal("▏"+strings.Repeat(" ", 1000)))
|
||||
if err != nil {
|
||||
logrus.Debug("unable to write to buffer: ", err)
|
||||
}
|
||||
@ -97,10 +103,28 @@ func (c *Status) Render() error {
|
||||
}
|
||||
|
||||
// KeyHelp indicates all the possible global actions a user can take when any pane is selected.
|
||||
func (c *Status) KeyHelp() string {
|
||||
func (v *Status) KeyHelp() string {
|
||||
var help string
|
||||
for _, binding := range c.helpKeys {
|
||||
for _, binding := range v.helpKeys {
|
||||
help += binding.RenderKeyHelp()
|
||||
}
|
||||
return help
|
||||
}
|
||||
|
||||
func (v *Status) Layout(g *gocui.Gui, minX, minY, maxX, maxY int, hasResized bool) error {
|
||||
logrus.Debugf("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name())
|
||||
|
||||
view, viewErr := g.SetView(v.Name(), minX, minY, maxX, maxY)
|
||||
if utils.IsNewView(viewErr) {
|
||||
err := v.Setup(view)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup status controller", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Status) RequestedSize(available int) *int {
|
||||
return &v.requestedHeight
|
||||
}
|
||||
|
56
runtime/ui/view/views.go
Normal file
56
runtime/ui/view/views.go
Normal file
@ -0,0 +1,56 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
||||
type Views struct {
|
||||
Tree *FileTree
|
||||
Layer *Layer
|
||||
Status *Status
|
||||
Filter *Filter
|
||||
Details *Details
|
||||
all []*Renderer
|
||||
}
|
||||
|
||||
func NewViews(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.Comparer) (*Views, error) {
|
||||
Layer, err := newLayerView("layers", g, analysis.Layers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
treeStack := analysis.RefTrees[0]
|
||||
Tree, err := newFileTreeView("filetree", g, treeStack, analysis.RefTrees, cache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Status := newStatusView("status", g)
|
||||
|
||||
// set the layer view as the first selected view
|
||||
Status.SetCurrentView(Layer)
|
||||
|
||||
Filter := newFilterView("filter", g)
|
||||
|
||||
Details := newDetailsView("details", g, analysis.Efficiency, analysis.Inefficiencies, analysis.SizeBytes)
|
||||
|
||||
return &Views{
|
||||
Tree: Tree,
|
||||
Layer: Layer,
|
||||
Status: Status,
|
||||
Filter: Filter,
|
||||
Details: Details,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (views *Views) All() []Renderer {
|
||||
return []Renderer{
|
||||
views.Tree,
|
||||
views.Layer,
|
||||
views.Status,
|
||||
views.Filter,
|
||||
views.Details,
|
||||
}
|
||||
}
|
20
utils/view.go
Normal file
20
utils/view.go
Normal file
@ -0,0 +1,20 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// isNewView determines if a view has already been created based on the set of errors given (a bit hokie)
|
||||
func IsNewView(errs ...error) bool {
|
||||
for _, err := range errs {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if err != gocui.ErrUnknownView {
|
||||
logrus.Errorf("IsNewView() unexpected error: %+v", err)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user