mirror of
https://github.com/wagoodman/dive
synced 2026-03-14 22:35:50 +01:00
viper configured filetree keybindings
- implement remaining filetree navegation commands Signed-off-by: dwillist <dthornton@vmware.com>
This commit is contained in:
parent
00a1b771a8
commit
92ce00a1a9
5 changed files with 298 additions and 53 deletions
70
cmd/root.go
70
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", "")
|
||||
|
||||
|
|
|
|||
|
|
@ -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??
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
64
runtime/ui/components/key_config.go
Normal file
64
runtime/ui/components/key_config.go
Normal 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)
|
||||
//}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue