first step at ui obj refactor

This commit is contained in:
Alex Goodman 2019-10-13 11:42:13 -04:00
parent 34d8cbcef5
commit 2069a3fede
No known key found for this signature in database
GPG key ID: 98AF011C5C78EB7E
28 changed files with 582 additions and 533 deletions

View file

@ -40,7 +40,7 @@ test-coverage: build
./.scripts/test-coverage.sh
validate:
grep -R 'const allowTestDataCapture = false' runtime/ui/
grep -R 'const allowTestDataCapture = false' runtime/ui/viewmodel
go vet ./...
@! gofmt -s -l . 2>&1 | grep -vE '^\.git/' | grep -vE '^\.cache/'
golangci-lint run

View file

@ -1,17 +1,13 @@
package ui
package controller
import (
"github.com/jroimartin/gocui"
)
type Renderable interface {
Update() error
Render() error
}
// Controller defines the a renderable terminal screen pane.
type Controller interface {
Renderable
Update() error
Render() error
Setup(*gocui.View, *gocui.View) error
CursorDown() error
CursorUp() error

View file

@ -0,0 +1,166 @@
package controller
import (
"errors"
"github.com/jroimartin/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
)
// var ccOnce sync.Once
var controllers *ControllerCollection
type ControllerCollection struct {
gui *gocui.Gui
Tree *FileTreeController
Layer *LayerController
Status *StatusController
Filter *FilterController
Details *DetailsController
lookup map[string]Controller
}
func NewControllerCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*ControllerCollection, error) {
var err error
controllers = &ControllerCollection{
gui: g,
}
controllers.lookup = make(map[string]Controller)
controllers.Layer, err = NewLayerController("layers", g, analysis.Layers)
if err != nil {
return nil, err
}
controllers.lookup[controllers.Layer.name] = controllers.Layer
treeStack, err := filetree.StackTreeRange(analysis.RefTrees, 0, 0)
if err != nil {
return nil, err
}
controllers.Tree, err = NewFileTreeController("filetree", g, treeStack, analysis.RefTrees, cache)
if err != nil {
return nil, err
}
controllers.lookup[controllers.Tree.name] = controllers.Tree
controllers.Status = NewStatusController("status", g)
controllers.lookup[controllers.Status.name] = controllers.Status
controllers.Filter = NewFilterController("filter", g)
controllers.lookup[controllers.Filter.name] = controllers.Filter
controllers.Details = NewDetailsController("details", g, analysis.Efficiency, analysis.Inefficiencies)
controllers.lookup[controllers.Details.name] = controllers.Details
return controllers, nil
}
func (c *ControllerCollection) UpdateAndRender() error {
err := c.Update()
if err != nil {
logrus.Debug("failed update: ", err)
return err
}
err = c.Render()
if err != nil {
logrus.Debug("failed render: ", err)
return err
}
return nil
}
// Update refreshes the state objects for future rendering.
func (c *ControllerCollection) Update() error {
for _, controller := range c.lookup {
err := controller.Update()
if err != nil {
logrus.Debug("unable to update controller: ")
return err
}
}
return nil
}
// Render flushes the state objects to the screen.
func (c *ControllerCollection) Render() error {
for _, controller := range c.lookup {
if controller.IsVisible() {
err := controller.Render()
if err != nil {
return err
}
}
}
return nil
}
// ToggleView switches between the file view and the layer view and re-renders the screen.
func (c *ControllerCollection) ToggleView() (err error) {
v := c.gui.CurrentView()
if v == nil || v.Name() == c.Layer.Name() {
_, err = c.gui.SetCurrentView(c.Tree.Name())
} else {
_, err = c.gui.SetCurrentView(c.Layer.Name())
}
if err != nil {
logrus.Error("unable to toggle view: ", err)
return err
}
return c.UpdateAndRender()
}
func (c *ControllerCollection) ToggleFilterView() error {
// delete all user input from the tree view
err := c.Filter.ToggleVisible()
if err != nil {
logrus.Error("unable to toggle filter visibility: ", err)
return err
}
// we have just hidden the filter view, adjust focus to a valid (visible) view
if !c.Filter.IsVisible() {
err = c.ToggleView()
if err != nil {
logrus.Error("unable to toggle filter view (back): ", err)
return err
}
}
return c.UpdateAndRender()
}
// CursorDown moves the cursor down in the currently selected gocui pane, scrolling the screen as needed.
func (c *ControllerCollection) CursorDown(g *gocui.Gui, v *gocui.View) error {
return c.CursorStep(g, v, 1)
}
// CursorUp moves the cursor up in the currently selected gocui pane, scrolling the screen as needed.
func (c *ControllerCollection) CursorUp(g *gocui.Gui, v *gocui.View) error {
return c.CursorStep(g, v, -1)
}
// Moves the cursor the given step distance, setting the origin to the new cursor line
func (c *ControllerCollection) CursorStep(g *gocui.Gui, v *gocui.View, step int) error {
cx, cy := v.Cursor()
// if there isn't a next line
line, err := v.Line(cy + step)
if err != nil {
return err
}
if len(line) == 0 {
return errors.New("unable to move the cursor, empty line")
}
if err := v.SetCursor(cx, cy+step); err != nil {
ox, oy := v.Origin()
if err := v.SetOrigin(ox, oy+step); err != nil {
return err
}
}
return nil
}

View file

