@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BrimVeyn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @brimveyn/aimux-config
|
|
2
|
+
|
|
3
|
+
TypeScript configuration API for [aimux](https://github.com/BrimVeyn/aimux) — the terminal multiplexer for AI CLIs.
|
|
4
|
+
|
|
5
|
+
Write your keymaps, theme, and backends in a typed TypeScript file. Method-chaining builder inspired by nvim, with a prefix-trie resolver that supports leader keys and multi-key sequences.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
mkdir -p ~/.config/aimux && cd ~/.config/aimux
|
|
11
|
+
bun init -y
|
|
12
|
+
bun add -d @brimveyn/aimux-config
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then create `~/.config/aimux/aimux.config.ts`:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { defineConfig, actions, themes } from '@brimveyn/aimux-config'
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
theme: themes.extend('tokyo-night', { accent: '#ff9e64' }),
|
|
22
|
+
|
|
23
|
+
keymaps: (k) =>
|
|
24
|
+
k
|
|
25
|
+
.leader('<Space>')
|
|
26
|
+
.timeout(300)
|
|
27
|
+
.mode('navigation', (m) =>
|
|
28
|
+
m
|
|
29
|
+
.map('j', actions.nextTab)
|
|
30
|
+
.map('k', actions.prevTab)
|
|
31
|
+
.map('<leader>g', actions.sessionPicker)
|
|
32
|
+
.group('<leader>t', 'tabs', (g) =>
|
|
33
|
+
g.map('n', actions.newTab).map('r', actions.renameTab).map('x', actions.closeTab)
|
|
34
|
+
)
|
|
35
|
+
),
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Key notation
|
|
40
|
+
|
|
41
|
+
| Notation | Matches |
|
|
42
|
+
| ----------------- | ------------------------------ |
|
|
43
|
+
| `j` | Bare character `j` |
|
|
44
|
+
| `J` | Shift+J (uppercase letter) |
|
|
45
|
+
| `<C-n>` | Ctrl+N |
|
|
46
|
+
| `<M-x>` / `<A-x>` | Meta/Alt+X |
|
|
47
|
+
| `<C-M-a>` | Ctrl+Alt+A |
|
|
48
|
+
| `<CR>` | Return/Enter |
|
|
49
|
+
| `<Esc>` | Escape |
|
|
50
|
+
| `<Space>` | Spacebar |
|
|
51
|
+
| `<Tab>` | Tab |
|
|
52
|
+
| `<BS>` | Backspace |
|
|
53
|
+
| `<Up>` `<Down>` | Arrow keys |
|
|
54
|
+
| `<leader>` | Configured leader chord |
|
|
55
|
+
| `dd` | Multi-key sequence (d, then d) |
|
|
56
|
+
| `<leader>tn` | Leader, then t, then n |
|
|
57
|
+
|
|
58
|
+
Ambiguous prefixes (e.g., `d` is bound AND `dd` is bound) are resolved after a configurable timeout (default 300ms).
|
|
59
|
+
|
|
60
|
+
## Builder API
|
|
61
|
+
|
|
62
|
+
### Top-level config
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
defineConfig({
|
|
66
|
+
theme?: ThemeId | ThemeDefinition
|
|
67
|
+
keymaps?: (k: KeymapBuilder) => KeymapBuilder
|
|
68
|
+
backends?: Record<string, BackendConfig> // stub for future use
|
|
69
|
+
sidebar?: SidebarConfig // stub
|
|
70
|
+
hooks?: HooksConfig // stub
|
|
71
|
+
snippets?: SnippetDef[] // stub
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Keymap builder
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
k.leader(keys) // default: '<Space>'
|
|
79
|
+
.timeout(ms) // default: 300
|
|
80
|
+
.mode(id, configure) // define bindings for a mode
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Mode builder
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
m.map(keys, action) // bind a key/sequence to an action
|
|
87
|
+
.unmap(keys) // remove a default binding
|
|
88
|
+
.group(prefix, name, g) // sugar for leader-prefixed sub-trees
|
|
89
|
+
.passthrough() // for text-input modes: unmatched keys route to text input
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Groups
|
|
93
|
+
|
|
94
|
+
Groups organize leader-key sub-trees. `.group('<leader>t', 'tabs', g => g.map('n', ...))` is sugar for `.map('<leader>tn', ...)` with a `name` label used by the help modal.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
.group('<leader>t', 'tabs', (g) => g
|
|
98
|
+
.map('n', actions.newTab)
|
|
99
|
+
.map('r', actions.renameTab)
|
|
100
|
+
.map('x', actions.closeTab))
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Actions
|
|
104
|
+
|
|
105
|
+
Pre-built actions cover every built-in aimux operation:
|
|
106
|
+
|
|
107
|
+
**Tab control** — `nextTab`, `prevTab`, `newTab`, `renameTab`, `closeTab`, `restartTab`, `moveTab(n)`, `reorderTab(n)`
|
|
108
|
+
|
|
109
|
+
**Modals** — `sessionPicker`, `snippetPicker`, `themePicker`, `helpModal`, `closeModal`
|
|
110
|
+
|
|
111
|
+
**Sidebar / panels** — `toggleSidebar`, `resizeSidebar(n)`, `toggleGitPanel`, `resizeGitPanel(n)`
|
|
112
|
+
|
|
113
|
+
**Layout / splits** — `splitVertical`, `splitHorizontal`, `focusPane('left'|'right'|'up'|'down')`, `resizePane(n, 'horizontal'|'vertical')`, `closePane`
|
|
114
|
+
|
|
115
|
+
**Mode transitions** — `enterInsert`, `enterLayoutMode`, `leaveTerminalInput`, `quit`
|
|
116
|
+
|
|
117
|
+
**Custom actions** — write an `ActionFn` for dynamic logic:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
.mode('navigation', (m) => m
|
|
121
|
+
.map('gT', (ctx) => {
|
|
122
|
+
const tabId = ctx.state.activeTabId
|
|
123
|
+
if (!tabId) return null
|
|
124
|
+
return {
|
|
125
|
+
actions: [{ type: 'close-active-tab' }],
|
|
126
|
+
effects: [{ type: 'close-tab', tabId }],
|
|
127
|
+
}
|
|
128
|
+
}))
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Themes
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { themes } from '@brimveyn/aimux-config'
|
|
135
|
+
|
|
136
|
+
// Extend a built-in theme
|
|
137
|
+
themes.extend('tokyo-night', {
|
|
138
|
+
accent: '#ff9e64',
|
|
139
|
+
background: '#1a1b26',
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Create a custom theme
|
|
143
|
+
themes.create({
|
|
144
|
+
accent: '#...',
|
|
145
|
+
accentAlt: '#...',
|
|
146
|
+
background: '#...',
|
|
147
|
+
// ... all ThemeColors keys required
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Built-in themes: `aimux`, `tokyo-night`, `dracula`, `dracula-at-night`, `everforest`, `gruvbox-dark`, `catppuccin-mocha`, `nord`, `solarized-dark`, `one-dark`, `kanagawa`.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brimveyn/aimux-config",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript configuration API for aimux — keymaps, themes, and backends with a fluent builder.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"aimux",
|
|
7
|
+
"config",
|
|
8
|
+
"keybindings",
|
|
9
|
+
"keymap",
|
|
10
|
+
"leader-key",
|
|
11
|
+
"multiplexer",
|
|
12
|
+
"terminal",
|
|
13
|
+
"trie",
|
|
14
|
+
"tui"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/BrimVeyn/aimux#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/BrimVeyn/aimux/issues"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "BrimVeyn",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/BrimVeyn/aimux.git",
|
|
25
|
+
"directory": "packages/aimux-config"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": "./src/index.ts",
|
|
35
|
+
"./backends": "./src/backends.ts"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/actions.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import type { ActionFn, KeyResult, ModeContext } from './types'
|
|
2
|
+
|
|
3
|
+
function r(
|
|
4
|
+
actions: KeyResult['actions'] = [],
|
|
5
|
+
effects: KeyResult['effects'] = [],
|
|
6
|
+
transition?: KeyResult['transition']
|
|
7
|
+
): KeyResult {
|
|
8
|
+
return transition ? { actions, effects, transition } : { actions, effects }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Static actions (KeyResult values — no ctx needed)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export const nextTab: KeyResult = r([{ delta: 1, type: 'move-active-tab' }])
|
|
16
|
+
export const prevTab: KeyResult = r([{ delta: -1, type: 'move-active-tab' }])
|
|
17
|
+
|
|
18
|
+
export const newTab: KeyResult = r([{ type: 'open-new-tab-modal' }], [], 'modal.new-tab')
|
|
19
|
+
export const renameTab: KeyResult = r([{ type: 'open-rename-tab-modal' }], [], 'modal.rename-tab')
|
|
20
|
+
export const sessionPicker: KeyResult = r(
|
|
21
|
+
[{ type: 'open-session-picker' }],
|
|
22
|
+
[],
|
|
23
|
+
'modal.session-picker'
|
|
24
|
+
)
|
|
25
|
+
export const snippetPicker: KeyResult = r(
|
|
26
|
+
[{ type: 'open-snippet-picker' }],
|
|
27
|
+
[],
|
|
28
|
+
'modal.snippet-picker'
|
|
29
|
+
)
|
|
30
|
+
export const themePicker: KeyResult = r(
|
|
31
|
+
[{ type: 'open-theme-picker' }],
|
|
32
|
+
[{ action: 'open', type: 'apply-theme' }],
|
|
33
|
+
'modal.theme-picker'
|
|
34
|
+
)
|
|
35
|
+
export const helpModal: KeyResult = r([{ type: 'open-help-modal' }], [], 'modal.help')
|
|
36
|
+
|
|
37
|
+
export const toggleSidebar: KeyResult = r([{ type: 'toggle-sidebar' }])
|
|
38
|
+
export const toggleGitPanel: KeyResult = r([{ type: 'toggle-git-panel' }])
|
|
39
|
+
|
|
40
|
+
export const splitVertical: KeyResult = r(
|
|
41
|
+
[{ direction: 'vertical', type: 'open-split-picker' }],
|
|
42
|
+
[],
|
|
43
|
+
'modal.split-picker'
|
|
44
|
+
)
|
|
45
|
+
export const splitHorizontal: KeyResult = r(
|
|
46
|
+
[{ direction: 'horizontal', type: 'open-split-picker' }],
|
|
47
|
+
[],
|
|
48
|
+
'modal.split-picker'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
export const enterInsert: ActionFn = (ctx: ModeContext) => {
|
|
52
|
+
if (!ctx.state.activeTabId) return null
|
|
53
|
+
return r([{ focusMode: 'terminal-input', type: 'set-focus-mode' }], [], 'terminal-input')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const closeModal: KeyResult = r([{ type: 'close-modal' }], [], 'navigation')
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Dynamic actions (need ctx at runtime — ActionFn)
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export const closeTab: ActionFn = (ctx: ModeContext) => {
|
|
63
|
+
const tabId = ctx.state.activeTabId
|
|
64
|
+
if (!tabId) return null
|
|
65
|
+
return r([{ type: 'close-active-tab' }], [{ tabId, type: 'close-tab' }])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const restartTab: ActionFn = (ctx: ModeContext) => {
|
|
69
|
+
const tab = ctx.state.tabs.find((t) => t.id === ctx.state.activeTabId)
|
|
70
|
+
if (!tab) return null
|
|
71
|
+
return r([], [{ tab, type: 'restart-tab' }])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const quit: ActionFn = (ctx: ModeContext) => {
|
|
75
|
+
return r([], [{ state: ctx.state, type: 'quit' }])
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Parameterized factories
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
export function moveTab(delta: number): KeyResult {
|
|
83
|
+
return r([{ delta, type: 'move-active-tab' }])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function reorderTab(delta: number): KeyResult {
|
|
87
|
+
return r([{ delta, type: 'reorder-active-tab' }])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resizeSidebar(delta: number): KeyResult {
|
|
91
|
+
return r([{ delta, type: 'resize-sidebar' }])
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resizeGitPanel(delta: number): KeyResult {
|
|
95
|
+
return r([{ delta, type: 'resize-git-panel' }])
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function focusPane(direction: 'left' | 'right' | 'up' | 'down'): KeyResult {
|
|
99
|
+
return r(
|
|
100
|
+
[
|
|
101
|
+
{ direction, type: 'focus-pane-direction' },
|
|
102
|
+
{ focusMode: 'terminal-input', type: 'set-focus-mode' },
|
|
103
|
+
],
|
|
104
|
+
[],
|
|
105
|
+
'terminal-input'
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resizePane(delta: number, axis: 'horizontal' | 'vertical'): ActionFn {
|
|
110
|
+
return (ctx: ModeContext) => {
|
|
111
|
+
const tabId = ctx.state.activeTabId
|
|
112
|
+
if (!tabId) return null
|
|
113
|
+
return r([{ axis, delta, tabId, type: 'resize-pane' }])
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function moveModalSelection(delta: number): KeyResult {
|
|
118
|
+
return r([{ delta, type: 'move-modal-selection' }])
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function moveModalSelectionWithPreview(
|
|
122
|
+
delta: number,
|
|
123
|
+
effects: KeyResult['effects']
|
|
124
|
+
): KeyResult {
|
|
125
|
+
return r([{ delta, type: 'move-modal-selection' }], effects)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Modal-specific actions
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
export const launchSelectedAssistant: KeyResult = r([], [{ type: 'launch-selected-assistant' }])
|
|
133
|
+
|
|
134
|
+
export const beginCommandEdit: KeyResult = r(
|
|
135
|
+
[{ type: 'begin-command-edit' }],
|
|
136
|
+
[],
|
|
137
|
+
'modal.new-tab.command-edit'
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
export const cancelCommandEdit = (returnTo: KeyResult['transition']): KeyResult =>
|
|
141
|
+
r([{ type: 'cancel-command-edit' }], [], returnTo)
|
|
142
|
+
|
|
143
|
+
export const commitCommandEdit: KeyResult = r(
|
|
144
|
+
[{ type: 'commit-command-edit' }],
|
|
145
|
+
[{ type: 'save-custom-command' }],
|
|
146
|
+
'modal.new-tab'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
export const confirmSelectedSession: KeyResult = r([], [{ type: 'confirm-selected-session' }])
|
|
150
|
+
|
|
151
|
+
export const openCreateSessionModal: KeyResult = r(
|
|
152
|
+
[{ type: 'open-create-session-modal' }],
|
|
153
|
+
[],
|
|
154
|
+
'modal.create-session'
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
export const openRenameSelectedSession: KeyResult = r(
|
|
158
|
+
[],
|
|
159
|
+
[{ type: 'open-rename-selected-session' }]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
export const deleteSelectedSession: KeyResult = r([], [{ type: 'delete-selected-session' }])
|
|
163
|
+
|
|
164
|
+
export const beginSessionFilter: KeyResult = r(
|
|
165
|
+
[{ type: 'begin-session-filter' }],
|
|
166
|
+
[],
|
|
167
|
+
'modal.session-picker.filtering'
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
export const openSnippetEditor: KeyResult = r(
|
|
171
|
+
[{ type: 'open-snippet-editor' }],
|
|
172
|
+
[],
|
|
173
|
+
'modal.snippet-editor'
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
export const editSelectedSnippet: KeyResult = r([], [{ type: 'edit-selected-snippet' }])
|
|
177
|
+
export const deleteSelectedSnippet: KeyResult = r([], [{ type: 'delete-selected-snippet' }])
|
|
178
|
+
|
|
179
|
+
export const pasteSelectedSnippet: KeyResult = r(
|
|
180
|
+
[{ type: 'close-modal' }],
|
|
181
|
+
[{ type: 'paste-selected-snippet' }],
|
|
182
|
+
'navigation'
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
export const pasteSnippetToGroup: KeyResult = r(
|
|
186
|
+
[{ type: 'close-modal' }],
|
|
187
|
+
[{ type: 'paste-snippet-to-group' }],
|
|
188
|
+
'navigation'
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
export const beginSnippetFilter: KeyResult = r(
|
|
192
|
+
[{ type: 'begin-snippet-filter' }],
|
|
193
|
+
[],
|
|
194
|
+
'modal.snippet-picker.filtering'
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
export const confirmSplit: KeyResult = r([], [{ type: 'confirm-split' }])
|
|
198
|
+
|
|
199
|
+
export const restoreTheme: KeyResult = r(
|
|
200
|
+
[{ type: 'close-modal' }],
|
|
201
|
+
[{ action: 'restore', type: 'apply-theme' }],
|
|
202
|
+
'navigation'
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
export const confirmTheme: KeyResult = r(
|
|
206
|
+
[{ type: 'close-modal' }],
|
|
207
|
+
[{ action: 'confirm', type: 'apply-theme' }],
|
|
208
|
+
'navigation'
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
export function previewTheme(delta: 1 | -1): KeyResult {
|
|
212
|
+
return r(
|
|
213
|
+
[{ delta, type: 'move-modal-selection' }],
|
|
214
|
+
[{ action: 'preview', delta, type: 'apply-theme' }]
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const switchField: KeyResult = r([{ type: 'switch-create-session-field' }])
|
|
219
|
+
export const selectDirectory: KeyResult = r([{ type: 'select-directory' }])
|
|
220
|
+
|
|
221
|
+
export const backToSessionPicker: KeyResult = r(
|
|
222
|
+
[{ type: 'open-session-picker' }],
|
|
223
|
+
[],
|
|
224
|
+
'modal.session-picker'
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
export const backToSnippetPicker: KeyResult = r(
|
|
228
|
+
[{ type: 'open-snippet-picker' }],
|
|
229
|
+
[],
|
|
230
|
+
'modal.snippet-picker'
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
export const saveSnippetEditor: KeyResult = r(
|
|
234
|
+
[{ type: 'close-modal' }],
|
|
235
|
+
[{ type: 'save-snippet-editor' }],
|
|
236
|
+
'navigation'
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
// Layout-specific
|
|
240
|
+
export const exitLayoutToInput: KeyResult = r(
|
|
241
|
+
[{ focusMode: 'terminal-input', type: 'set-focus-mode' }],
|
|
242
|
+
[],
|
|
243
|
+
'terminal-input'
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
export const exitLayoutToNavigation: ActionFn = (ctx: ModeContext) => {
|
|
247
|
+
const actions: KeyResult['actions'] = [{ focusMode: 'navigation', type: 'set-focus-mode' }]
|
|
248
|
+
if (!ctx.state.sidebar.visible) {
|
|
249
|
+
actions.push({ type: 'toggle-sidebar' })
|
|
250
|
+
}
|
|
251
|
+
return r(actions, [], 'navigation')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export const closePane: ActionFn = (ctx: ModeContext) => {
|
|
255
|
+
const tabId = ctx.state.activeTabId
|
|
256
|
+
if (!tabId) return null
|
|
257
|
+
return r(
|
|
258
|
+
[
|
|
259
|
+
{ tabId, type: 'close-pane' },
|
|
260
|
+
{ focusMode: 'terminal-input', type: 'set-focus-mode' },
|
|
261
|
+
],
|
|
262
|
+
[{ tabId, type: 'close-tab' }],
|
|
263
|
+
'terminal-input'
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Navigation-specific dynamic actions
|
|
268
|
+
export const ctrlZSidebar: ActionFn = (ctx: ModeContext) => {
|
|
269
|
+
if (!ctx.state.sidebar.visible) {
|
|
270
|
+
return r([{ type: 'toggle-sidebar' }])
|
|
271
|
+
}
|
|
272
|
+
return r([])
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Terminal-input escape shortcuts (used by raw-input-handler)
|
|
277
|
+
// These fire while the user is actively in terminal-input mode.
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
export const leaveTerminalInput: KeyResult = r(
|
|
281
|
+
[{ focusMode: 'navigation', type: 'set-focus-mode' }],
|
|
282
|
+
[],
|
|
283
|
+
'navigation'
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
export const enterLayoutMode: KeyResult = r(
|
|
287
|
+
[{ focusMode: 'layout', type: 'set-focus-mode' }],
|
|
288
|
+
[],
|
|
289
|
+
'layout'
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
export const toggleSidebarFromInput: KeyResult = r([{ type: 'toggle-sidebar' }])
|
|
293
|
+
|
|
294
|
+
// Session name modal
|
|
295
|
+
export const confirmSessionRename: ActionFn = (ctx: ModeContext) => {
|
|
296
|
+
const trimmed = (ctx.state.modal.editBuffer ?? '').trim()
|
|
297
|
+
const sessionId = ctx.state.modal.sessionTargetId
|
|
298
|
+
if (trimmed && sessionId) {
|
|
299
|
+
return r(
|
|
300
|
+
[{ type: 'open-session-picker' }],
|
|
301
|
+
[{ name: trimmed, sessionId, type: 'rename-session' }],
|
|
302
|
+
'modal.session-picker'
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
return r([{ type: 'open-session-picker' }], [], 'modal.session-picker')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Rename tab modal
|
|
309
|
+
export const confirmRenameTab: ActionFn = (ctx: ModeContext) => {
|
|
310
|
+
const trimmed = (ctx.state.modal.editBuffer ?? '').trim()
|
|
311
|
+
const tabId = ctx.state.modal.sessionTargetId
|
|
312
|
+
const actions: KeyResult['actions'] = []
|
|
313
|
+
if (trimmed && tabId) {
|
|
314
|
+
actions.push({ tabId, title: trimmed, type: 'rename-tab' })
|
|
315
|
+
}
|
|
316
|
+
actions.push({ type: 'close-modal' })
|
|
317
|
+
return r(actions, [], 'navigation')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Create session modal
|
|
321
|
+
export const confirmCreateSession: ActionFn = (ctx: ModeContext) => {
|
|
322
|
+
const modal = ctx.state.modal as {
|
|
323
|
+
activeField?: string
|
|
324
|
+
editBuffer?: string
|
|
325
|
+
pendingProjectPath?: string
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (modal.activeField === 'directory') {
|
|
329
|
+
return r([{ type: 'select-directory' }])
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const trimmed = (modal.editBuffer ?? '').trim()
|
|
333
|
+
const projectPath = modal.pendingProjectPath ?? undefined
|
|
334
|
+
const sessionName = trimmed || getDefaultSessionName(projectPath)
|
|
335
|
+
if (sessionName) {
|
|
336
|
+
return r(
|
|
337
|
+
[{ type: 'close-modal' }],
|
|
338
|
+
[{ name: sessionName, projectPath, type: 'create-session' }],
|
|
339
|
+
'navigation'
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
return r([{ type: 'close-modal' }], [], 'navigation')
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function getDefaultSessionName(projectPath?: string): string {
|
|
346
|
+
if (!projectPath) return ''
|
|
347
|
+
const segments = projectPath.split('/').filter(Boolean)
|
|
348
|
+
return segments.at(-1) ?? ''
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Session picker escape (conditional)
|
|
352
|
+
export const sessionPickerEscape: ActionFn = (ctx: ModeContext) => {
|
|
353
|
+
if (!ctx.state.currentSessionId) return null
|
|
354
|
+
return r([{ type: 'close-modal' }], [], 'navigation')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Snippet filter: paste from filter + close
|
|
358
|
+
export const snippetFilterPaste: KeyResult = r(
|
|
359
|
+
[{ type: 'close-modal' }],
|
|
360
|
+
[{ type: 'paste-selected-snippet' }],
|
|
361
|
+
'navigation'
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
export const snippetFilterPasteToGroup: KeyResult = r(
|
|
365
|
+
[{ type: 'close-modal' }],
|
|
366
|
+
[{ type: 'paste-snippet-to-group' }],
|
|
367
|
+
'navigation'
|
|
368
|
+
)
|
package/src/backends.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BackendConfig } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a Claude backend configuration.
|
|
5
|
+
* Stub — runtime wiring deferred to follow-up work.
|
|
6
|
+
*/
|
|
7
|
+
export function claudeBackend(opts?: { model?: string }): BackendConfig {
|
|
8
|
+
return {
|
|
9
|
+
args: opts?.model ? ['--model', opts.model] : [],
|
|
10
|
+
command: 'claude',
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a Codex backend configuration.
|
|
16
|
+
* Stub — runtime wiring deferred to follow-up work.
|
|
17
|
+
*/
|
|
18
|
+
export function codexBackend(opts?: { args?: string[] }): BackendConfig {
|
|
19
|
+
return {
|
|
20
|
+
args: opts?.args ?? [],
|
|
21
|
+
command: 'codex',
|
|
22
|
+
}
|
|
23
|
+
}
|