decouple views with listeners

This commit is contained in:
Alex Goodman 2019-10-13 20:56:58 -04:00
parent 939741625d
commit 74e4fe2560
No known key found for this signature in database
GPG Key ID: 98AF011C5C78EB7E
14 changed files with 449 additions and 270 deletions

View File

@ -2,7 +2,6 @@ package ui
import (
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/controller"
"github.com/wagoodman/dive/runtime/ui/key"
"sync"
@ -16,7 +15,7 @@ const debug = false
// type global
type app struct {
gui *gocui.Gui
controllers *controller.Collection
controllers *Controller
layout *layoutManager
}
@ -28,10 +27,10 @@ var (
func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*app, error) {
var err error
once.Do(func() {
var theControls *controller.Collection
var theControls *Controller
var globalHelpKeys []*key.Binding
theControls, err = controller.NewCollection(gui, analysis, cache)
theControls, err = NewCollection(gui, analysis, cache)
if err != nil {
return
}
@ -110,7 +109,7 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC
var lastX, lastY int
// quit is the gocui callback invoked when the user hits Ctrl+C
func (ui *app) quit() error {
func (a *app) quit() error {
// profileObj.Stop()
// onExit()

212
runtime/ui/controller.go Normal file
View File

@ -0,0 +1,212 @@
package ui
import (
"github.com/jroimartin/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/view"
"github.com/wagoodman/dive/runtime/ui/viewmodel"
"regexp"
)
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
}
func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*Controller, error) {
var err error
controller := &Controller{
gui: g,
}
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
}
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)
// update the status pane when a filetree option is changed by the user
controller.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
// propagate initial conditions to necessary views
err = controller.onLayerChange(viewmodel.LayerSelection{
Layer: controller.Layer.CurrentLayer(),
BottomTreeStart: 0,
BottomTreeStop: 0,
TopTreeStart: 0,
TopTreeStop: 0,
})
if err != nil {
return nil, err
}
return controller, nil
}
func (c *Controller) onFileTreeViewOptionChange() error {
err := c.Status.Update()
if err != nil {
return err
}
return c.Status.Render()
}
func (c *Controller) onFilterEdit(filter string) error {
var filterRegex *regexp.Regexp
var err error
if len(filter) > 0 {
filterRegex, err = regexp.Compile(filter)
if err != nil {
return err
}
}
c.Tree.SetFilterRegex(filterRegex)
err = c.Tree.Update()
if err != nil {
return err
}
return c.Tree.Render()
}
func (c *Controller) onLayerChange(selection viewmodel.LayerSelection) error {
// update the details
c.Details.SetCurrentLayer(selection.Layer)
// update the filetree
err := c.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")
} else {
c.Tree.SetTitle("Current Layer Contents")
}
// update details and filetree panes
return c.UpdateAndRender()
}
func (c *Controller) 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 *Controller) 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 *Controller) 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 *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)
} else {
_, err = c.gui.SetCurrentView(c.Layer.Name())
c.Status.SetCurrentView(c.Layer)
}
if err != nil {
logrus.Error("unable to toggle view: ", err)
return err
}
return c.UpdateAndRender()
}
func (c *Controller) 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...
if !c.Filter.IsVisible() {
// ...remove any filter from the tree
c.Tree.SetFilterRegex(nil)
// ...adjust focus to a valid (visible) view
err = c.ToggleView()
if err != nil {
logrus.Error("unable to toggle filter view (back): ", err)
return err
}
}
return c.UpdateAndRender()
}

View File

@ -1,166 +0,0 @@
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 *Collection
type Collection struct {
gui *gocui.Gui
Tree *FileTree
Layer *Layer
Status *Status
Filter *Filter
Details *Details
lookup map[string]Controller
}
func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*Collection, error) {
var err error
controllers = &Collection{
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 *Collection) 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 *Collection) 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 *Collection) 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 *Collection) 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 *Collection) 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 *Collection) 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 *Collection) 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 *Collection) 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,16 +0,0 @@
package controller
import (
"github.com/jroimartin/gocui"
)
// Controller defines the a renderable terminal screen pane.
type Controller interface {
Update() error
Render() error
Setup(*gocui.View, *gocui.View) error
CursorDown() error
CursorUp() error
KeyHelp() string
IsVisible() bool
}

