@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.
@@ -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
+ }
@@ -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
+ }