From 92ce00a1a95fd4905e0cf9da5e28bf25133463ed Mon Sep 17 00:00:00 2001 From: dwillist Date: Mon, 4 Jan 2021 00:01:59 -0500 Subject: viper configured filetree keybindings - implement remaining filetree navegation commands Signed-off-by: dwillist --- runtime/ui/app.go | 5 +- runtime/ui/components/filetree_primative.go | 173 +++++++++++++++++++++++----- runtime/ui/components/key_config.go | 64 ++++++++++ runtime/ui/viewmodels/tree_view_model.go | 39 ++++--- 4 files changed, 240 insertions(+), 41 deletions(-) create mode 100644 runtime/ui/components/key_config.go (limited to 'runtime') diff --git a/runtime/ui/app.go b/runtime/ui/app.go index 1360012..b81d2b8 100644 --- a/runtime/ui/app.go +++ b/runtime/ui/app.go @@ -30,6 +30,8 @@ type diveApp struct { func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetree.Comparer, isCNB bool) (*diveApp, error) { var err error once.Do(func() { + config := components.NewKeyConfig() + // ensure the background color is inherited from the terminal emulator //tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault //tview.Styles.PrimaryTextColor = tcell.ColorDefault @@ -68,7 +70,8 @@ func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetr layersView := components.NewLayerList(treeViewModel).Setup() layersBox := components.NewWrapper("Layers", "subtitle!", layersView).Setup() - fileTreeView := components.NewTreeView(treeViewModel).Setup() + fileTreeView := components.NewTreeView(treeViewModel) + fileTreeView = fileTreeView.Setup(config) fileTreeBox := components.NewWrapper("Current Layer Contents", "subtitle!", fileTreeView).Setup() // Implementation notes: should we factor out this setup?? diff --git a/runtime/ui/components/filetree_primative.go b/runtime/ui/components/filetree_primative.go index af95a11..ce2f015 100644 --- a/runtime/ui/components/filetree_primative.go +++ b/runtime/ui/components/filetree_primative.go @@ -2,6 +2,7 @@ package components import ( "bytes" + "fmt" "io" "strings" @@ -9,6 +10,7 @@ import ( "github.com/rivo/tview" "github.com/sirupsen/logrus" "github.com/wagoodman/dive/dive/filetree" + "go.uber.org/zap" ) // TODO simplify this interface. @@ -19,6 +21,7 @@ type TreeModel interface { RemovePath(path string) error VisibleSize() int SetLayerIndex(int) bool + ToggleHiddenFileType(filetype filetree.DiffType) bool } type TreeView struct { @@ -31,39 +34,63 @@ type TreeView struct { 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, + Box: tview.NewBox(), + tree: tree, + globalCollapseAll: true, + showAttributes: true, } } -func (t *TreeView) Setup() *TreeView { - t.SetBorder(true). - SetTitle("Files"). - SetTitleAlign(tview.AlignLeft) - t.tree.SetLayerIndex(0) - - return t +type KeyBindingConfig interface { + GetKeyBinding(key string) (KeyBinding, error) } -func (ll *TreeView) getBox() *tview.Box { - return ll.Box -} +// 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) -func (ll *TreeView) getDraw() drawFn { - return ll.Draw -} + 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() }, + } -func (ll *TreeView) getInputWrapper() inputFn { - return ll.InputHandler -} + bindingArray := []KeyBinding{} + actionArray := []keyAction{} -// TODO: make these keys configurable -func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + 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() @@ -74,11 +101,43 @@ func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p tv case tcell.KeyLeft: t.keyLeft() } - switch event.Rune() { - case ' ': - t.spaceDown() + + 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)) { @@ -91,7 +150,7 @@ func (t *TreeView) HasFocus() bool { // Private helper methods -func (t *TreeView) spaceDown() bool { +func (t *TreeView) collapseDir() bool { node := t.getAbsPositionNode() if node != nil && node.Data.FileInfo.IsDir { logrus.Debugf("collapsing node %s", node.Path()) @@ -108,6 +167,32 @@ func (t *TreeView) spaceDown() bool { 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 @@ -168,6 +253,7 @@ func (t *TreeView) keyUp() bool { return true } + // TODO add regex filtering func (t *TreeView) keyRight() bool { node := t.getAbsPositionNode() @@ -236,6 +322,31 @@ func (t *TreeView) keyLeft() bool { return true } +// deisred behavior, +// move the selected cursor 1 screen height up or down, then reset the screen appropriately +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) + if t.treeIndex >= t.bufferIndexUpperBound() { + t.bufferIndexLowerBound = t.treeIndex + } + 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 @@ -245,7 +356,7 @@ 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 + 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") @@ -277,3 +388,11 @@ func (t *TreeView) Draw(screen tcell.Screen) { } } + + +func intMin(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/runtime/ui/components/key_config.go b/runtime/ui/components/key_config.go new file mode 100644 index 0000000..187c719 --- /dev/null +++ b/runtime/ui/components/key_config.go @@ -0,0 +1,64 @@ +package components + +import ( + "fmt" + + "github.com/gdamore/tcell/v2" + "github.com/spf13/viper" +) + +// TODO move this to a more appropriate place +type KeyConfig struct{} + + +type KeyBinding struct{ + *tcell.EventKey + Display string +} + +type keyAction func() bool + + +func NewKeyBinding(name string, key *tcell.EventKey) KeyBinding { + return KeyBinding{ + EventKey: key, + Display: name, + } +} + +func (k *KeyBinding) Match(event *tcell.EventKey) bool { + if k.Key() == tcell.KeyRune { + return k.Rune() == event.Rune() && (k.Modifiers() == event.Modifiers()) + } + + return k.Key() == event.Key() +} + +type MissingConfigError struct { + Field string +} + +func NewMissingConfigErr(field string) MissingConfigError { + return MissingConfigError{ + Field: field, + } +} + +func (e MissingConfigError) Error() string { + return fmt.Sprintf("error configuration %s: not found", e.Field) +} + +func NewKeyConfig() *KeyConfig { + return &KeyConfig{} +} + +func (k *KeyConfig) GetKeyBinding(key string) (result KeyBinding, err error) { + err = viper.UnmarshalKey(key, &result) + if err != nil { + return KeyBinding{}, err + } + return result, err + //if config == "" { + // return "", NewMissingConfigErr(lookupName) + //} +} diff --git a/runtime/ui/viewmodels/tree_view_model.go b/runtime/ui/viewmodels/tree_view_model.go index 4e50dcc..462540e 100644 --- a/runtime/ui/viewmodels/tree_view_model.go +++ b/runtime/ui/viewmodels/tree_view_model.go @@ -23,24 +23,26 @@ type LayersModel interface { } type TreeViewModel struct { - currentTree *filetree.FileTree - cache filetree.Comparer + currentTree *filetree.FileTree + cache filetree.Comparer + hiddenDiffTypes []bool // Make this an interface that is composed with the FilterView FilterModel LayersModel } -func NewTreeViewModel(cache filetree.Comparer, lModel LayersModel, fmodel FilterModel) (*TreeViewModel, error) { +func NewTreeViewModel(cache filetree.Comparer, lModel LayersModel, fModel FilterModel) (*TreeViewModel, error) { curTreeIndex := filetree.NewTreeIndexKey(0, 0, 0, 0) tree, err := cache.GetTree(curTreeIndex) if err != nil { return nil, err } return &TreeViewModel{ - currentTree: tree, - cache: cache, - FilterModel: fmodel, - LayersModel: lModel, + currentTree: tree, + cache: cache, + hiddenDiffTypes: make([]bool, 4), + FilterModel: fModel, + LayersModel: lModel, }, nil } @@ -70,17 +72,28 @@ func (tvm *TreeViewModel) SetFilter(filterRegex *regexp.Regexp) { } } +// TODO: this seems like a very expensive operration, look for ways to optimize. +// TODO make type int a strongly typed argument +// TODO: handle errors correctly +func (tvm *TreeViewModel) ToggleHiddenFileType(filetype filetree.DiffType) bool { + tvm.hiddenDiffTypes[filetype] = !tvm.hiddenDiffTypes[filetype] + if err := tvm.FilterUpdate(); err != nil { + zap.S().Error("error updating file type filter ", err.Error()) + //panic(err) + return false + } + return true + +} + +// TODO: maek this method private, cant think of a reason for this to be public func (tvm *TreeViewModel) FilterUpdate() error { // keep the t selection in parity with the current DiffType selection filter := tvm.GetFilter() err := tvm.currentTree.VisitDepthChildFirst(func(node *filetree.FileNode) error { + node.Data.ViewInfo.Hidden = tvm.hiddenDiffTypes[node.Data.DiffType] visibleChild := false - if filter == nil { - node.Data.ViewInfo.Hidden = false - return nil - } - for _, child := range node.Children { if !child.Data.ViewInfo.Hidden { visibleChild = true @@ -89,7 +102,7 @@ func (tvm *TreeViewModel) FilterUpdate() error { } } - if !visibleChild { // hide nodes that do not match the current file filter regex (also don't unhide nodes that are already hidden) + if filter != nil && !node.Data.ViewInfo.Hidden && !visibleChild { // hide nodes that do not match the current file filter regex (also don't unhide nodes that are already hidden) match := filter.FindString(node.Path()) node.Data.ViewInfo.Hidden = len(match) == 0 } -- cgit v1.2.3