@brimveyn/aimux-config 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/package.json +40 -0
- package/src/actions.ts +368 -0
- package/src/backends.ts +23 -0
- package/src/defaults.ts +244 -0
- package/src/define-config.ts +20 -0
- package/src/index.ts +50 -0
- package/src/keymap-builder.ts +112 -0
- package/src/resolver.ts +76 -0
- package/src/themes.ts +252 -0
- package/src/types.ts +568 -0
package/src/defaults.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { ResolvedKeymapConfig } from './types'
|
|
2
|
+
|
|
3
|
+
import * as actions from './actions'
|
|
4
|
+
import { KeymapBuilder } from './keymap-builder'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build the default keymap — 1:1 transcription of all existing handlers.
|
|
8
|
+
*/
|
|
9
|
+
export function getDefaultKeymapConfig(): ResolvedKeymapConfig {
|
|
10
|
+
const kb = new KeymapBuilder()
|
|
11
|
+
.leader('<C-w>')
|
|
12
|
+
.timeout(300)
|
|
13
|
+
|
|
14
|
+
// -----------------------------------------------------------------------
|
|
15
|
+
// Navigation mode (src/input/modes/handlers/navigation.ts)
|
|
16
|
+
// -----------------------------------------------------------------------
|
|
17
|
+
.mode('navigation', (m) =>
|
|
18
|
+
m
|
|
19
|
+
.map('<C-c>', actions.quit)
|
|
20
|
+
.map('<C-n>', actions.newTab)
|
|
21
|
+
.map('<C-g>', actions.sessionPicker)
|
|
22
|
+
.map('<C-z>', actions.ctrlZSidebar)
|
|
23
|
+
.map('dd', actions.closeTab)
|
|
24
|
+
.map('<C-b>', actions.toggleSidebar)
|
|
25
|
+
.map('<C-r>', actions.restartTab)
|
|
26
|
+
.map('<C-s>', actions.snippetPicker)
|
|
27
|
+
.map('<C-t>', actions.themePicker)
|
|
28
|
+
.map('<C-h>', actions.resizeSidebar(-2))
|
|
29
|
+
.map('<C-l>', actions.resizeSidebar(2))
|
|
30
|
+
.map('G', actions.toggleGitPanel)
|
|
31
|
+
.map('<C-j>', actions.resizeGitPanel(-0.05))
|
|
32
|
+
.map('<C-k>', actions.resizeGitPanel(0.05))
|
|
33
|
+
.map('J', actions.reorderTab(1))
|
|
34
|
+
.map('j', actions.nextTab)
|
|
35
|
+
.map('K', actions.reorderTab(-1))
|
|
36
|
+
.map('k', actions.prevTab)
|
|
37
|
+
.map('r', actions.renameTab)
|
|
38
|
+
.map('i', actions.enterInsert)
|
|
39
|
+
.map('?', actions.helpModal)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// -----------------------------------------------------------------------
|
|
43
|
+
// Terminal-input mode
|
|
44
|
+
// Bindings here are processed by raw-input-handler.ts before bytes reach
|
|
45
|
+
// the PTY. Only single-chord <C-*> sequences are supported (no multi-key).
|
|
46
|
+
// -----------------------------------------------------------------------
|
|
47
|
+
.mode('terminal-input', (m) =>
|
|
48
|
+
m
|
|
49
|
+
.map('<C-z>', actions.leaveTerminalInput)
|
|
50
|
+
.map('<Leader>', actions.enterLayoutMode)
|
|
51
|
+
.map('<C-b>', actions.toggleSidebarFromInput)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// -----------------------------------------------------------------------
|
|
55
|
+
// Layout mode (src/input/modes/handlers/layout.ts)
|
|
56
|
+
// -----------------------------------------------------------------------
|
|
57
|
+
.mode('layout', (m) =>
|
|
58
|
+
m
|
|
59
|
+
.map('<Esc>', actions.exitLayoutToInput)
|
|
60
|
+
.map('<Leader>', actions.exitLayoutToInput)
|
|
61
|
+
.map('<C-z>', actions.exitLayoutToNavigation)
|
|
62
|
+
.map('H', actions.resizePane(-1, 'vertical'))
|
|
63
|
+
.map('L', actions.resizePane(1, 'vertical'))
|
|
64
|
+
.map('K', actions.resizePane(-1, 'horizontal'))
|
|
65
|
+
.map('J', actions.resizePane(1, 'horizontal'))
|
|
66
|
+
.map('h', actions.focusPane('left'))
|
|
67
|
+
.map('j', actions.focusPane('down'))
|
|
68
|
+
.map('k', actions.focusPane('up'))
|
|
69
|
+
.map('l', actions.focusPane('right'))
|
|
70
|
+
.map('|', actions.splitVertical)
|
|
71
|
+
.map('-', actions.splitHorizontal)
|
|
72
|
+
.map('q', actions.closePane)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// -----------------------------------------------------------------------
|
|
76
|
+
// Modal: help (src/input/modes/handlers/modal-help.ts)
|
|
77
|
+
// -----------------------------------------------------------------------
|
|
78
|
+
.mode('modal.help', (m) => m.map('<Esc>', actions.closeModal))
|
|
79
|
+
|
|
80
|
+
// -----------------------------------------------------------------------
|
|
81
|
+
// Modal: theme-picker (src/input/modes/handlers/modal-theme-picker.ts)
|
|
82
|
+
// -----------------------------------------------------------------------
|
|
83
|
+
.mode('modal.theme-picker', (m) =>
|
|
84
|
+
m
|
|
85
|
+
.map('<Esc>', actions.restoreTheme)
|
|
86
|
+
.map('j', actions.previewTheme(1))
|
|
87
|
+
.map('k', actions.previewTheme(-1))
|
|
88
|
+
.map('<Down>', actions.previewTheme(1))
|
|
89
|
+
.map('<Up>', actions.previewTheme(-1))
|
|
90
|
+
.map('<CR>', actions.confirmTheme)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// -----------------------------------------------------------------------
|
|
94
|
+
// Modal: rename-tab (src/input/modes/handlers/modal-rename-tab.ts)
|
|
95
|
+
// -----------------------------------------------------------------------
|
|
96
|
+
.mode('modal.rename-tab', (m) =>
|
|
97
|
+
m.map('<Esc>', actions.closeModal).map('<CR>', actions.confirmRenameTab).passthrough()
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// -----------------------------------------------------------------------
|
|
101
|
+
// Modal: new-tab (src/input/modes/handlers/modal-new-tab.ts)
|
|
102
|
+
// -----------------------------------------------------------------------
|
|
103
|
+
.mode('modal.new-tab', (m) =>
|
|
104
|
+
m
|
|
105
|
+
.map('<Esc>', actions.closeModal)
|
|
106
|
+
.map('j', actions.moveModalSelection(1))
|
|
107
|
+
.map('k', actions.moveModalSelection(-1))
|
|
108
|
+
.map('<Down>', actions.moveModalSelection(1))
|
|
109
|
+
.map('<Up>', actions.moveModalSelection(-1))
|
|
110
|
+
.map('<CR>', actions.launchSelectedAssistant)
|
|
111
|
+
.map('e', actions.beginCommandEdit)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// -----------------------------------------------------------------------
|
|
115
|
+
// Modal: new-tab command-edit
|
|
116
|
+
// (src/input/modes/handlers/modal-new-tab-command-edit.ts)
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
.mode('modal.new-tab.command-edit', (m) =>
|
|
119
|
+
m
|
|
120
|
+
.map('<Esc>', actions.cancelCommandEdit('modal.new-tab'))
|
|
121
|
+
.map('<CR>', actions.commitCommandEdit)
|
|
122
|
+
.map('<C-n>', actions.moveModalSelection(1))
|
|
123
|
+
.map('<C-p>', actions.moveModalSelection(-1))
|
|
124
|
+
.passthrough()
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// -----------------------------------------------------------------------
|
|
128
|
+
// Modal: session-picker (src/input/modes/handlers/modal-session-picker.ts)
|
|
129
|
+
// -----------------------------------------------------------------------
|
|
130
|
+
.mode('modal.session-picker', (m) =>
|
|
131
|
+
m
|
|
132
|
+
.map('<Esc>', actions.sessionPickerEscape)
|
|
133
|
+
.map('j', actions.moveModalSelection(1))
|
|
134
|
+
.map('k', actions.moveModalSelection(-1))
|
|
135
|
+
.map('<Down>', actions.moveModalSelection(1))
|
|
136
|
+
.map('<Up>', actions.moveModalSelection(-1))
|
|
137
|
+
.map('<CR>', actions.confirmSelectedSession)
|
|
138
|
+
.map('n', actions.openCreateSessionModal)
|
|
139
|
+
.map('r', actions.openRenameSelectedSession)
|
|
140
|
+
.map('d', actions.deleteSelectedSession)
|
|
141
|
+
.map('/', actions.beginSessionFilter)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// -----------------------------------------------------------------------
|
|
145
|
+
// Modal: session-picker filtering
|
|
146
|
+
// (src/input/modes/handlers/modal-session-picker-filter.ts)
|
|
147
|
+
// -----------------------------------------------------------------------
|
|
148
|
+
.mode('modal.session-picker.filtering', (m) =>
|
|
149
|
+
m
|
|
150
|
+
.map('<Esc>', actions.cancelCommandEdit('modal.session-picker'))
|
|
151
|
+
.map('<CR>', actions.confirmSelectedSession)
|
|
152
|
+
.map('<C-n>', actions.moveModalSelection(1))
|
|
153
|
+
.map('<C-p>', actions.moveModalSelection(-1))
|
|
154
|
+
.passthrough()
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
// -----------------------------------------------------------------------
|
|
158
|
+
// Modal: session-name (src/input/modes/handlers/modal-session-name.ts)
|
|
159
|
+
// -----------------------------------------------------------------------
|
|
160
|
+
.mode('modal.session-name', (m) =>
|
|
161
|
+
m
|
|
162
|
+
.map('<Esc>', actions.backToSessionPicker)
|
|
163
|
+
.map('<CR>', actions.confirmSessionRename)
|
|
164
|
+
.passthrough()
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
// -----------------------------------------------------------------------
|
|
168
|
+
// Modal: create-session
|
|
169
|
+
// (src/input/modes/handlers/modal-create-session.ts)
|
|
170
|
+
// -----------------------------------------------------------------------
|
|
171
|
+
.mode('modal.create-session', (m) =>
|
|
172
|
+
m
|
|
173
|
+
.map('<Esc>', actions.backToSessionPicker)
|
|
174
|
+
.map('<Tab>', actions.switchField)
|
|
175
|
+
.map('<CR>', actions.confirmCreateSession)
|
|
176
|
+
.map('<C-n>', actions.moveModalSelection(1))
|
|
177
|
+
.map('<C-p>', actions.moveModalSelection(-1))
|
|
178
|
+
.passthrough()
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// -----------------------------------------------------------------------
|
|
182
|
+
// Modal: snippet-picker
|
|
183
|
+
// (src/input/modes/handlers/modal-snippet-picker.ts)
|
|
184
|
+
// -----------------------------------------------------------------------
|
|
185
|
+
.mode('modal.snippet-picker', (m) =>
|
|
186
|
+
m
|
|
187
|
+
.map('<Esc>', actions.closeModal)
|
|
188
|
+
.map('j', actions.moveModalSelection(1))
|
|
189
|
+
.map('k', actions.moveModalSelection(-1))
|
|
190
|
+
.map('<Down>', actions.moveModalSelection(1))
|
|
191
|
+
.map('<Up>', actions.moveModalSelection(-1))
|
|
192
|
+
.map('<CR>', actions.pasteSelectedSnippet)
|
|
193
|
+
.map('a', actions.pasteSnippetToGroup)
|
|
194
|
+
.map('n', actions.openSnippetEditor)
|
|
195
|
+
.map('r', actions.editSelectedSnippet)
|
|
196
|
+
.map('e', actions.editSelectedSnippet)
|
|
197
|
+
.map('d', actions.deleteSelectedSnippet)
|
|
198
|
+
.map('/', actions.beginSnippetFilter)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// -----------------------------------------------------------------------
|
|
202
|
+
// Modal: snippet-picker filtering
|
|
203
|
+
// (src/input/modes/handlers/modal-snippet-picker-filter.ts)
|
|
204
|
+
// -----------------------------------------------------------------------
|
|
205
|
+
.mode('modal.snippet-picker.filtering', (m) =>
|
|
206
|
+
m
|
|
207
|
+
.map('<Esc>', actions.cancelCommandEdit('modal.snippet-picker'))
|
|
208
|
+
.map('<CR>', actions.snippetFilterPaste)
|
|
209
|
+
.map('<C-a>', actions.snippetFilterPasteToGroup)
|
|
210
|
+
.map('<C-n>', actions.moveModalSelection(1))
|
|
211
|
+
.map('<C-p>', actions.moveModalSelection(-1))
|
|
212
|
+
.passthrough()
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
// -----------------------------------------------------------------------
|
|
216
|
+
// Modal: snippet-editor
|
|
217
|
+
// (src/input/modes/handlers/modal-snippet-editor.ts)
|
|
218
|
+
// -----------------------------------------------------------------------
|
|
219
|
+
.mode('modal.snippet-editor', (m) =>
|
|
220
|
+
m
|
|
221
|
+
.map('<Esc>', actions.backToSnippetPicker)
|
|
222
|
+
.map('<Tab>', actions.switchField)
|
|
223
|
+
.map('<CR>', actions.saveSnippetEditor)
|
|
224
|
+
.map('<C-n>', actions.moveModalSelection(1))
|
|
225
|
+
.map('<C-p>', actions.moveModalSelection(-1))
|
|
226
|
+
.passthrough()
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
// -----------------------------------------------------------------------
|
|
230
|
+
// Modal: split-picker
|
|
231
|
+
// (src/input/modes/handlers/modal-split-picker.ts)
|
|
232
|
+
// -----------------------------------------------------------------------
|
|
233
|
+
.mode('modal.split-picker', (m) =>
|
|
234
|
+
m
|
|
235
|
+
.map('<Esc>', actions.closeModal)
|
|
236
|
+
.map('j', actions.moveModalSelection(1))
|
|
237
|
+
.map('k', actions.moveModalSelection(-1))
|
|
238
|
+
.map('<Down>', actions.moveModalSelection(1))
|
|
239
|
+
.map('<Up>', actions.moveModalSelection(-1))
|
|
240
|
+
.map('<CR>', actions.confirmSplit)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return kb._build()
|
|
244
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AimuxUserConfig } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Define an aimux configuration.
|
|
5
|
+
* This is the entry point for user config files (`~/.config/aimux/aimux.config.ts`).
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { defineConfig, actions } from '@brimveyn/aimux-config'
|
|
9
|
+
*
|
|
10
|
+
* export default defineConfig({
|
|
11
|
+
* keymaps: (k) => k
|
|
12
|
+
* .mode('navigation', (m) => m
|
|
13
|
+
* .map('j', actions.nextTab)
|
|
14
|
+
* .map('k', actions.prevTab)),
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function defineConfig(config: AimuxUserConfig): AimuxUserConfig {
|
|
19
|
+
return config
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Public API — this is what users import from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
export { defineConfig } from './define-config'
|
|
4
|
+
export * as actions from './actions'
|
|
5
|
+
export { THEME_IDS, themes, THEMES } from './themes'
|
|
6
|
+
export { GroupBuilder, KeymapBuilder, ModeBindingBuilder } from './keymap-builder'
|
|
7
|
+
export { getDefaultKeymapConfig } from './defaults'
|
|
8
|
+
export { resolveConfig } from './resolver'
|
|
9
|
+
|
|
10
|
+
// Type exports
|
|
11
|
+
export type {
|
|
12
|
+
// Action types
|
|
13
|
+
Action,
|
|
14
|
+
ActionFn,
|
|
15
|
+
// User-facing config
|
|
16
|
+
AimuxUserConfig,
|
|
17
|
+
AppAction,
|
|
18
|
+
AppState,
|
|
19
|
+
BackendConfig,
|
|
20
|
+
BindingDef,
|
|
21
|
+
FocusMode,
|
|
22
|
+
GroupBuilderApi,
|
|
23
|
+
HooksConfig,
|
|
24
|
+
KeyInput,
|
|
25
|
+
// Keymap builder API (type surface)
|
|
26
|
+
KeymapBuilderApi,
|
|
27
|
+
KeyResult,
|
|
28
|
+
// Layout
|
|
29
|
+
LayoutNode,
|
|
30
|
+
ModalState,
|
|
31
|
+
ModeBindingBuilderApi,
|
|
32
|
+
ModeContext,
|
|
33
|
+
// Mode / state
|
|
34
|
+
ModeId,
|
|
35
|
+
ModeKeymapDef,
|
|
36
|
+
// Resolved config (internal, but exported for tooling)
|
|
37
|
+
ResolvedConfig,
|
|
38
|
+
ResolvedKeymapConfig,
|
|
39
|
+
SessionRecord,
|
|
40
|
+
SidebarConfig,
|
|
41
|
+
SideEffect,
|
|
42
|
+
SnippetDef,
|
|
43
|
+
SnippetRecord,
|
|
44
|
+
SplitDirection,
|
|
45
|
+
TabSession,
|
|
46
|
+
// Themes
|
|
47
|
+
ThemeColors,
|
|
48
|
+
ThemeDefinition,
|
|
49
|
+
ThemeId,
|
|
50
|
+
} from './types'
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Action,
|
|
3
|
+
BindingDef,
|
|
4
|
+
GroupBuilderApi,
|
|
5
|
+
KeymapBuilderApi,
|
|
6
|
+
ModeBindingBuilderApi,
|
|
7
|
+
ModeId,
|
|
8
|
+
ModeKeymapDef,
|
|
9
|
+
ResolvedKeymapConfig,
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// GroupBuilder — sugar for <leader>-prefixed sub-trees
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export class GroupBuilder implements GroupBuilderApi {
|
|
17
|
+
readonly bindings: BindingDef[] = []
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly prefix: string,
|
|
21
|
+
private readonly groupName: string
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
map(keys: string, action: Action): this {
|
|
25
|
+
this.bindings.push({ group: this.groupName, keys: `${this.prefix}${keys}`, result: action })
|
|
26
|
+
return this
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
group(prefix: string, name: string, configure: (g: GroupBuilderApi) => GroupBuilderApi): this {
|
|
30
|
+
const sub = new GroupBuilder(`${this.prefix}${prefix}`, name)
|
|
31
|
+
configure(sub)
|
|
32
|
+
this.bindings.push(...sub.bindings)
|
|
33
|
+
return this
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// ModeBindingBuilder
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export class ModeBindingBuilder implements ModeBindingBuilderApi {
|
|
42
|
+
private readonly bindings: BindingDef[] = []
|
|
43
|
+
private readonly removals: string[] = []
|
|
44
|
+
private _passthrough = false
|
|
45
|
+
|
|
46
|
+
map(keys: string, action: Action): this {
|
|
47
|
+
this.bindings.push({ keys, result: action })
|
|
48
|
+
return this
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
unmap(keys: string): this {
|
|
52
|
+
this.removals.push(keys)
|
|
53
|
+
return this
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
group(prefix: string, name: string, configure: (g: GroupBuilderApi) => GroupBuilderApi): this {
|
|
57
|
+
const gb = new GroupBuilder(prefix, name)
|
|
58
|
+
configure(gb)
|
|
59
|
+
this.bindings.push(...gb.bindings)
|
|
60
|
+
return this
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
passthrough(): this {
|
|
64
|
+
this._passthrough = true
|
|
65
|
+
return this
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @internal */
|
|
69
|
+
_build(): ModeKeymapDef {
|
|
70
|
+
return {
|
|
71
|
+
bindings: this.bindings,
|
|
72
|
+
isPassthrough: this._passthrough,
|
|
73
|
+
removals: this.removals,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// KeymapBuilder
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
export class KeymapBuilder implements KeymapBuilderApi {
|
|
83
|
+
private _leader = '<Space>'
|
|
84
|
+
private _timeout = 300
|
|
85
|
+
private readonly modes = new Map<ModeId, ModeKeymapDef>()
|
|
86
|
+
|
|
87
|
+
leader(key: string): this {
|
|
88
|
+
this._leader = key
|
|
89
|
+
return this
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
timeout(ms: number): this {
|
|
93
|
+
this._timeout = ms
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
mode(id: ModeId, configure: (m: ModeBindingBuilderApi) => ModeBindingBuilderApi): this {
|
|
98
|
+
const builder = new ModeBindingBuilder()
|
|
99
|
+
configure(builder)
|
|
100
|
+
this.modes.set(id, builder._build())
|
|
101
|
+
return this
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @internal — build the resolved config */
|
|
105
|
+
_build(): ResolvedKeymapConfig {
|
|
106
|
+
return {
|
|
107
|
+
leader: this._leader,
|
|
108
|
+
modes: this.modes,
|
|
109
|
+
timeout: this._timeout,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/resolver.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AimuxUserConfig,
|
|
3
|
+
ModeId,
|
|
4
|
+
ModeKeymapDef,
|
|
5
|
+
ResolvedConfig,
|
|
6
|
+
ResolvedKeymapConfig,
|
|
7
|
+
} from './types'
|
|
8
|
+
|
|
9
|
+
import { getDefaultKeymapConfig } from './defaults'
|
|
10
|
+
import { KeymapBuilder } from './keymap-builder'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a user config by merging it with defaults.
|
|
14
|
+
* User bindings override defaults; user unmaps remove defaults.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveConfig(userConfig: AimuxUserConfig): ResolvedConfig {
|
|
17
|
+
const keymaps = resolveKeymaps(userConfig)
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
backends: userConfig.backends ?? {},
|
|
21
|
+
hooks: userConfig.hooks ?? {},
|
|
22
|
+
keymaps,
|
|
23
|
+
sidebar: userConfig.sidebar ?? {},
|
|
24
|
+
snippets: userConfig.snippets ?? [],
|
|
25
|
+
theme: userConfig.theme,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveKeymaps(userConfig: AimuxUserConfig): ResolvedKeymapConfig {
|
|
30
|
+
const defaults = getDefaultKeymapConfig()
|
|
31
|
+
|
|
32
|
+
if (!userConfig.keymaps) {
|
|
33
|
+
return defaults
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Run the user's keymaps callback on a fresh builder
|
|
37
|
+
const userBuilder = new KeymapBuilder()
|
|
38
|
+
userConfig.keymaps(userBuilder)
|
|
39
|
+
const userKeymaps = userBuilder._build()
|
|
40
|
+
|
|
41
|
+
// Merge: user leader/timeout override defaults
|
|
42
|
+
const leader = userKeymaps.leader !== '<Space>' ? userKeymaps.leader : defaults.leader
|
|
43
|
+
const timeout = userKeymaps.timeout !== 300 ? userKeymaps.timeout : defaults.timeout
|
|
44
|
+
|
|
45
|
+
// Merge modes: for each mode, user bindings overlay on top of defaults
|
|
46
|
+
const mergedModes = new Map<ModeId, ModeKeymapDef>(defaults.modes)
|
|
47
|
+
|
|
48
|
+
for (const [modeId, userModeDef] of userKeymaps.modes) {
|
|
49
|
+
const defaultModeDef = defaults.modes.get(modeId)
|
|
50
|
+
|
|
51
|
+
if (!defaultModeDef) {
|
|
52
|
+
// User defines a mode that has no defaults — use as-is
|
|
53
|
+
mergedModes.set(modeId, userModeDef)
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Start with default bindings, remove unmaps, then overlay user bindings
|
|
58
|
+
const removedKeys = new Set(userModeDef.removals)
|
|
59
|
+
const filteredDefaults = defaultModeDef.bindings.filter((b) => !removedKeys.has(b.keys))
|
|
60
|
+
|
|
61
|
+
// Build a map for deduplication: user bindings override same-key defaults
|
|
62
|
+
const userKeySet = new Set(userModeDef.bindings.map((b) => b.keys))
|
|
63
|
+
const mergedBindings = [
|
|
64
|
+
...filteredDefaults.filter((b) => !userKeySet.has(b.keys)),
|
|
65
|
+
...userModeDef.bindings,
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
mergedModes.set(modeId, {
|
|
69
|
+
bindings: mergedBindings,
|
|
70
|
+
isPassthrough: userModeDef.isPassthrough || defaultModeDef.isPassthrough,
|
|
71
|
+
removals: [],
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { leader, modes: mergedModes, timeout }
|
|
76
|
+
}
|