@ -1,4 +1,4 @@
package ui
package controller
import (
"fmt"
@ -14,9 +14,9 @@ import (
"github.com/lunixbochs/vtclean"
)
// detailsController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
// DetailsController 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 detailsController struct {
type DetailsController struct {
name string
gui *gocui.Gui
view *gocui.View
@ -25,9 +25,9 @@ type detailsController struct {
inefficiencies filetree.EfficiencySlice
}
// newDetailsController creates a new view object attached the the global [gocui] screen object.
func newDetailsController(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice) (controller *detailsController) {
controller = new(detailsController)
// NewDetailsController creates a new view object attached the the global [gocui] screen object.
func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice) (controller *DetailsController) {
controller = new(DetailsController)
// populate main fields
controller.name = name
@ -38,8 +38,12 @@ func newDetailsController(name string, gui *gocui.Gui, efficiency float64, ineff
return controller
}
func (controller *DetailsController) Name() string {
return controller.name
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (controller *detailsController) Setup(v *gocui.View, header *gocui.View) error {
func (controller *DetailsController) Setup(v *gocui.View, header *gocui.View) error {
// set controller options
controller.view = v
@ -75,22 +79,22 @@ func (controller *detailsController) Setup(v *gocui.View, header *gocui.View) er
}
// IsVisible indicates if the details view pane is currently initialized.
func (controller *detailsController) IsVisible() bool {
func (controller *DetailsController) IsVisible() bool {
return controller != nil
}
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
func (controller *detailsController) CursorDown() error {
return CursorDown(controller.gui, controller.view)
func (controller *DetailsController) CursorDown() error {
return controllers.CursorDown(controller.gui, controller.view)
}
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
func (controller *detailsController) CursorUp() error {
return CursorUp(controller.gui, controller.view)
func (controller *DetailsController) CursorUp() error {
return controllers.CursorUp(controller.gui, controller.view)
}
// Update refreshes the state objects for future rendering.
func (controller *detailsController) Update() error {
func (controller *DetailsController) Update() error {
return nil
}
@ -99,7 +103,7 @@ func (controller *detailsController) Update() error {
// 2. the image efficiency score
// 3. the estimated wasted image space
// 4. a list of inefficient file allocations
func (controller *detailsController) Render() error {
func (controller *DetailsController) Render() error {
currentLayer := controllers.Layer.currentLayer()
var wastedSpace int64
@ -168,6 +172,6 @@ func (controller *detailsController) Render() error {
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
func (controller *detailsController) KeyHelp() string {
func (controller *DetailsController) KeyHelp() string {
return "TBD"
}

View file

@ -1,9 +1,10 @@
package ui
package controller
import (
"fmt"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"github.com/wagoodman/dive/runtime/ui/viewmodel"
"regexp"
"strings"
@ -19,36 +20,43 @@ const (
type CompareType int
// fileTreeController holds the UI objects and data models for populating the right pane. Specifically the pane that
// FileTreeController holds the UI objects and data models for populating the right pane. Specifically the pane that
// shows selected layer or aggregate file ASCII tree.
type fileTreeController struct {
type FileTreeController struct {
name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
vm *fileTreeViewModel
vm *viewmodel.FileTreeViewModel
helpKeys []*key.Binding
}
// newFileTreeController creates a new view object attached the the global [gocui] screen object.
func newFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *fileTreeController, err error) {
controller = new(fileTreeController)
// NewFileTreeController creates a new view object attached the the global [gocui] screen object.
func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *FileTreeController, err error) {
controller = new(FileTreeController)
// populate main fields
controller.name = name
controller.gui = gui
controller.vm, err = newFileTreeViewModel(tree, refTrees, cache)
controller.vm, err = viewmodel.NewFileTreeViewModel(tree, refTrees, cache)
if err != nil {
return nil, err
}
return controller, err
}
func (controller *FileTreeController) Name() string {
return controller.name
}
func (controller *FileTreeController) AreAttributesVisible() bool {
return controller.vm.ShowAttributes
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (controller *fileTreeController) Setup(v *gocui.View, header *gocui.View) error {
func (controller *FileTreeController) Setup(v *gocui.View, header *gocui.View) error {
// set controller options
controller.view = v
@ -147,23 +155,23 @@ func (controller *fileTreeController) Setup(v *gocui.View, header *gocui.View) e
}
// IsVisible indicates if the file tree view pane is currently initialized
func (controller *fileTreeController) IsVisible() bool {
func (controller *FileTreeController) IsVisible() bool {
return controller != nil
}
// resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
func (controller *fileTreeController) resetCursor() {
// ResetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
func (controller *FileTreeController) resetCursor() {
_ = controller.view.SetCursor(0, 0)
controller.vm.resetCursor()
controller.vm.ResetCursor()
}
// setTreeByLayer populates the view model by stacking the indicated image layer file trees.
func (controller *fileTreeController) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
err := controller.vm.setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
// SetTreeByLayer populates the view model by stacking the indicated image layer file trees.
func (controller *FileTreeController) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
err := controller.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
if err != nil {
return err
}
// controller.resetCursor()
// controller.ResetCursor()
_ = controller.Update()
return controller.Render()
@ -173,7 +181,7 @@ func (controller *fileTreeController) setTreeByLayer(bottomTreeStart, bottomTree
// 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 (controller *fileTreeController) CursorDown() error {
func (controller *FileTreeController) CursorDown() error {
if controller.vm.CursorDown() {
return controller.Render()
}
@ -184,7 +192,7 @@ func (controller *fileTreeController) 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 (controller *fileTreeController) CursorUp() error {
func (controller *FileTreeController) CursorUp() error {
if controller.vm.CursorUp() {
return controller.Render()
}
@ -192,7 +200,7 @@ func (controller *fileTreeController) CursorUp() error {
}
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
func (controller *fileTreeController) CursorLeft() error {
func (controller *FileTreeController) CursorLeft() error {
err := controller.vm.CursorLeft(filterRegex())
if err != nil {
return err
@ -202,7 +210,7 @@ func (controller *fileTreeController) CursorLeft() error {
}
// CursorRight descends into directory expanding it if needed
func (controller *fileTreeController) CursorRight() error {
func (controller *FileTreeController) CursorRight() error {
err := controller.vm.CursorRight(filterRegex())
if err != nil {
return err
@ -212,7 +220,7 @@ func (controller *fileTreeController) CursorRight() error {
}
// PageDown moves to next page putting the cursor on top
func (controller *fileTreeController) PageDown() error {
func (controller *FileTreeController) PageDown() error {
err := controller.vm.PageDown()
if err != nil {
return err
@ -221,7 +229,7 @@ func (controller *fileTreeController) PageDown() error {
}
// PageUp moves to previous page putting the cursor on top
func (controller *fileTreeController) PageUp() error {
func (controller *FileTreeController) PageUp() error {
err := controller.vm.PageUp()
if err != nil {
return err
@ -230,13 +238,13 @@ func (controller *fileTreeController) PageUp() error {
}
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
// func (controller *fileTreeController) getAbsPositionNode() (node *filetree.FileNode) {
// func (controller *FileTreeController) getAbsPositionNode() (node *filetree.FileNode) {
// return controller.vm.getAbsPositionNode(filterRegex())
// }
// toggleCollapse will collapse/expand the selected FileNode.
func (controller *fileTreeController) toggleCollapse() error {
err := controller.vm.toggleCollapse(filterRegex())
// ToggleCollapse will collapse/expand the selected FileNode.
func (controller *FileTreeController) toggleCollapse() error {
err := controller.vm.ToggleCollapse(filterRegex())
if err != nil {
return err
}
@ -244,9 +252,9 @@ func (controller *fileTreeController) toggleCollapse() error {
return controller.Render()
}
// toggleCollapseAll will collapse/expand the all directories.
func (controller *fileTreeController) toggleCollapseAll() error {
err := controller.vm.toggleCollapseAll()
// ToggleCollapseAll will collapse/expand the all directories.
func (controller *FileTreeController) toggleCollapseAll() error {
err := controller.vm.ToggleCollapseAll()
if err != nil {
return err
}
@ -257,21 +265,21 @@ func (controller *fileTreeController) toggleCollapseAll() error {
return controller.Render()
}
// toggleAttributes will show/hide file attributes
func (controller *fileTreeController) toggleAttributes() error {
err := controller.vm.toggleAttributes()
// ToggleAttributes will show/hide file attributes
func (controller *FileTreeController) toggleAttributes() error {
err := controller.vm.ToggleAttributes()
if err != nil {
return err
}
// we need to render the changes to the status pane as well (not just this contoller/view)
return UpdateAndRender()
return controllers.UpdateAndRender()
}
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
func (controller *fileTreeController) toggleShowDiffType(diffType filetree.DiffType) error {
controller.vm.toggleShowDiffType(diffType)
// ToggleShowDiffType will show/hide the selected DiffType in the filetree pane.
func (controller *FileTreeController) toggleShowDiffType(diffType filetree.DiffType) error {
controller.vm.ToggleShowDiffType(diffType)
// we need to render the changes to the status pane as well (not just this contoller/view)
return UpdateAndRender()
return controllers.UpdateAndRender()
}
// filterRegex will return a regular expression object to match the user's filter input.
@ -292,8 +300,8 @@ func filterRegex() *regexp.Regexp {
return regex
}
// onLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions
func (controller *fileTreeController) onLayoutChange(resized bool) error {
// OnLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions
func (controller *FileTreeController) OnLayoutChange(resized bool) error {
_ = controller.Update()
if resized {
return controller.Render()
@ -302,7 +310,7 @@ func (controller *fileTreeController) onLayoutChange(resized bool) error {
}
// Update refreshes the state objects for future rendering.
func (controller *fileTreeController) Update() error {
func (controller *FileTreeController) Update() error {
var width, height int
if controller.view != nil {
@ -316,7 +324,7 @@ func (controller *fileTreeController) Update() error {
}
// Render flushes the state objects (file tree) to the pane.
func (controller *fileTreeController) Render() error {
func (controller *FileTreeController) Render() error {
title := "Current Layer Contents"
if controllers.Layer.CompareMode == CompareAll {
title = "Aggregated Layer Contents"
@ -344,7 +352,7 @@ func (controller *fileTreeController) Render() error {
if err != nil {
return err
}
_, err = fmt.Fprint(controller.view, controller.vm.mainBuf.String())
_, err = fmt.Fprint(controller.view, controller.vm.Buffer.String())
return err
})
@ -352,7 +360,7 @@ func (controller *fileTreeController) Render() error {
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
func (controller *fileTreeController) KeyHelp() string {
func (controller *FileTreeController) KeyHelp() string {
var help string
for _, binding := range controller.helpKeys {
help += binding.RenderKeyHelp()

View file

@ -1,4 +1,4 @@
package ui
package controller
import (
"fmt"
@ -7,9 +7,9 @@ import (
"github.com/wagoodman/dive/runtime/ui/format"
)
// filterController holds the UI objects and data models for populating the bottom row. Specifically the pane that
// FilterController 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 filterController struct {
type FilterController struct {
name string
gui *gocui.Gui
view *gocui.View
@ -19,9 +19,9 @@ type filterController struct {
hidden bool
}
// newFilterController creates a new view object attached the the global [gocui] screen object.
func newFilterController(name string, gui *gocui.Gui) (controller *filterController) {
controller = new(filterController)
// NewFilterController creates a new view object attached the the global [gocui] screen object.
func NewFilterController(name string, gui *gocui.Gui) (controller *FilterController) {
controller = new(FilterController)
// populate main fields
controller.name = name
@ -32,8 +32,12 @@ func newFilterController(name string, gui *gocui.Gui) (controller *filterControl
return controller
}
func (controller *FilterController) Name() string {
return controller.name
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (controller *filterController) Setup(v *gocui.View, header *gocui.View) error {
func (controller *FilterController) Setup(v *gocui.View, header *gocui.View) error {
// set controller options
controller.view = v
@ -52,8 +56,36 @@ func (controller *filterController) Setup(v *gocui.View, header *gocui.View) err
return controller.Render()
}
// ToggleFilterView shows/hides the file tree filter pane.
func (controller *FilterController) ToggleVisible() error {
// delete all user input from the tree view
controller.view.Clear()
// toggle hiding
controller.hidden = !controller.hidden
if !controller.hidden {
_, err := controller.gui.SetCurrentView(controller.name)
if err != nil {
logrus.Error("unable to toggle filter view: ", err)
return err
}
return controllers.UpdateAndRender()
}
// 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 controller.view.SetCursor(0, 0)
}
// todo: remove the need for this
func (controller *FilterController) HeaderStr() string {
return controller.headerStr
}
// IsVisible indicates if the filter view pane is currently initialized
func (controller *filterController) IsVisible() bool {
func (controller *FilterController) IsVisible() bool {
if controller == nil {
return false
}
@ -61,17 +93,17 @@ func (controller *filterController) IsVisible() bool {
}
// CursorDown moves the cursor down in the filter pane (currently indicates nothing).
func (controller *filterController) CursorDown() error {
func (controller *FilterController) CursorDown() error {
return nil
}
// CursorUp moves the cursor up in the filter pane (currently indicates nothing).
func (controller *filterController) CursorUp() error {
func (controller *FilterController) CursorUp() error {
return nil
}
// Edit intercepts the key press events in the filer view to update the file view in real time.
func (controller *filterController) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
func (controller *FilterController) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
if !controller.IsVisible() {
return
}
@ -94,12 +126,12 @@ func (controller *filterController) Edit(v *gocui.View, key gocui.Key, ch rune,
}
// Update refreshes the state objects for future rendering (currently does nothing).
func (controller *filterController) Update() error {
func (controller *FilterController) Update() error {
return nil
}
// Render flushes the state objects to the screen. Currently this is the users path filter input.
func (controller *filterController) Render() error {
func (controller *FilterController) Render() error {
controller.gui.Update(func(g *gocui.Gui) error {
_, err := fmt.Fprintln(controller.header, format.Header(controller.headerStr))
if err != nil {
@ -111,6 +143,6 @@ func (controller *filterController) Render() error {
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
func (controller *filterController) KeyHelp() string {
func (controller *FilterController) KeyHelp() string {
return format.StatusControlNormal("▏Type to filter the file tree ")
}

View file

@ -1,4 +1,4 @@
package ui
package controller
import (
"fmt"
@ -13,9 +13,9 @@ import (
"github.com/spf13/viper"
)
// layerController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
// LayerController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
// shows the image layers and layer selector.
type layerController struct {
type LayerController struct {
name string
gui *gocui.Gui
view *gocui.View
@ -29,9 +29,9 @@ type layerController struct {
helpKeys []*key.Binding
}
// newLayerController creates a new view object attached the the global [gocui] screen object.
func newLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (controller *layerController, err error) {
controller = new(layerController)
// NewLayerController creates a new view object attached the the global [gocui] screen object.
func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (controller *LayerController, err error) {
controller = new(LayerController)
// populate main fields
controller.name = name
@ -50,8 +50,12 @@ func newLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (con
return controller, err
}
func (controller *LayerController) Name() string {
return controller.name
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (controller *layerController) Setup(v *gocui.View, header *gocui.View) error {
func (controller *LayerController) Setup(v *gocui.View, header *gocui.View) error {
// set controller options
controller.view = v
@ -64,17 +68,16 @@ func (controller *layerController) Setup(v *gocui.View, header *gocui.View) erro
controller.header.Wrap = false
controller.header.Frame = false
var infos = []key.BindingInfo{
{
ConfigKeys: []string{"keybinding.compare-layer"},
OnAction: func() error { return controller.setCompareMode(CompareLayer) },
OnAction: func() error { return controller.setCompareMode(CompareLayer) },
IsSelected: func() bool { return controller.CompareMode == CompareLayer },
Display: "Show layer changes",
},
{
ConfigKeys: []string{"keybinding.compare-all"},
OnAction: func() error { return controller.setCompareMode(CompareAll) },
OnAction: func() error { return controller.setCompareMode(CompareAll) },
IsSelected: func() bool { return controller.CompareMode == CompareAll },
Display: "Show aggregated changes",
},
@ -114,23 +117,22 @@ func (controller *layerController) Setup(v *gocui.View, header *gocui.View) erro
}
controller.helpKeys = helpKeys
return controller.Render()
}
// height obtains the height of the current pane (taking into account the lost space due to the header).
func (controller *layerController) height() uint {
func (controller *LayerController) height() uint {
_, height := controller.view.Size()
return uint(height - 1)
}
// IsVisible indicates if the layer view pane is currently initialized.
func (controller *layerController) IsVisible() bool {
func (controller *LayerController) IsVisible() bool {
return controller != nil
}
// PageDown moves to next page putting the cursor on top
func (controller *layerController) PageDown() error {
func (controller *LayerController) PageDown() error {
step := int(controller.height()) + 1
targetLayerIndex := controller.LayerIndex + step
@ -139,7 +141,7 @@ func (controller *layerController) PageDown() error {
}
if step > 0 {
err := CursorStep(controller.gui, controller.view, step)
err := controllers.CursorStep(controller.gui, controller.view, step)
if err == nil {
return controller.SetCursor(controller.LayerIndex + step)
}
@ -148,7 +150,7 @@ func (controller *layerController) PageDown() error {
}
// PageUp moves to previous page putting the cursor on top
func (controller *layerController) PageUp() error {
func (controller *LayerController) PageUp() error {
step := int(controller.height()) + 1
targetLayerIndex := controller.LayerIndex - step
@ -157,7 +159,7 @@ func (controller *layerController) PageUp() error {
}
if step > 0 {
err := CursorStep(controller.gui, controller.view, -step)
err := controllers.CursorStep(controller.gui, controller.view, -step)
if err == nil {
return controller.SetCursor(controller.LayerIndex - step)
}
@ -166,9 +168,9 @@ func (controller *layerController) PageUp() error {
}
// CursorDown moves the cursor down in the layer pane (selecting a higher layer).
func (controller *layerController) CursorDown() error {
func (controller *LayerController) CursorDown() error {
if controller.LayerIndex < len(controller.Layers) {
err := CursorDown(controller.gui, controller.view)
err := controllers.CursorDown(controller.gui, controller.view)
if err == nil {
return controller.SetCursor(controller.LayerIndex + 1)
}
@ -177,9 +179,9 @@ func (controller *layerController) CursorDown() error {
}
// CursorUp moves the cursor up in the layer pane (selecting a lower layer).
func (controller *layerController) CursorUp() error {
func (controller *LayerController) CursorUp() error {
if controller.LayerIndex > 0 {
err := CursorUp(controller.gui, controller.view)
err := controllers.CursorUp(controller.gui, controller.view)
if err == nil {
return controller.SetCursor(controller.LayerIndex - 1)
}
@ -188,7 +190,7 @@ func (controller *layerController) CursorUp() error {
}
// SetCursor resets the cursor and orients the file tree view based on the given layer index.
func (controller *layerController) SetCursor(layer int) error {
func (controller *LayerController) SetCursor(layer int) error {
controller.LayerIndex = layer
err := controllers.Tree.setTreeByLayer(controller.getCompareIndexes())
if err != nil {
@ -201,14 +203,14 @@ func (controller *layerController) SetCursor(layer int) error {
}
// currentLayer returns the Layer object currently selected.
func (controller *layerController) currentLayer() *image.Layer {
func (controller *LayerController) currentLayer() *image.Layer {
return controller.Layers[controller.LayerIndex]
}
// setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison.
func (controller *layerController) setCompareMode(compareMode CompareType) error {
func (controller *LayerController) setCompareMode(compareMode CompareType) error {
controller.CompareMode = compareMode
err := UpdateAndRender()
err := controllers.UpdateAndRender()
if err != nil {
logrus.Errorf("unable to set compare mode: %+v", err)
return err
@ -217,7 +219,7 @@ func (controller *layerController) setCompareMode(compareMode CompareType) error
}
// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode)
func (controller *layerController) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) {
func (controller *LayerController) getCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) {
bottomTreeStart = controller.CompareStartIndex
topTreeStop = controller.LayerIndex
@ -236,7 +238,7 @@ func (controller *layerController) getCompareIndexes() (bottomTreeStart, bottomT
}
// renderCompareBar returns the formatted string for the given layer.
func (controller *layerController) renderCompareBar(layerIdx int) string {
func (controller *LayerController) renderCompareBar(layerIdx int) string {
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := controller.getCompareIndexes()
result := " "
@ -251,7 +253,7 @@ func (controller *layerController) renderCompareBar(layerIdx int) string {
}
// Update refreshes the state objects for future rendering (currently does nothing).
func (controller *layerController) Update() error {
func (controller *LayerController) Update() error {
controller.ImageSize = 0
for idx := 0; idx < len(controller.Layers); idx++ {
controller.ImageSize += controller.Layers[idx].Size
@ -262,7 +264,7 @@ func (controller *layerController) Update() error {
// 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 (controller *layerController) Render() error {
func (controller *LayerController) Render() error {
// indicate when selected
title := "Layers"
@ -306,7 +308,7 @@ func (controller *layerController) Render() error {
}
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
func (controller *layerController) KeyHelp() string {
func (controller *LayerController) KeyHelp() string {
var help string
for _, binding := range controller.helpKeys {
help += binding.RenderKeyHelp()

View file

@ -1,4 +1,4 @@
package ui
package controller
import (
"fmt"
@ -10,9 +10,9 @@ import (
"github.com/jroimartin/gocui"
)
// statusController holds the UI objects and data models for populating the bottom-most pane. Specifically the panel
// StatusController holds the UI objects and data models for populating the bottom-most pane. Specifically the panel
// shows the user a set of possible actions to take in the window and currently selected pane.
type statusController struct {
type StatusController struct {
name string
gui *gocui.Gui
view *gocui.View
@ -20,9 +20,9 @@ type statusController struct {
helpKeys []*key.Binding
}
// newStatusController creates a new view object attached the the global [gocui] screen object.
func newStatusController(name string, gui *gocui.Gui) (controller *statusController) {
controller = new(statusController)
// NewStatusController creates a new view object attached the the global [gocui] screen object.
func NewStatusController(name string, gui *gocui.Gui) (controller *StatusController) {
controller = new(StatusController)
// populate main fields
controller.name = name
@ -32,14 +32,16 @@ func newStatusController(name string, gui *gocui.Gui) (controller *statusControl
return controller
}
func (controller *statusController) AddHelpKeys(keys ...*key.Binding) {
for _, k := range keys {
controller.helpKeys = append(controller.helpKeys, k)
}
func (controller *StatusController) Name() string {
return controller.name
}
func (controller *StatusController) AddHelpKeys(keys ...*key.Binding) {
controller.helpKeys = append(controller.helpKeys, keys...)
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (controller *statusController) Setup(v *gocui.View, header *gocui.View) error {
func (controller *StatusController) Setup(v *gocui.View, header *gocui.View) error {
// set controller options
controller.view = v
@ -49,30 +51,30 @@ func (controller *statusController) Setup(v *gocui.View, header *gocui.View) err
}
// IsVisible indicates if the status view pane is currently initialized.
func (controller *statusController) IsVisible() bool {
func (controller *StatusController) IsVisible() bool {
return controller != nil
}
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
func (controller *statusController) CursorDown() error {
func (controller *StatusController) CursorDown() error {
return nil
}
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
func (controller *statusController) CursorUp() error {
func (controller *StatusController) CursorUp() error {
return nil
}
// Update refreshes the state objects for future rendering (currently does nothing).
func (controller *statusController) Update() error {
func (controller *StatusController) Update() error {
return nil
}
// Render flushes the state objects to the screen.
func (controller *statusController) Render() error {
func (controller *StatusController) Render() error {
controller.gui.Update(func(g *gocui.Gui) error {
controller.view.Clear()
_, err := fmt.Fprintln(controller.view, controller.KeyHelp()+controllers.lookup[controller.gui.CurrentView().Name()].KeyHelp()+format.StatusNormal("▏"+strings.Repeat(" ", 1000)))
_, err := fmt.Fprintln(controller.view, controller.KeyHelp()+format.StatusNormal("▏"+strings.Repeat(" ", 1000)))
if err != nil {
logrus.Debug("unable to write to buffer: ", err)
}
@ -83,7 +85,7 @@ func (controller *statusController) Render() error {
}
// KeyHelp indicates all the possible global actions a user can take when any pane is selected.
func (controller *statusController) KeyHelp() string {
func (controller *StatusController) KeyHelp() string {
var help string
for _, binding := range controller.helpKeys {
help += binding.RenderKeyHelp()

View file

@ -1,52 +0,0 @@
package ui
import (
"github.com/jroimartin/gocui"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
)
// var ccOnce sync.Once
var controllers *controllerCollection
type controllerCollection struct {
Tree *fileTreeController
Layer *layerController
Status *statusController
Filter *filterController
Details *detailsController
lookup map[string]Controller
}
func newControllerCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*controllerCollection, error) {
var err error
controllers = &controllerCollection{}
controllers.lookup = make(map[string]Controller)
controllers.Layer, err = newLayerController("layers", g, analysis.Layers)
if err != nil {
return nil, err
}
controllers.lookup[controllers.Layer.name] = controllers.Layer
treeStack, err := filetree.StackTreeRange(analysis.RefTrees, 0, 0)
if err != nil {
return nil, err
}
controllers.Tree, err = newFileTreeController("filetree", g, treeStack, analysis.RefTrees, cache)
if err != nil {
return nil, err
}
controllers.lookup[controllers.Tree.name] = controllers.Tree
controllers.Status = newStatusController("status", g)
controllers.lookup[controllers.Status.name] = controllers.Status
controllers.Filter = newFilterController("filter", g)
controllers.lookup[controllers.Filter.name] = controllers.Filter
controllers.Details = newDetailsController("details", g, analysis.Efficiency, analysis.Inefficiencies)
controllers.lookup[controllers.Details.name] = controllers.Details
return controllers, nil
}

View file

@ -97,7 +97,6 @@ func (binding *Binding) RegisterSelectionFn(selectedFn func() bool) {
}
func (binding *Binding) onAction(*gocui.Gui, *gocui.View) error {
logrus.Debugf("keybinding invoked: %+v", binding)
if binding.actionFn == nil {
return fmt.Errorf("no action configured for '%+v'", binding)
}

View file

@ -1,11 +1,169 @@
package ui
import (
"github.com/jroimartin/gocui"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/runtime/ui/controller"
)
type layoutManager struct {
fileTreeSplitRatio float64
controllers *controller.ControllerCollection
}
func newLayoutManager(fileTreeSplitRatio float64) *layoutManager {
func newLayoutManager(c *controller.ControllerCollection) *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
}

View file

@ -1,14 +1,13 @@
package ui
import (
"errors"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/controller"
"github.com/wagoodman/dive/runtime/ui/key"
"sync"
"github.com/jroimartin/gocui"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive/filetree"
)
@ -16,8 +15,9 @@ const debug = false
// type global
type app struct {
gui *gocui.Gui
controllers *controllerCollection
gui *gocui.Gui
controllers *controller.ControllerCollection
layout *layoutManager
}
var (
@ -28,17 +28,19 @@ var (
func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*app, error) {
var err error
once.Do(func() {
var theControls *controllerCollection
var theControls *controller.ControllerCollection
var globalHelpKeys []*key.Binding
theControls, err = newControllerCollection(gui, analysis, cache)
theControls, err = controller.NewControllerCollection(gui, analysis, cache)
if err != nil {
return
}
lm := newLayoutManager(theControls)
gui.Cursor = false
//g.Mouse = true
gui.SetManagerFunc(layout)
gui.SetManagerFunc(lm.layout)
// var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook)
//
@ -46,27 +48,27 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC
// profileObj.Stop()
// }
appSingleton = &app{
gui: gui,
controllers: theControls,
layout: lm,
}
var infos = []key.BindingInfo{
{
ConfigKeys: []string{"keybinding.quit"},
OnAction: quit,
OnAction: appSingleton.quit,
Display: "Quit",
},
{
ConfigKeys: []string{"keybinding.toggle-view"},
OnAction: appSingleton.toggleView,
OnAction: theControls.ToggleView,
Display: "Switch view",
},
{
ConfigKeys: []string{"keybinding.filter-files"},
OnAction: appSingleton.toggleFilterView,
IsSelected: controllers.Filter.IsVisible,
OnAction: theControls.ToggleFilterView,
IsSelected: theControls.Filter.IsVisible,
Display: "Filter",
},
}
@ -78,14 +80,12 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC
theControls.Status.AddHelpKeys(globalHelpKeys...)
// perform the first update and render now that all resources have been loaded
err = UpdateAndRender()
err = theControls.UpdateAndRender()
if err != nil {
return
}
})
return appSingleton, err
@ -109,103 +109,8 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC
var lastX, lastY int
func UpdateAndRender() error {
err := Update()
if err != nil {
logrus.Debug("failed update: ", err)
return err
}
err = Render()
if err != nil {
logrus.Debug("failed render: ", err)
return err
}
return nil
}
// toggleView switches between the file view and the layer view and re-renders the screen.
func (ui *app) toggleView() (err error) {
v := ui.gui.CurrentView()
if v == nil || v.Name() == controllers.Layer.name {
_, err = ui.gui.SetCurrentView(controllers.Tree.name)
} else {
_, err = ui.gui.SetCurrentView(controllers.Layer.name)
}
if err != nil {
logrus.Error("unable to toggle view: ", err)
return err
}
return UpdateAndRender()
}
// toggleFilterView shows/hides the file tree filter pane.
func (ui *app) toggleFilterView() error {
// delete all user input from the tree view
controllers.Filter.view.Clear()
// toggle hiding
controllers.Filter.hidden = !controllers.Filter.hidden
if !controllers.Filter.hidden {
_, err := ui.gui.SetCurrentView(controllers.Filter.name)
if err != nil {
logrus.Error("unable to toggle filter view: ", err)
return err
}
return UpdateAndRender()
}
err := ui.toggleView()
if err != nil {
logrus.Error("unable to toggle filter view (back): ", err)
return err
}
err = controllers.Filter.view.SetCursor(0, 0)
if err != nil {
return err
}
return nil
}
// CursorDown moves the cursor down in the currently selected gocui pane, scrolling the screen as needed.
func CursorDown(g *gocui.Gui, v *gocui.View) error {
return CursorStep(g, v, 1)
}
// CursorUp moves the cursor up in the currently selected gocui pane, scrolling the screen as needed.
func CursorUp(g *gocui.Gui, v *gocui.View) error {
return CursorStep(g, v, -1)
}
// Moves the cursor the given step distance, setting the origin to the new cursor line
func CursorStep(g *gocui.Gui, v *gocui.View, step int) error {
cx, cy := v.Cursor()
// if there isn't a next line
line, err := v.Line(cy + step)
if err != nil {
return err
}
if len(line) == 0 {
return errors.New("unable to move the cursor, empty line")
}
if err := v.SetCursor(cx, cy+step); err != nil {
ox, oy := v.Origin()
if err := v.SetOrigin(ox, oy+step); err != nil {
return err
}
}
return nil
}
// quit is the gocui callback invoked when the user hits Ctrl+C
func quit() error {
func (ui *app) quit() error {
// profileObj.Stop()
// onExit()
@ -213,177 +118,6 @@ func quit() error {
return gocui.ErrQuit
}
// 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 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
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
}
splitCols := int(float64(maxX) * (1.0 - 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(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 controllers.Filter.hidden {
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(controllers.Layer.name, -1, -1+headerRows, splitCols, layersHeight)
header, headerErr = g.SetView(controllers.Layer.name+"header", -1, -1, splitCols, headerRows)
if isNewView(viewErr, headerErr) {
err = controllers.Layer.Setup(view, header)
if err != nil {
logrus.Error("unable to setup layer controller", err)
return err
}
if _, err = g.SetCurrentView(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 = controllers.Layer.Render()
if err != nil {
logrus.Error("unable to render layer view", err)
return err
}
}
// Details
view, viewErr = g.SetView(controllers.Details.name, -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows)
header, headerErr = g.SetView(controllers.Details.name+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows)
if isNewView(viewErr, headerErr) {
err = controllers.Details.Setup(view, header)
if err != nil {
return err
}
}
// Filetree
offset := 0
if !controllers.Tree.vm.ShowAttributes {
offset = 1
}
view, viewErr = g.SetView(controllers.Tree.name, splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows)
header, headerErr = g.SetView(controllers.Tree.name+"header", splitCols, -1, debugCols, headerRows-offset)
if isNewView(viewErr, headerErr) {
err = controllers.Tree.Setup(view, header)
if err != nil {
logrus.Error("unable to setup tree controller", err)
return err
}
}
err = controllers.Tree.onLayoutChange(resized)
if err != nil {
logrus.Error("unable to setup layer controller onLayoutChange", err)
return err
}
// Status Bar
view, viewErr = g.SetView(controllers.Status.name, -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1))
if isNewView(viewErr, headerErr) {
err = controllers.Status.Setup(view, nil)
if err != nil {
logrus.Error("unable to setup status controller", err)
return err
}
}
// Filter Bar
view, viewErr = g.SetView(controllers.Filter.name, len(controllers.Filter.headerStr)-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1))
header, headerErr = g.SetView(controllers.Filter.name+"header", -1, maxY-filterBarHeight-filterBarIndex, len(controllers.Filter.headerStr), maxY-(filterBarIndex-1))
if isNewView(viewErr, headerErr) {
err = controllers.Filter.Setup(view, header)
if err != nil {
logrus.Error("unable to setup filter controller", err)
return err
}
}
return nil
}
// Update refreshes the state objects for future rendering.
func Update() error {
for _, controller := range controllers.lookup {
err := controller.Update()
if err != nil {
logrus.Debug("unable to update controller: ")
return err
}
}
return nil
}
// Render flushes the state objects to the screen.
func Render() error {
for _, controller := range controllers.lookup {
if controller.IsVisible() {
err := controller.Render()
if err != nil {
return err
}
}
}
return nil
}
// Run is the UI entrypoint.
func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error {
var err error

View file

@ -1,4 +1,4 @@
package ui
package viewmodel
import (
"bytes"
@ -13,9 +13,9 @@ import (
"github.com/wagoodman/dive/dive/filetree"
)
// fileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically the pane that
// FileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically the pane that
// shows selected layer or aggregate file ASCII tree.
type fileTreeViewModel struct {
type FileTreeViewModel struct {
ModelTree *filetree.FileTree
ViewTree *filetree.FileTree
RefTrees []*filetree.FileTree
@ -31,12 +31,12 @@ type fileTreeViewModel struct {
refHeight int
refWidth int
mainBuf bytes.Buffer
Buffer bytes.Buffer
}
// newFileTreeViewModel creates a new view object attached the the global [gocui] screen object.
func newFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeViewModel *fileTreeViewModel, err error) {
treeViewModel = new(fileTreeViewModel)
// NewFileTreeViewModel creates a new view object attached the the global [gocui] screen object.
func NewFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (treeViewModel *FileTreeViewModel, err error) {
treeViewModel = new(FileTreeViewModel)
// populate main fields
treeViewModel.ShowAttributes = viper.GetBool("filetree.show-attributes")
@ -66,13 +66,13 @@ func newFileTreeViewModel(tree *filetree.FileTree, refTrees []*filetree.FileTree
}
// Setup initializes the UI concerns within the context of a global [gocui] view object.
func (vm *fileTreeViewModel) Setup(lowerBound, height int) {
func (vm *FileTreeViewModel) Setup(lowerBound, height int) {
vm.bufferIndexLowerBound = lowerBound
vm.refHeight = height
}
// height returns the current height and considers the header
func (vm *fileTreeViewModel) height() int {
func (vm *FileTreeViewModel) height() int {
if vm.ShowAttributes {
return vm.refHeight - 1
}
@ -80,24 +80,24 @@ func (vm *fileTreeViewModel) height() int {
}
// bufferIndexUpperBound returns the current upper bounds for the view
func (vm *fileTreeViewModel) bufferIndexUpperBound() int {
func (vm *FileTreeViewModel) bufferIndexUpperBound() int {
return vm.bufferIndexLowerBound + vm.height()
}
// IsVisible indicates if the file tree view pane is currently initialized
func (vm *fileTreeViewModel) IsVisible() bool {
func (vm *FileTreeViewModel) IsVisible() bool {
return vm != nil
}
// resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
func (vm *fileTreeViewModel) resetCursor() {
// ResetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
func (vm *FileTreeViewModel) ResetCursor() {
vm.TreeIndex = 0
vm.bufferIndex = 0
vm.bufferIndexLowerBound = 0
}
// setTreeByLayer populates the view model by stacking the indicated image layer file trees.
func (vm *fileTreeViewModel) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
// SetTreeByLayer populates the view model by stacking the indicated image layer file trees.
func (vm *FileTreeViewModel) SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
if topTreeStop > len(vm.RefTrees)-1 {
return fmt.Errorf("invalid layer index given: %d of %d", topTreeStop, len(vm.RefTrees)-1)
}
@ -126,7 +126,7 @@ func (vm *fileTreeViewModel) setTreeByLayer(bottomTreeStart, bottomTreeStop, top
}
// doCursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer.
func (vm *fileTreeViewModel) CursorUp() bool {
func (vm *FileTreeViewModel) CursorUp() bool {
if vm.TreeIndex <= 0 {
return false
}
@ -141,7 +141,7 @@ func (vm *fileTreeViewModel) CursorUp() bool {
}
// doCursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer.
func (vm *fileTreeViewModel) CursorDown() bool {
func (vm *FileTreeViewModel) CursorDown() bool {
if vm.TreeIndex >= vm.ModelTree.VisibleSize() {
return false
}
@ -157,7 +157,7 @@ func (vm *fileTreeViewModel) CursorDown() bool {
}
// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
func (vm *fileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error {
func (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error {
var visitor func(*filetree.FileNode) error
var evaluator func(*filetree.FileNode) bool
var dfsCounter, newIndex int
@ -208,7 +208,7 @@ func (vm *fileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error {
}
// CursorRight descends into directory expanding it if needed
func (vm *fileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error {
func (vm *FileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error {
node := vm.getAbsPositionNode(filterRegex)
if node == nil {
return nil
@ -240,7 +240,7 @@ func (vm *fileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error {
}
// PageDown moves to next page putting the cursor on top
func (vm *fileTreeViewModel) PageDown() error {
func (vm *FileTreeViewModel) PageDown() error {
nextBufferIndexLowerBound := vm.bufferIndexLowerBound + vm.height()
nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height()
@ -266,7 +266,7 @@ func (vm *fileTreeViewModel) PageDown() error {
}
// PageUp moves to previous page putting the cursor on top
func (vm *fileTreeViewModel) PageUp() error {
func (vm *FileTreeViewModel) PageUp() error {
nextBufferIndexLowerBound := vm.bufferIndexLowerBound - vm.height()
nextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height()
@ -291,7 +291,7 @@ func (vm *fileTreeViewModel) PageUp() error {
}
// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
func (vm *fileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) {
func (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) {
var visitor func(*filetree.FileNode) error
var evaluator func(*filetree.FileNode) bool
var dfsCounter int
@ -321,8 +321,8 @@ func (vm *fileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (nod
return node
}
// toggleCollapse will collapse/expand the selected FileNode.
func (vm *fileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error {
// ToggleCollapse will collapse/expand the selected FileNode.
func (vm *FileTreeViewModel) ToggleCollapse(filterRegex *regexp.Regexp) error {
node := vm.getAbsPositionNode(filterRegex)
if node != nil && node.Data.FileInfo.IsDir {
node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed
@ -330,8 +330,8 @@ func (vm *fileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error {
return nil
}
// toggleCollapseAll will collapse/expand the all directories.
func (vm *fileTreeViewModel) toggleCollapseAll() error {
// ToggleCollapseAll will collapse/expand the all directories.
func (vm *FileTreeViewModel) ToggleCollapseAll() error {
vm.CollapseAll = !vm.CollapseAll
visitor := func(curNode *filetree.FileNode) error {
@ -345,25 +345,25 @@ func (vm *fileTreeViewModel) toggleCollapseAll() error {
err := vm.ModelTree.VisitDepthChildFirst(visitor, evaluator)
if err != nil {
logrus.Errorf("unable to propagate tree on toggleCollapseAll: %+v", err)
logrus.Errorf("unable to propagate tree on ToggleCollapseAll: %+v", err)
}
return nil
}
// toggleCollapse will collapse/expand the selected FileNode.
func (vm *fileTreeViewModel) toggleAttributes() error {
// ToggleCollapse will collapse/expand the selected FileNode.
func (vm *FileTreeViewModel) ToggleAttributes() error {
vm.ShowAttributes = !vm.ShowAttributes
return nil
}
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
func (vm *fileTreeViewModel) toggleShowDiffType(diffType filetree.DiffType) {
// ToggleShowDiffType will show/hide the selected DiffType in the filetree pane.
func (vm *FileTreeViewModel) ToggleShowDiffType(diffType filetree.DiffType) {
vm.HiddenDiffTypes[diffType] = !vm.HiddenDiffTypes[diffType]
}
// Update refreshes the state objects for future rendering.
func (vm *fileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error {
func (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error {
vm.refWidth = width
vm.refHeight = height
@ -411,21 +411,21 @@ func (vm *fileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height in
}
// Render flushes the state objects (file tree) to the pane.
func (vm *fileTreeViewModel) Render() error {
func (vm *FileTreeViewModel) Render() error {
treeString := vm.ViewTree.StringBetween(vm.bufferIndexLowerBound, vm.bufferIndexUpperBound(), vm.ShowAttributes)
lines := strings.Split(treeString, "\n")
// update the contents
vm.mainBuf.Reset()
vm.Buffer.Reset()
for idx, line := range lines {
if idx == vm.bufferIndex {
_, err := fmt.Fprintln(&vm.mainBuf, format.Selected(vtclean.Clean(line, false)))
_, err := fmt.Fprintln(&vm.Buffer, format.Selected(vtclean.Clean(line, false)))
if err != nil {
logrus.Debug("unable to write to buffer: ", err)
return err
}
} else {
_, err := fmt.Fprintln(&vm.mainBuf, line)
_, err := fmt.Fprintln(&vm.Buffer, line)
if err != nil {
logrus.Debug("unable to write to buffer: ", err)
return err

View file

@ -1,4 +1,4 @@
package ui
package viewmodel
import (
"bytes"
@ -73,8 +73,8 @@ func assertTestData(t *testing.T, actualBytes []byte) {
helperCheckDiff(t, expectedBytes, actualBytes)
}
func initializeTestViewModel(t *testing.T) *fileTreeViewModel {
result := docker.TestAnalysisFromArchive(t, "../../.data/test-docker-image.tar")
func initializeTestViewModel(t *testing.T) *FileTreeViewModel {
result := docker.TestAnalysisFromArchive(t, "../../../.data/test-docker-image.tar")
cache := filetree.NewFileTreeCache(result.RefTrees)
err := cache.Build()
@ -88,14 +88,14 @@ func initializeTestViewModel(t *testing.T) *fileTreeViewModel {
if err != nil {
t.Fatalf("%s: unable to stack trees: %v", t.Name(), err)
}
vm, err := newFileTreeViewModel(treeStack, result.RefTrees, cache)
vm, err := NewFileTreeViewModel(treeStack, result.RefTrees, cache)
if err != nil {
t.Fatalf("%s: unable to create tree ViewModel: %+v", t.Name(), err)
}
return vm
}
func runTestCase(t *testing.T, vm *fileTreeViewModel, width, height int, filterRegex *regexp.Regexp) {
func runTestCase(t *testing.T, vm *FileTreeViewModel, width, height int, filterRegex *regexp.Regexp) {
err := vm.Update(filterRegex, width, height)
if err != nil {
t.Errorf("failed to update viewmodel: %v", err)
@ -106,7 +106,7 @@ func runTestCase(t *testing.T, vm *fileTreeViewModel, width, height int, filterR
t.Errorf("failed to render viewmodel: %v", err)
}
assertTestData(t, vm.mainBuf.Bytes())
assertTestData(t, vm.Buffer.Bytes())
}
func checkError(t *testing.T, err error, message string) {
@ -153,7 +153,7 @@ func TestFileTreeDirCollapse(t *testing.T) {
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
err := vm.ToggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
moved := vm.CursorDown()
@ -167,7 +167,7 @@ func TestFileTreeDirCollapse(t *testing.T) {
}
// collapse /etc
err = vm.toggleCollapse(nil)
err = vm.ToggleCollapse(nil)
checkError(t, err, "unable to collapse /etc")
runTestCase(t, vm, width, height, nil)
@ -180,7 +180,7 @@ func TestFileTreeDirCollapseAll(t *testing.T) {
vm.Setup(0, height)
vm.ShowAttributes = true
err := vm.toggleCollapseAll()
err := vm.ToggleCollapseAll()
checkError(t, err, "unable to collapse all dir")
runTestCase(t, vm, width, height, nil)
@ -194,13 +194,13 @@ func TestFileTreeSelectLayer(t *testing.T) {
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
err := vm.ToggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
// select the next layer, compareMode = layer
err = vm.setTreeByLayer(0, 0, 1, 1)
err = vm.SetTreeByLayer(0, 0, 1, 1)
if err != nil {
t.Errorf("unable to setTreeByLayer: %v", err)
t.Errorf("unable to SetTreeByLayer: %v", err)
}
runTestCase(t, vm, width, height, nil)
}
@ -213,12 +213,12 @@ func TestFileShowAggregateChanges(t *testing.T) {
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
err := vm.ToggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
// select the next layer, compareMode = layer
err = vm.setTreeByLayer(0, 0, 1, 13)
checkError(t, err, "unable to setTreeByLayer")
err = vm.SetTreeByLayer(0, 0, 1, 13)
checkError(t, err, "unable to SetTreeByLayer")
runTestCase(t, vm, width, height, nil)
}
@ -275,7 +275,7 @@ func TestFileTreeDirCursorRight(t *testing.T) {
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
err := vm.ToggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
moved := vm.CursorDown()
@ -289,7 +289,7 @@ func TestFileTreeDirCursorRight(t *testing.T) {
}
// collapse /etc
err = vm.toggleCollapse(nil)
err = vm.ToggleCollapse(nil)
checkError(t, err, "unable to collapse /etc")
// expand /etc
@ -322,23 +322,23 @@ func TestFileTreeHideAddedRemovedModified(t *testing.T) {
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
err := vm.ToggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
// select the 7th layer, compareMode = layer
err = vm.setTreeByLayer(0, 0, 1, 7)
err = vm.SetTreeByLayer(0, 0, 1, 7)
if err != nil {
t.Errorf("unable to setTreeByLayer: %v", err)
t.Errorf("unable to SetTreeByLayer: %v", err)
}
// hide added files
vm.toggleShowDiffType(filetree.Added)
vm.ToggleShowDiffType(filetree.Added)
// hide modified files
vm.toggleShowDiffType(filetree.Modified)
vm.ToggleShowDiffType(filetree.Modified)
// hide removed files
vm.toggleShowDiffType(filetree.Removed)
vm.ToggleShowDiffType(filetree.Removed)
runTestCase(t, vm, width, height, nil)
}
@ -351,17 +351,17 @@ func TestFileTreeHideUnmodified(t *testing.T) {
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
err := vm.ToggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
// select the 7th layer, compareMode = layer
err = vm.setTreeByLayer(0, 0, 1, 7)
err = vm.SetTreeByLayer(0, 0, 1, 7)
if err != nil {
t.Errorf("unable to setTreeByLayer: %v", err)
t.Errorf("unable to SetTreeByLayer: %v", err)
}
// hide unmodified files
vm.toggleShowDiffType(filetree.Unmodified)
vm.ToggleShowDiffType(filetree.Unmodified)
runTestCase(t, vm, width, height, nil)
}
@ -374,17 +374,17 @@ func TestFileTreeHideTypeWithFilter(t *testing.T) {
vm.ShowAttributes = true
// collapse /bin
err := vm.toggleCollapse(nil)
err := vm.ToggleCollapse(nil)
checkError(t, err, "unable to collapse /bin")
// select the 7th layer, compareMode = layer
err = vm.setTreeByLayer(0, 0, 1, 7)
err = vm.SetTreeByLayer(0, 0, 1, 7)
if err != nil {
t.Errorf("unable to setTreeByLayer: %v", err)
t.Errorf("unable to SetTreeByLayer: %v", err)
}
// hide added files
vm.toggleShowDiffType(filetree.Added)
vm.ToggleShowDiffType(filetree.Added)
regex, err := regexp.Compile("saved")
if err != nil {