@brimveyn/aimux 1.7.1 → 1.7.3
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/package.json +3 -4
- package/src/app-runtime/pty-write.ts +15 -3
- package/src/app-runtime/side-effects.ts +10 -2
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app.tsx +5 -8
- package/src/config.ts +2 -10
- package/src/daemon/daemon.ts +7 -2
- package/src/input/modes/bridge.ts +1 -1
- package/src/input/modes/types.ts +6 -1
- package/src/input/raw-input-handler.ts +12 -3
- package/src/pty/terminal-snapshot.ts +4 -4
- package/src/state/reducers/modal-state.ts +3 -2
- package/src/state/reducers/session-state.ts +0 -7
- package/src/state/types.ts +7 -1
- package/src/ui/components/bare-input.tsx +5 -5
- package/src/ui/components/context-menu-box.tsx +21 -0
- package/src/ui/components/context-menu-overlay.tsx +114 -0
- package/src/ui/components/create-session-modal.tsx +12 -41
- package/src/ui/components/diff-renderer/fold-strip.tsx +7 -7
- package/src/ui/components/diff-renderer/highlight.ts +6 -10
- package/src/ui/components/diff-renderer/pierre-diff.tsx +3 -3
- package/src/ui/components/diff-renderer/prepare-diff.ts +2 -3
- package/src/ui/components/diff-renderer/split-view.tsx +22 -29
- package/src/ui/components/diff-renderer/stacked-view.tsx +18 -31
- package/src/ui/components/diff-renderer/use-diff-prefetch.ts +0 -1
- package/src/ui/components/diff-renderer/use-diff-preparation.ts +1 -1
- package/src/ui/components/git-commit-modal.tsx +4 -16
- package/src/ui/components/git-panel.tsx +37 -73
- package/src/ui/components/git-view.tsx +17 -19
- package/src/ui/components/help-modal.tsx +5 -13
- package/src/ui/components/input-field.tsx +5 -7
- package/src/ui/components/list-item.tsx +3 -11
- package/src/ui/components/modal-keybinds-overlay.tsx +4 -4
- package/src/ui/components/modal-shell.tsx +6 -11
- package/src/ui/components/new-tab-modal.tsx +7 -15
- package/src/ui/components/pending-chord-overlay.tsx +5 -5
- package/src/ui/components/picker.tsx +6 -6
- package/src/ui/components/session-bar.tsx +34 -20
- package/src/ui/components/session-picker-modal.tsx +7 -20
- package/src/ui/components/sidebar.tsx +36 -35
- package/src/ui/components/snippet-editor-modal.tsx +4 -18
- package/src/ui/components/snippet-picker-modal.tsx +5 -9
- package/src/ui/components/split-layout.tsx +3 -3
- package/src/ui/components/status-bar.tsx +12 -13
- package/src/ui/components/surface.tsx +8 -11
- package/src/ui/components/tab-item.tsx +54 -29
- package/src/ui/components/terminal-pane.tsx +59 -25
- package/src/ui/components/theme-picker-modal.tsx +7 -21
- package/src/ui/components/update-available-modal.tsx +3 -13
- package/src/ui/context-menu/controller.ts +34 -0
- package/src/ui/root.tsx +5 -2
- package/src/ui/shiki.ts +29 -21
- package/src/ui/theme-store.ts +110 -21
- package/src/ui/theme.ts +6 -3
- package/src/ui/themes.ts +18 -13
|
@@ -6,7 +6,10 @@ import type { TerminalContentOrigin } from '../../input/raw-input-handler'
|
|
|
6
6
|
import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../state/types'
|
|
7
7
|
|
|
8
8
|
import { logInputDebug } from '../../debug/input-log'
|
|
9
|
-
import {
|
|
9
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
10
|
+
import { type ContextMenuItem, openContextMenu } from '../context-menu/controller'
|
|
11
|
+
import { getCurrentTokens, useBg, useTokens } from '../theme'
|
|
12
|
+
import { ContextMenuBox } from './context-menu-box'
|
|
10
13
|
|
|
11
14
|
interface TerminalPaneProps {
|
|
12
15
|
tab?: TabSession
|
|
@@ -45,15 +48,16 @@ function getTitle(
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
function getBorderColor(isActive: boolean, focusMode: TerminalPaneProps['focusMode']): string {
|
|
51
|
+
const t = getCurrentTokens()
|
|
48
52
|
if (isActive && focusMode === 'terminal-input') {
|
|
49
|
-
return
|
|
53
|
+
return t.palette.primary
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
if (isActive) {
|
|
53
|
-
return
|
|
57
|
+
return t.accent
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
return
|
|
60
|
+
return t.hover
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
function renderSpan(span: TerminalSpan, key: string): ReactNode {
|
|
@@ -72,7 +76,7 @@ function renderSpan(span: TerminalSpan, key: string): ReactNode {
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
return (
|
|
75
|
-
<span key={key} fg={span.fg ??
|
|
79
|
+
<span key={key} fg={span.fg ?? getCurrentTokens().palette.ink} bg={span.bg}>
|
|
76
80
|
{node}
|
|
77
81
|
</span>
|
|
78
82
|
)
|
|
@@ -87,11 +91,11 @@ const TerminalViewport = memo(function TerminalViewport({
|
|
|
87
91
|
buffer,
|
|
88
92
|
viewport,
|
|
89
93
|
}: TerminalViewportProps) {
|
|
90
|
-
const
|
|
94
|
+
const t = useTokens()
|
|
91
95
|
if (viewport && viewport.lines.length > 0) {
|
|
92
96
|
const lines = viewport.lines
|
|
93
97
|
return (
|
|
94
|
-
<text fg={
|
|
98
|
+
<text fg={t.palette.ink}>
|
|
95
99
|
{lines.map((line, lineIndex) => (
|
|
96
100
|
<span key={`line-${lineIndex}`}>
|
|
97
101
|
{line.spans.map((span, spanIndex) => renderSpan(span, `s-${spanIndex}`))}
|
|
@@ -103,9 +107,7 @@ const TerminalViewport = memo(function TerminalViewport({
|
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
return (
|
|
106
|
-
<text fg={
|
|
107
|
-
{buffer.length > 0 ? buffer : 'Waiting for session output...'}
|
|
108
|
-
</text>
|
|
110
|
+
<text fg={t.palette.ink}>{buffer.length > 0 ? buffer : 'Waiting for session output...'}</text>
|
|
109
111
|
)
|
|
110
112
|
})
|
|
111
113
|
|
|
@@ -124,12 +126,47 @@ export function TerminalPane({
|
|
|
124
126
|
tab,
|
|
125
127
|
tabId,
|
|
126
128
|
}: TerminalPaneProps) {
|
|
127
|
-
const
|
|
128
|
-
const editorBg = useBg('
|
|
129
|
+
const t = useTokens()
|
|
130
|
+
const editorBg = useBg('base')
|
|
129
131
|
const paneIsActive = isActive ?? true
|
|
130
132
|
const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
|
|
131
133
|
const canUseLocalScrollback = focusMode === 'terminal-input' && !!tab && localScrollbackEnabled
|
|
134
|
+
const rightClickMenu: ContextMenuItem[] | undefined = tabId
|
|
135
|
+
? [
|
|
136
|
+
[
|
|
137
|
+
'Split vertically',
|
|
138
|
+
() =>
|
|
139
|
+
runSideEffectGlobal({
|
|
140
|
+
direction: 'vertical',
|
|
141
|
+
sourceTabId: tabId,
|
|
142
|
+
type: 'split-pane',
|
|
143
|
+
}),
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
'Split horizontally',
|
|
147
|
+
() =>
|
|
148
|
+
runSideEffectGlobal({
|
|
149
|
+
direction: 'horizontal',
|
|
150
|
+
sourceTabId: tabId,
|
|
151
|
+
type: 'split-pane',
|
|
152
|
+
}),
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
'Close pane',
|
|
156
|
+
() => {
|
|
157
|
+
dispatchGlobal({ tabId, type: 'close-pane' })
|
|
158
|
+
runSideEffectGlobal({ tabId, type: 'close-tab' })
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
]
|
|
162
|
+
: undefined
|
|
132
163
|
const forwardMouseEvent = (event: OtuiMouseEvent) => {
|
|
164
|
+
if (event.type === 'down' && event.button === 2 && rightClickMenu) {
|
|
165
|
+
event.preventDefault()
|
|
166
|
+
event.stopPropagation()
|
|
167
|
+
openContextMenu(event.x, event.y, rightClickMenu)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
133
170
|
if (event.type === 'down') {
|
|
134
171
|
logInputDebug('pane.mouseDown', {
|
|
135
172
|
button: event.button,
|
|
@@ -179,7 +216,7 @@ export function TerminalPane({
|
|
|
179
216
|
}
|
|
180
217
|
return (
|
|
181
218
|
<box flexDirection="column" flexGrow={1} gap={0}>
|
|
182
|
-
<
|
|
219
|
+
<ContextMenuBox
|
|
183
220
|
border
|
|
184
221
|
borderColor={getBorderColor(paneIsActive, focusMode)}
|
|
185
222
|
title={getTitle(tab, paneIsActive, focusMode)}
|
|
@@ -187,6 +224,7 @@ export function TerminalPane({
|
|
|
187
224
|
flexDirection="column"
|
|
188
225
|
flexGrow={1}
|
|
189
226
|
backgroundColor={editorBg}
|
|
227
|
+
rightClickMenu={rightClickMenu}
|
|
190
228
|
onMouseDown={forwardMouseEvent}
|
|
191
229
|
onMouseDrag={forwardMouseEvent}
|
|
192
230
|
onMouseScroll={forwardScrollEvent}
|
|
@@ -194,12 +232,12 @@ export function TerminalPane({
|
|
|
194
232
|
>
|
|
195
233
|
{!tab ? (
|
|
196
234
|
<box flexGrow={1} justifyContent="center" alignItems="center" flexDirection="column">
|
|
197
|
-
<text fg={
|
|
198
|
-
<text fg={
|
|
235
|
+
<text fg={t.hover}>· · ·</text>
|
|
236
|
+
<text fg={t.muted}> </text>
|
|
199
237
|
<box flexDirection="row">
|
|
200
|
-
<text fg={
|
|
201
|
-
<text fg={
|
|
202
|
-
<text fg={
|
|
238
|
+
<text fg={t.muted}>Press </text>
|
|
239
|
+
<text fg={t.palette.primary}>Ctrl+n</text>
|
|
240
|
+
<text fg={t.muted}> to launch an assistant</text>
|
|
203
241
|
</box>
|
|
204
242
|
</box>
|
|
205
243
|
) : (
|
|
@@ -218,15 +256,11 @@ export function TerminalPane({
|
|
|
218
256
|
<TerminalViewport viewport={tab.viewport} buffer={tab.buffer} />
|
|
219
257
|
</box>
|
|
220
258
|
)}
|
|
221
|
-
</
|
|
259
|
+
</ContextMenuBox>
|
|
222
260
|
{tab?.status === 'disconnected' ? (
|
|
223
|
-
<text fg={
|
|
224
|
-
Restored snapshot. Press Ctrl+r to restart this session.
|
|
225
|
-
</text>
|
|
226
|
-
) : null}
|
|
227
|
-
{tab?.errorMessage ? (
|
|
228
|
-
<text fg={theme.colors['editorError.foreground']}>{tab.errorMessage}</text>
|
|
261
|
+
<text fg={t.palette.warning}>Restored snapshot. Press Ctrl+r to restart this session.</text>
|
|
229
262
|
) : null}
|
|
263
|
+
{tab?.errorMessage ? <text fg={t.palette.error}>{tab.errorMessage}</text> : null}
|
|
230
264
|
</box>
|
|
231
265
|
)
|
|
232
266
|
}
|
|
@@ -2,7 +2,7 @@ import { useLayoutEffect, useMemo } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
4
4
|
import { filterThemeIds } from '../filter-themes'
|
|
5
|
-
import {
|
|
5
|
+
import { useTokens, useTransparent } from '../theme'
|
|
6
6
|
import { type ThemeId, THEMES } from '../themes'
|
|
7
7
|
import { uiTokens } from '../ui-tokens'
|
|
8
8
|
import { Picker, type PickerItem } from './picker'
|
|
@@ -25,7 +25,7 @@ export function ThemePickerModal({
|
|
|
25
25
|
filter,
|
|
26
26
|
selectedIndex,
|
|
27
27
|
}: ThemePickerModalProps) {
|
|
28
|
-
const
|
|
28
|
+
const t = useTokens()
|
|
29
29
|
const transparent = useTransparent()
|
|
30
30
|
const filtered = useMemo(() => filterThemeIds(filter), [filter])
|
|
31
31
|
|
|
@@ -47,16 +47,8 @@ export function ThemePickerModal({
|
|
|
47
47
|
dispatchGlobal({ type: 'close-modal' })
|
|
48
48
|
runSideEffectGlobal({ action: 'confirm', type: 'apply-theme' })
|
|
49
49
|
},
|
|
50
|
-
title:
|
|
51
|
-
|
|
52
|
-
fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
53
|
-
>
|
|
54
|
-
{entry.name}
|
|
55
|
-
</text>
|
|
56
|
-
),
|
|
57
|
-
trailing: isCurrent ? (
|
|
58
|
-
<text fg={theme.colors['textLink.foreground']}>current</text>
|
|
59
|
-
) : undefined,
|
|
50
|
+
title: <text fg={active ? t.palette.ink : t.muted}>{entry.displayName}</text>,
|
|
51
|
+
trailing: isCurrent ? <text fg={t.palette.primary}>current</text> : undefined,
|
|
60
52
|
},
|
|
61
53
|
]
|
|
62
54
|
})
|
|
@@ -70,21 +62,15 @@ export function ThemePickerModal({
|
|
|
70
62
|
cursorPos={cursorPos}
|
|
71
63
|
footer={
|
|
72
64
|
<box flexDirection="column" gap={0}>
|
|
73
|
-
<text fg={
|
|
65
|
+
<text fg={t.hover}>
|
|
74
66
|
{filtered.length === 0 ? '' : ` ${effectiveIndex + 1} / ${filtered.length}`}
|
|
75
67
|
</text>
|
|
76
|
-
<text fg={
|
|
77
|
-
{` transparent: ${transparent ? 'on' : 'off'} (ctrl-t)`}
|
|
78
|
-
</text>
|
|
68
|
+
<text fg={t.muted}>{` transparent: ${transparent ? 'on' : 'off'} (ctrl-t)`}</text>
|
|
79
69
|
</box>
|
|
80
70
|
}
|
|
81
71
|
items={items}
|
|
82
72
|
selectedIndex={effectiveIndex}
|
|
83
|
-
emptyState={
|
|
84
|
-
<text fg={theme.colors['descriptionForeground']}>
|
|
85
|
-
{filter ? 'No matching themes.' : 'No themes available.'}
|
|
86
|
-
</text>
|
|
87
|
-
}
|
|
73
|
+
emptyState={<text fg={t.muted}>No themes available.</text>}
|
|
88
74
|
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
89
75
|
/>
|
|
90
76
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useTokens } from '../theme'
|
|
2
2
|
import { uiTokens } from '../ui-tokens'
|
|
3
3
|
import { ListItem } from './list-item'
|
|
4
4
|
import { ModalShell } from './modal-shell'
|
|
@@ -19,7 +19,7 @@ export function UpdateAvailableModal({
|
|
|
19
19
|
latestVersion,
|
|
20
20
|
selectedIndex,
|
|
21
21
|
}: UpdateAvailableModalProps) {
|
|
22
|
-
const
|
|
22
|
+
const t = useTokens()
|
|
23
23
|
return (
|
|
24
24
|
<ModalShell
|
|
25
25
|
title="Update available"
|
|
@@ -36,17 +36,7 @@ export function UpdateAvailableModal({
|
|
|
36
36
|
key={option.label}
|
|
37
37
|
active={active}
|
|
38
38
|
direction="row"
|
|
39
|
-
title={
|
|
40
|
-
<text
|
|
41
|
-
fg={
|
|
42
|
-
active
|
|
43
|
-
? theme.colors['editor.foreground']
|
|
44
|
-
: theme.colors['descriptionForeground']
|
|
45
|
-
}
|
|
46
|
-
>
|
|
47
|
-
{option.label}
|
|
48
|
-
</text>
|
|
49
|
-
}
|
|
39
|
+
title={<text fg={active ? t.palette.ink : t.muted}>{option.label}</text>}
|
|
50
40
|
/>
|
|
51
41
|
)
|
|
52
42
|
})}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ContextMenuItem = [label: string, onSelect: () => void]
|
|
2
|
+
|
|
3
|
+
export interface ContextMenuState {
|
|
4
|
+
anchorX: number
|
|
5
|
+
anchorY: number
|
|
6
|
+
items: ContextMenuItem[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type Listener = (state: ContextMenuState | null) => void
|
|
10
|
+
|
|
11
|
+
let current: ContextMenuState | null = null
|
|
12
|
+
const listeners = new Set<Listener>()
|
|
13
|
+
|
|
14
|
+
export function openContextMenu(anchorX: number, anchorY: number, items: ContextMenuItem[]): void {
|
|
15
|
+
if (items.length === 0) {
|
|
16
|
+
closeContextMenu()
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
current = { anchorX, anchorY, items }
|
|
20
|
+
for (const l of listeners) l(current)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function closeContextMenu(): void {
|
|
24
|
+
if (current === null) return
|
|
25
|
+
current = null
|
|
26
|
+
for (const l of listeners) l(null)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function subscribeContextMenu(listener: Listener): () => void {
|
|
30
|
+
listeners.add(listener)
|
|
31
|
+
return () => {
|
|
32
|
+
listeners.delete(listener)
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/ui/root.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import type { ThemeId } from './themes'
|
|
|
6
6
|
|
|
7
7
|
import { useAppStore } from '../state/app-store'
|
|
8
8
|
import { getTreeForTab, PANE_BORDER, type SplitDirection } from '../state/layout-tree'
|
|
9
|
+
import { ContextMenuOverlay } from './components/context-menu-overlay'
|
|
9
10
|
import { CreateSessionModal } from './components/create-session-modal'
|
|
10
11
|
import { GitCommitModal } from './components/git-commit-modal'
|
|
11
12
|
import { GitPaneWidget } from './components/git-pane-widget'
|
|
@@ -222,7 +223,7 @@ export function RootView({
|
|
|
222
223
|
terminalRows,
|
|
223
224
|
themeId,
|
|
224
225
|
}: RootViewProps) {
|
|
225
|
-
const editorBg = useBg('
|
|
226
|
+
const editorBg = useBg('base')
|
|
226
227
|
const tabs = useAppStore((s) => s.tabs)
|
|
227
228
|
const activeTabId = useAppStore((s) => s.activeTabId)
|
|
228
229
|
const layoutTrees = useAppStore((s) => s.layoutTrees)
|
|
@@ -252,6 +253,7 @@ export function RootView({
|
|
|
252
253
|
<GitView themeId={themeId} />
|
|
253
254
|
<StatusBar />
|
|
254
255
|
<PendingChordOverlay />
|
|
256
|
+
<ContextMenuOverlay />
|
|
255
257
|
{renderModal(modal, {
|
|
256
258
|
createSessionFields,
|
|
257
259
|
currentSessionId,
|
|
@@ -325,6 +327,7 @@ export function RootView({
|
|
|
325
327
|
{sessionBarPosition === 'bottom' && <SessionBar />}
|
|
326
328
|
<StatusBar />
|
|
327
329
|
<PendingChordOverlay />
|
|
330
|
+
<ContextMenuOverlay />
|
|
328
331
|
{renderModal(modal, {
|
|
329
332
|
createSessionFields,
|
|
330
333
|
currentSessionId,
|
|
@@ -340,7 +343,7 @@ export function RootView({
|
|
|
340
343
|
}
|
|
341
344
|
|
|
342
345
|
function GitPaneInPaneMode({ ratio }: { ratio: number }) {
|
|
343
|
-
const bg = useBg('
|
|
346
|
+
const bg = useBg('elevated')
|
|
344
347
|
// Ratio maps to a fixed column count (20..80), mirroring the reservation in
|
|
345
348
|
// use-terminal-resize so the terminal-content area stays in sync.
|
|
346
349
|
const width = Math.max(20, Math.min(80, Math.round(ratio * 80)))
|
package/src/ui/shiki.ts
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import {
|
|
2
|
+
type BundledLanguage,
|
|
3
|
+
createHighlighter,
|
|
4
|
+
type Highlighter,
|
|
5
|
+
type ThemeRegistrationRaw,
|
|
6
|
+
} from 'shiki'
|
|
7
|
+
|
|
8
|
+
import { getCurrentTheme } from './theme-store'
|
|
9
|
+
import { paletteToShikiTheme } from './themes'
|
|
10
|
+
|
|
11
|
+
function buildActiveTheme(): { id: string; raw: ThemeRegistrationRaw } {
|
|
12
|
+
const theme = getCurrentTheme()
|
|
13
|
+
const id = `${theme.name}-${theme.mode}`
|
|
14
|
+
return { id, raw: paletteToShikiTheme({ mode: theme.mode, name: id, palette: theme.palette }) }
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
let highlighterPromise: Promise<Highlighter> | null = null
|
|
18
|
+
let activeThemeId: string | null = null
|
|
10
19
|
|
|
11
20
|
export async function getShikiHighlighter(): Promise<Highlighter> {
|
|
12
21
|
if (!highlighterPromise) {
|
|
13
|
-
|
|
22
|
+
const initial = buildActiveTheme()
|
|
23
|
+
activeThemeId = initial.id
|
|
24
|
+
highlighterPromise = createHighlighter({ langs: [], themes: [initial.raw] })
|
|
14
25
|
}
|
|
15
26
|
return highlighterPromise
|
|
16
27
|
}
|
|
@@ -28,22 +39,19 @@ export async function ensureShikiLang(h: Highlighter, lang: string): Promise<boo
|
|
|
28
39
|
}
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
const loadedThemes = new Set<string>([WARM_THEME])
|
|
32
|
-
|
|
33
42
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
43
|
+
* Make sure the active aimux theme (light or dark, with any palette overrides)
|
|
44
|
+
* is loaded into the highlighter. Returns the registered theme name so the
|
|
45
|
+
* caller can pass it to `codeToTokens`.
|
|
36
46
|
*/
|
|
37
|
-
export async function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!entry) return false
|
|
47
|
+
export async function ensureActiveShikiTheme(h: Highlighter): Promise<string> {
|
|
48
|
+
const { id, raw } = buildActiveTheme()
|
|
49
|
+
if (id === activeThemeId) return id
|
|
41
50
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
loadedThemes.add(id)
|
|
45
|
-
return true
|
|
51
|
+
await h.loadTheme(raw)
|
|
52
|
+
activeThemeId = id
|
|
46
53
|
} catch {
|
|
47
|
-
|
|
54
|
+
// Reloading the theme is best-effort; on failure we keep using the prior id.
|
|
48
55
|
}
|
|
56
|
+
return activeThemeId ?? id
|
|
49
57
|
}
|
package/src/ui/theme-store.ts
CHANGED
|
@@ -1,39 +1,108 @@
|
|
|
1
1
|
import { useStore } from 'zustand'
|
|
2
2
|
import { createStore } from 'zustand/vanilla'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
accent,
|
|
6
|
+
type AimuxPalette,
|
|
7
|
+
type AimuxTheme,
|
|
8
|
+
border,
|
|
9
|
+
diffAddBg,
|
|
10
|
+
diffDeleteBg,
|
|
11
|
+
elevated,
|
|
12
|
+
extendPalette,
|
|
13
|
+
faint,
|
|
14
|
+
hover,
|
|
15
|
+
muted,
|
|
16
|
+
selected,
|
|
17
|
+
type ThemeId,
|
|
18
|
+
THEMES,
|
|
19
|
+
} from './themes'
|
|
20
|
+
|
|
21
|
+
export interface ThemeTokens {
|
|
22
|
+
/** `accent` with `primary` fallback. */
|
|
23
|
+
accent: string
|
|
24
|
+
/** Subtle border between panels. */
|
|
25
|
+
border: string
|
|
26
|
+
/** Background tint for inserted diff lines. */
|
|
27
|
+
diffAddBg: string
|
|
28
|
+
/** Background tint for removed diff lines. */
|
|
29
|
+
diffDeleteBg: string
|
|
30
|
+
/** Surface a notch above the base background — sidebars, headers. */
|
|
31
|
+
elevated: string
|
|
32
|
+
/** Fainter still — line numbers, placeholders, disabled. */
|
|
33
|
+
faint: string
|
|
34
|
+
/** Hover/highlight surface (line highlight, list hover). */
|
|
35
|
+
hover: string
|
|
36
|
+
/** Half-way between background and ink — labels, captions, paths. */
|
|
37
|
+
muted: string
|
|
38
|
+
palette: AimuxPalette
|
|
39
|
+
/** Selected/active row surface. */
|
|
40
|
+
selected: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function computeTokens(palette: AimuxPalette): ThemeTokens {
|
|
44
|
+
return {
|
|
45
|
+
accent: accent(palette),
|
|
46
|
+
border: border(palette),
|
|
47
|
+
diffAddBg: diffAddBg(palette),
|
|
48
|
+
diffDeleteBg: diffDeleteBg(palette),
|
|
49
|
+
elevated: elevated(palette),
|
|
50
|
+
faint: faint(palette),
|
|
51
|
+
hover: hover(palette),
|
|
52
|
+
muted: muted(palette),
|
|
53
|
+
palette,
|
|
54
|
+
selected: selected(palette),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Memoize by palette reference so `useStore` selectors return stable objects.
|
|
59
|
+
let cachedPalette: AimuxPalette | null = null
|
|
60
|
+
let cachedTokens: ThemeTokens | null = null
|
|
61
|
+
|
|
62
|
+
function deriveTokens(palette: AimuxPalette): ThemeTokens {
|
|
63
|
+
if (cachedPalette === palette && cachedTokens) return cachedTokens
|
|
64
|
+
cachedPalette = palette
|
|
65
|
+
cachedTokens = computeTokens(palette)
|
|
66
|
+
return cachedTokens
|
|
67
|
+
}
|
|
5
68
|
|
|
6
69
|
interface ThemeStore {
|
|
7
|
-
theme:
|
|
70
|
+
theme: AimuxTheme
|
|
8
71
|
transparent: boolean
|
|
9
72
|
}
|
|
10
73
|
|
|
11
|
-
const DEFAULT = THEMES['aimux']
|
|
74
|
+
const DEFAULT = THEMES['aimux-dark']
|
|
12
75
|
if (!DEFAULT) throw new Error('default theme missing')
|
|
13
76
|
|
|
14
77
|
const themeStore = createStore<ThemeStore>(() => ({ theme: DEFAULT, transparent: false }))
|
|
15
78
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export function
|
|
23
|
-
|
|
24
|
-
return useStore(themeStore, (s) => (selector ? selector(s.theme) : s.theme))
|
|
79
|
+
/** Subscribe to the active palette plus precomputed derived shades. */
|
|
80
|
+
export function useTokens(): ThemeTokens {
|
|
81
|
+
return useStore(themeStore, (s) => deriveTokens(s.theme.palette))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Synchronous tokens for non-React callers. */
|
|
85
|
+
export function getCurrentTokens(): ThemeTokens {
|
|
86
|
+
return deriveTokens(themeStore.getState().theme.palette)
|
|
25
87
|
}
|
|
26
88
|
|
|
27
89
|
/** Synchronous snapshot for non-React callers (reducers, side effects). */
|
|
28
|
-
export function getCurrentTheme():
|
|
90
|
+
export function getCurrentTheme(): AimuxTheme {
|
|
29
91
|
return themeStore.getState().theme
|
|
30
92
|
}
|
|
31
93
|
|
|
32
|
-
/** Swap the active theme
|
|
33
|
-
export function applyTheme(id: ThemeId): void {
|
|
94
|
+
/** Swap the active theme by id, optionally merging palette overrides. */
|
|
95
|
+
export function applyTheme(id: ThemeId, paletteOverrides?: Partial<AimuxPalette>): void {
|
|
34
96
|
const entry = THEMES[id]
|
|
35
97
|
if (!entry) return
|
|
36
|
-
|
|
98
|
+
const palette = extendPalette(entry.palette, paletteOverrides)
|
|
99
|
+
const merged: AimuxTheme = {
|
|
100
|
+
...entry,
|
|
101
|
+
bg: palette.neutral,
|
|
102
|
+
fg: palette.ink,
|
|
103
|
+
palette,
|
|
104
|
+
}
|
|
105
|
+
themeStore.setState({ theme: merged })
|
|
37
106
|
}
|
|
38
107
|
|
|
39
108
|
/** Subscribe to the transparent-mode flag. */
|
|
@@ -50,11 +119,31 @@ export function setTransparent(value: boolean): void {
|
|
|
50
119
|
themeStore.setState({ transparent: value })
|
|
51
120
|
}
|
|
52
121
|
|
|
122
|
+
export type SurfaceToken = 'base' | 'elevated' | 'hover' | 'selected' | 'border'
|
|
123
|
+
|
|
124
|
+
function resolveSurface(palette: AimuxPalette, token: SurfaceToken): string {
|
|
125
|
+
switch (token) {
|
|
126
|
+
case 'base':
|
|
127
|
+
return palette.neutral
|
|
128
|
+
case 'elevated':
|
|
129
|
+
return elevated(palette)
|
|
130
|
+
case 'hover':
|
|
131
|
+
return hover(palette)
|
|
132
|
+
case 'selected':
|
|
133
|
+
return selected(palette)
|
|
134
|
+
case 'border':
|
|
135
|
+
return border(palette)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
53
139
|
/**
|
|
54
|
-
* Resolve a
|
|
55
|
-
* `undefined` when transparent mode is on, letting the
|
|
56
|
-
* own background show through.
|
|
140
|
+
* Resolve a surface color through the transparent-mode flag. Returns
|
|
141
|
+
* `undefined` for the base surface when transparent mode is on, letting the
|
|
142
|
+
* terminal emulator's own background show through.
|
|
57
143
|
*/
|
|
58
|
-
export function useBg(
|
|
59
|
-
return useStore(themeStore, (s) =>
|
|
144
|
+
export function useBg(token: SurfaceToken): string | undefined {
|
|
145
|
+
return useStore(themeStore, (s) => {
|
|
146
|
+
if (s.transparent && token === 'base') return undefined
|
|
147
|
+
return resolveSurface(s.theme.palette, token)
|
|
148
|
+
})
|
|
60
149
|
}
|
package/src/ui/theme.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Back-compat shim: the theme singleton has been replaced with a Zustand store.
|
|
2
|
-
// React components
|
|
3
|
-
//
|
|
2
|
+
// React components use `useTokens` for derived shades; non-React callers use
|
|
3
|
+
// `getCurrentTheme()` / `getCurrentTokens()`.
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
applyTheme,
|
|
7
7
|
getCurrentTheme,
|
|
8
|
+
getCurrentTokens,
|
|
8
9
|
getTransparent,
|
|
9
10
|
setTransparent,
|
|
11
|
+
type SurfaceToken,
|
|
12
|
+
type ThemeTokens,
|
|
10
13
|
useBg,
|
|
11
|
-
|
|
14
|
+
useTokens,
|
|
12
15
|
useTransparent,
|
|
13
16
|
} from './theme-store'
|
package/src/ui/themes.ts
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
|
-
// Thin runtime re-export. Theme data, types,
|
|
2
|
-
// all live in `@brimveyn/aimux-config
|
|
3
|
-
// the single source of truth. The mutable theme singleton + `applyTheme` stay
|
|
4
|
-
// in `./theme.ts` since they're UI-layer concerns.
|
|
1
|
+
// Thin runtime re-export. Theme data, types, palette utilities, and the
|
|
2
|
+
// palette-to-Shiki generator all live in `@brimveyn/aimux-config`.
|
|
5
3
|
|
|
6
4
|
export type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
AimuxPalette,
|
|
6
|
+
AimuxTheme,
|
|
7
|
+
AimuxThemeConfig,
|
|
10
8
|
Theme,
|
|
11
|
-
ThemeColorMap,
|
|
12
9
|
ThemeId,
|
|
13
|
-
|
|
14
|
-
ThemeTokenRule,
|
|
10
|
+
ThemeMode,
|
|
15
11
|
} from '@brimveyn/aimux-config'
|
|
16
12
|
|
|
17
13
|
export {
|
|
18
|
-
|
|
14
|
+
accent,
|
|
15
|
+
border,
|
|
16
|
+
diffAddBg,
|
|
17
|
+
diffDeleteBg,
|
|
18
|
+
elevated,
|
|
19
|
+
extendPalette,
|
|
20
|
+
faint,
|
|
21
|
+
hover,
|
|
19
22
|
isKnownThemeId,
|
|
20
23
|
migrateThemeId,
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
mix,
|
|
25
|
+
muted,
|
|
26
|
+
paletteToShikiTheme,
|
|
27
|
+
selected,
|
|
23
28
|
THEME_IDS,
|
|
24
29
|
THEMES,
|
|
25
30
|
} from '@brimveyn/aimux-config'
|