add keybinding package

This commit is contained in:
Alex Goodman 2019-10-11 12:11:07 -04:00
parent c8ab7098d8
commit 45604e5c66
No known key found for this signature in database
GPG Key ID: 98AF011C5C78EB7E
14 changed files with 692 additions and 495 deletions

View File

@ -103,7 +103,7 @@ func run(enableUi bool, options Options, imageResolver image.Resolver, events ev
err = ui.Run(analysis, cache)
if err != nil {
events.exitWithErrorMessage("runtime error", err)
events.exitWithError(err)
return
}
}
@ -131,7 +131,6 @@ func Run(options Options) {
}
if event.stderr != "" {
logrus.Error(event.stderr)
_, err := fmt.Fprintln(os.Stderr, event.stderr)
if err != nil {
fmt.Println("error: could not write to buffer:", err)
@ -140,6 +139,10 @@ func Run(options Options) {
if event.err != nil {
logrus.Error(event.err)
_, err := fmt.Fprintln(os.Stderr, event.err.Error())
if err != nil {
fmt.Println("error: could not write to buffer:", err)
}
}
if event.errorOnExit {

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

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

View File

@ -0,0 +1,52 @@
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

@ -4,6 +4,8 @@ import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"strconv"
"strings"
@ -12,10 +14,10 @@ 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 {
Name string
type detailsController struct {
name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
@ -23,12 +25,12 @@ 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
controller.name = name
controller.gui = gui
controller.efficiency = efficiency
controller.inefficiencies = inefficiencies
@ -37,7 +39,7 @@ func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, ineff
}
// 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
@ -51,11 +53,21 @@ func (controller *DetailsController) Setup(v *gocui.View, header *gocui.View) er
controller.header.Wrap = false
controller.header.Frame = false
// set keybindings
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
return err
var infos = []key.BindingInfo{
{
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
OnAction: controller.CursorDown,
},
{
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
OnAction: controller.CursorUp,
},
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
_, err := key.GenerateBindings(controller.gui, controller.name, infos)
if err != nil {
return err
}
@ -63,22 +75,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 {
func (controller *detailsController) CursorDown() error {
return CursorDown(controller.gui, controller.view)
}
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
func (controller *DetailsController) CursorUp() error {
func (controller *detailsController) CursorUp() error {
return 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
}
@ -87,13 +99,13 @@ 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 {
currentLayer := Controllers.Layer.currentLayer()
func (controller *detailsController) Render() error {
currentLayer := controllers.Layer.currentLayer()
var wastedSpace int64
template := "%5s %12s %-s\n"
inefficiencyReport := fmt.Sprintf(Formatting.Header(template), "Count", "Total Space", "Path")
inefficiencyReport := fmt.Sprintf(format.Header(template), "Count", "Total Space", "Path")
height := 100
if controller.view != nil {
@ -110,9 +122,9 @@ func (controller *DetailsController) Render() error {
}
}
imageSizeStr := fmt.Sprintf("%s %s", Formatting.Header("Total Image size:"), humanize.Bytes(Controllers.Layer.ImageSize))
effStr := fmt.Sprintf("%s %d %%", Formatting.Header("Image efficiency score:"), int(100.0*controller.efficiency))
wastedSpaceStr := fmt.Sprintf("%s %s", Formatting.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(controllers.Layer.ImageSize))
effStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*controller.efficiency))
wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
controller.gui.Update(func(g *gocui.Gui) error {
// update header
@ -122,7 +134,7 @@ func (controller *DetailsController) Render() error {
layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15))
imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15))
_, err := fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(layerHeaderStr, false)))
_, err := fmt.Fprintln(controller.header, format.Header(vtclean.Clean(layerHeaderStr, false)))
if err != nil {
return err
}
@ -132,15 +144,15 @@ func (controller *DetailsController) Render() error {
var lines = make([]string, 0)
if currentLayer.Names != nil && len(currentLayer.Names) > 0 {
lines = append(lines, Formatting.Header("Tags: ")+strings.Join(currentLayer.Names, ", "))
lines = append(lines, format.Header("Tags: ")+strings.Join(currentLayer.Names, ", "))
} else {
lines = append(lines, Formatting.Header("Tags: ")+"(none)")
lines = append(lines, format.Header("Tags: ")+"(none)")
}
lines = append(lines, Formatting.Header("Id: ")+currentLayer.Id)
lines = append(lines, Formatting.Header("Digest: ")+currentLayer.Digest)
lines = append(lines, Formatting.Header("Command:"))
lines = append(lines, format.Header("Id: ")+currentLayer.Id)
lines = append(lines, format.Header("Digest: ")+currentLayer.Digest)
lines = append(lines, format.Header("Command:"))
lines = append(lines, currentLayer.Command)
lines = append(lines, "\n"+Formatting.Header(vtclean.Clean(imageHeaderStr, false)))
lines = append(lines, "\n"+format.Header(vtclean.Clean(imageHeaderStr, false)))
lines = append(lines, imageSizeStr)
lines = append(lines, wastedSpaceStr)
lines = append(lines, effStr+"\n")
@ -156,6 +168,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

@ -2,15 +2,13 @@ package ui
import (
"fmt"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"regexp"
"strings"
"github.com/lunixbochs/vtclean"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/keybinding"
"github.com/jroimartin/gocui"
"github.com/lunixbochs/vtclean"
"github.com/wagoodman/dive/dive/filetree"
)
@ -21,92 +19,36 @@ 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 {
Name string
type fileTreeController struct {
name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
vm *FileTreeViewModel
vm *fileTreeViewModel
keybindingToggleCollapse []keybinding.Key
keybindingToggleCollapseAll []keybinding.Key
keybindingToggleAttributes []keybinding.Key
keybindingToggleAdded []keybinding.Key
keybindingToggleRemoved []keybinding.Key
keybindingToggleModified []keybinding.Key
keybindingToggleUnmodified []keybinding.Key
keybindingPageDown []keybinding.Key
keybindingPageUp []keybinding.Key
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.name = name
controller.gui = gui
controller.vm, err = NewFileTreeViewModel(tree, refTrees, cache)
controller.vm, err = newFileTreeViewModel(tree, refTrees, cache)
if err != nil {
return nil, err
}
controller.keybindingToggleCollapse, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-dir"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleCollapseAll, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-all-dir"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleAttributes, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-filetree-attributes"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleAdded, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-added-files"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleRemoved, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-removed-files"))
if err != nil {
logrus.Error(err)
}
controller.keybindingToggleModified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-modified-files"))
if err != nil {
logrus.Error(err)
}
// support legacy behavior first, then use default behavior
controller.keybindingToggleUnmodified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unchanged-files"))
if err != nil {
controller.keybindingToggleUnmodified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unmodified-files"))
if err != nil {
logrus.Error(err)
}
}
controller.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up"))
if err != nil {
logrus.Error(err)
}
controller.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down"))
if err != nil {
logrus.Error(err)
}
return controller, err
}
// 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
@ -119,65 +61,82 @@ func (controller *FileTreeController) Setup(v *gocui.View, header *gocui.View) e
controller.header.Wrap = false
controller.header.Frame = false
// set keybindings
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorLeft() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorRight() }); err != nil {
return err
var infos = []key.BindingInfo{
{
ConfigKeys: []string{"keybinding.toggle-collapse-dir"},
OnAction: controller.toggleCollapse,
Display: "Collapse dir",
},
{
ConfigKeys: []string{"keybinding.toggle-collapse-all-dir"},
OnAction: controller.toggleCollapseAll,
Display: "Collapse all dir",
},
{
ConfigKeys: []string{"keybinding.toggle-added-files"},
OnAction: func() error { return controller.toggleShowDiffType(filetree.Added) },
IsSelected: func() bool { return !controller.vm.HiddenDiffTypes[filetree.Added] },
Display: "Added",
},
{
ConfigKeys: []string{"keybinding.toggle-removed-files"},
OnAction: func() error { return controller.toggleShowDiffType(filetree.Removed) },
IsSelected: func() bool { return !controller.vm.HiddenDiffTypes[filetree.Removed] },
Display: "Removed",
},
{
ConfigKeys: []string{"keybinding.toggle-modified-files"},
OnAction: func() error { return controller.toggleShowDiffType(filetree.Modified) },
IsSelected: func() bool { return !controller.vm.HiddenDiffTypes[filetree.Modified] },
Display: "Modified",
},
{
ConfigKeys: []string{"keybinding.toggle-unchanged-files", "keybinding.toggle-unmodified-files"},
OnAction: func() error { return controller.toggleShowDiffType(filetree.Unmodified) },
IsSelected: func() bool { return !controller.vm.HiddenDiffTypes[filetree.Unmodified] },
Display: "Unmodified",
},
{
ConfigKeys: []string{"keybinding.toggle-filetree-attributes"},
OnAction: controller.toggleAttributes,
IsSelected: func() bool { return controller.vm.ShowAttributes },
Display: "Attributes",
},
{
ConfigKeys: []string{"keybinding.page-up"},
OnAction: controller.PageUp,
},
{
ConfigKeys: []string{"keybinding.page-down"},
OnAction: controller.PageDown,
},
{
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
OnAction: controller.CursorDown,
},
{
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
OnAction: controller.CursorUp,
},
{
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
OnAction: controller.CursorLeft,
},
{
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
OnAction: controller.CursorRight,
},
}
for _, key := range controller.keybindingPageUp {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageUp() }); err != nil {
return err
}
}
for _, key := range controller.keybindingPageDown {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageDown() }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleCollapse {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleCollapse() }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleCollapseAll {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleCollapseAll() }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleAttributes {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleAttributes() }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleAdded {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Added) }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleRemoved {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Removed) }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleModified {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Modified) }); err != nil {
return err
}
}
for _, key := range controller.keybindingToggleUnmodified {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Unmodified) }); err != nil {
return err
}
helpKeys, err := key.GenerateBindings(controller.gui, controller.name, infos)
if err != nil {
return err
}
controller.helpKeys = helpKeys
_, height := controller.view.Size()
controller.vm.Setup(0, height)
@ -188,18 +147,18 @@ 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() {
func (controller *fileTreeController) resetCursor() {
_ = controller.view.SetCursor(0, 0)
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 {
func (controller *fileTreeController) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
err := controller.vm.setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
if err != nil {
return err
@ -214,7 +173,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()
}
@ -225,7 +184,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()
}
@ -233,7 +192,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
@ -243,7 +202,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
@ -253,7 +212,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
@ -262,7 +221,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
@ -271,12 +230,12 @@ 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 {
func (controller *fileTreeController) toggleCollapse() error {
err := controller.vm.toggleCollapse(filterRegex())
if err != nil {
return err
@ -286,7 +245,7 @@ func (controller *FileTreeController) toggleCollapse() error {
}
// toggleCollapseAll will collapse/expand the all directories.
func (controller *FileTreeController) toggleCollapseAll() error {
func (controller *fileTreeController) toggleCollapseAll() error {
err := controller.vm.toggleCollapseAll()
if err != nil {
return err
@ -299,7 +258,7 @@ func (controller *FileTreeController) toggleCollapseAll() error {
}
// toggleAttributes will show/hide file attributes
func (controller *FileTreeController) toggleAttributes() error {
func (controller *fileTreeController) toggleAttributes() error {
err := controller.vm.toggleAttributes()
if err != nil {
return err
@ -309,7 +268,7 @@ func (controller *FileTreeController) toggleAttributes() error {
}
// toggleShowDiffType will show/hide the selected DiffType in the filetree pane.
func (controller *FileTreeController) toggleShowDiffType(diffType filetree.DiffType) error {
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()
@ -317,10 +276,10 @@ func (controller *FileTreeController) toggleShowDiffType(diffType filetree.DiffT
// 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 {
if controllers.Filter == nil || controllers.Filter.view == nil {
return nil
}
filterString := strings.TrimSpace(Controllers.Filter.view.Buffer())
filterString := strings.TrimSpace(controllers.Filter.view.Buffer())
if len(filterString) == 0 {
return nil
}
@ -334,7 +293,7 @@ func filterRegex() *regexp.Regexp {
}
// onLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions
func (controller *FileTreeController) onLayoutChange(resized bool) error {
func (controller *fileTreeController) onLayoutChange(resized bool) error {
_ = controller.Update()
if resized {
return controller.Render()
@ -343,7 +302,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 {
@ -357,9 +316,9 @@ 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 {
if controllers.Layer.CompareMode == CompareAll {
title = "Aggregated Layer Contents"
}
@ -377,7 +336,7 @@ func (controller *FileTreeController) Render() error {
headerStr += fmt.Sprintf(filetree.AttributeFormat+" %s", "P", "ermission", "UID:GID", "Size", "Filetree")
}
_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false)))
_, _ = fmt.Fprintln(controller.header, format.Header(vtclean.Clean(headerStr, false)))
// update the contents
controller.view.Clear()
@ -393,12 +352,10 @@ 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 {
return renderStatusOption(controller.keybindingToggleCollapse[0].String(), "Collapse dir", false) +
renderStatusOption(controller.keybindingToggleCollapseAll[0].String(), "Collapse all dir", false) +
renderStatusOption(controller.keybindingToggleAdded[0].String(), "Added", !controller.vm.HiddenDiffTypes[filetree.Added]) +
renderStatusOption(controller.keybindingToggleRemoved[0].String(), "Removed", !controller.vm.HiddenDiffTypes[filetree.Removed]) +
renderStatusOption(controller.keybindingToggleModified[0].String(), "Modified", !controller.vm.HiddenDiffTypes[filetree.Modified]) +
renderStatusOption(controller.keybindingToggleUnmodified[0].String(), "Unmodified", !controller.vm.HiddenDiffTypes[filetree.Unmodified]) +
renderStatusOption(controller.keybindingToggleAttributes[0].String(), "Attributes", controller.vm.ShowAttributes)
func (controller *fileTreeController) KeyHelp() string {
var help string
for _, binding := range controller.helpKeys {
help += binding.RenderKeyHelp()
}
return help
}

View File

@ -3,6 +3,7 @@ package ui
import (
"bytes"
"fmt"
"github.com/wagoodman/dive/runtime/ui/format"
"regexp"
"strings"
@ -12,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
@ -33,9 +34,9 @@ type FileTreeViewModel struct {
mainBuf bytes.Buffer
}
// NewFileTreeController 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")
@ -65,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
}
@ -79,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() {
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 {
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)
}
@ -125,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
}
@ -140,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
}
@ -156,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
@ -207,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
@ -239,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()
@ -265,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()
@ -290,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,7 +322,7 @@ func (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (nod
}
// toggleCollapse will collapse/expand the selected FileNode.
func (vm *FileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error {
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,7 +331,7 @@ func (vm *FileTreeViewModel) toggleCollapse(filterRegex *regexp.Regexp) error {
}
// toggleCollapseAll will collapse/expand the all directories.
func (vm *FileTreeViewModel) toggleCollapseAll() error {
func (vm *fileTreeViewModel) toggleCollapseAll() error {
vm.CollapseAll = !vm.CollapseAll
visitor := func(curNode *filetree.FileNode) error {
@ -351,18 +352,18 @@ func (vm *FileTreeViewModel) toggleCollapseAll() error {
}
// toggleCollapse will collapse/expand the selected FileNode.
func (vm *FileTreeViewModel) toggleAttributes() error {
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) {
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
@ -410,7 +411,7 @@ 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")
@ -418,7 +419,7 @@ func (vm *FileTreeViewModel) Render() error {
vm.mainBuf.Reset()
for idx, line := range lines {
if idx == vm.bufferIndex {
_, err := fmt.Fprintln(&vm.mainBuf, Formatting.Selected(vtclean.Clean(line, false)))
_, err := fmt.Fprintln(&vm.mainBuf, format.Selected(vtclean.Clean(line, false)))
if err != nil {
logrus.Debug("unable to write to buffer: ", err)
return err

View File

@ -3,6 +3,7 @@ package ui
import (
"bytes"
"github.com/wagoodman/dive/dive/image/docker"
"github.com/wagoodman/dive/runtime/ui/format"
"io/ioutil"
"os"
"path/filepath"
@ -72,7 +73,7 @@ func assertTestData(t *testing.T, actualBytes []byte) {
helperCheckDiff(t, expectedBytes, actualBytes)
}
func initializeTestViewModel(t *testing.T) *FileTreeViewModel {
func initializeTestViewModel(t *testing.T) *fileTreeViewModel {
result := docker.TestAnalysisFromArchive(t, "../../.data/test-docker-image.tar")
cache := filetree.NewFileTreeCache(result.RefTrees)
@ -81,20 +82,20 @@ func initializeTestViewModel(t *testing.T) *FileTreeViewModel {
t.Fatalf("%s: unable to build cache: %+v", t.Name(), err)
}
Formatting.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc()
format.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc()
treeStack, err := filetree.StackTreeRange(result.RefTrees, 0, 0)
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)

View File

@ -4,12 +4,13 @@ import (
"fmt"
"github.com/jroimartin/gocui"
"github.com/sirupsen/logrus"
"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 {
Name string
type filterController struct {
name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
@ -18,12 +19,12 @@ 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
controller.name = name
controller.gui = gui
controller.headerStr = "Path Filter: "
controller.hidden = true
@ -32,7 +33,7 @@ func NewFilterController(name string, gui *gocui.Gui) (controller *FilterControl
}
// 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,7 +53,7 @@ func (controller *FilterController) Setup(v *gocui.View, header *gocui.View) err
}
// 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
}
@ -60,17 +61,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
}
@ -86,21 +87,21 @@ func (controller *FilterController) Edit(v *gocui.View, key gocui.Key, ch rune,
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
}
if Controllers.Tree != nil {
_ = Controllers.Tree.Update()
_ = Controllers.Tree.Render()
if controllers.Tree != nil {
_ = controllers.Tree.Update()
_ = controllers.Tree.Render()
}
}
// 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, Formatting.Header(controller.headerStr))
_, err := fmt.Fprintln(controller.header, format.Header(controller.headerStr))
if err != nil {
logrus.Error("unable to write to buffer: ", err)
}
@ -110,6 +111,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 {
return Formatting.StatusControlNormal("▏Type to filter the file tree ")
func (controller *filterController) KeyHelp() string {
return format.StatusControlNormal("▏Type to filter the file tree ")
}

View File

@ -0,0 +1,35 @@
package format
import (
"github.com/fatih/color"
)
var (
Header func(...interface{}) string
Selected func(...interface{}) string
StatusSelected func(...interface{}) string
StatusNormal func(...interface{}) string
StatusControlSelected func(...interface{}) string
StatusControlNormal func(...interface{}) string
CompareTop func(...interface{}) string
CompareBottom func(...interface{}) string
)
func init() {
Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc()
Header = color.New(color.Bold).SprintFunc()
StatusSelected = color.New(color.BgMagenta, color.FgWhite).SprintFunc()
StatusNormal = color.New(color.ReverseVideo).SprintFunc()
StatusControlSelected = color.New(color.BgMagenta, color.FgWhite, color.Bold).SprintFunc()
StatusControlNormal = color.New(color.ReverseVideo, color.Bold).SprintFunc()
CompareTop = color.New(color.BgMagenta).SprintFunc()
CompareBottom = color.New(color.BgGreen).SprintFunc()
}
func RenderHelpKey(control, title string, selected bool) string {
if selected {
return StatusSelected("▏") + StatusControlSelected(control) + StatusSelected(" "+title+" ")
} else {
return StatusNormal("▏") + StatusControlNormal(control) + StatusNormal(" "+title+" ")
}
}

117
runtime/ui/key/binding.go Normal file
View File

@ -0,0 +1,117 @@
package key
import (
"fmt"
"github.com/jroimartin/gocui"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/keybinding"
)
type BindingInfo struct {
Key gocui.Key
Modifier gocui.Modifier
ConfigKeys []string
OnAction func() error
IsSelected func() bool
Display string
}
type Binding struct {
key []keybinding.Key
displayName string
selectedFn func() bool
actionFn func() error
}
func GenerateBindings(gui *gocui.Gui, influence string, infos []BindingInfo) ([]*Binding, error) {
var result = make([]*Binding, 0)
for _, info := range infos {
var err error
var binding *Binding
if info.ConfigKeys != nil && len(info.ConfigKeys) > 0 {
binding, err = NewBindingFromConfig(gui, influence, info.ConfigKeys, info.Display, info.OnAction)
} else {
binding, err = NewBinding(gui, influence, info.Key, info.Modifier, info.Display, info.OnAction)
}
if err != nil {
return nil, err
}
if info.IsSelected != nil {
binding.RegisterSelectionFn(info.IsSelected)
}
if len(info.Display) > 0 {
result = append(result, binding)
}
}
return result, nil
}
func NewBinding(gui *gocui.Gui, influence string, key gocui.Key, mod gocui.Modifier, displayName string, actionFn func() error) (*Binding, error) {
return newBinding(gui, influence, []keybinding.Key{{Value: key, Modifier: mod}}, displayName, actionFn)
}
func NewBindingFromConfig(gui *gocui.Gui, influence string, configKeys []string, displayName string, actionFn func() error) (*Binding, error) {
var parsedKeys []keybinding.Key
for _, configKey := range configKeys {
bindStr := viper.GetString(configKey)
logrus.Debugf("parsing keybinding '%s' --> '%s'", configKey, bindStr)
keys, err := keybinding.ParseAll(bindStr)
if err == nil && keys != nil && len(keys) > 0 {
parsedKeys = keys
break
}
}
if parsedKeys == nil {
return nil, fmt.Errorf("could not find configured keybindings for: %+v", configKeys)
}
return newBinding(gui, influence, parsedKeys, displayName, actionFn)
}
func newBinding(gui *gocui.Gui, influence string, keys []keybinding.Key, displayName string, actionFn func() error) (*Binding, error) {
binding := &Binding{
key: keys,
displayName: displayName,
actionFn: actionFn,
}
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
}
}
return binding, nil
}
func (binding *Binding) RegisterSelectionFn(selectedFn func() bool) {
binding.selectedFn = selectedFn
}
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)
}
return binding.actionFn()
}
func (binding *Binding) isSelected() bool {
if binding.selectedFn == nil {
return false
}
return binding.selectedFn()
}
func (binding *Binding) RenderKeyHelp() string {
return format.RenderHelpKey(binding.key[0].String(), binding.displayName, binding.isSelected())
}

View File

@ -3,19 +3,20 @@ package ui
import (
"fmt"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"strings"
"github.com/jroimartin/gocui"
"github.com/lunixbochs/vtclean"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/keybinding"
)
// 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 {
Name string
type layerController struct {
name string
gui *gocui.Gui
view *gocui.View
header *gocui.View
@ -25,18 +26,15 @@ type LayerController struct {
CompareStartIndex int
ImageSize uint64
keybindingCompareAll []keybinding.Key
keybindingCompareLayer []keybinding.Key
keybindingPageDown []keybinding.Key
keybindingPageUp []keybinding.Key
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
controller.name = name
controller.gui = gui
controller.Layers = layers
@ -49,31 +47,11 @@ func NewLayerController(name string, gui *gocui.Gui, layers []*image.Layer) (con
return nil, fmt.Errorf("unknown layer.show-aggregated-changes value: %v", mode)
}
controller.keybindingCompareAll, err = keybinding.ParseAll(viper.GetString("keybinding.compare-all"))
if err != nil {
logrus.Error(err)
}
controller.keybindingCompareLayer, err = keybinding.ParseAll(viper.GetString("keybinding.compare-layer"))
if err != nil {
logrus.Error(err)
}
controller.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up"))
if err != nil {
logrus.Error(err)
}
controller.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down"))
if err != nil {
logrus.Error(err)
}
return controller, err
}
// 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
@ -86,59 +64,73 @@ func (controller *LayerController) Setup(v *gocui.View, header *gocui.View) erro
controller.header.Wrap = false
controller.header.Frame = false
// set keybindings
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
return err
}
if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
return err
var infos = []key.BindingInfo{
{
ConfigKeys: []string{"keybinding.compare-layer"},
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) },
IsSelected: func() bool { return controller.CompareMode == CompareAll },
Display: "Show aggregated changes",
},
{
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
OnAction: controller.CursorDown,
},
{
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
OnAction: controller.CursorUp,
},
{
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
OnAction: controller.CursorUp,
},
{
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
OnAction: controller.CursorDown,
},
{
ConfigKeys: []string{"keybinding.page-up"},
OnAction: controller.PageUp,
},
{
ConfigKeys: []string{"keybinding.page-down"},
OnAction: controller.PageDown,
},
}
for _, key := range controller.keybindingPageUp {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageUp() }); err != nil {
return err
}
}
for _, key := range controller.keybindingPageDown {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageDown() }); err != nil {
return err
}
helpKeys, err := key.GenerateBindings(controller.gui, controller.name, infos)
if err != nil {
return err
}
controller.helpKeys = helpKeys
for _, key := range controller.keybindingCompareLayer {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.setCompareMode(CompareLayer) }); err != nil {
return err
}
}
for _, key := range controller.keybindingCompareAll {
if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.setCompareMode(CompareAll) }); err != nil {
return err
}
}
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
@ -156,7 +148,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
@ -174,7 +166,7 @@ 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)
if err == nil {
@ -185,7 +177,7 @@ 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)
if err == nil {
@ -196,36 +188,36 @@ 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())
err := controllers.Tree.setTreeByLayer(controller.getCompareIndexes())
if err != nil {
return err
}
_ = Controllers.Details.Render()
_ = controllers.Details.Render()
return controller.Render()
}
// 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()
if err != nil {
logrus.Errorf("unable to set compare mode: %+v", err)
return err
}
return Controllers.Tree.setTreeByLayer(controller.getCompareIndexes())
return controllers.Tree.setTreeByLayer(controller.getCompareIndexes())
}
// 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
@ -244,22 +236,22 @@ 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 := " "
if layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop {
result = Formatting.CompareBottom(" ")
result = format.CompareBottom(" ")
}
if layerIdx >= topTreeStart && layerIdx <= topTreeStop {
result = Formatting.CompareTop(" ")
result = format.CompareTop(" ")
}
return result
}
// 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
@ -270,7 +262,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"
@ -284,7 +276,7 @@ func (controller *LayerController) Render() error {
width, _ := g.Size()
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command")
_, err := fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false)))
_, err := fmt.Fprintln(controller.header, format.Header(vtclean.Clean(headerStr, false)))
if err != nil {
return err
}
@ -297,7 +289,7 @@ func (controller *LayerController) Render() error {
compareBar := controller.renderCompareBar(idx)
if idx == controller.LayerIndex {
_, err = fmt.Fprintln(controller.view, compareBar+" "+Formatting.Selected(layerStr))
_, err = fmt.Fprintln(controller.view, compareBar+" "+format.Selected(layerStr))
} else {
_, err = fmt.Fprintln(controller.view, compareBar+" "+layerStr)
}
@ -314,7 +306,10 @@ 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 {
return renderStatusOption(controller.keybindingCompareLayer[0].String(), "Show layer changes", controller.CompareMode == CompareLayer) +
renderStatusOption(controller.keybindingCompareAll[0].String(), "Show aggregated changes", controller.CompareMode == CompareAll)
func (controller *layerController) KeyHelp() string {
var help string
for _, binding := range controller.helpKeys {
help += binding.RenderKeyHelp()
}
return help
}

View File

@ -0,0 +1,11 @@
package ui
type layoutManager struct {
fileTreeSplitRatio float64
}
func newLayoutManager(fileTreeSplitRatio float64) *layoutManager {
return &layoutManager{
fileTreeSplitRatio: fileTreeSplitRatio,
}
}

View File

@ -3,33 +3,43 @@ package ui
import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"strings"
"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 {
Name string
type statusController struct {
name string
gui *gocui.Gui
view *gocui.View
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
controller.name = name
controller.gui = gui
controller.helpKeys = make([]*key.Binding, 0)
return controller
}
func (controller *statusController) AddHelpKeys(keys ...*key.Binding) {
for _, k := range keys {
controller.helpKeys = append(controller.helpKeys, k)
}
}
// 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
@ -39,30 +49,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()+Formatting.StatusNormal("▏"+strings.Repeat(" ", 1000)))
_, err := fmt.Fprintln(controller.view, controller.KeyHelp()+controllers.lookup[controller.gui.CurrentView().Name()].KeyHelp()+format.StatusNormal("▏"+strings.Repeat(" ", 1000)))
if err != nil {
logrus.Debug("unable to write to buffer: ", err)
}
@ -73,8 +83,13 @@ 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 {
return renderStatusOption(GlobalKeybindings.quit[0].String(), "Quit", false) +
renderStatusOption(GlobalKeybindings.toggleView[0].String(), "Switch view", false) +
renderStatusOption(GlobalKeybindings.filterView[0].String(), "Filter", Controllers.Filter.IsVisible())
func (controller *statusController) KeyHelp() string {
// return renderStatusOption(globalKeybinding.quit[0].String(), "Quit", false) +
// renderStatusOption(globalKeybinding.toggleView[0].String(), "Switch view", false) +
// renderStatusOption(globalKeybinding.filterView[0].String(), "Filter", controllers.Filter.IsVisible())
var help string
for _, binding := range controller.helpKeys {
help += binding.RenderKeyHelp()
}
return help
}

View File

@ -3,24 +3,82 @@ package ui
import (
"errors"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/runtime/ui/format"
"github.com/wagoodman/dive/runtime/ui/key"
"sync"
"github.com/fatih/color"
"github.com/jroimartin/gocui"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/keybinding"
)
const debug = false
// type global
type Ui struct {
controllers *controllerCollection
}
var (
once sync.Once
uiSingleton *Ui
)
func NewUi(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*Ui, error) {
var err error
once.Do(func() {
var theControls *controllerCollection
var globalHelpKeys []*key.Binding
theControls, err = newControllerCollection(g, analysis, cache)
if err != nil {
return
}
var infos = []key.BindingInfo{
{
ConfigKeys: []string{"keybinding.quit"},
OnAction: quit,
Display: "Removed",
},
{
ConfigKeys: []string{"keybinding.toggle-view"},
OnAction: quit,
// OnAction: toggleView,
Display: "Modified",
},
{
ConfigKeys: []string{"keybinding.filter-files"},
OnAction: quit,
// OnAction: toggleFilterView,
IsSelected: controllers.Filter.IsVisible,
Display: "Unmodified",
},
}
globalHelpKeys, err = key.GenerateBindings(g, "", infos)
if err != nil {
return
}
theControls.Status.AddHelpKeys(globalHelpKeys...)
uiSingleton = &Ui{
controllers: theControls,
}
})
return uiSingleton, err
}
// var profileObj = profile.Start(profile.MemProfile, profile.ProfilePath("."), profile.NoShutdownHook)
// var onExit func()
// debugPrint writes the given string to the debug pane (if the debug pane is enabled)
// func debugPrint(s string) {
// if Controllers.Tree != nil && Controllers.Tree.gui != nil {
// v, _ := Controllers.Tree.gui.View("debug")
// if controllers.Tree != nil && controllers.Tree.gui != nil {
// v, _ := controllers.Tree.gui.View("debug")
// if v != nil {
// if len(v.BufferLines()) > 20 {
// v.Clear()
@ -30,47 +88,8 @@ const debug = false
// }
// }
// Formatting defines standard functions for formatting UI sections.
var Formatting struct {
Header func(...interface{}) string
Selected func(...interface{}) string
StatusSelected func(...interface{}) string
StatusNormal func(...interface{}) string
StatusControlSelected func(...interface{}) string
StatusControlNormal func(...interface{}) string
CompareTop func(...interface{}) string
CompareBottom func(...interface{}) string
}
// Controllers contains all rendered UI panes.
var Controllers struct {
Tree *FileTreeController
Layer *LayerController
Status *StatusController
Filter *FilterController
Details *DetailsController
lookup map[string]View
}
var GlobalKeybindings struct {
quit []keybinding.Key
toggleView []keybinding.Key
filterView []keybinding.Key
}
var lastX, lastY int
// View defines the a renderable terminal screen pane.
type View interface {
Setup(*gocui.View, *gocui.View) error
CursorDown() error
CursorUp() error
Render() error
Update() error
KeyHelp() string
IsVisible() bool
}
func UpdateAndRender() error {
err := Update()
if err != nil {
@ -88,11 +107,12 @@ func UpdateAndRender() error {
}
// toggleView switches between the file view and the layer view and re-renders the screen.
func toggleView(g *gocui.Gui, v *gocui.View) (err error) {
if v == nil || v.Name() == Controllers.Layer.Name {
_, err = g.SetCurrentView(Controllers.Tree.Name)
func toggleView(g *gocui.Gui) (err error) {
v := g.CurrentView()
if v == nil || v.Name() == controllers.Layer.name {
_, err = g.SetCurrentView(controllers.Tree.name)
} else {
_, err = g.SetCurrentView(Controllers.Layer.Name)
_, err = g.SetCurrentView(controllers.Layer.name)
}
if err != nil {
@ -104,15 +124,15 @@ func toggleView(g *gocui.Gui, v *gocui.View) (err error) {
}
// toggleFilterView shows/hides the file tree filter pane.
func toggleFilterView(g *gocui.Gui, v *gocui.View) error {
func toggleFilterView(g *gocui.Gui) error {
// delete all user input from the tree view
Controllers.Filter.view.Clear()
controllers.Filter.view.Clear()
// toggle hiding
Controllers.Filter.hidden = !Controllers.Filter.hidden
controllers.Filter.hidden = !controllers.Filter.hidden
if !Controllers.Filter.hidden {
_, err := g.SetCurrentView(Controllers.Filter.Name)
if !controllers.Filter.hidden {
_, err := g.SetCurrentView(controllers.Filter.name)
if err != nil {
logrus.Error("unable to toggle filter view: ", err)
return err
@ -120,13 +140,13 @@ func toggleFilterView(g *gocui.Gui, v *gocui.View) error {
return UpdateAndRender()
}
err := toggleView(g, v)
err := toggleView(g)
if err != nil {
logrus.Error("unable to toggle filter view (back): ", err)
return err
}
err = Controllers.Filter.view.SetCursor(0, 0)
err = controllers.Filter.view.SetCursor(0, 0)
if err != nil {
return err
}
@ -166,7 +186,7 @@ func CursorStep(g *gocui.Gui, v *gocui.View, step int) error {
}
// quit is the gocui callback invoked when the user hits Ctrl+C
func quit(g *gocui.Gui, v *gocui.View) error {
func quit() error {
// profileObj.Stop()
// onExit()
@ -174,29 +194,6 @@ func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
// keyBindings registers global key press actions, valid when in any pane.
func keyBindings(g *gocui.Gui) error {
for _, key := range GlobalKeybindings.quit {
if err := g.SetKeybinding("", key.Value, key.Modifier, quit); err != nil {
return err
}
}
for _, key := range GlobalKeybindings.toggleView {
if err := g.SetKeybinding("", key.Value, key.Modifier, toggleView); err != nil {
return err
}
}
for _, key := range GlobalKeybindings.filterView {
if err := g.SetKeybinding("", key.Value, key.Modifier, toggleFilterView); err != nil {
return err
}
}
return nil
}
// 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 {
@ -244,7 +241,7 @@ func layout(g *gocui.Gui) error {
statusBarIndex := 1
filterBarIndex := 2
layersHeight := len(Controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
layersHeight := len(controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
maxLayerHeight := int(0.75 * float64(maxY))
if layersHeight > maxLayerHeight {
layersHeight = maxLayerHeight
@ -253,7 +250,7 @@ func layout(g *gocui.Gui) error {
var view, header *gocui.View
var viewErr, headerErr, err error
if Controllers.Filter.hidden {
if controllers.Filter.hidden {
bottomRows--
filterBarHeight = 0
}
@ -268,21 +265,21 @@ func layout(g *gocui.Gui) error {
}
// 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)
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)
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 {
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()
err = controllers.Layer.Render()
if err != nil {
logrus.Error("unable to render layer view", err)
return err
@ -290,10 +287,10 @@ func layout(g *gocui.Gui) error {
}
// 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)
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)
err = controllers.Details.Setup(view, header)
if err != nil {
return err
}
@ -301,28 +298,28 @@ func layout(g *gocui.Gui) error {
// Filetree
offset := 0
if !Controllers.Tree.vm.ShowAttributes {
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)
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)
err = controllers.Tree.Setup(view, header)
if err != nil {
logrus.Error("unable to setup tree controller", err)
return err
}
}
err = Controllers.Tree.onLayoutChange(resized)
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))
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)
err = controllers.Status.Setup(view, nil)
if err != nil {
logrus.Error("unable to setup status controller", err)
return err
@ -330,10 +327,10 @@ func layout(g *gocui.Gui) error {
}
// 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))
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)
err = controllers.Filter.Setup(view, header)
if err != nil {
logrus.Error("unable to setup filter controller", err)
return err
@ -345,7 +342,7 @@ func layout(g *gocui.Gui) error {
// Update refreshes the state objects for future rendering.
func Update() error {
for _, controller := range Controllers.lookup {
for _, controller := range controllers.lookup {
err := controller.Update()
if err != nil {
logrus.Debug("unable to update controller: ")
@ -357,7 +354,7 @@ func Update() error {
// Render flushes the state objects to the screen.
func Render() error {
for _, controller := range Controllers.lookup {
for _, controller := range controllers.lookup {
if controller.IsVisible() {
err := controller.Render()
if err != nil {
@ -371,37 +368,15 @@ func Render() error {
// renderStatusOption formats key help bindings-to-title pairs.
func renderStatusOption(control, title string, selected bool) string {
if selected {
return Formatting.StatusSelected("▏") + Formatting.StatusControlSelected(control) + Formatting.StatusSelected(" "+title+" ")
return format.StatusSelected("▏") + format.StatusControlSelected(control) + format.StatusSelected(" "+title+" ")
} else {
return Formatting.StatusNormal("▏") + Formatting.StatusControlNormal(control) + Formatting.StatusNormal(" "+title+" ")
return format.StatusNormal("▏") + format.StatusControlNormal(control) + format.StatusNormal(" "+title+" ")
}
}
// Run is the UI entrypoint.
func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error {
Formatting.Selected = color.New(color.ReverseVideo, color.Bold).SprintFunc()
Formatting.Header = color.New(color.Bold).SprintFunc()
Formatting.StatusSelected = color.New(color.BgMagenta, color.FgWhite).SprintFunc()
Formatting.StatusNormal = color.New(color.ReverseVideo).SprintFunc()
Formatting.StatusControlSelected = color.New(color.BgMagenta, color.FgWhite, color.Bold).SprintFunc()
Formatting.StatusControlNormal = color.New(color.ReverseVideo, color.Bold).SprintFunc()
Formatting.CompareTop = color.New(color.BgMagenta).SprintFunc()
Formatting.CompareBottom = color.New(color.BgGreen).SprintFunc()
var err error
GlobalKeybindings.quit, err = keybinding.ParseAll(viper.GetString("keybinding.quit"))
if err != nil {
return err
}
GlobalKeybindings.toggleView, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-view"))
if err != nil {
return err
}
GlobalKeybindings.filterView, err = keybinding.ParseAll(viper.GetString("keybinding.filter-files"))
if err != nil {
return err
}
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
@ -409,32 +384,10 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error {
}
defer g.Close()
Controllers.lookup = make(map[string]View)
Controllers.Layer, err = NewLayerController("side", g, analysis.Layers)
_, err = newControllerCollection(g, analysis, cache)
if err != nil {
return err
}
Controllers.lookup[Controllers.Layer.Name] = Controllers.Layer
treeStack, err := filetree.StackTreeRange(analysis.RefTrees, 0, 0)
if err != nil {
return err
}
Controllers.Tree, err = NewFileTreeController("main", g, treeStack, analysis.RefTrees, cache)
if err != nil {
return err
}
Controllers.lookup[Controllers.Tree.Name] = Controllers.Tree
Controllers.Status = NewStatusController("status", g)
Controllers.lookup[Controllers.Status.Name] = Controllers.Status
Controllers.Filter = NewFilterController("command", g)
Controllers.lookup[Controllers.Filter.Name] = Controllers.Filter
Controllers.Details = NewDetailsController("details", g, analysis.Efficiency, analysis.Inefficiencies)
Controllers.lookup[Controllers.Details.Name] = Controllers.Details
g.Cursor = false
//g.Mouse = true
@ -446,13 +399,37 @@ func Run(analysis *image.AnalysisResult, cache filetree.TreeCache) error {
// profileObj.Stop()
// }
// perform the first update and render now that all resources have been loaded
err = UpdateAndRender()
var infos = []key.BindingInfo{
{
ConfigKeys: []string{"keybinding.quit"},
OnAction: quit,
Display: "Removed",
},
{
ConfigKeys: []string{"keybinding.toggle-view"},
OnAction: quit,
// OnAction: toggleView,
Display: "Modified",
},
{
ConfigKeys: []string{"keybinding.filter-files"},
OnAction: quit,
// OnAction: toggleFilterView,
// IsSelected: controllers.Filter.IsVisible,
Display: "Unmodified",
},
}
globalHelpKeys, err := key.GenerateBindings(g, "", infos)
if err != nil {
return err
}
controllers.Status.AddHelpKeys(globalHelpKeys...)
if err := keyBindings(g); err != nil {
// perform the first update and render now that all resources have been loaded
err = UpdateAndRender()
if err != nil {
return err
}