@brimveyn/aimux 1.1.0 → 1.2.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/package.json +2 -2
- package/src/app-runtime/side-effects.ts +120 -0
- package/src/app-runtime/use-renderer-bindings.ts +6 -0
- package/src/app.tsx +6 -0
- package/src/git/command-queue.ts +7 -0
- package/src/git/git-diff.ts +83 -0
- package/src/git/git-poller.ts +4 -4
- package/src/git/git-status.ts +32 -0
- package/src/input/modes/bridge.ts +2 -0
- package/src/input/modes/handlers/shared.ts +16 -0
- package/src/input/modes/transitions.ts +3 -0
- package/src/input/modes/types.ts +9 -0
- package/src/state/dispatch-ref.ts +13 -0
- package/src/state/reducers/git-mode-state.ts +136 -0
- package/src/state/reducers/git-panel-state.ts +15 -3
- package/src/state/reducers/modal-state.ts +129 -22
- package/src/state/store.ts +5 -0
- package/src/state/types.ts +56 -1
- package/src/ui/components/git-commit-modal.tsx +42 -0
- package/src/ui/components/git-panel.tsx +42 -13
- package/src/ui/components/git-view.tsx +237 -0
- package/src/ui/components/input-field.tsx +26 -5
- package/src/ui/components/status-bar.tsx +8 -0
- package/src/ui/git-view-controls.ts +11 -0
- package/src/ui/root.tsx +36 -2
- package/src/ui/status-bar-model.ts +18 -0
- package/src/ui/syntax.ts +102 -0
- package/src/ui/theme.ts +9 -0
- package/src/ui/themes.ts +24 -0
- package/src/ui/components/pending-chord-indicator.tsx +0 -41
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { DiffRenderable } from '@opentui/core'
|
|
2
|
+
|
|
3
|
+
import { memo, useEffect, useRef } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { DiffData } from '../../state/types'
|
|
6
|
+
|
|
7
|
+
import { fetchDiff } from '../../git/git-diff'
|
|
8
|
+
import { useGitPanelPolling } from '../../git/git-poller'
|
|
9
|
+
import { useAppStore } from '../../state/app-store'
|
|
10
|
+
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
11
|
+
import { setGitDiffScroller } from '../git-view-controls'
|
|
12
|
+
import { getSyntaxClient, getSyntaxStyle } from '../syntax'
|
|
13
|
+
import { theme } from '../theme'
|
|
14
|
+
import { fileKey, GitPanel } from './git-panel'
|
|
15
|
+
|
|
16
|
+
interface CodePaneLike {
|
|
17
|
+
scrollY: number
|
|
18
|
+
maxScrollY: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DiffRenderableInternals {
|
|
22
|
+
leftCodeRenderable?: CodePaneLike
|
|
23
|
+
rightCodeRenderable?: CodePaneLike
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function filetypeFromPath(path: string): string | undefined {
|
|
27
|
+
const dot = path.lastIndexOf('.')
|
|
28
|
+
if (dot < 0) return undefined
|
|
29
|
+
const ext = path.slice(dot + 1).toLowerCase()
|
|
30
|
+
const map: Record<string, string> = {
|
|
31
|
+
cjs: 'javascript',
|
|
32
|
+
js: 'javascript',
|
|
33
|
+
jsx: 'javascript',
|
|
34
|
+
markdown: 'markdown',
|
|
35
|
+
md: 'markdown',
|
|
36
|
+
mdx: 'markdown',
|
|
37
|
+
mjs: 'javascript',
|
|
38
|
+
ts: 'typescript',
|
|
39
|
+
tsx: 'typescript',
|
|
40
|
+
zig: 'zig',
|
|
41
|
+
}
|
|
42
|
+
return map[ext]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface DiffStageProps {
|
|
46
|
+
diff: DiffData | undefined
|
|
47
|
+
loading: boolean
|
|
48
|
+
diffRef: React.RefObject<DiffRenderable | null>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function placeholderText(diff: DiffData): string | null {
|
|
52
|
+
if (diff.status === 'binary') {
|
|
53
|
+
const before = diff.binarySizeBefore ?? 0
|
|
54
|
+
const after = diff.binarySizeAfter ?? 0
|
|
55
|
+
return `(binary file — ${before} → ${after} bytes)`
|
|
56
|
+
}
|
|
57
|
+
if (diff.rawDiff.length === 0) {
|
|
58
|
+
if (diff.status === 'new') return '(new file — no diff)'
|
|
59
|
+
if (diff.status === 'deleted') return '(deleted — no diff)'
|
|
60
|
+
return '(no changes)'
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DiffStage = memo(function DiffStage({ diff, diffRef, loading }: DiffStageProps) {
|
|
66
|
+
if (loading && !diff) {
|
|
67
|
+
return (
|
|
68
|
+
<box flexGrow={1} padding={1}>
|
|
69
|
+
<text fg={theme.textMuted}>Loading diff…</text>
|
|
70
|
+
</box>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
if (!diff) {
|
|
74
|
+
return (
|
|
75
|
+
<box flexGrow={1} padding={1}>
|
|
76
|
+
<text fg={theme.textMuted}>Select a file.</text>
|
|
77
|
+
</box>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
if (diff.errorMessage) {
|
|
81
|
+
return (
|
|
82
|
+
<box flexGrow={1} padding={1}>
|
|
83
|
+
<text fg={theme.danger}>{diff.errorMessage}</text>
|
|
84
|
+
</box>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const placeholder = placeholderText(diff)
|
|
89
|
+
if (placeholder) {
|
|
90
|
+
return (
|
|
91
|
+
<box flexGrow={1} padding={1}>
|
|
92
|
+
<text fg={theme.textMuted}>{placeholder}</text>
|
|
93
|
+
</box>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const filetype = filetypeFromPath(diff.path)
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
101
|
+
{diff.oldPath ? (
|
|
102
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
103
|
+
<text fg={theme.textMuted}>
|
|
104
|
+
renamed: {diff.oldPath} → {diff.path}
|
|
105
|
+
</text>
|
|
106
|
+
</box>
|
|
107
|
+
) : null}
|
|
108
|
+
<diff
|
|
109
|
+
ref={diffRef}
|
|
110
|
+
diff={diff.rawDiff}
|
|
111
|
+
view="split"
|
|
112
|
+
syncScroll
|
|
113
|
+
showLineNumbers
|
|
114
|
+
wrapMode="none"
|
|
115
|
+
filetype={filetype}
|
|
116
|
+
treeSitterClient={filetype ? getSyntaxClient() : undefined}
|
|
117
|
+
syntaxStyle={filetype ? getSyntaxStyle() : undefined}
|
|
118
|
+
addedBg={theme.diffAddBg}
|
|
119
|
+
removedBg={theme.diffRemoveBg}
|
|
120
|
+
addedSignColor={theme.success}
|
|
121
|
+
removedSignColor={theme.danger}
|
|
122
|
+
flexGrow={1}
|
|
123
|
+
/>
|
|
124
|
+
</box>
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
export const GitView = memo(function GitView() {
|
|
129
|
+
const sidebarWidth = useAppStore((s) => s.sidebar.width)
|
|
130
|
+
const gitPanel = useAppStore((s) => s.gitPanel)
|
|
131
|
+
const gitMode = useAppStore((s) => s.gitMode)
|
|
132
|
+
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
133
|
+
const sessions = useAppStore((s) => s.sessions)
|
|
134
|
+
const focusMode = useAppStore((s) => s.focusMode)
|
|
135
|
+
const diffRef = useRef<DiffRenderable | null>(null)
|
|
136
|
+
|
|
137
|
+
const currentSession = currentSessionId
|
|
138
|
+
? sessions.find((s) => s.id === currentSessionId)
|
|
139
|
+
: undefined
|
|
140
|
+
const projectPath = currentSession?.projectPath
|
|
141
|
+
|
|
142
|
+
useGitPanelPolling({ enabled: focusMode === 'git', projectPath })
|
|
143
|
+
|
|
144
|
+
const selectedFile = gitPanel.files[gitMode.selectedFileIndex]
|
|
145
|
+
const diff = selectedFile ? gitMode.diffs[selectedFile.path] : undefined
|
|
146
|
+
const loading = selectedFile ? !!gitMode.loading[selectedFile.path] : false
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
setGitDiffScroller((delta: number) => {
|
|
150
|
+
const node = diffRef.current as unknown as DiffRenderableInternals | null
|
|
151
|
+
if (!node) return
|
|
152
|
+
const left = node.leftCodeRenderable
|
|
153
|
+
const right = node.rightCodeRenderable
|
|
154
|
+
if (!left || !right) return
|
|
155
|
+
const cap = Math.max(left.maxScrollY, right.maxScrollY)
|
|
156
|
+
const base = left.scrollY
|
|
157
|
+
const nextScroll = Math.max(0, Math.min(cap, base + delta))
|
|
158
|
+
left.scrollY = nextScroll
|
|
159
|
+
right.scrollY = nextScroll
|
|
160
|
+
})
|
|
161
|
+
return () => setGitDiffScroller(null)
|
|
162
|
+
}, [])
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (focusMode !== 'git') return
|
|
166
|
+
if (!selectedFile || !projectPath) return
|
|
167
|
+
const path = selectedFile.path
|
|
168
|
+
if (diff || loading) return
|
|
169
|
+
dispatchGlobal({ loading: true, path, type: 'git-mode-set-loading' })
|
|
170
|
+
void fetchDiff(projectPath, selectedFile)
|
|
171
|
+
.then((d) => dispatchGlobal({ diff: d, path, type: 'git-mode-set-diff' }))
|
|
172
|
+
.catch(() => dispatchGlobal({ loading: false, path, type: 'git-mode-set-loading' }))
|
|
173
|
+
}, [focusMode, projectPath, selectedFile, diff, loading])
|
|
174
|
+
|
|
175
|
+
const pendingPath = gitMode.pendingDeletePath
|
|
176
|
+
const pendingIsUntracked =
|
|
177
|
+
pendingPath !== null &&
|
|
178
|
+
selectedFile?.path === pendingPath &&
|
|
179
|
+
selectedFile.section === 'untracked'
|
|
180
|
+
let pendingHint: string | null = null
|
|
181
|
+
if (pendingPath !== null) {
|
|
182
|
+
pendingHint = pendingIsUntracked
|
|
183
|
+
? 'press d again to delete file'
|
|
184
|
+
: 'press d again to discard changes'
|
|
185
|
+
}
|
|
186
|
+
const actionMessage = gitMode.actionMessage
|
|
187
|
+
let footerNode: React.ReactNode = null
|
|
188
|
+
if (pendingHint) {
|
|
189
|
+
footerNode = (
|
|
190
|
+
<text fg={theme.warning}>
|
|
191
|
+
<strong>{pendingHint}</strong>
|
|
192
|
+
</text>
|
|
193
|
+
)
|
|
194
|
+
} else if (actionMessage) {
|
|
195
|
+
footerNode = actionMessage.split('\n').map((line, idx) => (
|
|
196
|
+
<text key={idx} fg={theme.accent}>
|
|
197
|
+
{line}
|
|
198
|
+
</text>
|
|
199
|
+
))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<box flexDirection="column" flexGrow={1}>
|
|
204
|
+
<box flexDirection="row" flexGrow={1}>
|
|
205
|
+
<box
|
|
206
|
+
width={sidebarWidth}
|
|
207
|
+
flexDirection="column"
|
|
208
|
+
backgroundColor={theme.panel}
|
|
209
|
+
padding={0}
|
|
210
|
+
gap={0}
|
|
211
|
+
>
|
|
212
|
+
<text fg={theme.accent}>
|
|
213
|
+
<strong>aimux · git</strong>
|
|
214
|
+
</text>
|
|
215
|
+
{gitPanel.branch ? (
|
|
216
|
+
<box flexDirection="row">
|
|
217
|
+
<text fg={theme.accent}>{'\u{e702}'} </text>
|
|
218
|
+
<text fg={theme.textMuted}>{gitPanel.branch}</text>
|
|
219
|
+
</box>
|
|
220
|
+
) : null}
|
|
221
|
+
<text fg={theme.dim}>{'·'.repeat(Math.max(0, sidebarWidth - 2))}</text>
|
|
222
|
+
<GitPanel
|
|
223
|
+
gitPanel={gitPanel}
|
|
224
|
+
projectPath={projectPath}
|
|
225
|
+
selectedFileKey={selectedFile ? fileKey(selectedFile) : null}
|
|
226
|
+
/>
|
|
227
|
+
</box>
|
|
228
|
+
<DiffStage diff={diff} diffRef={diffRef} loading={loading} />
|
|
229
|
+
</box>
|
|
230
|
+
{footerNode ? (
|
|
231
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={theme.panel} flexDirection="column">
|
|
232
|
+
{footerNode}
|
|
233
|
+
</box>
|
|
234
|
+
) : null}
|
|
235
|
+
</box>
|
|
236
|
+
)
|
|
237
|
+
})
|
|
@@ -4,14 +4,35 @@ import { Surface } from './surface'
|
|
|
4
4
|
interface InputFieldProps {
|
|
5
5
|
active: boolean
|
|
6
6
|
value: string
|
|
7
|
+
cursorPos?: number
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export function InputField({ active, value }: InputFieldProps) {
|
|
10
|
+
export function InputField({ active, cursorPos, value }: InputFieldProps) {
|
|
11
|
+
const fg = active ? theme.text : theme.textMuted
|
|
12
|
+
if (!active) {
|
|
13
|
+
return (
|
|
14
|
+
<Surface tone="input" padding={1}>
|
|
15
|
+
<text fg={fg}>{value}</text>
|
|
16
|
+
</Surface>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const safePos =
|
|
21
|
+
cursorPos === undefined ? value.length : Math.max(0, Math.min(value.length, cursorPos))
|
|
22
|
+
const before = value.slice(0, safePos)
|
|
23
|
+
const atChar = safePos < value.length ? value.charAt(safePos) : undefined
|
|
24
|
+
const cursorOnLineEnd = atChar === undefined || atChar === '\n'
|
|
25
|
+
const cursorDisplay = cursorOnLineEnd ? ' ' : (atChar as string)
|
|
26
|
+
const trailing = cursorOnLineEnd ? value.slice(safePos) : value.slice(safePos + 1)
|
|
27
|
+
|
|
10
28
|
return (
|
|
11
|
-
<Surface tone=
|
|
12
|
-
<text fg={
|
|
13
|
-
{
|
|
14
|
-
{
|
|
29
|
+
<Surface tone="inputActive" padding={1}>
|
|
30
|
+
<text fg={fg}>
|
|
31
|
+
{before}
|
|
32
|
+
<span bg={theme.text} fg={theme.background}>
|
|
33
|
+
{cursorDisplay}
|
|
34
|
+
</span>
|
|
35
|
+
{trailing}
|
|
15
36
|
</text>
|
|
16
37
|
</Surface>
|
|
17
38
|
)
|
|
@@ -12,6 +12,10 @@ function getModeColor(focusMode: AppState['focusMode']): string {
|
|
|
12
12
|
return theme.warning
|
|
13
13
|
case 'modal':
|
|
14
14
|
return theme.warning
|
|
15
|
+
case 'command-edit':
|
|
16
|
+
return theme.warning
|
|
17
|
+
case 'git':
|
|
18
|
+
return theme.success
|
|
15
19
|
case 'navigation':
|
|
16
20
|
default:
|
|
17
21
|
return theme.accentAlt
|
|
@@ -26,6 +30,10 @@ function getModeLabel(focusMode: AppState['focusMode']): string {
|
|
|
26
30
|
return 'layout'
|
|
27
31
|
case 'modal':
|
|
28
32
|
return 'modal'
|
|
33
|
+
case 'command-edit':
|
|
34
|
+
return 'edit'
|
|
35
|
+
case 'git':
|
|
36
|
+
return 'git'
|
|
29
37
|
case 'navigation':
|
|
30
38
|
default:
|
|
31
39
|
return 'nav'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Scroller = (delta: number) => void
|
|
2
|
+
|
|
3
|
+
let activeScroller: Scroller | null = null
|
|
4
|
+
|
|
5
|
+
export function setGitDiffScroller(scroller: Scroller | null): void {
|
|
6
|
+
activeScroller = scroller
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function scrollGitDiff(delta: number): void {
|
|
10
|
+
activeScroller?.(delta)
|
|
11
|
+
}
|
package/src/ui/root.tsx
CHANGED
|
@@ -7,9 +7,10 @@ import type { ThemeId } from './themes'
|
|
|
7
7
|
import { useAppStore } from '../state/app-store'
|
|
8
8
|
import { getTreeForTab, PANE_BORDER, type SplitDirection } from '../state/layout-tree'
|
|
9
9
|
import { CreateSessionModal } from './components/create-session-modal'
|
|
10
|
+
import { GitCommitModal } from './components/git-commit-modal'
|
|
11
|
+
import { GitView } from './components/git-view'
|
|
10
12
|
import { HelpModal } from './components/help-modal'
|
|
11
13
|
import { NewTabModal } from './components/new-tab-modal'
|
|
12
|
-
import { PendingChordIndicator } from './components/pending-chord-indicator'
|
|
13
14
|
import { SessionNameModal } from './components/session-name-modal'
|
|
14
15
|
import { SessionPickerModal } from './components/session-picker-modal'
|
|
15
16
|
import { Sidebar } from './components/sidebar'
|
|
@@ -133,6 +134,20 @@ function renderModal(
|
|
|
133
134
|
)
|
|
134
135
|
case 'help':
|
|
135
136
|
return <HelpModal />
|
|
137
|
+
case 'git-commit': {
|
|
138
|
+
const titleText =
|
|
139
|
+
modal.activeField === 'title' ? (modal.editBuffer ?? '') : modal.contentBuffer
|
|
140
|
+
const bodyText =
|
|
141
|
+
modal.activeField === 'title' ? modal.contentBuffer : (modal.editBuffer ?? '')
|
|
142
|
+
return (
|
|
143
|
+
<GitCommitModal
|
|
144
|
+
activeField={modal.activeField}
|
|
145
|
+
body={bodyText}
|
|
146
|
+
cursorPos={modal.cursorPos ?? (modal.editBuffer ?? '').length}
|
|
147
|
+
title={titleText}
|
|
148
|
+
/>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
136
151
|
case null:
|
|
137
152
|
return null
|
|
138
153
|
default:
|
|
@@ -195,6 +210,26 @@ export function RootView({
|
|
|
195
210
|
const snippetEditorFields = getSnippetEditorFields(modal)
|
|
196
211
|
const splitChrome = PANE_BORDER * 2
|
|
197
212
|
|
|
213
|
+
const inGitMode = focusMode === 'git' || modal.type === 'git-commit'
|
|
214
|
+
if (inGitMode) {
|
|
215
|
+
return (
|
|
216
|
+
<box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
|
|
217
|
+
<GitView />
|
|
218
|
+
<StatusBar />
|
|
219
|
+
{renderModal(modal, {
|
|
220
|
+
createSessionFields,
|
|
221
|
+
currentSessionId,
|
|
222
|
+
currentTabCount: tabs.length,
|
|
223
|
+
customCommands,
|
|
224
|
+
sessions,
|
|
225
|
+
snippetEditorFields,
|
|
226
|
+
snippets,
|
|
227
|
+
themeId,
|
|
228
|
+
})}
|
|
229
|
+
</box>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
198
233
|
return (
|
|
199
234
|
<box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
|
|
200
235
|
<box flexDirection="row" gap={0} padding={0} flexGrow={1}>
|
|
@@ -255,7 +290,6 @@ export function RootView({
|
|
|
255
290
|
snippets,
|
|
256
291
|
themeId,
|
|
257
292
|
})}
|
|
258
|
-
<PendingChordIndicator />
|
|
259
293
|
</box>
|
|
260
294
|
)
|
|
261
295
|
}
|
|
@@ -49,6 +49,14 @@ function getInputHint(activeTab?: TabSession): string {
|
|
|
49
49
|
return 'Ctrl+z unfocus Ctrl+w layout typing goes to active tab'
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function getGitHint(modalType: AppState['modal']['type']): string {
|
|
53
|
+
if (modalType === 'git-commit') {
|
|
54
|
+
return 'Tab switch field Enter newline Ctrl+Enter commit Esc cancel'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 'j/k file Ctrl+d/u page a stage d unstage/delete c commit p push Esc exit'
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
export function getStatusBarModel(state: AppState, activeTab?: TabSession): StatusBarModel {
|
|
53
61
|
const currentSession = state.currentSessionId
|
|
54
62
|
? state.sessions.find((session) => session.id === state.currentSessionId)
|
|
@@ -77,6 +85,16 @@ export function getStatusBarModel(state: AppState, activeTab?: TabSession): Stat
|
|
|
77
85
|
left: `${getActiveTabLabel(activeTab)} ${sessionIcon} ${sessionLabel}`,
|
|
78
86
|
right: 'h/j/k/l focus |/- split H/L resize q close Esc cancel',
|
|
79
87
|
}
|
|
88
|
+
case 'git':
|
|
89
|
+
return {
|
|
90
|
+
left: `${sessionIcon} ${sessionLabel}`,
|
|
91
|
+
right: getGitHint(state.modal.type),
|
|
92
|
+
}
|
|
93
|
+
case 'command-edit':
|
|
94
|
+
return {
|
|
95
|
+
left: `${sessionIcon} ${sessionLabel}`,
|
|
96
|
+
right: state.modal.type === 'git-commit' ? getGitHint('git-commit') : 'Esc cancel',
|
|
97
|
+
}
|
|
80
98
|
case 'navigation':
|
|
81
99
|
default:
|
|
82
100
|
return {
|
package/src/ui/syntax.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getTreeSitterClient, SyntaxStyle, type ThemeTokenStyle } from '@opentui/core'
|
|
2
|
+
|
|
3
|
+
import { onThemeChange, theme } from './theme'
|
|
4
|
+
|
|
5
|
+
let cachedStyle: SyntaxStyle | null = null
|
|
6
|
+
let clientInitPromise: Promise<void> | null = null
|
|
7
|
+
|
|
8
|
+
onThemeChange(() => {
|
|
9
|
+
cachedStyle = null
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
function buildTokens(): ThemeTokenStyle[] {
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
scope: ['comment', 'spell'],
|
|
16
|
+
style: { dim: true, foreground: theme.textMuted, italic: true },
|
|
17
|
+
},
|
|
18
|
+
{ scope: ['comment.documentation'], style: { foreground: theme.textMuted, italic: true } },
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
scope: ['string', 'character', 'character.special', 'string.special'],
|
|
22
|
+
style: { foreground: theme.warning },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
scope: ['string.escape', 'string.regexp', 'string.special.url'],
|
|
26
|
+
style: { foreground: theme.accentAlt },
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
scope: ['number', 'boolean', 'constant.builtin', 'constant.numeric'],
|
|
31
|
+
style: { foreground: theme.danger },
|
|
32
|
+
},
|
|
33
|
+
{ scope: ['constant', 'constant.character'], style: { foreground: theme.danger } },
|
|
34
|
+
|
|
35
|
+
{ scope: ['keyword'], style: { bold: true, foreground: theme.accentAlt } },
|
|
36
|
+
{
|
|
37
|
+
scope: [
|
|
38
|
+
'keyword.conditional',
|
|
39
|
+
'keyword.exception',
|
|
40
|
+
'keyword.import',
|
|
41
|
+
'keyword.modifier',
|
|
42
|
+
'keyword.repeat',
|
|
43
|
+
],
|
|
44
|
+
style: { bold: true, foreground: theme.accentAlt },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
scope: [
|
|
48
|
+
'keyword.coroutine',
|
|
49
|
+
'keyword.directive',
|
|
50
|
+
'keyword.function',
|
|
51
|
+
'keyword.return',
|
|
52
|
+
'keyword.type',
|
|
53
|
+
],
|
|
54
|
+
style: { bold: true, foreground: theme.accentAlt, italic: true },
|
|
55
|
+
},
|
|
56
|
+
{ scope: ['keyword.operator', 'operator'], style: { foreground: theme.accent } },
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
scope: ['function', 'function.call', 'function.method', 'function.method.call'],
|
|
60
|
+
style: { foreground: theme.success },
|
|
61
|
+
},
|
|
62
|
+
{ scope: ['function.builtin'], style: { foreground: theme.success, italic: true } },
|
|
63
|
+
{ scope: ['constructor'], style: { bold: true, foreground: theme.success } },
|
|
64
|
+
|
|
65
|
+
{ scope: ['type', 'type.definition'], style: { foreground: theme.accent, italic: true } },
|
|
66
|
+
{ scope: ['type.builtin'], style: { bold: true, foreground: theme.accent } },
|
|
67
|
+
|
|
68
|
+
{ scope: ['variable'], style: { foreground: theme.text } },
|
|
69
|
+
{ scope: ['variable.builtin'], style: { foreground: theme.danger, italic: true } },
|
|
70
|
+
{ scope: ['variable.parameter', 'parameter'], style: { foreground: theme.text, italic: true } },
|
|
71
|
+
{ scope: ['variable.member', 'property', 'field'], style: { foreground: theme.accent } },
|
|
72
|
+
|
|
73
|
+
{ scope: ['attribute'], style: { foreground: theme.warning, italic: true } },
|
|
74
|
+
{ scope: ['tag'], style: { foreground: theme.danger } },
|
|
75
|
+
{ scope: ['tag.attribute'], style: { foreground: theme.warning, italic: true } },
|
|
76
|
+
|
|
77
|
+
{ scope: ['label'], style: { foreground: theme.warning } },
|
|
78
|
+
{ scope: ['module', 'namespace'], style: { foreground: theme.accentAlt, italic: true } },
|
|
79
|
+
{ scope: ['module.builtin'], style: { bold: true, foreground: theme.accentAlt, italic: true } },
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
scope: ['punctuation.bracket', 'punctuation.delimiter'],
|
|
83
|
+
style: { foreground: theme.textMuted },
|
|
84
|
+
},
|
|
85
|
+
{ scope: ['punctuation.special'], style: { foreground: theme.warning } },
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getSyntaxStyle(): SyntaxStyle {
|
|
90
|
+
if (!cachedStyle) {
|
|
91
|
+
cachedStyle = SyntaxStyle.fromTheme(buildTokens())
|
|
92
|
+
}
|
|
93
|
+
return cachedStyle
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getSyntaxClient() {
|
|
97
|
+
const client = getTreeSitterClient()
|
|
98
|
+
if (!clientInitPromise) {
|
|
99
|
+
clientInitPromise = client.initialize().catch(() => {})
|
|
100
|
+
}
|
|
101
|
+
return client
|
|
102
|
+
}
|
package/src/ui/theme.ts
CHANGED
|
@@ -2,8 +2,17 @@ import { type ThemeColors, type ThemeId, THEMES } from './themes'
|
|
|
2
2
|
|
|
3
3
|
export const theme: ThemeColors = { ...THEMES.aimux.colors }
|
|
4
4
|
|
|
5
|
+
type ThemeListener = () => void
|
|
6
|
+
const themeListeners = new Set<ThemeListener>()
|
|
7
|
+
|
|
8
|
+
export function onThemeChange(listener: ThemeListener): () => void {
|
|
9
|
+
themeListeners.add(listener)
|
|
10
|
+
return () => themeListeners.delete(listener)
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
export function applyTheme(id: ThemeId): void {
|
|
6
14
|
const entry = THEMES[id]
|
|
7
15
|
if (!entry) return
|
|
8
16
|
Object.assign(theme, entry.colors)
|
|
17
|
+
for (const listener of themeListeners) listener()
|
|
9
18
|
}
|
package/src/ui/themes.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface ThemeColors {
|
|
|
14
14
|
danger: string
|
|
15
15
|
success: string
|
|
16
16
|
dim: string
|
|
17
|
+
diffAddBg: string
|
|
18
|
+
diffRemoveBg: string
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export type ThemeId =
|
|
@@ -38,6 +40,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
38
40
|
border: '#2d3f52',
|
|
39
41
|
borderActive: '#7cd1b8',
|
|
40
42
|
danger: '#f38ba8',
|
|
43
|
+
diffAddBg: '#1e3d2b',
|
|
44
|
+
diffRemoveBg: '#3b1e27',
|
|
41
45
|
dim: '#243242',
|
|
42
46
|
overlay: '#0b1016',
|
|
43
47
|
panel: '#16202b',
|
|
@@ -58,6 +62,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
58
62
|
border: '#45475a',
|
|
59
63
|
borderActive: '#cba6f7',
|
|
60
64
|
danger: '#f38ba8',
|
|
65
|
+
diffAddBg: '#273c32',
|
|
66
|
+
diffRemoveBg: '#3b2838',
|
|
61
67
|
dim: '#313244',
|
|
62
68
|
overlay: '#181825',
|
|
63
69
|
panel: '#232334',
|
|
@@ -78,6 +84,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
78
84
|
border: '#44475a',
|
|
79
85
|
borderActive: '#bd93f9',
|
|
80
86
|
danger: '#ff5555',
|
|
87
|
+
diffAddBg: '#2f4630',
|
|
88
|
+
diffRemoveBg: '#4a2f3b',
|
|
81
89
|
dim: '#383a4a',
|
|
82
90
|
overlay: '#1f2029',
|
|
83
91
|
panel: '#2d2f3d',
|
|
@@ -98,6 +106,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
98
106
|
border: '#2a3440',
|
|
99
107
|
borderActive: '#bd93f9',
|
|
100
108
|
danger: '#ff5555',
|
|
109
|
+
diffAddBg: '#162b1d',
|
|
110
|
+
diffRemoveBg: '#2a1820',
|
|
101
111
|
dim: '#1e2630',
|
|
102
112
|
overlay: '#090d11',
|
|
103
113
|
panel: '#131920',
|
|
@@ -118,6 +128,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
118
128
|
border: '#4f585e',
|
|
119
129
|
borderActive: '#a7c080',
|
|
120
130
|
danger: '#e67e80',
|
|
131
|
+
diffAddBg: '#35473a',
|
|
132
|
+
diffRemoveBg: '#45353a',
|
|
121
133
|
dim: '#3d484d',
|
|
122
134
|
overlay: '#232a2e',
|
|
123
135
|
panel: '#343f44',
|
|
@@ -138,6 +150,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
138
150
|
border: '#504945',
|
|
139
151
|
borderActive: '#b8bb26',
|
|
140
152
|
danger: '#fb4934',
|
|
153
|
+
diffAddBg: '#2f3a28',
|
|
154
|
+
diffRemoveBg: '#3a2828',
|
|
141
155
|
dim: '#3c3836',
|
|
142
156
|
overlay: '#1f1d1b',
|
|
143
157
|
panel: '#2e2e2e',
|
|
@@ -158,6 +172,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
158
172
|
border: '#363646',
|
|
159
173
|
borderActive: '#7e9cd8',
|
|
160
174
|
danger: '#e82424',
|
|
175
|
+
diffAddBg: '#28332a',
|
|
176
|
+
diffRemoveBg: '#35252b',
|
|
161
177
|
dim: '#2a2a37',
|
|
162
178
|
overlay: '#171720',
|
|
163
179
|
panel: '#24242e',
|
|
@@ -178,6 +194,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
178
194
|
border: '#434c5e',
|
|
179
195
|
borderActive: '#88c0d0',
|
|
180
196
|
danger: '#bf616a',
|
|
197
|
+
diffAddBg: '#2f3d38',
|
|
198
|
+
diffRemoveBg: '#3d3035',
|
|
181
199
|
dim: '#3b4252',
|
|
182
200
|
overlay: '#252a33',
|
|
183
201
|
panel: '#333a47',
|
|
@@ -198,6 +216,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
198
216
|
border: '#3e4452',
|
|
199
217
|
borderActive: '#61afef',
|
|
200
218
|
danger: '#e06c75',
|
|
219
|
+
diffAddBg: '#2c3b2f',
|
|
220
|
+
diffRemoveBg: '#3d2c33',
|
|
201
221
|
dim: '#3b4048',
|
|
202
222
|
overlay: '#1f2329',
|
|
203
223
|
panel: '#2c313a',
|
|
@@ -218,6 +238,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
218
238
|
border: '#2e5560',
|
|
219
239
|
borderActive: '#268bd2',
|
|
220
240
|
danger: '#dc322f',
|
|
241
|
+
diffAddBg: '#1a3a30',
|
|
242
|
+
diffRemoveBg: '#3a1f28',
|
|
221
243
|
dim: '#073642',
|
|
222
244
|
overlay: '#00242c',
|
|
223
245
|
panel: '#003340',
|
|
@@ -238,6 +260,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
|
|
|
238
260
|
border: '#3b4261',
|
|
239
261
|
borderActive: '#7aa2f7',
|
|
240
262
|
danger: '#f7768e',
|
|
263
|
+
diffAddBg: '#22362d',
|
|
264
|
+
diffRemoveBg: '#362330',
|
|
241
265
|
dim: '#292e42',
|
|
242
266
|
overlay: '#141722',
|
|
243
267
|
panel: '#1f2335',
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { useAppStore } from '../../state/app-store'
|
|
2
|
-
import { theme } from '../theme'
|
|
3
|
-
|
|
4
|
-
function formatChord(chord: string): string {
|
|
5
|
-
if (chord === 'space') return '␣'
|
|
6
|
-
if (chord === 'return') return '⏎'
|
|
7
|
-
if (chord === 'escape') return 'Esc'
|
|
8
|
-
if (chord === 'tab') return 'Tab'
|
|
9
|
-
if (chord === 'backspace') return '⌫'
|
|
10
|
-
if (chord === 'up') return '↑'
|
|
11
|
-
if (chord === 'down') return '↓'
|
|
12
|
-
if (chord === 'left') return '←'
|
|
13
|
-
if (chord === 'right') return '→'
|
|
14
|
-
return chord
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function formatSequence(chords: string[]): string {
|
|
18
|
-
return chords.map(formatChord).join(' ')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function PendingChordIndicator() {
|
|
22
|
-
const pendingChords = useAppStore((s) => s.pendingChords)
|
|
23
|
-
|
|
24
|
-
if (!pendingChords || pendingChords.length === 0) return null
|
|
25
|
-
|
|
26
|
-
const label = formatSequence(pendingChords)
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<box
|
|
30
|
-
position="absolute"
|
|
31
|
-
bottom={2}
|
|
32
|
-
right={2}
|
|
33
|
-
paddingLeft={1}
|
|
34
|
-
paddingRight={1}
|
|
35
|
-
backgroundColor={theme.accent}
|
|
36
|
-
borderColor={theme.borderActive}
|
|
37
|
-
>
|
|
38
|
-
<text fg={theme.background}>{label}…</text>
|
|
39
|
-
</box>
|
|
40
|
-
)
|
|
41
|
-
}
|