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.
Files changed (50) hide show
  1. package/bin/saeeol.cjs +187 -0
  2. package/npm/bin/saeeol +0 -0
  3. package/package.json +2 -2
  4. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +3 -3
  5. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +1 -1
  6. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +5 -5
  7. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +4 -4
  8. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +5 -5
  9. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +3 -3
  10. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +2 -2
  11. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +3 -3
  12. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +4 -4
  13. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +4 -4
  14. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +4 -4
  15. package/src/cli/cmd/tui/context/app/args.tsx +15 -0
  16. package/src/cli/cmd/tui/context/app/directory.ts +15 -0
  17. package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
  18. package/src/cli/cmd/tui/context/app/editor.ts +425 -0
  19. package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
  20. package/src/cli/cmd/tui/context/app/project.tsx +109 -0
  21. package/src/cli/cmd/tui/context/app/route.tsx +67 -0
  22. package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
  23. package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
  24. package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
  25. package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
  26. package/src/cli/cmd/tui/context/args.tsx +1 -15
  27. package/src/cli/cmd/tui/context/directory.ts +1 -15
  28. package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
  29. package/src/cli/cmd/tui/context/editor.ts +1 -425
  30. package/src/cli/cmd/tui/context/event.ts +1 -45
  31. package/src/cli/cmd/tui/context/exit.tsx +1 -67
  32. package/src/cli/cmd/tui/context/helper.tsx +1 -25
  33. package/src/cli/cmd/tui/context/keybind.tsx +1 -105
  34. package/src/cli/cmd/tui/context/kv.tsx +1 -76
  35. package/src/cli/cmd/tui/context/local.tsx +1 -478
  36. package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
  37. package/src/cli/cmd/tui/context/project.tsx +1 -109
  38. package/src/cli/cmd/tui/context/prompt.tsx +1 -18
  39. package/src/cli/cmd/tui/context/route.tsx +1 -67
  40. package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
  41. package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
  42. package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
  43. package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
  44. package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
  45. package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
  46. package/src/cli/cmd/tui/context/sdk.tsx +1 -142
  47. package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
  48. package/src/cli/cmd/tui/context/sync.tsx +1 -713
  49. package/src/cli/cmd/tui/context/theme.tsx +1 -307
  50. package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
@@ -1,425 +1 @@
1
- import { expDelay } from "@saeeol/boxes/schedule"
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
- import type { Event } from "@saeeol/sdk/v2"
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
- import { useRenderer } from "@opentui/solid"
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
- import { createContext, Show, useContext, type ParentProps } from "solid-js"
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
- import { createMemo } from "solid-js"
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"