keyspec.go RawHistoryFolder

1package keys
2
3import (
4	"fmt"
5	"sort"
6	"strings"
7	"unicode/utf8"
8
9	"github.com/bnema/vev/internal/domain"
10)
11
12const altPrefix = "alt+"
13
14// KeySpec is one supported user-facing key binding specification.
15type KeySpec struct {
16	altRunes []rune
17	altArrow byte
18}
19
20// ParseKeySpec parses vev's config key grammar.
21func ParseKeySpec(spec string) (KeySpec, error) {
22	if !strings.HasPrefix(spec, altPrefix) {
23		return KeySpec{}, fmt.Errorf("key spec must start with %q", altPrefix)
24	}
25	key := strings.TrimPrefix(spec, altPrefix)
26	switch key {
27	case "space":
28		return KeySpec{altRunes: []rune{' '}}, nil
29	case "left":
30		return KeySpec{altArrow: 'D'}, nil
31	case "right":
32		return KeySpec{altArrow: 'C'}, nil
33	case "up":
34		return KeySpec{altArrow: 'A'}, nil
35	case "down":
36		return KeySpec{altArrow: 'B'}, nil
37	}
38	if utf8.RuneCountInString(key) != 1 {
39		return KeySpec{}, fmt.Errorf("key spec must be alt+<char>, alt+space, or alt+left/right/up/down")
40	}
41	r, _ := utf8.DecodeRuneInString(key)
42	if idx, ok := topRowDigitIndex(r); ok {
43		return KeySpec{altRunes: append([]rune(nil), topRowDigitAliases[idx]...)}, nil
44	}
45	if r >= '0' && r <= '9' {
46		return KeySpec{}, fmt.Errorf("only alt+1 through alt+9 are supported digit bindings")
47	}
48	return KeySpec{altRunes: []rune{r}}, nil
49}
50
51var actionNames = map[Action]string{
52	ActionOpenPalette:             "open-palette",
53	ActionToggleFloatingPane:      "toggle-floating-pane",
54	ActionJumpAttention:           "jump-attention",
55	ActionFocusPaneLeft:           "focus-pane-left",
56	ActionFocusPaneRight:          "focus-pane-right",
57	ActionFocusPaneUp:             "focus-pane-up",
58	ActionFocusPaneDown:           "focus-pane-down",
59	ActionSwitchTab1:              "switch-tab-1",
60	ActionSwitchTab2:              "switch-tab-2",
61	ActionSwitchTab3:              "switch-tab-3",
62	ActionSwitchTab4:              "switch-tab-4",
63	ActionSwitchTab5:              "switch-tab-5",
64	ActionSwitchTab6:              "switch-tab-6",
65	ActionSwitchTab7:              "switch-tab-7",
66	ActionSwitchTab8:              "switch-tab-8",
67	ActionSwitchTab9:              "switch-tab-9",
68	ActionGrowPaneWidth:           "grow-pane-width",
69	ActionShrinkPaneWidth:         "shrink-pane-width",
70	ActionGrowPaneHeight:          "grow-pane-height",
71	ActionShrinkPaneHeight:        "shrink-pane-height",
72	ActionEqualizePanes:           "equalize-panes",
73	ActionConsumeOrExpelPaneLeft:  "consume-or-expel-pane-left",
74	ActionConsumeOrExpelPaneRight: "consume-or-expel-pane-right",
75}
76
77var actionsByName = func() map[string]Action {
78	byName := make(map[string]Action, len(actionNames))
79	for action, name := range actionNames {
80		if _, dup := byName[name]; dup {
81			panic(fmt.Sprintf("keys: duplicate action name %q", name))
82		}
83		byName[name] = action
84	}
85	return byName
86}()
87
88// Name returns the canonical config name for an action.
89func (a Action) Name() string {
90	name, ok := actionNames[a]
91	if !ok {
92		return "unknown"
93	}
94	return name
95}
96
97// ActionByName returns the action for a canonical config name.
98func ActionByName(name string) (Action, bool) {
99	action, ok := actionsByName[name]
100	return action, ok
101}
102
103// BuildBindings starts from vev's defaults and applies config overrides. Map
104// inputs have no file order, so names are applied lexicographically for stable
105// behavior. Parsed configs should call BuildBindingEntries to preserve file
106// order for duplicate-key first-wins semantics.
107func BuildBindings(overrides map[string]string) (*Bindings, []domain.Warning) {
108	entries := make([]domain.ConfigEntry, 0, len(overrides))
109	for _, name := range sortedOverrideNames(overrides) {
110		entries = append(entries, domain.ConfigEntry{Key: name, Value: overrides[name]})
111	}
112	return BuildBindingEntries(entries)
113}
114
115// BuildBindingEntries starts from vev's defaults and applies config overrides
116// in the order they appeared in the config file. When two actions request the
117// same key, the first valid override wins and later actions keep their default.
118func BuildBindingEntries(overrides []domain.ConfigEntry) (*Bindings, []domain.Warning) {
119	bindings := DefaultBindings().clone()
120	var warnings []domain.Warning
121	var desired []bindingOverride
122	for _, entry := range overrides {
123		action, ok := ActionByName(entry.Key)
124		if !ok {
125			warnings = append(warnings, domain.Warning{Msg: fmt.Sprintf("unknown action %q", entry.Key)})
126			continue
127		}
128		spec, err := ParseKeySpec(entry.Value)
129		if err != nil {
130			warnings = append(warnings, domain.Warning{Msg: fmt.Sprintf("invalid key for %q: %v", entry.Key, err)})
131			continue
132		}
133		desired = append(desired, bindingOverride{name: entry.Key, action: action, spec: spec})
134	}
135	for _, override := range desired {
136		bindings.removeAction(override.action)
137	}
138	for _, override := range desired {
139		if conflict, ok := bindings.conflictingAction(override.spec, override.action); ok {
140			warnings = append(warnings, domain.Warning{Msg: fmt.Sprintf("duplicate key for %q conflicts with %q", override.name, conflict.Name())})
141			bindings.restoreDefaultAction(override.action)
142			continue
143		}
144		bindings.bind(override.action, override.spec)
145	}
146	return bindings, warnings
147}
148
149type bindingOverride struct {
150	name   string
151	action Action
152	spec   KeySpec
153}
154
155func sortedOverrideNames(overrides map[string]string) []string {
156	names := make([]string, 0, len(overrides))
157	for name := range overrides {
158		names = append(names, name)
159	}
160	sort.Strings(names)
161	return names
162}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close