viper configured filetree keybindings

- implement remaining filetree navegation commands

Signed-off-by: dwillist <dthornton@vmware.com>
This commit is contained in:
dwillist 2021-01-04 00:01:59 -05:00
commit 92ce00a1a9
5 changed files with 298 additions and 53 deletions

View file

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

View file

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

View file

@ -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)
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)
return t
}
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) getBox() *tview.Box {
return ll.Box
}
bindingArray := []KeyBinding{}
actionArray := []keyAction{}
func (ll *TreeView) getDraw() drawFn {
return ll.Draw
}
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)
}
func (ll *TreeView) getInputWrapper() inputFn {
return ll.InputHandler
}
// 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)) {
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
}

View file

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

View file

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