View File

@ -83,7 +83,6 @@ func newBinding(gui *gocui.Gui, influence string, keys []keybinding.Key, display
}
for _, key := range keys {
logrus.Debugf("registering %d %d (%+v)", key.Value, key.Modifier, key.Tokens)
if err := gui.SetKeybinding(influence, key.Value, key.Modifier, binding.onAction); err != nil {
return nil, err
}

View File

@ -4,15 +4,15 @@ 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.Collection
controllers *Controller
}
func newLayoutManager(c *controller.Collection) *layoutManager {
// 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 {

37
runtime/ui/view/cursor.go Normal file
View File

@ -0,0 +1,37 @@
package view
import (
"errors"
"github.com/jroimartin/gocui"
)
// 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
}

View File

@ -1,9 +1,10 @@
package controller
package view
import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"strconv"
@ -23,10 +24,13 @@ type Details struct {
header *gocui.View
efficiency float64
inefficiencies filetree.EfficiencySlice
imageSize uint64
currentLayer *image.Layer
}
// 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 *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
@ -34,6 +38,7 @@ func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, ineff
controller.gui = gui
controller.efficiency = efficiency
controller.inefficiencies = inefficiencies
controller.imageSize = imageSize
return controller
}
@ -85,12 +90,12 @@ func (c *Details) IsVisible() bool {
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
func (c *Details) CursorDown() error {
return controllers.CursorDown(c.gui, c.view)
return CursorDown(c.gui, c.view)
}
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
func (c *Details) CursorUp() error {
return controllers.CursorUp(c.gui, c.view)
return CursorUp(c.gui, c.view)
}
// Update refreshes the state objects for future rendering.
@ -98,13 +103,19 @@ func (c *Details) Update() error {
return nil
}
func (c *Details) SetCurrentLayer(layer *image.Layer) {
c.currentLayer = layer
}
// Render flushes the state objects to the screen. The details pane reports:
// 1. the current selected layer's command string
// 2. the image efficiency score
// 3. the estimated wasted image space
// 4. a list of inefficient file allocations
func (c *Details) Render() error {
currentLayer := controllers.Layer.currentLayer()
if c.currentLayer == nil {
return fmt.Errorf("no layer selected")
}
var wastedSpace int64
@ -126,7 +137,7 @@ func (c *Details) Render() error {
}
}
imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(controllers.Layer.ImageSize))
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))
wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
@ -147,15 +158,15 @@ func (c *Details) Render() error {
c.view.Clear()
var lines = make([]string, 0)
if currentLayer.Names != nil && len(currentLayer.Names) > 0 {
lines = append(lines, format.Header("Tags: ")+strings.Join(currentLayer.Names, ", "))
if c.currentLayer.Names != nil && len(c.currentLayer.Names) > 0 {
lines = append(lines, format.Header("Tags: ")+strings.Join(c.currentLayer.Names, ", "))
} else {
lines = append(lines, format.Header("Tags: ")+"(none)")
}
lines = append(lines, format.Header("Id: ")+currentLayer.Id)
lines = append(lines, format.Header("Digest: ")+currentLayer.Digest)
lines = append(lines, format.Header("Id: ")+c.currentLayer.Id)
lines = append(lines, format.Header("Digest: ")+c.currentLayer.Digest)
lines = append(lines, format.Header("Command:"))
lines = append(lines, currentLayer.Command)
lines = append(lines, c.currentLayer.Command)
lines = append(lines, "\n"+format.Header(vtclean.Clean(imageHeaderStr, false)))
lines = append(lines, imageSizeStr)
lines = append(lines, wastedSpaceStr)

View File

@ -1,7 +1,8 @@
package controller
package view
import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"github.com/wagoodman/dive/runtime/ui/viewmodel"
@ -20,6 +21,8 @@ const (
type CompareType int
type ViewOptionChangeListener func() error
// FileTree 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 FileTree struct {
@ -28,13 +31,19 @@ type FileTree struct {
view *gocui.View
header *gocui.View
vm *viewmodel.FileTree
title string
filterRegex *regexp.Regexp
listeners []ViewOptionChangeListener
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 *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.TreeCache) (controller *FileTree, err error) {
controller = new(FileTree)
controller.listeners = make([]ViewOptionChangeListener, 0)
// populate main fields
controller.name = name
@ -47,6 +56,18 @@ func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree,
return controller, err
}
func (c *FileTree) AddViewOptionChangeListener(listener ...ViewOptionChangeListener) {
c.listeners = append(c.listeners, listener...)
}
func (c *FileTree) SetTitle(title string) {
c.title = title
}
func (c *FileTree) SetFilterRegex(filterRegex *regexp.Regexp) {
c.filterRegex = filterRegex
}
func (c *FileTree) Name() string {
return c.name
}
@ -166,12 +187,11 @@ func (c *FileTree) resetCursor() {
}
// SetTreeByLayer populates the view model by stacking the indicated image layer file trees.
func (c *FileTree) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
func (c *FileTree) SetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
err := c.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
if err != nil {
return err
}
// controller.ResetCursor()
_ = c.Update()
return c.Render()
@ -201,7 +221,7 @@ func (c *FileTree) CursorUp() error {
// 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(filterRegex())
err := c.vm.CursorLeft(c.filterRegex)
if err != nil {
return err
}
@ -211,7 +231,7 @@ func (c *FileTree) CursorLeft() error {
// CursorRight descends into directory expanding it if needed
func (c *FileTree) CursorRight() error {
err := c.vm.CursorRight(filterRegex())
err := c.vm.CursorRight(c.filterRegex)
if err != nil {
return err
}
@ -244,7 +264,7 @@ func (c *FileTree) PageUp() error {
// ToggleCollapse will collapse/expand the selected FileNode.
func (c *FileTree) toggleCollapse() error {
err := c.vm.ToggleCollapse(filterRegex())
err := c.vm.ToggleCollapse(c.filterRegex)
if err != nil {
return err
}
@ -265,44 +285,61 @@ func (c *FileTree) toggleCollapseAll() error {
return c.Render()
}
func (c *FileTree) notifyOnViewOptionChangeListeners() error {
for _, listener := range c.listeners {
err := listener()
if err != nil {
logrus.Errorf("notifyOnViewOptionChangeListeners error: %+v", err)
return err
}
}
return nil
}
// ToggleAttributes will show/hide file attributes
func (c *FileTree) toggleAttributes() error {
err := c.vm.ToggleAttributes()
if err != nil {
return err
}
err = c.Update()
if err != nil {
return err
}
err = c.Render()
if err != nil {
return err
}
// we need to render the changes to the status pane as well (not just this contoller/view)
return controllers.UpdateAndRender()
return c.notifyOnViewOptionChangeListeners()
}
// ToggleShowDiffType will show/hide the selected DiffType in the filetree pane.
func (c *FileTree) toggleShowDiffType(diffType filetree.DiffType) error {
c.vm.ToggleShowDiffType(diffType)
// we need to render the changes to the status pane as well (not just this contoller/view)
return controllers.UpdateAndRender()
}
// filterRegex will return a regular expression object to match the user's filter input.
func filterRegex() *regexp.Regexp {
if controllers.Filter == nil || controllers.Filter.view == nil {
return nil
}
filterString := strings.TrimSpace(controllers.Filter.view.Buffer())
if len(filterString) == 0 {
return nil
}
regex, err := regexp.Compile(filterString)
err := c.Update()
if err != nil {
return nil
return err
}
err = c.Render()
if err != nil {
return err
}
return regex
// we need to render the changes to the status pane as well (not just this contoller/view)
return c.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 {
_ = c.Update()
err := c.Update()
if err != nil {
return err
}
if resized {
return c.Render()
}
@ -320,19 +357,15 @@ func (c *FileTree) Update() error {
width, height = c.gui.Size()
}
// height should account for the header
return c.vm.Update(filterRegex(), width, height-1)
return c.vm.Update(c.filterRegex, width, height-1)
}
// Render flushes the state objects (file tree) to the pane.
func (c *FileTree) Render() error {
title := "Current Layer Contents"
if controllers.Layer.CompareMode == CompareAll {
title = "Aggregated Layer Contents"
}
title := c.title
// indicate when selected
if c.gui.CurrentView() == c.view {
title = "● " + title
title = "● " + c.title
}
c.gui.Update(func(g *gocui.Gui) error {

View File

@ -1,12 +1,15 @@
package controller
package view
import (
"fmt"
"github.com/jroimartin/gocui"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/runtime/ui/format"
"strings"
)
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 {
@ -17,12 +20,16 @@ type Filter struct {
headerStr string
maxLength int
hidden bool
filterEditListeners []FilterEditListener
}
// NewFilterController creates a new view object attached the the global [gocui] screen object.
func NewFilterController(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)
// populate main fields
controller.name = name
controller.gui = gui
@ -32,6 +39,10 @@ func NewFilterController(name string, gui *gocui.Gui) (controller *Filter) {
return controller
}
func (c *Filter) AddFilterEditListener(listener ...FilterEditListener) {
c.filterEditListeners = append(c.filterEditListeners, listener...)
}
func (c *Filter) Name() string {
return c.name
}
@ -70,7 +81,7 @@ func (c *Filter) ToggleVisible() error {
logrus.Error("unable to toggle filter view: ", err)
return err
}
return controllers.UpdateAndRender()
return nil
}
// reset the cursor for the next time it is visible
@ -119,9 +130,19 @@ func (c *Filter) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier)
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
}
if controllers.Tree != nil {
_ = controllers.Tree.Update()
_ = controllers.Tree.Render()
// notify listeners
c.notifyFilterEditListeners()
}
func (c *Filter) notifyFilterEditListeners() {
currentValue := strings.TrimSpace(c.view.Buffer())
for _, listener := range c.filterEditListeners {
err := listener(currentValue)
if err != nil {
// note: cannot propagate error from here since this is from the main gogui thread
logrus.Errorf("notifyFilterEditListeners: %+v", err)
}
}
}

View File

@ -1,10 +1,11 @@
package controller
package view
import (
"fmt"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"github.com/wagoodman/dive/runtime/ui/viewmodel"
"strings"
"github.com/jroimartin/gocui"
@ -13,6 +14,8 @@ import (
"github.com/spf13/viper"
)
type LayerChangeListener func(viewmodel.LayerSelection) error
// Layer holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
// shows the image layers and layer selector.
type Layer struct {
@ -24,15 +27,18 @@ type Layer struct {
Layers []*image.Layer
CompareMode CompareType
CompareStartIndex int
ImageSize uint64
listeners []LayerChangeListener
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 *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)
// populate main fields
controller.name = name
controller.gui = gui
@ -50,6 +56,29 @@ func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (con
return controller, err
}
func (c *Layer) AddLayerChangeListener(listener ...LayerChangeListener) {
c.listeners = append(c.listeners, listener...)
}
func (c *Layer) notifyLayerChangeListeners() error {
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := c.getCompareIndexes()
selection := viewmodel.LayerSelection{
Layer: c.CurrentLayer(),
BottomTreeStart: bottomTreeStart,
BottomTreeStop: bottomTreeStop,
TopTreeStart: topTreeStart,
TopTreeStop: topTreeStop,
}
for _, listener := range c.listeners {
err := listener(selection)
if err != nil {
logrus.Errorf("notifyLayerChangeListeners error: %+v", err)
return err
}
}
return nil
}
func (c *Layer) Name() string {
return c.name
}
@ -141,7 +170,7 @@ func (c *Layer) PageDown() error {
}
if step > 0 {
err := controllers.CursorStep(c.gui, c.view, step)
err := CursorStep(c.gui, c.view, step)
if err == nil {
return c.SetCursor(c.LayerIndex + step)
}
@ -159,7 +188,7 @@ func (c *Layer) PageUp() error {
}
if step > 0 {
err := controllers.CursorStep(c.gui, c.view, -step)
err := CursorStep(c.gui, c.view, -step)
if err == nil {
return c.SetCursor(c.LayerIndex - step)
}
@ -170,7 +199,7 @@ func (c *Layer) PageUp() error {
// 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 := controllers.CursorDown(c.gui, c.view)
err := CursorDown(c.gui, c.view)
if err == nil {
return c.SetCursor(c.LayerIndex + 1)
}
@ -181,7 +210,7 @@ func (c *Layer) CursorDown() error {
// CursorUp moves the cursor up in the layer pane (selecting a lower layer).
func (c *Layer) CursorUp() error {
if c.LayerIndex > 0 {
err := controllers.CursorUp(c.gui, c.view)
err := CursorUp(c.gui, c.view)
if err == nil {
return c.SetCursor(c.LayerIndex - 1)
}
@ -192,30 +221,23 @@ func (c *Layer) CursorUp() error {
// 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 := controllers.Tree.setTreeByLayer(c.getCompareIndexes())
err := c.notifyLayerChangeListeners()
if err != nil {
return err
}
_ = controllers.Details.Render()
return c.Render()
}
// currentLayer returns the Layer object currently selected.
func (c *Layer) currentLayer() *image.Layer {
// CurrentLayer returns the Layer object currently selected.
func (c *Layer) CurrentLayer() *image.Layer {
return c.Layers[c.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
err := controllers.UpdateAndRender()
if err != nil {
logrus.Errorf("unable to set compare mode: %+v", err)
return err
}
return controllers.Tree.setTreeByLayer(c.getCompareIndexes())
return c.notifyLayerChangeListeners()
}
// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode)
@ -254,10 +276,6 @@ func (c *Layer) renderCompareBar(layerIdx int) string {
// Update refreshes the state objects for future rendering (currently does nothing).
func (c *Layer) Update() error {
c.ImageSize = 0
for idx := 0; idx < len(c.Layers); idx++ {
c.ImageSize += c.Layers[idx].Size
}
return nil
}

View File

@ -0,0 +1,9 @@
package view
// Controller defines the a renderable terminal screen pane.
type Renderer interface {
Update() error
Render() error
IsVisible() bool
KeyHelp() string
}

View File

@ -1,4 +1,4 @@
package controller
package view
import (
"fmt"
@ -17,11 +17,13 @@ type Status struct {
gui *gocui.Gui
view *gocui.View
selectedView Renderer
helpKeys []*key.Binding
}
// NewStatusController creates a new view object attached the the global [gocui] screen object.
func NewStatusController(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
@ -32,6 +34,10 @@ func NewStatusController(name string, gui *gocui.Gui) (controller *Status) {
return controller
}
func (c *Status) SetCurrentView(r Renderer) {
c.selectedView = r
}
func (c *Status) Name() string {
return c.name
}
@ -74,7 +80,13 @@ func (c *Status) Update() error {
func (c *Status) Render() error {
c.gui.Update(func(g *gocui.Gui) error {
c.view.Clear()
_, err := fmt.Fprintln(c.view, c.KeyHelp()+format.StatusNormal("▏"+strings.Repeat(" ", 1000)))
var selectedHelp string
if c.selectedView != nil {
selectedHelp = c.selectedView.KeyHelp()
}
_, err := fmt.Fprintln(c.view, c.KeyHelp()+selectedHelp+format.StatusNormal("▏"+strings.Repeat(" ", 1000)))
if err != nil {
logrus.Debug("unable to write to buffer: ", err)
}

View File

@ -0,0 +1,10 @@
package viewmodel
import (
"github.com/wagoodman/dive/dive/image"
)
type LayerSelection struct {
Layer *image.Layer
BottomTreeStart, BottomTreeStop, TopTreeStart, TopTreeStop int
}