summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordwillist <dthornton@vmware.com>2021-01-04 00:01:59 -0500
committerdwillist <dthornton@vmware.com>2021-01-04 00:03:02 -0500
commit92ce00a1a95fd4905e0cf9da5e28bf25133463ed (patch)
tree88bb0d4595bbffd05915f7472aacdab631b65b14
parent00a1b771a844063f8dbeceb13b70f3cb2f15b5e0 (diff)
viper configured filetree keybindings
- implement remaining filetree navegation commands Signed-off-by: dwillist <dthornton@vmware.com>
-rw-r--r--cmd/root.go70
-rw-r--r--runtime/ui/app.go5
-rw-r--r--runtime/ui/components/filetree_primative.go173
-rw-r--r--runtime/ui/components/key_config.go64
-rw-r--r--runtime/ui/viewmodels/tree_view_model.go39
5 files changed, 298 insertions, 53 deletions
diff --git a/cmd/root.go b/cmd/root.go
index c5b73e3..ac12031 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -2,13 +2,16 @@ package cmd
import (
"fmt"
- "github.com/wagoodman/dive/dive"
- "github.com/wagoodman/dive/dive/filetree"
"io/ioutil"
"os"
"path"
"strings"
+ "github.com/gdamore/tcell/v2"
+ "github.com/wagoodman/dive/dive"
+ "github.com/wagoodman/dive/dive/filetree"
+ "github.com/wagoodman/dive/runtime/ui/components"
+
"github.com/mitchellh/go-homedir"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -81,18 +84,61 @@ func initConfig() {
viper.SetDefault("keybinding.toggle-view", "tab")
viper.SetDefault("keybinding.filter-files", "ctrl+f, ctrl+slash")
// keybindings: layer view
- viper.SetDefault("keybinding.compare-all", "ctrl+a")
+ viper.SetDefault("keybinding.compare-all", components.NewKeyBinding(
+ "Compare All",
+ tcell.NewEventKey(tcell.KeyCtrlA, rune(0), tcell.ModNone),
+ ))
+ viper.SetDefault("keybinding.compare-layer", "ctrl+l")
+
+ 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-dir", "space")
- viper.SetDefault("keybinding.toggle-collapse-all-dir", "ctrl+space")
- viper.SetDefault("keybinding.toggle-filetree-attributes", "ctrl+b")
- viper.SetDefault("keybinding.toggle-added-files", "ctrl+a")
- viper.SetDefault("keybinding.toggle-removed-files", "ctrl+r")
- viper.SetDefault("keybinding.toggle-modified-files", "ctrl+m")
- viper.SetDefault("keybinding.toggle-unmodified-files", "ctrl+u")
- viper.SetDefault("keybinding.page-up", "pgup")
- viper.SetDefault("keybinding.page-down", "pgdn")
+ //viper.SetDefault("keybinding.toggle-collapse-all-dir", "ctrl+space")
+
+ viper.SetDefault("keybinding.toggle-collapse-all-dir", components.NewKeyBinding(
+ "Collapse All",
+ tcell.NewEventKey(tcell.KeyCtrlSpace, rune(0), tcell.ModCtrl),
+ ))
+ //viper.SetDefault("keybinding.toggle-filetree-attributes", "ctrl+b")
+ viper.SetDefault("keybinding.toggle-filetree-attributes", components.NewKeyBinding(
+ "FileTree Attributes",
+ tcell.NewEventKey(tcell.KeyCtrlB, rune(0), tcell.ModCtrl),
+ ))
+
+
+ viper.SetDefault("keybinding.toggle-added-files", components.NewKeyBinding(
+ "Added Files",
+ tcell.NewEventKey(tcell.KeyCtrlA, rune(0), tcell.ModCtrl),
+ ))
+
+
+ viper.SetDefault("keybinding.toggle-removed-files", components.NewKeyBinding(
+ "Removed Files",
+ tcell.NewEventKey(tcell.KeyCtrlR, rune(0), tcell.ModCtrl),
+ ))
+
+ viper.SetDefault("keybinding.toggle-modified-files", components.NewKeyBinding(
+ "Modified Files",
+ tcell.NewEventKey(tcell.KeyCtrlM, rune(0), tcell.ModCtrl),
+ ))
+
+ viper.SetDefault("keybinding.toggle-unmodified-files", components.NewKeyBinding(
+ "Unmodified Files",
+ tcell.NewEventKey(tcell.KeyCtrlU, rune(0), tcell.ModCtrl),
+ ))
+
+ viper.SetDefault("keybinding.page-up", components.NewKeyBinding(
+ "Page Up",
+ tcell.NewEventKey(tcell.KeyPgUp, rune(0), tcell.ModNone),
+ ))
+
+ viper.SetDefault("keybinding.page-down", components.NewKeyBinding(
+ "Page Down",
+ tcell.NewEventKey(tcell.KeyPgDn, rune(0), tcell.ModNone),
+ ))
viper.SetDefault("diff.hide", "")
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
}