From be945d35019f9604cb4232e558db589626cdda83 Mon Sep 17 00:00:00 2001 From: dwillist Date: Tue, 12 Jan 2021 23:53:52 -0500 Subject: [PATCH] updates to layer_primitive - add custom keybindings - add pagedown functionality (should be factored out evenutally) - add toggleable layer compare modes Signed-off-by: dwillist --- ' | 398 ++++++++++++++++++++ cmd/root.go | 10 +- runtime/ui/app.go | 3 +- runtime/ui/components/filetree_primative.go | 10 +- runtime/ui/components/layers_primative.go | 127 +++++-- runtime/ui/viewmodels/layers_view_model.go | 6 +- runtime/ui/viewmodels/tree_view_model.go | 9 +- 7 files changed, 523 insertions(+), 40 deletions(-) create mode 100644 ' diff --git a/' b/' new file mode 100644 index 0000000..e593e5e --- /dev/null +++ b/' @@ -0,0 +1,398 @@ +package components + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + "github.com/sirupsen/logrus" + "github.com/wagoodman/dive/dive/filetree" + "go.uber.org/zap" +) + +// TODO simplify this interface. +type TreeModel interface { + StringBetween(int, int, bool) string + VisitDepthParentFirst(filetree.Visitor, filetree.VisitEvaluator) error + VisitDepthChildFirst(filetree.Visitor, filetree.VisitEvaluator) error + RemovePath(path string) error + VisibleSize() int + SetLayerIndex(int) bool + ToggleHiddenFileType(filetype filetree.DiffType) bool +} + +type TreeView struct { + *tview.Box + tree TreeModel + + // Note that the following two fields are distinct + // treeIndex is the index about where we are in the current fileTree + // this should be updated every keypress + treeIndex int + + bufferIndexLowerBound int + + globalCollapseAll bool + + inputHandler func(event *tcell.EventKey, setFocus func(p tview.Primitive)) + + keyBindings map[string]KeyBinding + + showAttributes bool +} + +func NewTreeView(tree TreeModel) *TreeView { + return &TreeView{ + Box: tview.NewBox(), + tree: tree, + globalCollapseAll: true, + showAttributes: true, + inputHandler: nil, + } +} + +type KeyBindingConfig interface { + GetKeyBinding(key string) (KeyBinding, error) +} + +// Implementation notes: +// need to set up our input handler here, +// Should probably factor out keybinding initialization into a new function +// +func (t *TreeView) Setup(config KeyBindingConfig) *TreeView { + t.tree.SetLayerIndex(0) + + bindingSettings := map[string]keyAction{ + "keybinding.toggle-collapse-dir": t.collapseDir, + "keybinding.toggle-collapse-all-dir": t.collapseOrExpandAll, + "keybinding.toggle-filetree-attributes": func() bool { t.showAttributes = !t.showAttributes; return true }, + "keybinding.toggle-added-files": func() bool { t.tree.ToggleHiddenFileType(filetree.Added); return false }, + "keybinding.toggle-removed-files": func() bool { return t.tree.ToggleHiddenFileType(filetree.Removed)}, + "keybinding.toggle-modified-files": func() bool { return t.tree.ToggleHiddenFileType(filetree.Modified)}, + "keybinding.toggle-unmodified-files": func() bool { return t.tree.ToggleHiddenFileType(filetree.Unmodified)}, + "keybinding.page-up": func() bool { return t.pageUp() }, + "keybinding.page-down": func() bool { return t.pageDown() }, + } + + bindingArray := []KeyBinding{} + actionArray := []keyAction{} + + for keybinding, action := range bindingSettings { + binding, err := config.GetKeyBinding(keybinding) + if err != nil { + panic(fmt.Errorf("setup error during %s: %w", keybinding, err)) + // TODO handle this error + //return nil + } + bindingArray = append(bindingArray, binding) + actionArray = append(actionArray, action) + } + + t.inputHandler = func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + switch event.Key() { + case tcell.KeyUp: + t.keyUp() + case tcell.KeyDown: + t.keyDown() + case tcell.KeyRight: + t.keyRight() + case tcell.KeyLeft: + t.keyLeft() + } + + for idx, binding := range bindingArray { + if binding.Match(event) { + actionArray[idx]() + } + } + } + + return t +} + +// TODO: do we need all of these?? or is there an alternative API we could use for the wrappers???? +func (t *TreeView) getBox() *tview.Box { + return t.Box +} + +func (t *TreeView) getDraw() drawFn { + return t.Draw +} + +func (t *TreeView) getInputWrapper() inputFn { + return t.InputHandler +} + +// Implementation note: +// what do we want here??? a binding object?? yes +func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + return t.inputHandler +} + +func (t *TreeView) SetInputHandler(handler func(event *tcell.EventKey, setFocus func(p tview.Primitive))) *TreeView { + t.inputHandler = handler + return t +} + +func (t *TreeView) WrapInputHandler() func(*tcell.EventKey, func(tview.Primitive)) { + return t.Box.WrapInputHandler(t.inputHandler) +} + +func (t *TreeView) Focus(delegate func(p tview.Primitive)) { + t.Box.Focus(delegate) +} + +func (t *TreeView) HasFocus() bool { + return t.Box.HasFocus() +} + +// Private helper methods + +func (t *TreeView) collapseDir() bool { + node := t.getAbsPositionNode() + if node != nil && node.Data.FileInfo.IsDir { + logrus.Debugf("collapsing node %s", node.Path()) + node.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed + return true + } + if node != nil { + logrus.Debugf("unable to collapse node %s", node.Path()) + logrus.Debugf(" IsDir: %t", node.Data.FileInfo.IsDir) + + } else { + logrus.Debugf("unable to collapse nil node") + } + return false +} + +func (t *TreeView) collapseOrExpandAll() bool { + zap.S().Info("collapsing all directories") + visitor := func(n *filetree.FileNode) error { + if n.Data.FileInfo.IsDir { + n.Data.ViewInfo.Collapsed = t.globalCollapseAll + } + return nil + } + + evaluator := func(n *filetree.FileNode) bool { + return true + } + if err := t.tree.VisitDepthParentFirst(visitor, evaluator); err != nil { + zap.S().Panic("error collapsing all: ", err.Error()) + panic(fmt.Errorf("error callapsing all dir: %w", err)) + // TODO log error here + //return false + } + + zap.S().Info("finished collapsing all directories") + + t.globalCollapseAll = !t.globalCollapseAll + return true + +} + +// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode. +func (t *TreeView) getAbsPositionNode() (node *filetree.FileNode) { + var visitor func(*filetree.FileNode) error + var evaluator func(*filetree.FileNode) bool + var dfsCounter int + + visitor = func(curNode *filetree.FileNode) error { + if dfsCounter == t.treeIndex { + node = curNode + } + dfsCounter++ + return nil + } + + evaluator = func(curNode *filetree.FileNode) bool { + return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden + } + + err := t.tree.VisitDepthParentFirst(visitor, evaluator) + if err != nil { + logrus.Errorf("unable to get node position: %+v", err) + } + + return node +} + +func (t *TreeView) keyDown() bool { + _, _, _, height := t.Box.GetInnerRect() + + // treeIndex is the index about where we are in the current file + if t.treeIndex >= t.tree.VisibleSize() { + return false + } + t.treeIndex++ + if (t.treeIndex - t.bufferIndexLowerBound) >= height { + t.bufferIndexLowerBound++ + } + + logrus.Debugf(" treeIndex: %d", t.treeIndex) + logrus.Debugf(" bufferIndexLowerBound: %d", t.bufferIndexLowerBound) + logrus.Debugf(" height: %d", height) + + return true +} + +func (t *TreeView) keyUp() bool { + if t.treeIndex <= 0 { + return false + } + t.treeIndex-- + if t.treeIndex < t.bufferIndexLowerBound { + t.bufferIndexLowerBound-- + } + + logrus.Debugf("keyUp end at: %s", t.getAbsPositionNode().Path()) + logrus.Debugf(" treeIndex: %d", t.treeIndex) + logrus.Debugf(" bufferIndexLowerBound: %d", t.bufferIndexLowerBound) + return true +} + + +// TODO add regex filtering +func (t *TreeView) keyRight() bool { + node := t.getAbsPositionNode() + + _, _, _, height := t.Box.GetInnerRect() + if node == nil { + return false + } + + if !node.Data.FileInfo.IsDir { + return false + } + + if len(node.Children) == 0 { + return false + } + + if node.Data.ViewInfo.Collapsed { + node.Data.ViewInfo.Collapsed = false + } + + t.treeIndex++ + if (t.treeIndex - t.bufferIndexLowerBound) >= height { + t.bufferIndexLowerBound++ + } + + return true +} + +func (t *TreeView) keyLeft() bool { + var visitor func(*filetree.FileNode) error + var evaluator func(*filetree.FileNode) bool + var dfsCounter, newIndex int + //oldIndex := t.treeIndex + currentNode := t.getAbsPositionNode() + + if currentNode == nil { + return true + } + parentPath := currentNode.Parent.Path() + + visitor = func(curNode *filetree.FileNode) error { + if strings.Compare(parentPath, curNode.Path()) == 0 { + newIndex = dfsCounter + } + dfsCounter++ + return nil + } + + evaluator = func(curNode *filetree.FileNode) bool { + return !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden + } + + err := t.tree.VisitDepthParentFirst(visitor, evaluator) + if err != nil { + // TODO: remove this panic + panic(err) + } + + t.treeIndex = newIndex + //moveIndex := oldIndex - newIndex + if newIndex < t.bufferIndexLowerBound { + t.bufferIndexLowerBound = t.treeIndex + } + + return true +} + +// TODO make all movement rely on a single function (shouldn't be too dificult really) +func (t *TreeView) pageDown() bool { + + _,_,_,height := t.GetInnerRect() + visibleSize := t.tree.VisibleSize() + t.treeIndex = intMin(t.treeIndex + height, visibleSize) + if t.treeIndex >= t.bufferIndexUpperBound() { + t.bufferIndexLowerBound = intMin(t.treeIndex, visibleSize - height + 1) + } + return true +} + + +func (t *TreeView) pageUp() bool { + _,_,_,height := t.GetInnerRect() + + t.treeIndex = intMax(0, t.treeIndex - height) + if t.treeIndex < t.bufferIndexLowerBound { + t.bufferIndexLowerBound = t.treeIndex + } + + return true +} + +func (t *TreeView) bufferIndexUpperBound() int { + _, _, _, height := t.Box.GetInnerRect() + return t.bufferIndexLowerBound + height +} + +func (t *TreeView) Draw(screen tcell.Screen) { + t.Box.Draw(screen) + selectedIndex := t.treeIndex - t.bufferIndexLowerBound + x, y, width, height := t.Box.GetInnerRect() + showAttributes := width > 80 && t.showAttributes + // TODO add switch for showing attributes. + treeString := t.tree.StringBetween(t.bufferIndexLowerBound, t.bufferIndexUpperBound(), showAttributes) + lines := strings.Split(treeString, "\n") + + // update the contents + for yIndex, line := range lines { + if yIndex >= height { + break + } + // Strip out ansi colors, Tview cannot use these + stripLine := bytes.NewBuffer(nil) + w := tview.ANSIWriter(stripLine) + if _, err := io.Copy(w, strings.NewReader(line)); err != nil { + //TODO: handle panic gracefully + panic(err) + } + + tview.Print(screen, stripLine.String(), x, y+yIndex, width, tview.AlignLeft, tcell.ColorDefault) + for xIndex := 0; xIndex < width; xIndex++ { + m, c, style, _ := screen.GetContent(x+xIndex, y+yIndex) + style = style.Background(tcell.ColorWhite).Foreground(tcell.ColorBlack).Bold(true) + if yIndex == selectedIndex { + screen.SetContent(x+xIndex, y+yIndex, m, c, style) + screen.SetContent(x+xIndex, y+yIndex, m, c, style) + } else if yIndex > selectedIndex { + break + } + } + } + +} + + +func intMin(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/cmd/root.go b/cmd/root.go index ac12031..b682947 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -88,15 +88,17 @@ func initConfig() { "Compare All", tcell.NewEventKey(tcell.KeyCtrlA, rune(0), tcell.ModNone), )) - viper.SetDefault("keybinding.compare-layer", "ctrl+l") + viper.SetDefault("keybinding.compare-layer", components.NewKeyBinding( + "Compare Layer", + tcell.NewEventKey(tcell.KeyCtrlL, rune(0), tcell.ModNone), + )) + + // keybindings: filetree view viper.SetDefault("keybinding.toggle-collapse-dir", components.NewKeyBinding( "Collapse", tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone), )) - viper.SetDefault("keybinding.compare-layer", "ctrl+l") - // keybindings: filetree view - //viper.SetDefault("keybinding.toggle-collapse-all-dir", "ctrl+space") viper.SetDefault("keybinding.toggle-collapse-all-dir", components.NewKeyBinding( "Collapse All", diff --git a/runtime/ui/app.go b/runtime/ui/app.go index b81d2b8..f9bcf5c 100644 --- a/runtime/ui/app.go +++ b/runtime/ui/app.go @@ -1,6 +1,7 @@ package ui import ( + "os" "sync" "github.com/gdamore/tcell/v2" @@ -67,7 +68,7 @@ func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetr filterView := components.NewFilterView(treeViewModel).Setup() - layersView := components.NewLayerList(treeViewModel).Setup() + layersView := components.NewLayerList(treeViewModel).Setup(config) layersBox := components.NewWrapper("Layers", "subtitle!", layersView).Setup() fileTreeView := components.NewTreeView(treeViewModel) diff --git a/runtime/ui/components/filetree_primative.go b/runtime/ui/components/filetree_primative.go index ce2f015..e593e5e 100644 --- a/runtime/ui/components/filetree_primative.go +++ b/runtime/ui/components/filetree_primative.go @@ -50,6 +50,7 @@ func NewTreeView(tree TreeModel) *TreeView { tree: tree, globalCollapseAll: true, showAttributes: true, + inputHandler: nil, } } @@ -322,15 +323,14 @@ func (t *TreeView) keyLeft() bool { return true } -// deisred behavior, -// move the selected cursor 1 screen height up or down, then reset the screen appropriately +// TODO make all movement rely on a single function (shouldn't be too dificult really) func (t *TreeView) pageDown() bool { - // two parts of this are moving both the currently selected item & the window as a whole _,_,_,height := t.GetInnerRect() - t.treeIndex = intMin(t.treeIndex + height, t.tree.VisibleSize() -1) + visibleSize := t.tree.VisibleSize() + t.treeIndex = intMin(t.treeIndex + height, visibleSize) if t.treeIndex >= t.bufferIndexUpperBound() { - t.bufferIndexLowerBound = t.treeIndex + t.bufferIndexLowerBound = intMin(t.treeIndex, visibleSize - height + 1) } return true } diff --git a/runtime/ui/components/layers_primative.go b/runtime/ui/components/layers_primative.go index 5968a6a..c6443be 100644 --- a/runtime/ui/components/layers_primative.go +++ b/runtime/ui/components/layers_primative.go @@ -6,11 +6,15 @@ import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "github.com/sirupsen/logrus" + "github.com/wagoodman/dive/runtime/ui/viewmodels" + "go.uber.org/zap" ) type LayersViewModel interface { SetLayerIndex(int) bool GetPrintableLayers() []fmt.Stringer + SwitchMode() + GetMode() viewmodels.LayerCompareMode } type LayerList struct { @@ -18,6 +22,7 @@ type LayerList struct { bufferIndexLowerBound int cmpIndex int changed LayerListHandler + inputHandler func(event *tcell.EventKey, setFocus func(p tview.Primitive)) LayersViewModel } @@ -28,10 +33,66 @@ func NewLayerList(model LayersViewModel) *LayerList { Box: tview.NewBox(), cmpIndex: 0, LayersViewModel: model, + inputHandler: nil, } } -func (ll *LayerList) Setup() *LayerList { +func (ll *LayerList) Setup(config KeyBindingConfig) *LayerList { + bindingSettings := map[string]keyAction{ + "keybinding.page-up": func() bool { return ll.pageUp() }, + "keybinding.page-down": func() bool { return ll.pageDown() }, + "keybinding.compare-all": func() bool { + if ll.GetMode() == viewmodels.CompareSingleLayer { + ll.SwitchMode() + return true + } + return false + }, + "keybinding.compare-layer": func() bool { + if ll.GetMode() == viewmodels.CompareAllLayers { + ll.SwitchMode() + return true + } + return false + }, + } + + bindingArray := []KeyBinding{} + actionArray := []keyAction{} + + for keybinding, action := range bindingSettings { + binding, err := config.GetKeyBinding(keybinding) + if err != nil { + panic(fmt.Errorf("setup error during %s: %w", keybinding, err)) + // TODO handle this error + //return nil + } + bindingArray = append(bindingArray, binding) + actionArray = append(actionArray, action) + } + + ll.inputHandler = func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + switch event.Key() { + case tcell.KeyUp, tcell.KeyLeft: + if ll.SetLayerIndex(ll.cmpIndex - 1) { + ll.keyUp() + //ll.cmpIndex-- + //logrus.Debugf("KeyUp pressed, index: %d", ll.cmpIndex) + } + case tcell.KeyDown, tcell.KeyRight: + if ll.SetLayerIndex(ll.cmpIndex + 1) { + ll.keyDown() + //ll.cmpIndex++ + //logrus.Debugf("KeyUp pressed, index: %d", ll.cmpIndex) + + } + } + for idx, binding := range bindingArray { + if binding.Match(event) { + actionArray[idx]() + } + } + } return ll } @@ -62,9 +123,11 @@ func (ll *LayerList) Draw(screen tcell.Screen) { layer := printableLayers[layerIndex] var cmpColor tcell.Color switch { - case yIndex == ll.cmpIndex: + case layerIndex == ll.cmpIndex: cmpColor = tcell.ColorRed - case yIndex < ll.cmpIndex: + case layerIndex > 0 && layerIndex < ll.cmpIndex && ll.GetMode() == viewmodels.CompareAllLayers: + cmpColor = tcell.ColorRed + case layerIndex < ll.cmpIndex: cmpColor = tcell.ColorBlue default: cmpColor = tcell.ColorDefault @@ -76,10 +139,10 @@ func (ll *LayerList) Draw(screen tcell.Screen) { fg, bg, _ := style.Decompose() style = style.Background(fg).Foreground(bg) switch { - case yIndex == ll.cmpIndex: + case layerIndex == ll.cmpIndex: screen.SetContent(x+xIndex, y+yIndex, m, c, style) screen.SetContent(x+xIndex, y+yIndex, m, c, style) - case yIndex < ll.cmpIndex && xIndex < len(cmpString): + case layerIndex < ll.cmpIndex && xIndex < len(cmpString): screen.SetContent(x+xIndex, y+yIndex, m, c, style) screen.SetContent(x+xIndex, y+yIndex, m, c, style) default: @@ -91,23 +154,7 @@ func (ll *LayerList) Draw(screen tcell.Screen) { } func (ll *LayerList) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - return ll.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - switch event.Key() { - case tcell.KeyUp: - if ll.SetLayerIndex(ll.cmpIndex - 1) { - ll.keyUp() - //ll.cmpIndex-- - //logrus.Debugf("KeyUp pressed, index: %d", ll.cmpIndex) - } - case tcell.KeyDown: - if ll.SetLayerIndex(ll.cmpIndex + 1) { - ll.keyDown() - //ll.cmpIndex++ - //logrus.Debugf("KeyUp pressed, index: %d", ll.cmpIndex) - - } - } - }) + return ll.WrapInputHandler(ll.inputHandler) } func (ll *LayerList) Focus(delegate func(p tview.Primitive)) { @@ -138,19 +185,17 @@ func (ll *LayerList) keyUp() bool { return true } +// TODO (simplify all page increments to rely an a single function) func (ll *LayerList) keyDown() bool { _, _, _, height := ll.Box.GetInnerRect() - adjustedHeight := height - 1 - // treeIndex is the index about where we are in the current file visibleSize := len(ll.GetPrintableLayers()) - if ll.cmpIndex+1+ll.bufferIndexLowerBound >= visibleSize { + if ll.cmpIndex+1 >= visibleSize { return false } - if ll.cmpIndex+1 >= adjustedHeight { + ll.cmpIndex++ + if ll.cmpIndex-ll.bufferIndexLowerBound >= height { ll.bufferIndexLowerBound++ - } else { - ll.cmpIndex++ } logrus.Debugln("keyDown in layers") logrus.Debugf(" cmpIndex: %d", ll.cmpIndex) @@ -158,3 +203,29 @@ func (ll *LayerList) keyDown() bool { return true } + +func (ll *LayerList) pageUp() bool { + zap.S().Info("layer page up call") + _, _, _, height := ll.Box.GetInnerRect() + + ll.cmpIndex = intMax(0, ll.cmpIndex-height) + if ll.cmpIndex < ll.bufferIndexLowerBound { + ll.bufferIndexLowerBound = ll.cmpIndex + } + + return ll.SetLayerIndex(ll.cmpIndex) +} + +func (ll *LayerList) pageDown() bool { + zap.S().Info("layer page down call") + // two parts of this are moving both the currently selected item & the window as a whole + + _, _, _, height := ll.Box.GetInnerRect() + upperBoundIndex := len(ll.GetPrintableLayers()) - 1 + ll.cmpIndex = intMin(ll.cmpIndex+height, upperBoundIndex) + if ll.cmpIndex >= ll.bufferIndexLowerBound+height { + ll.bufferIndexLowerBound = intMin(ll.cmpIndex, upperBoundIndex-height+1) + } + + return ll.SetLayerIndex(ll.cmpIndex) +} diff --git a/runtime/ui/viewmodels/layers_view_model.go b/runtime/ui/viewmodels/layers_view_model.go index 4418152..8ebeafe 100644 --- a/runtime/ui/viewmodels/layers_view_model.go +++ b/runtime/ui/viewmodels/layers_view_model.go @@ -46,10 +46,14 @@ func (lm *LayersViewModel) GetCompareIndicies() filetree.TreeIndexKey { bottomStart := 0 bottomStop := 0 + topStart := lm.index if lm.mode == CompareSingleLayer { bottomStop = intMax(lm.index-1, 0) + } else { + topStart = 1 } - return filetree.NewTreeIndexKey(bottomStart, bottomStop, lm.index, lm.index) + + return filetree.NewTreeIndexKey(bottomStart, bottomStop, topStart, lm.index) } func (lm *LayersViewModel) SetLayerIndex(index int) bool { diff --git a/runtime/ui/viewmodels/tree_view_model.go b/runtime/ui/viewmodels/tree_view_model.go index 462540e..9e004ec 100644 --- a/runtime/ui/viewmodels/tree_view_model.go +++ b/runtime/ui/viewmodels/tree_view_model.go @@ -20,6 +20,8 @@ type LayersModel interface { GetCompareIndicies() filetree.TreeIndexKey GetCurrentLayer() *image.Layer GetPrintableLayers() []fmt.Stringer + GetMode() LayerCompareMode + SwitchMode() } type TreeViewModel struct { @@ -118,7 +120,6 @@ func (tvm *TreeViewModel) FilterUpdate() error { } // Override functions - func (tvm *TreeViewModel) SetLayerIndex(index int) bool { if tvm.LayersModel.SetLayerIndex(index) { err := tvm.setCurrentTree(tvm.GetCompareIndicies()) @@ -167,3 +168,9 @@ func (tvm *TreeViewModel) setCurrentTree(key filetree.TreeIndexKey) error { } return nil } + +func (tvm *TreeViewModel) SwitchMode() { + tvm.LayersModel.SwitchMode() + // TODO: Handle this error + tvm.setCurrentTree(tvm.GetCompareIndicies()) +}