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}