saeeol 1.2.2 → 1.2.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/bin/saeeol.cjs +187 -0
- package/npm/bin/saeeol +0 -0
- package/package.json +2 -2
- package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +1 -1
- package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +2 -2
- package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +4 -4
- package/src/cli/cmd/tui/context/app/args.tsx +15 -0
- package/src/cli/cmd/tui/context/app/directory.ts +15 -0
- package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
- package/src/cli/cmd/tui/context/app/editor.ts +425 -0
- package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/app/project.tsx +109 -0
- package/src/cli/cmd/tui/context/app/route.tsx +67 -0
- package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
- package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
- package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
- package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/context/args.tsx +1 -15
- package/src/cli/cmd/tui/context/directory.ts +1 -15
- package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
- package/src/cli/cmd/tui/context/editor.ts +1 -425
- package/src/cli/cmd/tui/context/event.ts +1 -45
- package/src/cli/cmd/tui/context/exit.tsx +1 -67
- package/src/cli/cmd/tui/context/helper.tsx +1 -25
- package/src/cli/cmd/tui/context/keybind.tsx +1 -105
- package/src/cli/cmd/tui/context/kv.tsx +1 -76
- package/src/cli/cmd/tui/context/local.tsx +1 -478
- package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
- package/src/cli/cmd/tui/context/project.tsx +1 -109
- package/src/cli/cmd/tui/context/prompt.tsx +1 -18
- package/src/cli/cmd/tui/context/route.tsx +1 -67
- package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
- package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
- package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
- package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
- package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
- package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
- package/src/cli/cmd/tui/context/sdk.tsx +1 -142
- package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/sync.tsx +1 -713
- package/src/cli/cmd/tui/context/theme.tsx +1 -307
- package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
|
@@ -1,425 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
3
|
-
import os from "node:os"
|
|
4
|
-
import path from "node:path"
|
|
5
|
-
import { onCleanup, onMount } from "solid-js"
|
|
6
|
-
import { createStore } from "solid-js/store"
|
|
7
|
-
import z from "zod"
|
|
8
|
-
import { isRecord } from "@/util/record"
|
|
9
|
-
import { createSimpleContext } from "./helper"
|
|
10
|
-
import { resolveZedDbPath, resolveZedSelection } from "./editor-zed"
|
|
11
|
-
|
|
12
|
-
const MCP_PROTOCOL_VERSION = "2025-11-25"
|
|
13
|
-
|
|
14
|
-
const JsonRpcMessageSchema = z.object({
|
|
15
|
-
id: z.union([z.number(), z.string(), z.null()]).optional(),
|
|
16
|
-
method: z.string().optional(),
|
|
17
|
-
params: z.unknown().optional(),
|
|
18
|
-
result: z.unknown().optional(),
|
|
19
|
-
error: z
|
|
20
|
-
.object({
|
|
21
|
-
code: z.number().optional(),
|
|
22
|
-
message: z.string().optional(),
|
|
23
|
-
})
|
|
24
|
-
.optional(),
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
const PositionSchema = z.object({
|
|
28
|
-
line: z.number(),
|
|
29
|
-
character: z.number(),
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
const EditorSelectionRangeSchema = z.object({
|
|
33
|
-
text: z.string(),
|
|
34
|
-
selection: z.object({
|
|
35
|
-
start: PositionSchema,
|
|
36
|
-
end: PositionSchema,
|
|
37
|
-
}),
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
const EditorSelectionSchema = z
|
|
41
|
-
.union([
|
|
42
|
-
z.object({
|
|
43
|
-
filePath: z.string(),
|
|
44
|
-
source: z.enum(["websocket", "zed"]).optional(),
|
|
45
|
-
ranges: z.array(EditorSelectionRangeSchema).min(1),
|
|
46
|
-
}),
|
|
47
|
-
z.object({
|
|
48
|
-
text: z.string(),
|
|
49
|
-
filePath: z.string(),
|
|
50
|
-
source: z.enum(["websocket", "zed"]).optional(),
|
|
51
|
-
selection: z.object({
|
|
52
|
-
start: PositionSchema,
|
|
53
|
-
end: PositionSchema,
|
|
54
|
-
}),
|
|
55
|
-
}),
|
|
56
|
-
])
|
|
57
|
-
.transform((value) =>
|
|
58
|
-
"ranges" in value
|
|
59
|
-
? value
|
|
60
|
-
: {
|
|
61
|
-
filePath: value.filePath,
|
|
62
|
-
source: value.source,
|
|
63
|
-
ranges: [
|
|
64
|
-
{
|
|
65
|
-
text: value.text,
|
|
66
|
-
selection: value.selection,
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
const EditorMentionSchema = z.object({
|
|
73
|
-
filePath: z.string(),
|
|
74
|
-
lineStart: z.number(),
|
|
75
|
-
lineEnd: z.number(),
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
const EditorServerInfoSchema = z.object({
|
|
79
|
-
protocolVersion: z.string().optional(),
|
|
80
|
-
serverInfo: z
|
|
81
|
-
.object({
|
|
82
|
-
name: z.string().optional(),
|
|
83
|
-
version: z.string().optional(),
|
|
84
|
-
})
|
|
85
|
-
.optional(),
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
type JsonRpcMessage = z.infer<typeof JsonRpcMessageSchema>
|
|
89
|
-
export type EditorSelection = z.infer<typeof EditorSelectionSchema>
|
|
90
|
-
export type EditorMention = z.infer<typeof EditorMentionSchema>
|
|
91
|
-
type EditorServerInfo = z.infer<typeof EditorServerInfoSchema>
|
|
92
|
-
|
|
93
|
-
type EditorConnection = {
|
|
94
|
-
url: string
|
|
95
|
-
authToken?: string
|
|
96
|
-
source: string
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
type EditorLockFile = {
|
|
100
|
-
port: number
|
|
101
|
-
authToken?: string
|
|
102
|
-
transport?: string
|
|
103
|
-
workspaceFolders: string[]
|
|
104
|
-
mtimeMs: number
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export const { use: useEditorContext, provider: EditorContextProvider } = createSimpleContext({
|
|
108
|
-
name: "EditorContext",
|
|
109
|
-
init: (props: { WebSocketImpl?: typeof WebSocket }) => {
|
|
110
|
-
const mentionListeners = new Set<(mention: EditorMention) => void>()
|
|
111
|
-
const WebSocketImpl = props.WebSocketImpl ?? WebSocket
|
|
112
|
-
const [store, setStore] = createStore<{
|
|
113
|
-
status: "disabled" | "connecting" | "connected"
|
|
114
|
-
selection: EditorSelection | undefined
|
|
115
|
-
server: EditorServerInfo | undefined
|
|
116
|
-
}>({
|
|
117
|
-
status: "disabled",
|
|
118
|
-
selection: undefined,
|
|
119
|
-
server: undefined,
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
let socket: WebSocket | undefined
|
|
123
|
-
let closed = false
|
|
124
|
-
let reconnect: ReturnType<typeof setTimeout> | undefined
|
|
125
|
-
let attempt = 0
|
|
126
|
-
let requestID = 0
|
|
127
|
-
let zedSelection: Promise<void> | undefined
|
|
128
|
-
let lastZedSelectionKey: string | undefined
|
|
129
|
-
let directory = process.cwd()
|
|
130
|
-
const pending = new Map<number, string>()
|
|
131
|
-
|
|
132
|
-
const send = (payload: JsonRpcMessage) => {
|
|
133
|
-
if (!socket || socket.readyState !== 1) return
|
|
134
|
-
socket.send(JSON.stringify({ jsonrpc: "2.0", ...payload }))
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const request = (method: string, params?: unknown) => {
|
|
138
|
-
requestID += 1
|
|
139
|
-
pending.set(requestID, method)
|
|
140
|
-
send({ id: requestID, method, params })
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const connect = () => {
|
|
144
|
-
if (closed) return
|
|
145
|
-
|
|
146
|
-
const connection = resolveEditorConnection(directory)
|
|
147
|
-
if (!connection) {
|
|
148
|
-
const dbPath = resolveZedDbPath()
|
|
149
|
-
if (!dbPath) {
|
|
150
|
-
setStore("status", "disabled")
|
|
151
|
-
scheduleReconnect()
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
zedSelection ??= resolveZedSelection(dbPath, directory)
|
|
155
|
-
.then((result) => {
|
|
156
|
-
if (closed || socket) return
|
|
157
|
-
if (result.type === "unavailable") return
|
|
158
|
-
const selection = result.type === "selection" ? result.selection : undefined
|
|
159
|
-
const key = editorSelectionKey(selection)
|
|
160
|
-
if (key !== lastZedSelectionKey) {
|
|
161
|
-
lastZedSelectionKey = key
|
|
162
|
-
setStore("selection", selection)
|
|
163
|
-
setStore("status", selection ? "connected" : "disabled")
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
.catch(() => {
|
|
167
|
-
// Keep the last known Zed selection for transient polling failures.
|
|
168
|
-
})
|
|
169
|
-
.finally(() => {
|
|
170
|
-
zedSelection = undefined
|
|
171
|
-
})
|
|
172
|
-
scheduleZedPoll()
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
setStore("status", "connecting")
|
|
177
|
-
const current = openEditorSocket(connection, WebSocketImpl)
|
|
178
|
-
socket = current
|
|
179
|
-
|
|
180
|
-
current.addEventListener("open", () => {
|
|
181
|
-
if (socket !== current) {
|
|
182
|
-
current.close()
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
attempt = 0
|
|
187
|
-
setStore("status", "connected")
|
|
188
|
-
request("initialize", {
|
|
189
|
-
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
190
|
-
capabilities: {},
|
|
191
|
-
clientInfo: { name: "saeeol", version: "0.0.0" },
|
|
192
|
-
})
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
current.addEventListener("message", (event) => {
|
|
196
|
-
const message = parseMessage(event.data)
|
|
197
|
-
if (!message) return
|
|
198
|
-
|
|
199
|
-
const selection =
|
|
200
|
-
message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined
|
|
201
|
-
if (selection?.success) {
|
|
202
|
-
setStore("selection", { ...selection.data, source: "websocket" })
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const mention = message.method === "at_mentioned" ? EditorMentionSchema.safeParse(message.params) : undefined
|
|
207
|
-
if (mention?.success) {
|
|
208
|
-
mentionListeners.forEach((listener) => listener(mention.data))
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (typeof message.id !== "number") return
|
|
213
|
-
|
|
214
|
-
const method = pending.get(message.id)
|
|
215
|
-
if (!method) return
|
|
216
|
-
|
|
217
|
-
pending.delete(message.id)
|
|
218
|
-
if (message.error) return
|
|
219
|
-
|
|
220
|
-
const initialize = method === "initialize" ? EditorServerInfoSchema.safeParse(message.result) : undefined
|
|
221
|
-
if (initialize?.success) {
|
|
222
|
-
setStore("server", initialize.data)
|
|
223
|
-
send({ method: "notifications/initialized" })
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
current.addEventListener("close", () => {
|
|
229
|
-
if (socket !== current) return
|
|
230
|
-
|
|
231
|
-
socket = undefined
|
|
232
|
-
pending.clear()
|
|
233
|
-
if (closed) return
|
|
234
|
-
|
|
235
|
-
setStore("status", "connecting")
|
|
236
|
-
scheduleReconnect()
|
|
237
|
-
})
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const scheduleReconnect = () => {
|
|
241
|
-
if (closed) return
|
|
242
|
-
if (reconnect) clearTimeout(reconnect)
|
|
243
|
-
attempt += 1
|
|
244
|
-
const delay = Math.min(expDelay(1000, attempt - 1), 10_000)
|
|
245
|
-
reconnect = setTimeout(connect, delay)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const scheduleZedPoll = () => {
|
|
249
|
-
if (closed) return
|
|
250
|
-
if (reconnect) clearTimeout(reconnect)
|
|
251
|
-
reconnect = setTimeout(connect, 1000)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const reconnectWithDirectory = (nextDirectory?: string) => {
|
|
255
|
-
const resolved = nextDirectory || process.cwd()
|
|
256
|
-
if (directory === resolved) return
|
|
257
|
-
|
|
258
|
-
directory = resolved
|
|
259
|
-
attempt = 0
|
|
260
|
-
pending.clear()
|
|
261
|
-
lastZedSelectionKey = undefined
|
|
262
|
-
if (reconnect) clearTimeout(reconnect)
|
|
263
|
-
reconnect = undefined
|
|
264
|
-
if (socket) {
|
|
265
|
-
const current = socket
|
|
266
|
-
socket = undefined
|
|
267
|
-
current.close()
|
|
268
|
-
}
|
|
269
|
-
setStore("status", "disabled")
|
|
270
|
-
setStore("selection", undefined)
|
|
271
|
-
setStore("server", undefined)
|
|
272
|
-
connect()
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
onMount(() => {
|
|
276
|
-
connect()
|
|
277
|
-
|
|
278
|
-
onCleanup(() => {
|
|
279
|
-
closed = true
|
|
280
|
-
if (reconnect) clearTimeout(reconnect)
|
|
281
|
-
socket?.close()
|
|
282
|
-
})
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
return {
|
|
286
|
-
enabled() {
|
|
287
|
-
return Boolean(resolveEditorConnection(directory) || resolveZedDbPath())
|
|
288
|
-
},
|
|
289
|
-
connected() {
|
|
290
|
-
return store.status === "connected"
|
|
291
|
-
},
|
|
292
|
-
selection() {
|
|
293
|
-
return store.selection
|
|
294
|
-
},
|
|
295
|
-
clearSelection() {
|
|
296
|
-
lastZedSelectionKey = undefined
|
|
297
|
-
setStore("selection", undefined)
|
|
298
|
-
},
|
|
299
|
-
onMention(listener: (mention: EditorMention) => void) {
|
|
300
|
-
mentionListeners.add(listener)
|
|
301
|
-
return () => mentionListeners.delete(listener)
|
|
302
|
-
},
|
|
303
|
-
server() {
|
|
304
|
-
return store.server
|
|
305
|
-
},
|
|
306
|
-
reconnect(directory?: string) {
|
|
307
|
-
setStore("selection", undefined)
|
|
308
|
-
reconnectWithDirectory(directory)
|
|
309
|
-
},
|
|
310
|
-
}
|
|
311
|
-
},
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
function parsePort(value: string | undefined) {
|
|
315
|
-
if (!value) return
|
|
316
|
-
|
|
317
|
-
const parsed = Number.parseInt(value, 10)
|
|
318
|
-
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) return
|
|
319
|
-
return parsed
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function resolveEditorConnection(directory: string): EditorConnection | undefined {
|
|
323
|
-
const port = parsePort(process.env.CLAUDE_CODE_SSE_PORT || process.env.SAEEOL_EDITOR_SSE_PORT)
|
|
324
|
-
if (port) {
|
|
325
|
-
return {
|
|
326
|
-
url: `ws://127.0.0.1:${port}`,
|
|
327
|
-
source: `env:${port}`,
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const lock = resolveEditorLockFile(directory)
|
|
332
|
-
if (lock) {
|
|
333
|
-
return {
|
|
334
|
-
url: `ws://127.0.0.1:${lock.port}`,
|
|
335
|
-
authToken: lock.authToken,
|
|
336
|
-
source: `lock:${lock.port}`,
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function resolveEditorLockFile(activeDirectory: string) {
|
|
342
|
-
const directory = path.join(os.homedir(), ".claude", "ide")
|
|
343
|
-
let entries: string[]
|
|
344
|
-
|
|
345
|
-
try {
|
|
346
|
-
entries = readdirSync(directory)
|
|
347
|
-
} catch {
|
|
348
|
-
return
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// longest workspace folder that contains the active session directory; 0 if none match
|
|
352
|
-
const bestMatchLength = (lock: EditorLockFile) =>
|
|
353
|
-
Math.max(0, ...lock.workspaceFolders.map((folder) => pathContainsLength(folder, activeDirectory)))
|
|
354
|
-
const locks = entries
|
|
355
|
-
.filter((entry) => entry.endsWith(".lock"))
|
|
356
|
-
.map((entry) => readEditorLockFile(path.join(directory, entry)))
|
|
357
|
-
.filter((entry): entry is EditorLockFile => Boolean(entry))
|
|
358
|
-
.filter((entry) => bestMatchLength(entry) > 0)
|
|
359
|
-
// prefer locks with longer matching workspace folders, then more recent ones
|
|
360
|
-
.sort((left, right) => bestMatchLength(right) - bestMatchLength(left) || right.mtimeMs - left.mtimeMs)
|
|
361
|
-
return locks[0]
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function readEditorLockFile(filePath: string): EditorLockFile | undefined {
|
|
365
|
-
const port = parsePort(path.basename(filePath, ".lock"))
|
|
366
|
-
if (!port) return
|
|
367
|
-
|
|
368
|
-
try {
|
|
369
|
-
const parsed = JSON.parse(readFileSync(filePath, "utf-8")) as unknown
|
|
370
|
-
if (!isRecord(parsed)) return
|
|
371
|
-
if (parsed.transport !== undefined && parsed.transport !== "ws") return
|
|
372
|
-
|
|
373
|
-
return {
|
|
374
|
-
port,
|
|
375
|
-
authToken: typeof parsed.authToken === "string" ? parsed.authToken : undefined,
|
|
376
|
-
transport: typeof parsed.transport === "string" ? parsed.transport : undefined,
|
|
377
|
-
workspaceFolders: Array.isArray(parsed.workspaceFolders)
|
|
378
|
-
? parsed.workspaceFolders.filter((value): value is string => typeof value === "string")
|
|
379
|
-
: [],
|
|
380
|
-
mtimeMs: statSync(filePath).mtimeMs,
|
|
381
|
-
}
|
|
382
|
-
} catch {
|
|
383
|
-
return
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
export function editorSelectionKey(selection: EditorSelection | undefined) {
|
|
388
|
-
if (!selection) return ""
|
|
389
|
-
return [
|
|
390
|
-
selection.filePath,
|
|
391
|
-
...selection.ranges.flatMap((range) => [
|
|
392
|
-
range.selection.start.line,
|
|
393
|
-
range.selection.start.character,
|
|
394
|
-
range.selection.end.line,
|
|
395
|
-
range.selection.end.character,
|
|
396
|
-
range.text,
|
|
397
|
-
]),
|
|
398
|
-
].join("\0")
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function pathContainsLength(parent: string, child: string) {
|
|
402
|
-
const resolved = path.resolve(parent)
|
|
403
|
-
const relative = path.relative(resolved, path.resolve(child))
|
|
404
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved.length : 0
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function openEditorSocket(connection: EditorConnection, WebSocketImpl: typeof WebSocket) {
|
|
408
|
-
if (!connection.authToken) return new WebSocketImpl(connection.url)
|
|
409
|
-
|
|
410
|
-
return new WebSocketImpl(connection.url, {
|
|
411
|
-
headers: {
|
|
412
|
-
"x-claude-code-ide-authorization": connection.authToken,
|
|
413
|
-
},
|
|
414
|
-
} as any)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function parseMessage(value: unknown) {
|
|
418
|
-
if (typeof value !== "string") return
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
return JsonRpcMessageSchema.parse(JSON.parse(value))
|
|
422
|
-
} catch {
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
}
|
|
1
|
+
export * from "./app/editor"
|
|
@@ -1,45 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { useProject } from "./project"
|
|
3
|
-
import { useSDK } from "./sdk"
|
|
4
|
-
|
|
5
|
-
export function useEvent() {
|
|
6
|
-
const project = useProject()
|
|
7
|
-
const sdk = useSDK()
|
|
8
|
-
|
|
9
|
-
function subscribe(handler: (event: Event) => void) {
|
|
10
|
-
return sdk.event.on("event", (event) => {
|
|
11
|
-
if (event.payload.type === "sync") {
|
|
12
|
-
return
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Special hack for truly global events
|
|
16
|
-
if (event.directory === "global") {
|
|
17
|
-
handler(event.payload)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (project.workspace.current()) {
|
|
21
|
-
if (event.workspace === project.workspace.current()) {
|
|
22
|
-
handler(event.payload)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (event.directory === project.instance.directory()) {
|
|
29
|
-
handler(event.payload)
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function on<T extends Event["type"]>(type: T, handler: (event: Extract<Event, { type: T }>) => void) {
|
|
35
|
-
return subscribe((event) => {
|
|
36
|
-
if (event.type !== type) return
|
|
37
|
-
handler(event as Extract<Event, { type: T }>)
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
subscribe,
|
|
43
|
-
on,
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
export * from "./runtime/event"
|
|
@@ -1,67 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { createSimpleContext } from "./helper"
|
|
3
|
-
import { FormatError, FormatUnknownError } from "@/cli/error"
|
|
4
|
-
import { win32FlushInputBuffer } from "../win32"
|
|
5
|
-
import { resetTerminalState } from "@/saeeol/cli/cmd/tui/util/terminal"
|
|
6
|
-
type Exit = ((reason?: unknown) => Promise<void>) & {
|
|
7
|
-
message: {
|
|
8
|
-
set: (value?: string) => () => void
|
|
9
|
-
clear: () => void
|
|
10
|
-
get: () => string | undefined
|
|
11
|
-
}
|
|
12
|
-
restart: () => Promise<void>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
|
16
|
-
name: "Exit",
|
|
17
|
-
init: (input: { onBeforeExit?: () => Promise<void>; onExit?: () => Promise<void> }) => {
|
|
18
|
-
const renderer = useRenderer()
|
|
19
|
-
let message: string | undefined
|
|
20
|
-
let task: Promise<void> | undefined
|
|
21
|
-
const store = {
|
|
22
|
-
set: (value?: string) => {
|
|
23
|
-
const prev = message
|
|
24
|
-
message = value
|
|
25
|
-
return () => {
|
|
26
|
-
message = prev
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
clear: () => {
|
|
30
|
-
message = undefined
|
|
31
|
-
},
|
|
32
|
-
get: () => message,
|
|
33
|
-
}
|
|
34
|
-
const exit: Exit = Object.assign(
|
|
35
|
-
(reason?: unknown) => {
|
|
36
|
-
if (task) return task
|
|
37
|
-
task = (async () => {
|
|
38
|
-
await input.onBeforeExit?.()
|
|
39
|
-
// Reset window title before destroying renderer
|
|
40
|
-
renderer.setTerminalTitle("")
|
|
41
|
-
renderer.destroy()
|
|
42
|
-
win32FlushInputBuffer()
|
|
43
|
-
resetTerminalState()
|
|
44
|
-
if (reason) {
|
|
45
|
-
const formatted = FormatError(reason) ?? FormatUnknownError(reason)
|
|
46
|
-
if (formatted) {
|
|
47
|
-
process.stderr.write(formatted + "\n")
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const text = store.get()
|
|
51
|
-
if (text) process.stdout.write(text + "\n")
|
|
52
|
-
await input.onExit?.()
|
|
53
|
-
})()
|
|
54
|
-
return task
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
message: store,
|
|
58
|
-
restart: () => {
|
|
59
|
-
globalThis.__SAEEOL_RESTART = true
|
|
60
|
-
return exit()
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
)
|
|
64
|
-
process.on("SIGHUP", () => exit())
|
|
65
|
-
return exit
|
|
66
|
-
},
|
|
67
|
-
})
|
|
1
|
+
export * from "./runtime/exit"
|
|
@@ -1,25 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export function createSimpleContext<T, Props extends Record<string, any>>(input: {
|
|
4
|
-
name: string
|
|
5
|
-
init: ((input: Props) => T) | (() => T)
|
|
6
|
-
}) {
|
|
7
|
-
const ctx = createContext<T>()
|
|
8
|
-
|
|
9
|
-
return {
|
|
10
|
-
provider: (props: ParentProps<Props>) => {
|
|
11
|
-
const init = input.init(props)
|
|
12
|
-
return (
|
|
13
|
-
// @ts-expect-error
|
|
14
|
-
<Show when={init.ready === undefined || init.ready === true}>
|
|
15
|
-
<ctx.Provider value={init}>{props.children}</ctx.Provider>
|
|
16
|
-
</Show>
|
|
17
|
-
)
|
|
18
|
-
},
|
|
19
|
-
use() {
|
|
20
|
-
const value = useContext(ctx)
|
|
21
|
-
if (!value) throw new Error(`${input.name} context must be used within a context provider`)
|
|
22
|
-
return value
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
}
|
|
1
|
+
export * from "./app/helper"
|
|
@@ -1,105 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Keybind } from "@/util/keybind"
|
|
3
|
-
import { pipe, mapValues } from "remeda"
|
|
4
|
-
import type { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
|
5
|
-
import type { ParsedKey, Renderable } from "@opentui/core"
|
|
6
|
-
import { createStore } from "solid-js/store"
|
|
7
|
-
import { useKeyboard, useRenderer } from "@opentui/solid"
|
|
8
|
-
import { createSimpleContext } from "./helper"
|
|
9
|
-
import { useTuiConfig } from "./tui-config"
|
|
10
|
-
|
|
11
|
-
export type KeybindKey = keyof NonNullable<TuiConfig.Info["keybinds"]> & string
|
|
12
|
-
|
|
13
|
-
export const { use: useKeybind, provider: KeybindProvider } = createSimpleContext({
|
|
14
|
-
name: "Keybind",
|
|
15
|
-
init: () => {
|
|
16
|
-
const config = useTuiConfig()
|
|
17
|
-
const keybinds = createMemo<Record<string, Keybind.Info[]>>(() => {
|
|
18
|
-
return pipe(
|
|
19
|
-
(config.keybinds ?? {}) as Record<string, string>,
|
|
20
|
-
mapValues((value) => Keybind.parse(value)),
|
|
21
|
-
)
|
|
22
|
-
})
|
|
23
|
-
const [store, setStore] = createStore({
|
|
24
|
-
leader: false,
|
|
25
|
-
})
|
|
26
|
-
const renderer = useRenderer()
|
|
27
|
-
|
|
28
|
-
let focus: Renderable | null
|
|
29
|
-
let timeout: NodeJS.Timeout
|
|
30
|
-
function leader(active: boolean) {
|
|
31
|
-
if (active) {
|
|
32
|
-
setStore("leader", true)
|
|
33
|
-
focus = renderer.currentFocusedRenderable
|
|
34
|
-
focus?.blur()
|
|
35
|
-
if (timeout) clearTimeout(timeout)
|
|
36
|
-
timeout = setTimeout(() => {
|
|
37
|
-
if (!store.leader) return
|
|
38
|
-
leader(false)
|
|
39
|
-
if (!focus || focus.isDestroyed) return
|
|
40
|
-
focus.focus()
|
|
41
|
-
}, 2000)
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!active) {
|
|
46
|
-
if (focus && !renderer.currentFocusedRenderable) {
|
|
47
|
-
focus.focus()
|
|
48
|
-
}
|
|
49
|
-
setStore("leader", false)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
useKeyboard(async (evt) => {
|
|
54
|
-
if (!store.leader && result.match("leader", evt)) {
|
|
55
|
-
leader(true)
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (store.leader && evt.name) {
|
|
60
|
-
setImmediate(() => {
|
|
61
|
-
if (focus && renderer.currentFocusedRenderable === focus) {
|
|
62
|
-
focus.focus()
|
|
63
|
-
}
|
|
64
|
-
leader(false)
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
const result = {
|
|
70
|
-
get all() {
|
|
71
|
-
return keybinds()
|
|
72
|
-
},
|
|
73
|
-
get leader() {
|
|
74
|
-
return store.leader
|
|
75
|
-
},
|
|
76
|
-
parse(evt: ParsedKey): Keybind.Info {
|
|
77
|
-
// Handle special case for Ctrl+Underscore (represented as \x1F)
|
|
78
|
-
if (evt.name === "\x1F") {
|
|
79
|
-
return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader)
|
|
80
|
-
}
|
|
81
|
-
return Keybind.fromParsedKey(evt, store.leader)
|
|
82
|
-
},
|
|
83
|
-
match(key: string, evt: ParsedKey) {
|
|
84
|
-
const list = keybinds()[key] ?? Keybind.parse(key)
|
|
85
|
-
if (!list.length) return false
|
|
86
|
-
const parsed: Keybind.Info = result.parse(evt)
|
|
87
|
-
for (const item of list) {
|
|
88
|
-
if (Keybind.match(item, parsed)) {
|
|
89
|
-
return true
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return false
|
|
93
|
-
},
|
|
94
|
-
print(key: string) {
|
|
95
|
-
const first = keybinds()[key]?.at(0) ?? Keybind.parse(key).at(0)
|
|
96
|
-
if (!first) return ""
|
|
97
|
-
const text = Keybind.toString(first)
|
|
98
|
-
const lead = keybinds().leader?.[0]
|
|
99
|
-
if (!lead) return text
|
|
100
|
-
return text.replace("<leader>", Keybind.toString(lead))
|
|
101
|
-
},
|
|
102
|
-
}
|
|
103
|
-
return result
|
|
104
|
-
},
|
|
105
|
-
})
|
|
1
|
+
export * from "./runtime/keybind"
|