added configuration + custom keybinding support
This commit is contained in:
parent
4b6ff484e3
commit
6f005724d9
44
README.md
44
README.md
@ -96,3 +96,47 @@ For docker in windows (does not support pulling images yet):
|
|||||||
docker run --rm -it -v //var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest <dive arguments...>
|
docker run --rm -it -v //var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest <dive arguments...>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## KeyBindings
|
||||||
|
|
||||||
|
Key Binding | Description
|
||||||
|
-------------------------------------------|---------------------------------------------------------
|
||||||
|
<kbd>Ctrl + C</kbd> | Exit
|
||||||
|
<kbd>Tab</kbd> or <kbd>Ctrl + Space</kbd> | Switch between the layer and filetree views
|
||||||
|
<kbd>Ctrl + F</kbd> | Filter files
|
||||||
|
<kbd>Ctrl + A</kbd> | Layer view: see aggregated image modifications
|
||||||
|
<kbd>Ctrl + L</kbd> | Layer view: see current layer modifications
|
||||||
|
<kbd>Space</kbd> | Filetree view: collapse/uncollapse a directory
|
||||||
|
<kbd>Ctrl + A</kbd> | Filetree view: show/hide added files
|
||||||
|
<kbd>Ctrl + R</kbd> | Filetree view: show/hide removed files
|
||||||
|
<kbd>Ctrl + M</kbd> | Filetree view: show/hide modified files
|
||||||
|
<kbd>Ctrl + U</kbd> | Filetree view: show/hide unmodified files
|
||||||
|
<kbd>PageUp</kbd> | Filetree view: scroll up a page
|
||||||
|
<kbd>PageDown</kbd> | Filetree view: scroll down a page
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
No configuration is necessary, however, you can create a `~/.dive.yaml` file and override values:
|
||||||
|
```yaml
|
||||||
|
log:
|
||||||
|
enabled: true
|
||||||
|
path: ./dive.log
|
||||||
|
level: info
|
||||||
|
|
||||||
|
# note: you can specify multiple bindings by separating values with a comma. UI hinting shows the first binding.
|
||||||
|
keybinding:
|
||||||
|
# global bindings
|
||||||
|
quit: ctrl+c
|
||||||
|
toggle-view: tab, ctrl+space
|
||||||
|
filter-files: ctrl+f, ctrl+slash
|
||||||
|
# layer view specific bindings
|
||||||
|
compare-all: ctrl+a
|
||||||
|
compare-layer: ctrl+l
|
||||||
|
# file view specific bindings
|
||||||
|
toggle-collapse-dir: space
|
||||||
|
toggle-added-files: ctrl+a
|
||||||
|
toggle-removed-files: ctrl+r
|
||||||
|
toggle-modified-files: ctrl+m
|
||||||
|
toggle-unmodified-files: ctrl+u
|
||||||
|
page-up: pgup
|
||||||
|
page-down: pgdn
|
||||||
|
```
|
||||||
|
55
cmd/root.go
55
cmd/root.go
@ -3,6 +3,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/wagoodman/dive/utils"
|
"github.com/wagoodman/dive/utils"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/k0kubun/go-ansi"
|
"github.com/k0kubun/go-ansi"
|
||||||
@ -38,8 +39,7 @@ func init() {
|
|||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
cobra.OnInitialize(initLogging)
|
cobra.OnInitialize(initLogging)
|
||||||
|
|
||||||
// TODO: add config options
|
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml)")
|
||||||
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml)")
|
|
||||||
|
|
||||||
rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number")
|
rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number")
|
||||||
}
|
}
|
||||||
@ -62,6 +62,25 @@ func initConfig() {
|
|||||||
viper.SetConfigName(".dive")
|
viper.SetConfigName(".dive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viper.SetDefault("log.level", log.InfoLevel.String())
|
||||||
|
viper.SetDefault("log.path", "./dive.log")
|
||||||
|
viper.SetDefault("log.enabled", true)
|
||||||
|
// status view / global
|
||||||
|
viper.SetDefault("keybinding.quit", "ctrl+c")
|
||||||
|
viper.SetDefault("keybinding.toggle-view", "tab, ctrl+space")
|
||||||
|
viper.SetDefault("keybinding.filter-files", "ctrl+f, ctrl+slash")
|
||||||
|
// layer view
|
||||||
|
viper.SetDefault("keybinding.compare-all", "ctrl+a")
|
||||||
|
viper.SetDefault("keybinding.compare-layer", "ctrl+l")
|
||||||
|
// filetree view
|
||||||
|
viper.SetDefault("keybinding.toggle-collapse-dir", "space")
|
||||||
|
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-unchanged-files", "ctrl+u")
|
||||||
|
viper.SetDefault("keybinding.page-up", "pgup")
|
||||||
|
viper.SetDefault("keybinding.page-down", "pgdn")
|
||||||
|
|
||||||
viper.AutomaticEnv() // read in environment variables that match
|
viper.AutomaticEnv() // read in environment variables that match
|
||||||
|
|
||||||
// If a config file is found, read it in.
|
// If a config file is found, read it in.
|
||||||
@ -70,21 +89,33 @@ func initConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initLogging sets up the loggin object with a formatter and location
|
// initLogging sets up the logging object with a formatter and location
|
||||||
func initLogging() {
|
func initLogging() {
|
||||||
// TODO: clean this up and make more configurable
|
|
||||||
var filename string = "dive.log"
|
if viper.GetBool("log.enabled") == false {
|
||||||
// create the log file if doesn't exist. And append to it if it already exists.
|
log.SetOutput(ioutil.Discard)
|
||||||
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
}
|
||||||
|
|
||||||
|
logFileObj, err := os.OpenFile(viper.GetString("log.path"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
}
|
||||||
|
|
||||||
Formatter := new(log.TextFormatter)
|
Formatter := new(log.TextFormatter)
|
||||||
Formatter.DisableTimestamp = true
|
Formatter.DisableTimestamp = true
|
||||||
log.SetFormatter(Formatter)
|
log.SetFormatter(Formatter)
|
||||||
log.SetLevel(log.DebugLevel)
|
|
||||||
|
level, err := log.ParseLevel(viper.GetString("log.level"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// cannot open log file. Logging to stderr
|
fmt.Fprintln(os.Stderr, err)
|
||||||
fmt.Println(err)
|
|
||||||
} else {
|
|
||||||
log.SetOutput(f)
|
|
||||||
}
|
}
|
||||||
|
log.SetLevel(level)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
} else {
|
||||||
|
log.SetOutput(logFileObj)
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug("Starting Dive...")
|
log.Debug("Starting Dive...")
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/viper"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -33,6 +34,14 @@ type FileTreeView struct {
|
|||||||
bufferIndex uint
|
bufferIndex uint
|
||||||
bufferIndexUpperBound uint
|
bufferIndexUpperBound uint
|
||||||
bufferIndexLowerBound uint
|
bufferIndexLowerBound uint
|
||||||
|
|
||||||
|
keybindingToggleCollapse []Key
|
||||||
|
keybindingToggleAdded []Key
|
||||||
|
keybindingToggleRemoved []Key
|
||||||
|
keybindingToggleModified []Key
|
||||||
|
keybindingToggleUnchanged []Key
|
||||||
|
keybindingPageDown []Key
|
||||||
|
keybindingPageUp []Key
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFileTreeView creates a new view object attached the the global [gocui] screen object.
|
// NewFileTreeView creates a new view object attached the the global [gocui] screen object.
|
||||||
@ -46,6 +55,14 @@ func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTr
|
|||||||
treeView.RefTrees = refTrees
|
treeView.RefTrees = refTrees
|
||||||
treeView.HiddenDiffTypes = make([]bool, 4)
|
treeView.HiddenDiffTypes = make([]bool, 4)
|
||||||
|
|
||||||
|
treeView.keybindingToggleCollapse = getKeybindings(viper.GetString("keybinding.toggle-collapse-dir"))
|
||||||
|
treeView.keybindingToggleAdded = getKeybindings(viper.GetString("keybinding.toggle-added-files"))
|
||||||
|
treeView.keybindingToggleRemoved = getKeybindings(viper.GetString("keybinding.toggle-removed-files"))
|
||||||
|
treeView.keybindingToggleModified = getKeybindings(viper.GetString("keybinding.toggle-modified-files"))
|
||||||
|
treeView.keybindingToggleUnchanged = getKeybindings(viper.GetString("keybinding.toggle-unchanged-files"))
|
||||||
|
treeView.keybindingPageUp = getKeybindings(viper.GetString("keybinding.page-up"))
|
||||||
|
treeView.keybindingPageDown = getKeybindings(viper.GetString("keybinding.page-down"))
|
||||||
|
|
||||||
return treeView
|
return treeView
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,27 +93,42 @@ func (view *FileTreeView) Setup(v *gocui.View, header *gocui.View) error {
|
|||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorRight() }); err != nil {
|
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorRight() }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyPgdn, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.PageDown() }); err != nil {
|
|
||||||
|
for _, key := range view.keybindingPageUp {
|
||||||
|
if err := view.gui.SetKeybinding("", key.value, key.modifier, func(*gocui.Gui, *gocui.View) error { return view.PageUp() }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyPgup, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.PageUp() }); err != nil {
|
}
|
||||||
|
for _, key := range view.keybindingPageDown {
|
||||||
|
if err := view.gui.SetKeybinding("", key.value, key.modifier, func(*gocui.Gui, *gocui.View) error { return view.PageDown() }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeySpace, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.toggleCollapse() }); err != nil {
|
}
|
||||||
|
for _, key := range view.keybindingToggleCollapse {
|
||||||
|
if err := view.gui.SetKeybinding("", key.value, key.modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleCollapse() }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyCtrlA, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Added) }); err != nil {
|
}
|
||||||
|
for _, key := range view.keybindingToggleAdded {
|
||||||
|
if err := view.gui.SetKeybinding("", key.value, key.modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Added) }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyCtrlR, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Removed) }); err != nil {
|
}
|
||||||
|
for _, key := range view.keybindingToggleRemoved {
|
||||||
|
if err := view.gui.SetKeybinding("", key.value, key.modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Removed) }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyCtrlM, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Changed) }); err != nil {
|
}
|
||||||
|
for _, key := range view.keybindingToggleModified {
|
||||||
|
if err := view.gui.SetKeybinding("", key.value, key.modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Changed) }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyCtrlU, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Unchanged) }); err != nil {
|
}
|
||||||
|
for _, key := range view.keybindingToggleUnchanged {
|
||||||
|
if err := view.gui.SetKeybinding("", key.value, key.modifier, func(*gocui.Gui, *gocui.View) error { return view.toggleShowDiffType(filetree.Unchanged) }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view.bufferIndexLowerBound = 0
|
view.bufferIndexLowerBound = 0
|
||||||
view.bufferIndexUpperBound = view.height() // don't include the header or footer in the view size
|
view.bufferIndexUpperBound = view.height() // don't include the header or footer in the view size
|
||||||
@ -494,9 +526,9 @@ func (view *FileTreeView) Render() error {
|
|||||||
|
|
||||||
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||||
func (view *FileTreeView) KeyHelp() string {
|
func (view *FileTreeView) KeyHelp() string {
|
||||||
return renderStatusOption("Space", "Collapse dir", false) +
|
return renderStatusOption(view.keybindingToggleCollapse[0].String(), "Collapse dir", false) +
|
||||||
renderStatusOption("^A", "Added files", !view.HiddenDiffTypes[filetree.Added]) +
|
renderStatusOption(view.keybindingToggleAdded[0].String(), "Added files", !view.HiddenDiffTypes[filetree.Added]) +
|
||||||
renderStatusOption("^R", "Removed files", !view.HiddenDiffTypes[filetree.Removed]) +
|
renderStatusOption(view.keybindingToggleRemoved[0].String(), "Removed files", !view.HiddenDiffTypes[filetree.Removed]) +
|
||||||
renderStatusOption("^M", "Modified files", !view.HiddenDiffTypes[filetree.Changed]) +
|
renderStatusOption(view.keybindingToggleModified[0].String(), "Modified files", !view.HiddenDiffTypes[filetree.Changed]) +
|
||||||
renderStatusOption("^U", "Unmodified files", !view.HiddenDiffTypes[filetree.Unchanged])
|
renderStatusOption(view.keybindingToggleUnchanged[0].String(), "Unmodified files", !view.HiddenDiffTypes[filetree.Unchanged])
|
||||||
}
|
}
|
||||||
|
186
ui/keybinding.go
Normal file
186
ui/keybinding.go
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/jroimartin/gocui"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var translate = map[string]string{
|
||||||
|
"/": "Slash",
|
||||||
|
"\\": "Backslash",
|
||||||
|
"[": "LsqBracket",
|
||||||
|
"]": "RsqBracket",
|
||||||
|
"_": "Underscore",
|
||||||
|
"escape": "Esc",
|
||||||
|
"~": "Tilde",
|
||||||
|
"pageup": "Pgup",
|
||||||
|
"pagedown": "Pgdn",
|
||||||
|
"pgup": "Pgup",
|
||||||
|
"pgdown": "Pgdn",
|
||||||
|
// "up": "ArrowUp",
|
||||||
|
// "down": "ArrowDown",
|
||||||
|
// "right": "ArrowRight",
|
||||||
|
// "left": "ArrowLeft",
|
||||||
|
"ctl": "Ctrl",
|
||||||
|
}
|
||||||
|
|
||||||
|
var display = map[string]string{
|
||||||
|
"Slash": "/",
|
||||||
|
"Backslash": "\\",
|
||||||
|
"LsqBracket": "[",
|
||||||
|
"RsqBracket": "]",
|
||||||
|
"Underscore": "_",
|
||||||
|
"Tilde": "~",
|
||||||
|
"Ctrl": "^",
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportedKeybindings = map[string]gocui.Key{
|
||||||
|
"KeyF1": gocui.KeyF1,
|
||||||
|
"KeyF2": gocui.KeyF2,
|
||||||
|
"KeyF3": gocui.KeyF3,
|
||||||
|
"KeyF4": gocui.KeyF4,
|
||||||
|
"KeyF5": gocui.KeyF5,
|
||||||
|
"KeyF6": gocui.KeyF6,
|
||||||
|
"KeyF7": gocui.KeyF7,
|
||||||
|
"KeyF8": gocui.KeyF8,
|
||||||
|
"KeyF9": gocui.KeyF9,
|
||||||
|
"KeyF10": gocui.KeyF10,
|
||||||
|
"KeyF11": gocui.KeyF11,
|
||||||
|
"KeyF12": gocui.KeyF12,
|
||||||
|
"KeyInsert": gocui.KeyInsert,
|
||||||
|
"KeyDelete": gocui.KeyDelete,
|
||||||
|
"KeyHome": gocui.KeyHome,
|
||||||
|
"KeyEnd": gocui.KeyEnd,
|
||||||
|
"KeyPgup": gocui.KeyPgup,
|
||||||
|
"KeyPgdn": gocui.KeyPgdn,
|
||||||
|
// "KeyArrowUp": gocui.KeyArrowUp,
|
||||||
|
// "KeyArrowDown": gocui.KeyArrowDown,
|
||||||
|
// "KeyArrowLeft": gocui.KeyArrowLeft,
|
||||||
|
// "KeyArrowRight": gocui.KeyArrowRight,
|
||||||
|
"KeyCtrlTilde": gocui.KeyCtrlTilde,
|
||||||
|
"KeyCtrl2": gocui.KeyCtrl2,
|
||||||
|
"KeyCtrlSpace": gocui.KeyCtrlSpace,
|
||||||
|
"KeyCtrlA": gocui.KeyCtrlA,
|
||||||
|
"KeyCtrlB": gocui.KeyCtrlB,
|
||||||
|
"KeyCtrlC": gocui.KeyCtrlC,
|
||||||
|
"KeyCtrlD": gocui.KeyCtrlD,
|
||||||
|
"KeyCtrlE": gocui.KeyCtrlE,
|
||||||
|
"KeyCtrlF": gocui.KeyCtrlF,
|
||||||
|
"KeyCtrlG": gocui.KeyCtrlG,
|
||||||
|
"KeyBackspace": gocui.KeyBackspace,
|
||||||
|
"KeyCtrlH": gocui.KeyCtrlH,
|
||||||
|
"KeyTab": gocui.KeyTab,
|
||||||
|
"KeyCtrlI": gocui.KeyCtrlI,
|
||||||
|
"KeyCtrlJ": gocui.KeyCtrlJ,
|
||||||
|
"KeyCtrlK": gocui.KeyCtrlK,
|
||||||
|
"KeyCtrlL": gocui.KeyCtrlL,
|
||||||
|
"KeyEnter": gocui.KeyEnter,
|
||||||
|
"KeyCtrlM": gocui.KeyCtrlM,
|
||||||
|
"KeyCtrlN": gocui.KeyCtrlN,
|
||||||
|
"KeyCtrlO": gocui.KeyCtrlO,
|
||||||
|
"KeyCtrlP": gocui.KeyCtrlP,
|
||||||
|
"KeyCtrlQ": gocui.KeyCtrlQ,
|
||||||
|
"KeyCtrlR": gocui.KeyCtrlR,
|
||||||
|
"KeyCtrlS": gocui.KeyCtrlS,
|
||||||
|
"KeyCtrlT": gocui.KeyCtrlT,
|
||||||
|
"KeyCtrlU": gocui.KeyCtrlU,
|
||||||
|
"KeyCtrlV": gocui.KeyCtrlV,
|
||||||
|
"KeyCtrlW": gocui.KeyCtrlW,
|
||||||
|
"KeyCtrlX": gocui.KeyCtrlX,
|
||||||
|
"KeyCtrlY": gocui.KeyCtrlY,
|
||||||
|
"KeyCtrlZ": gocui.KeyCtrlZ,
|
||||||
|
"KeyEsc": gocui.KeyEsc,
|
||||||
|
"KeyCtrlLsqBracket": gocui.KeyCtrlLsqBracket,
|
||||||
|
"KeyCtrl3": gocui.KeyCtrl3,
|
||||||
|
"KeyCtrl4": gocui.KeyCtrl4,
|
||||||
|
"KeyCtrlBackslash": gocui.KeyCtrlBackslash,
|
||||||
|
"KeyCtrl5": gocui.KeyCtrl5,
|
||||||
|
"KeyCtrlRsqBracket": gocui.KeyCtrlRsqBracket,
|
||||||
|
"KeyCtrl6": gocui.KeyCtrl6,
|
||||||
|
"KeyCtrl7": gocui.KeyCtrl7,
|
||||||
|
"KeyCtrlSlash": gocui.KeyCtrlSlash,
|
||||||
|
"KeyCtrlUnderscore": gocui.KeyCtrlUnderscore,
|
||||||
|
"KeySpace": gocui.KeySpace,
|
||||||
|
"KeyBackspace2": gocui.KeyBackspace2,
|
||||||
|
"KeyCtrl8": gocui.KeyCtrl8,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Key struct {
|
||||||
|
value gocui.Key
|
||||||
|
modifier gocui.Modifier
|
||||||
|
tokens []string
|
||||||
|
input string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeybinding(input string) (Key, error) {
|
||||||
|
f := func(c rune) bool { return unicode.IsSpace(c) || c == '+' }
|
||||||
|
tokens := strings.FieldsFunc(input, f)
|
||||||
|
var normalizedTokens []string
|
||||||
|
var modifier = gocui.ModNone
|
||||||
|
|
||||||
|
for _, token := range tokens {
|
||||||
|
normalized := strings.ToLower(token)
|
||||||
|
|
||||||
|
if value, exists := translate[normalized]; exists {
|
||||||
|
normalized = value
|
||||||
|
} else {
|
||||||
|
normalized = strings.Title(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
if normalized == "Alt" {
|
||||||
|
modifier = gocui.ModAlt
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(normalized) == 1 {
|
||||||
|
normalizedTokens = append(normalizedTokens, strings.ToUpper(normalized))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedTokens = append(normalizedTokens, normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup := "Key" + strings.Join(normalizedTokens, "")
|
||||||
|
|
||||||
|
if key, exists := supportedKeybindings[lookup]; exists {
|
||||||
|
return Key{key, modifier, normalizedTokens, input}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if modifier != gocui.ModNone {
|
||||||
|
return Key{0, modifier, normalizedTokens, input}, fmt.Errorf("unsupported keybinding: %s (+%+v)", lookup, modifier)
|
||||||
|
}
|
||||||
|
return Key{0, modifier, normalizedTokens, input}, fmt.Errorf("unsupported keybinding: %s", lookup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeybindings(input string) []Key {
|
||||||
|
ret := make([]Key, 0)
|
||||||
|
for _, value := range strings.Split(input, ",") {
|
||||||
|
key, err := getKeybinding(value)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("could not parse keybinding '%s' from request '%s': %+v", value, input, err))
|
||||||
|
}
|
||||||
|
ret = append(ret, key)
|
||||||
|
}
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic(fmt.Errorf("must have at least one keybinding"))
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (key Key) String() string {
|
||||||
|
displayTokens := make([]string, 0)
|
||||||
|
prefix := ""
|
||||||
|
for _, token := range key.tokens {
|
||||||
|
if token == "Ctrl" {
|
||||||
|
prefix = "^"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if value, exists := display[token]; exists {
|
||||||
|
token = value
|
||||||
|
}
|
||||||
|
displayTokens = append(displayTokens, token)
|
||||||
|
}
|
||||||
|
return prefix + strings.Join(displayTokens, "+")
|
||||||
|
}
|
55
ui/keybinding_test.go
Normal file
55
ui/keybinding_test.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jroimartin/gocui"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetKeybinding(t *testing.T) {
|
||||||
|
var table = []struct {
|
||||||
|
input string
|
||||||
|
key gocui.Key
|
||||||
|
modifier gocui.Modifier
|
||||||
|
errStr string
|
||||||
|
}{
|
||||||
|
{"ctrl + A", gocui.KeyCtrlA, gocui.ModNone, ""},
|
||||||
|
{"Ctrl + a", gocui.KeyCtrlA, gocui.ModNone, ""},
|
||||||
|
{"Ctl + a", gocui.KeyCtrlA, gocui.ModNone, ""},
|
||||||
|
{"ctl + A", gocui.KeyCtrlA, gocui.ModNone, ""},
|
||||||
|
{"f2", gocui.KeyF2, gocui.ModNone, ""},
|
||||||
|
{"ctrl + [", gocui.KeyCtrlLsqBracket, gocui.ModNone, ""},
|
||||||
|
{" ctrl + ] ", gocui.KeyCtrlRsqBracket, gocui.ModNone, ""},
|
||||||
|
{"ctrl + /", gocui.KeyCtrlSlash, gocui.ModNone, ""},
|
||||||
|
{"ctrl + \\", gocui.KeyCtrlBackslash, gocui.ModNone, ""},
|
||||||
|
// {"left", gocui.KeyArrowLeft, gocui.ModNone, ""},
|
||||||
|
{"PageUp", gocui.KeyPgup, gocui.ModNone, ""},
|
||||||
|
{"PgUp", gocui.KeyPgup, gocui.ModNone, ""},
|
||||||
|
{"pageup", gocui.KeyPgup, gocui.ModNone, ""},
|
||||||
|
{"pgup", gocui.KeyPgup, gocui.ModNone, ""},
|
||||||
|
{"tab", gocui.KeyTab, gocui.ModNone, ""},
|
||||||
|
{"escape", gocui.KeyEsc, gocui.ModNone, ""},
|
||||||
|
{"enter", gocui.KeyEnter, gocui.ModNone, ""},
|
||||||
|
{"space", gocui.KeySpace, gocui.ModNone, ""},
|
||||||
|
{"ctrl + alt + z", gocui.KeyCtrlZ, gocui.ModAlt, ""},
|
||||||
|
{"f22", 0, gocui.ModNone, "unsupported keybinding: KeyF22"},
|
||||||
|
{"ctrl + alt + !", 0, gocui.ModAlt, "unsupported keybinding: KeyCtrl! (+1)"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, trial := range table {
|
||||||
|
actualKey, actualErr := getKeybinding(trial.input)
|
||||||
|
|
||||||
|
if actualKey.value != trial.key {
|
||||||
|
t.Errorf("Expected key '%+v' but got '%+v' (trial %d)", trial.key, actualKey, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actualKey.modifier != trial.modifier {
|
||||||
|
t.Errorf("Expected modifier '%+v' but got '%+v' (trial %d)", trial.modifier, actualKey, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actualErr == nil && trial.errStr != "" {
|
||||||
|
t.Errorf("Expected error message of '%s' but got no message (trial %d)", trial.errStr, idx)
|
||||||
|
} else if actualErr != nil && actualErr.Error() != trial.errStr {
|
||||||
|
t.Errorf("Expected error message '%s' but got '%s' (trial %d)", trial.errStr, actualErr.Error(), idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/jroimartin/gocui"
|
"github.com/jroimartin/gocui"
|
||||||
@ -21,6 +22,9 @@ type LayerView struct {
|
|||||||
Layers []*image.Layer
|
Layers []*image.Layer
|
||||||
CompareMode CompareType
|
CompareMode CompareType
|
||||||
CompareStartIndex int
|
CompareStartIndex int
|
||||||
|
|
||||||
|
keybindingCompareAll []Key
|
||||||
|
keybindingCompareLayer []Key
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDetailsView creates a new view object attached the the global [gocui] screen object.
|
// NewDetailsView creates a new view object attached the the global [gocui] screen object.
|
||||||
@ -33,6 +37,9 @@ func NewLayerView(name string, gui *gocui.Gui, layers []*image.Layer) (layerView
|
|||||||
layerView.Layers = layers
|
layerView.Layers = layers
|
||||||
layerView.CompareMode = CompareLayer
|
layerView.CompareMode = CompareLayer
|
||||||
|
|
||||||
|
layerView.keybindingCompareAll = getKeybindings(viper.GetString("keybinding.compare-all"))
|
||||||
|
layerView.keybindingCompareLayer = getKeybindings(viper.GetString("keybinding.compare-layer"))
|
||||||
|
|
||||||
return layerView
|
return layerView
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,12 +64,18 @@ func (view *LayerView) Setup(v *gocui.View, header *gocui.View) error {
|
|||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorUp() }); err != nil {
|
if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorUp() }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyCtrlL, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.setCompareMode(CompareLayer) }); err != nil {
|
|
||||||
|
for _, key := range view.keybindingCompareLayer {
|
||||||
|
if err := view.gui.SetKeybinding("", key.value, key.modifier, func(*gocui.Gui, *gocui.View) error { return view.setCompareMode(CompareLayer) }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := view.gui.SetKeybinding(view.Name, gocui.KeyCtrlA, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.setCompareMode(CompareAll) }); err != nil {
|
}
|
||||||
|
|
||||||
|
for _, key := range view.keybindingCompareAll {
|
||||||
|
if err := view.gui.SetKeybinding("", key.value, key.modifier, func(*gocui.Gui, *gocui.View) error { return view.setCompareMode(CompareAll) }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return view.Render()
|
return view.Render()
|
||||||
}
|
}
|
||||||
@ -212,6 +225,6 @@ func (view *LayerView) Render() error {
|
|||||||
|
|
||||||
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
// KeyHelp indicates all the possible actions a user can take while the current pane is selected.
|
||||||
func (view *LayerView) KeyHelp() string {
|
func (view *LayerView) KeyHelp() string {
|
||||||
return renderStatusOption("^L", "Show layer changes", view.CompareMode == CompareLayer) +
|
return renderStatusOption(view.keybindingCompareLayer[0].String(), "Show layer changes", view.CompareMode == CompareLayer) +
|
||||||
renderStatusOption("^A", "Show aggregated changes", view.CompareMode == CompareAll)
|
renderStatusOption(view.keybindingCompareAll[0].String(), "Show aggregated changes", view.CompareMode == CompareAll)
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ func (view *StatusView) Render() error {
|
|||||||
|
|
||||||
// KeyHelp indicates all the possible global actions a user can take when any pane is selected.
|
// KeyHelp indicates all the possible global actions a user can take when any pane is selected.
|
||||||
func (view *StatusView) KeyHelp() string {
|
func (view *StatusView) KeyHelp() string {
|
||||||
return renderStatusOption("^C", "Quit", false) +
|
return renderStatusOption(GlobalKeybindings.quit[0].String(), "Quit", false) +
|
||||||
renderStatusOption("TAB", "Switch view", false) +
|
renderStatusOption(GlobalKeybindings.toggleView[0].String(), "Switch view", false) +
|
||||||
renderStatusOption("^F", "Filter files", Views.Filter.IsVisible())
|
renderStatusOption(GlobalKeybindings.filterView[0].String(), "Filter files", Views.Filter.IsVisible())
|
||||||
}
|
}
|
||||||
|
35
ui/ui.go
35
ui/ui.go
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/jroimartin/gocui"
|
"github.com/jroimartin/gocui"
|
||||||
|
"github.com/spf13/viper"
|
||||||
"github.com/wagoodman/dive/filetree"
|
"github.com/wagoodman/dive/filetree"
|
||||||
"github.com/wagoodman/dive/image"
|
"github.com/wagoodman/dive/image"
|
||||||
"log"
|
"log"
|
||||||
@ -49,6 +50,12 @@ var Views struct {
|
|||||||
lookup map[string]View
|
lookup map[string]View
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var GlobalKeybindings struct {
|
||||||
|
quit []Key
|
||||||
|
toggleView []Key
|
||||||
|
filterView []Key
|
||||||
|
}
|
||||||
|
|
||||||
// View defines the a renderable terminal screen pane.
|
// View defines the a renderable terminal screen pane.
|
||||||
type View interface {
|
type View interface {
|
||||||
Setup(*gocui.View, *gocui.View) error
|
Setup(*gocui.View, *gocui.View) error
|
||||||
@ -140,23 +147,22 @@ func quit(g *gocui.Gui, v *gocui.View) error {
|
|||||||
|
|
||||||
// keyBindings registers global key press actions, valid when in any pane.
|
// keyBindings registers global key press actions, valid when in any pane.
|
||||||
func keyBindings(g *gocui.Gui) error {
|
func keyBindings(g *gocui.Gui) error {
|
||||||
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
for _, key := range GlobalKeybindings.quit {
|
||||||
|
if err := g.SetKeybinding("", key.value, key.modifier, quit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
//if err := g.SetKeybinding("main", gocui.MouseLeft, gocui.ModNone, toggleCollapse); err != nil {
|
}
|
||||||
// return err
|
|
||||||
//}
|
for _, key := range GlobalKeybindings.toggleView {
|
||||||
if err := g.SetKeybinding("", gocui.KeyCtrlSpace, gocui.ModNone, toggleView); err != nil {
|
if err := g.SetKeybinding("", key.value, key.modifier, toggleView); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, toggleView); err != nil {
|
}
|
||||||
|
|
||||||
|
for _, key := range GlobalKeybindings.filterView {
|
||||||
|
if err := g.SetKeybinding("", key.value, key.modifier, toggleFilterView); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("", gocui.KeyCtrlSlash, gocui.ModNone, toggleFilterView); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := g.SetKeybinding("", gocui.KeyCtrlF, gocui.ModNone, toggleFilterView); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -299,6 +305,10 @@ func Run(layers []*image.Layer, refTrees []*filetree.FileTree, efficiency float6
|
|||||||
Formatting.CompareTop = color.New(color.BgMagenta).SprintFunc()
|
Formatting.CompareTop = color.New(color.BgMagenta).SprintFunc()
|
||||||
Formatting.CompareBottom = color.New(color.BgGreen).SprintFunc()
|
Formatting.CompareBottom = color.New(color.BgGreen).SprintFunc()
|
||||||
|
|
||||||
|
GlobalKeybindings.quit = getKeybindings(viper.GetString("keybinding.quit"))
|
||||||
|
GlobalKeybindings.toggleView = getKeybindings(viper.GetString("keybinding.toggle-view"))
|
||||||
|
GlobalKeybindings.filterView = getKeybindings(viper.GetString("keybinding.filter-files"))
|
||||||
|
|
||||||
g, err := gocui.NewGui(gocui.OutputNormal)
|
g, err := gocui.NewGui(gocui.OutputNormal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicln(err)
|
log.Panicln(err)
|
||||||
@ -330,9 +340,6 @@ func Run(layers []*image.Layer, refTrees []*filetree.FileTree, efficiency float6
|
|||||||
Update()
|
Update()
|
||||||
Render()
|
Render()
|
||||||
|
|
||||||
// let the default position of the cursor be the last layer
|
|
||||||
// Views.Layer.SetCursor(len(Views.Layer.Layers)-1)
|
|
||||||
|
|
||||||
if err := keyBindings(g); err != nil {
|
if err := keyBindings(g); err != nil {
|
||||||
log.Panicln(err)
|
log.Panicln(err)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user