updates to layer_primitive

- add custom keybindings
- add pagedown functionality (should be factored out evenutally)
- add toggleable layer compare modes

Signed-off-by: dwillist <dthornton@vmware.com>
This commit is contained in:
dwillist 2021-01-12 23:53:52 -05:00
commit be945d3501
7 changed files with 523 additions and 40 deletions

398
' Normal file
View file

@ -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
}

View file

@ -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",

View file

@ -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)

View file

@ -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
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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())
}