termcast 1.3.54 → 1.4.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.
Files changed (169) hide show
  1. package/dist/action-utils.d.ts.map +1 -1
  2. package/dist/action-utils.js +17 -132
  3. package/dist/action-utils.js.map +1 -1
  4. package/dist/apis/cache.d.ts +8 -30
  5. package/dist/apis/cache.d.ts.map +1 -1
  6. package/dist/apis/cache.js +9 -271
  7. package/dist/apis/cache.js.map +1 -1
  8. package/dist/apis/clipboard.d.ts +4 -2
  9. package/dist/apis/clipboard.d.ts.map +1 -1
  10. package/dist/apis/clipboard.js +18 -31
  11. package/dist/apis/clipboard.js.map +1 -1
  12. package/dist/apis/environment.d.ts.map +1 -1
  13. package/dist/apis/environment.js +14 -49
  14. package/dist/apis/environment.js.map +1 -1
  15. package/dist/apis/localstorage.d.ts +7 -12
  16. package/dist/apis/localstorage.d.ts.map +1 -1
  17. package/dist/apis/localstorage.js +7 -184
  18. package/dist/apis/localstorage.js.map +1 -1
  19. package/dist/app.d.ts.map +1 -1
  20. package/dist/app.js +16 -15
  21. package/dist/app.js.map +1 -1
  22. package/dist/cli.js +7 -6
  23. package/dist/cli.js.map +1 -1
  24. package/dist/components/actions.d.ts.map +1 -1
  25. package/dist/components/actions.js +13 -2
  26. package/dist/components/actions.js.map +1 -1
  27. package/dist/components/extension-preferences.d.ts.map +1 -1
  28. package/dist/components/extension-preferences.js +7 -8
  29. package/dist/components/extension-preferences.js.map +1 -1
  30. package/dist/components/form/file-autocomplete.js +2 -2
  31. package/dist/components/form/file-autocomplete.js.map +1 -1
  32. package/dist/components/list.d.ts.map +1 -1
  33. package/dist/components/list.js +242 -14
  34. package/dist/components/list.js.map +1 -1
  35. package/dist/e2e-node.d.ts.map +1 -1
  36. package/dist/e2e-node.js +5 -4
  37. package/dist/e2e-node.js.map +1 -1
  38. package/dist/extensions/dev.d.ts.map +1 -1
  39. package/dist/extensions/dev.js +5 -2
  40. package/dist/extensions/dev.js.map +1 -1
  41. package/dist/globals.d.ts.map +1 -1
  42. package/dist/globals.js +2 -1
  43. package/dist/globals.js.map +1 -1
  44. package/dist/internal/error-handler.d.ts.map +1 -1
  45. package/dist/internal/error-handler.js +21 -19
  46. package/dist/internal/error-handler.js.map +1 -1
  47. package/dist/internal/providers.d.ts.map +1 -1
  48. package/dist/internal/providers.js +41 -1
  49. package/dist/internal/providers.js.map +1 -1
  50. package/dist/logger.d.ts.map +1 -1
  51. package/dist/logger.js +31 -29
  52. package/dist/logger.js.map +1 -1
  53. package/dist/platform/browser/cache.d.ts +41 -0
  54. package/dist/platform/browser/cache.d.ts.map +1 -0
  55. package/dist/platform/browser/cache.js +262 -0
  56. package/dist/platform/browser/cache.js.map +1 -0
  57. package/dist/platform/browser/localstorage.d.ts +20 -0
  58. package/dist/platform/browser/localstorage.d.ts.map +1 -0
  59. package/dist/platform/browser/localstorage.js +102 -0
  60. package/dist/platform/browser/localstorage.js.map +1 -0
  61. package/dist/platform/browser/runtime.d.ts +51 -0
  62. package/dist/platform/browser/runtime.d.ts.map +1 -0
  63. package/dist/platform/browser/runtime.js +164 -0
  64. package/dist/platform/browser/runtime.js.map +1 -0
  65. package/dist/platform/bun/sqlite.d.ts +17 -0
  66. package/dist/platform/bun/sqlite.d.ts.map +1 -0
  67. package/dist/platform/bun/sqlite.js +6 -0
  68. package/dist/platform/bun/sqlite.js.map +1 -0
  69. package/dist/platform/node/cache.d.ts +35 -0
  70. package/dist/platform/node/cache.d.ts.map +1 -0
  71. package/dist/platform/node/cache.js +269 -0
  72. package/dist/platform/node/cache.js.map +1 -0
  73. package/dist/platform/node/localstorage.d.ts +17 -0
  74. package/dist/platform/node/localstorage.d.ts.map +1 -0
  75. package/dist/platform/node/localstorage.js +186 -0
  76. package/dist/platform/node/localstorage.js.map +1 -0
  77. package/dist/platform/node/runtime.d.ts +52 -0
  78. package/dist/platform/node/runtime.d.ts.map +1 -0
  79. package/dist/platform/node/runtime.js +230 -0
  80. package/dist/platform/node/runtime.js.map +1 -0
  81. package/dist/platform/node/sqlite.d.ts +27 -0
  82. package/dist/platform/node/sqlite.d.ts.map +1 -0
  83. package/dist/platform/node/sqlite.js +21 -0
  84. package/dist/platform/node/sqlite.js.map +1 -0
  85. package/dist/state.d.ts +5 -0
  86. package/dist/state.d.ts.map +1 -1
  87. package/dist/state.js +6 -28
  88. package/dist/state.js.map +1 -1
  89. package/dist/utils/file-system.d.ts.map +1 -1
  90. package/dist/utils/file-system.js +17 -22
  91. package/dist/utils/file-system.js.map +1 -1
  92. package/dist/utils.d.ts +1 -1
  93. package/dist/utils.d.ts.map +1 -1
  94. package/dist/utils.js +42 -47
  95. package/dist/utils.js.map +1 -1
  96. package/dist/vim-mode.d.ts +40 -0
  97. package/dist/vim-mode.d.ts.map +1 -0
  98. package/dist/vim-mode.js +135 -0
  99. package/dist/vim-mode.js.map +1 -0
  100. package/fonts/Inconsolata.otf +0 -0
  101. package/fonts/SIL Open Font License.txt +41 -0
  102. package/package.json +60 -8
  103. package/src/action-utils.tsx +27 -124
  104. package/src/apis/cache.test.ts +1 -1
  105. package/src/apis/cache.tsx +9 -373
  106. package/src/apis/clipboard.tsx +29 -38
  107. package/src/apis/environment.tsx +25 -52
  108. package/src/apis/localstorage.tsx +8 -214
  109. package/src/app.tsx +16 -15
  110. package/src/cli.tsx +14 -15
  111. package/src/compile.vitest.tsx +2 -2
  112. package/src/components/actions.tsx +19 -1
  113. package/src/components/extension-preferences.tsx +7 -8
  114. package/src/components/form/file-autocomplete.tsx +2 -2
  115. package/src/components/list.tsx +279 -14
  116. package/src/e2e-node.tsx +7 -7
  117. package/src/examples/action-shortcut.vitest.tsx +2 -2
  118. package/src/examples/actions-context.vitest.tsx +1 -1
  119. package/src/examples/bar-graph-weekly.vitest.tsx +10 -36
  120. package/src/examples/detail-metadata-showcase.vitest.tsx +36 -36
  121. package/src/examples/form-basic.vitest.tsx +21 -17
  122. package/src/examples/github.vitest.tsx +4 -4
  123. package/src/examples/graph-bar-chart.vitest.tsx +13 -11
  124. package/src/examples/graph-polymarket.vitest.tsx +2 -2
  125. package/src/examples/graph-row.vitest.tsx +66 -66
  126. package/src/examples/graph-styles.vitest.tsx +12 -12
  127. package/src/examples/internal/simple-scrollbox.vitest.tsx +14 -48
  128. package/src/examples/list-detail-metadata.vitest.tsx +5 -5
  129. package/src/examples/list-fetch-data.vitest.tsx +3 -3
  130. package/src/examples/list-item-accessories.vitest.tsx +2 -2
  131. package/src/examples/list-loading-empty-view.vitest.tsx +1 -1
  132. package/src/examples/list-no-actions.vitest.tsx +2 -2
  133. package/src/examples/list-scrollbox.vitest.tsx +5 -5
  134. package/src/examples/list-spacing-mode.vitest.tsx +3 -3
  135. package/src/examples/list-with-detail.vitest.tsx +68 -68
  136. package/src/examples/list-with-dropdown.vitest.tsx +5 -5
  137. package/src/examples/list-with-sections.vitest.tsx +27 -27
  138. package/src/examples/simple-candle-chart.vitest.tsx +7 -7
  139. package/src/examples/simple-detail-markdown.vitest.tsx +8 -8
  140. package/src/examples/simple-detail-table.vitest.tsx +8 -8
  141. package/src/examples/simple-graph.vitest.tsx +3 -3
  142. package/src/examples/simple-grid.vitest.tsx +14 -14
  143. package/src/examples/simple-heatmap.vitest.tsx +1 -1
  144. package/src/examples/simple-navigation.vitest.tsx +17 -17
  145. package/src/examples/simple-progress-bar.vitest.tsx +1 -1
  146. package/src/examples/store.vitest.tsx +1 -1
  147. package/src/examples/swift-extension.vitest.tsx +2 -2
  148. package/src/examples/table-edge-cases.vitest.tsx +18 -18
  149. package/src/examples/toast-action.vitest.tsx +2 -2
  150. package/src/extensions/dev.tsx +5 -2
  151. package/src/extensions/dev.vitest.tsx +3 -3
  152. package/src/globals.ts +2 -1
  153. package/src/internal/error-handler.tsx +19 -21
  154. package/src/internal/providers.tsx +39 -0
  155. package/src/logger.tsx +38 -41
  156. package/src/platform/browser/cache.ts +327 -0
  157. package/src/platform/browser/localstorage.ts +119 -0
  158. package/src/platform/browser/runtime.ts +209 -0
  159. package/src/platform/bun/sqlite.ts +19 -0
  160. package/src/platform/node/cache.ts +372 -0
  161. package/src/platform/node/localstorage.ts +214 -0
  162. package/src/platform/node/runtime.ts +264 -0
  163. package/src/platform/node/sqlite.ts +43 -0
  164. package/src/state.tsx +17 -28
  165. package/src/utils/file-system.ts +17 -22
  166. package/src/utils.test.tsx +1 -1
  167. package/src/utils.tsx +56 -47
  168. package/src/vim-mode.tsx +153 -0
  169. package/src/apis/sqlite.ts +0 -14
package/src/utils.tsx CHANGED
@@ -2,9 +2,22 @@ import React, { type ReactNode } from 'react'
2
2
  import { createRoot } from '@opentui/react'
3
3
  import { createCliRenderer } from '@opentui/core'
4
4
  import { TermcastProvider } from 'termcast/src/internal/providers'
5
- import path from 'node:path'
6
- import fs from 'node:fs'
7
- import os from 'node:os'
5
+ import {
6
+ joinPath,
7
+ basename,
8
+ resolvePath,
9
+ homedir,
10
+ platform,
11
+ exit,
12
+ fileExists,
13
+ readFileSync,
14
+ ensureDir,
15
+ execCommand,
16
+ readdirSync,
17
+ rmSync,
18
+ mkdirSync,
19
+ cpSync,
20
+ } from '#platform/runtime'
8
21
  import {
9
22
  parsePackageJson,
10
23
  type RaycastPackageJson,
@@ -14,6 +27,7 @@ import {
14
27
  import { logger } from './logger'
15
28
  import { useStore } from './state'
16
29
  import { initializeTheme } from './theme'
30
+ import { initializeVimMode } from './vim-mode'
17
31
  import { isAppMode } from './apis/environment'
18
32
 
19
33
  export interface RenderWithProvidersOptions {
@@ -39,7 +53,7 @@ export async function renderWithProviders(
39
53
  const extensionName = options?.extensionName || 'termcast-app'
40
54
  const extensionPath =
41
55
  options?.extensionPath ||
42
- path.join(os.homedir(), '.termcast', 'compiled', extensionName)
56
+ joinPath(homedir(), '.termcast', 'compiled', extensionName)
43
57
  const packageJson: RaycastPackageJson = options?.packageJson || {
44
58
  name: extensionName,
45
59
  title: extensionName,
@@ -55,15 +69,16 @@ export async function renderWithProviders(
55
69
  extensionPackageJson: packageJson,
56
70
  })
57
71
 
58
- // Load theme after state reset — extensionPath is now set so it reads from the correct DB
72
+ // Load theme and vim mode after state reset — extensionPath is now set so it reads from the correct DB
59
73
  initializeTheme()
74
+ initializeVimMode()
60
75
 
61
76
  const renderer = await createCliRenderer({
62
77
  onDestroy: () => {
63
78
  // In app mode, destroying the renderer should not kill the process.
64
79
  // The app launcher manages the process lifecycle.
65
80
  if (!isAppMode()) {
66
- process.exit(0)
81
+ exit(0)
67
82
  }
68
83
  },
69
84
  })
@@ -71,7 +86,7 @@ export async function renderWithProviders(
71
86
  createRoot(renderer).render(<TermcastProvider>{element}</TermcastProvider>)
72
87
  }
73
88
 
74
- export const termcastMaxContentWidth = 140
89
+ export const termcastMaxContentWidth = 240
75
90
 
76
91
  export type CommonProps = {
77
92
  key?: any
@@ -92,7 +107,7 @@ export function sleep(ms: number): Promise<void> {
92
107
  }
93
108
 
94
109
  export async function getApplications(path?: PathLike): Promise<Application[]> {
95
- if (process.platform !== 'darwin') {
110
+ if (platform !== 'darwin') {
96
111
  // Windows/Linux: return empty for now
97
112
  return []
98
113
  }
@@ -117,7 +132,7 @@ export async function getApplications(path?: PathLike): Promise<Application[]> {
117
132
  try {
118
133
  const infoPlistPath = `${appPath}/Contents/Info.plist`
119
134
 
120
- if (!fs.existsSync(infoPlistPath)) {
135
+ if (!fileExists(infoPlistPath)) {
121
136
  continue
122
137
  }
123
138
 
@@ -164,7 +179,7 @@ export async function getDefaultApplication(
164
179
  }
165
180
 
166
181
  export async function getFrontmostApplication(): Promise<Application> {
167
- if (process.platform !== 'darwin') {
182
+ if (platform !== 'darwin') {
168
183
  throw new Error('getFrontmostApplication is only supported on macOS')
169
184
  }
170
185
 
@@ -196,10 +211,10 @@ export async function getFrontmostApplication(): Promise<Application> {
196
211
  export async function showInFinder(path: PathLike): Promise<void> {
197
212
  const pathStr = typeof path === 'string' ? path : path.toString()
198
213
 
199
- if (process.platform !== 'darwin') {
214
+ if (platform !== 'darwin') {
200
215
  // On non-macOS, just open the parent directory
201
- const { dirname } = await import('node:path')
202
- return open(dirname(pathStr))
216
+ const { dirname: pathDirname } = await import('node:path')
217
+ return open(pathDirname(pathStr))
203
218
  }
204
219
 
205
220
  const { spawn } = await import('node:child_process')
@@ -222,7 +237,7 @@ export async function trash(path: PathLike | PathLike[]): Promise<void> {
222
237
  const paths = Array.isArray(path) ? path : [path]
223
238
  const pathStrs = paths.map((p) => (typeof p === 'string' ? p : p.toString()))
224
239
 
225
- if (process.platform === 'darwin') {
240
+ if (platform === 'darwin') {
226
241
  const { execSync } = await import('node:child_process')
227
242
 
228
243
  for (const filePath of pathStrs) {
@@ -232,7 +247,7 @@ export async function trash(path: PathLike | PathLike[]): Promise<void> {
232
247
  { stdio: 'ignore' },
233
248
  )
234
249
  }
235
- } else if (process.platform === 'win32') {
250
+ } else if (platform === 'win32') {
236
251
  // Windows: use PowerShell to move to recycle bin
237
252
  const { execSync } = await import('node:child_process')
238
253
 
@@ -276,11 +291,11 @@ export async function open(
276
291
  let cmd: string
277
292
  let args: string[]
278
293
 
279
- if (process.platform === 'darwin') {
294
+ if (platform === 'darwin') {
280
295
  // macOS
281
296
  cmd = 'open'
282
297
  args = appName ? ['-a', appName, target] : [target]
283
- } else if (process.platform === 'win32') {
298
+ } else if (platform === 'win32') {
284
299
  // Windows
285
300
  cmd = 'cmd'
286
301
  args = ['/c', 'start', '', target]
@@ -304,11 +319,11 @@ export function captureException(exception: unknown): void {
304
319
 
305
320
  export function getExtensionPath(extensionName: string): string {
306
321
  const storeDir = getStoreDirectory()
307
- return path.join(storeDir, extensionName)
322
+ return joinPath(storeDir, extensionName)
308
323
  }
309
324
 
310
325
  export function getExtensionPackageJsonPath(extensionName: string): string {
311
- return path.join(getExtensionPath(extensionName), 'package.json')
326
+ return joinPath(getExtensionPath(extensionName), 'package.json')
312
327
  }
313
328
 
314
329
  export function getExtensionPackageJson(
@@ -316,7 +331,7 @@ export function getExtensionPackageJson(
316
331
  ): RaycastPackageJson | null {
317
332
  const packageJsonPath = getExtensionPackageJsonPath(extensionName)
318
333
 
319
- if (!fs.existsSync(packageJsonPath)) {
334
+ if (!fileExists(packageJsonPath)) {
320
335
  return null
321
336
  }
322
337
 
@@ -394,15 +409,15 @@ export function resolveCommandPath({
394
409
  dir: string
395
410
  }): string {
396
411
  // First check for .termcast-bundle directory
397
- const bundleDir = path.join(dir, '.termcast-bundle')
398
- const bundledPath = path.join(bundleDir, `${commandName}.js`)
399
- if (fs.existsSync(bundledPath)) {
412
+ const bundleDir = joinPath(dir, '.termcast-bundle')
413
+ const bundledPath = joinPath(bundleDir, `${commandName}.js`)
414
+ if (fileExists(bundledPath)) {
400
415
  return bundledPath
401
416
  }
402
417
 
403
418
  // Then check for top-level command file
404
- const topLevelPath = path.join(dir, `${commandName}.js`)
405
- if (fs.existsSync(topLevelPath)) {
419
+ const topLevelPath = joinPath(dir, `${commandName}.js`)
420
+ if (fileExists(topLevelPath)) {
406
421
  return topLevelPath
407
422
  }
408
423
 
@@ -411,13 +426,11 @@ export function resolveCommandPath({
411
426
  }
412
427
 
413
428
  export function getStoreDirectory(): string {
414
- const homeDir = os.homedir()
415
- const storeDir = path.join(homeDir, '.termcast', 'store')
429
+ const homeDir = homedir()
430
+ const storeDir = joinPath(homeDir, '.termcast', 'store')
416
431
 
417
432
  // Ensure store directory exists
418
- if (!fs.existsSync(storeDir)) {
419
- fs.mkdirSync(storeDir, { recursive: true })
420
- }
433
+ ensureDir(storeDir)
421
434
 
422
435
  return storeDir
423
436
  }
@@ -430,18 +443,14 @@ export function installExtension({
430
443
  extensionSourcePath: string
431
444
  }): void {
432
445
  const storeDir = getStoreDirectory()
433
- const extensionDir = path.join(storeDir, extensionName)
446
+ const extensionDir = joinPath(storeDir, extensionName)
434
447
 
435
- // Remove existing extension directory if it exists
436
- if (fs.existsSync(extensionDir)) {
437
- fs.rmSync(extensionDir, { recursive: true, force: true })
448
+ if (fileExists(extensionDir)) {
449
+ rmSync(extensionDir)
438
450
  }
439
451
 
440
- // Create extension directory
441
- fs.mkdirSync(extensionDir, { recursive: true })
442
-
443
- // Copy entire extension source directory
444
- fs.cpSync(extensionSourcePath, extensionDir, { recursive: true })
452
+ mkdirSync(extensionDir)
453
+ cpSync(extensionSourcePath, extensionDir)
445
454
 
446
455
  logger.log(`Extension '${extensionName}' installed to ${extensionDir}`)
447
456
  }
@@ -450,19 +459,19 @@ export function getStoredExtensions(): StoredExtension[] {
450
459
  const storeDir = getStoreDirectory()
451
460
  const extensions: StoredExtension[] = []
452
461
 
453
- if (!fs.existsSync(storeDir)) {
462
+ if (!fileExists(storeDir)) {
454
463
  return extensions
455
464
  }
456
465
 
457
- const entries = fs.readdirSync(storeDir, { withFileTypes: true })
466
+ const entries = readdirSync(storeDir)
458
467
 
459
468
  for (const entry of entries) {
460
469
  if (!entry.isDirectory()) continue
461
470
 
462
- const extensionDir = path.join(storeDir, entry.name)
463
- const packageJsonPath = path.join(extensionDir, 'package.json')
471
+ const extensionDir = joinPath(storeDir, entry.name)
472
+ const packageJsonPath = joinPath(extensionDir, 'package.json')
464
473
 
465
- if (!fs.existsSync(packageJsonPath)) {
474
+ if (!fileExists(packageJsonPath)) {
466
475
  logger.log(`Skipping ${entry.name}: no package.json found`)
467
476
  continue
468
477
  }
@@ -560,7 +569,7 @@ export async function runAppleScript<T = string>(
560
569
  arguments_?: string[],
561
570
  options?: ExecuteOptions<T>,
562
571
  ): Promise<T> {
563
- if (process.platform !== 'darwin') {
572
+ if (platform !== 'darwin') {
564
573
  throw new Error('runAppleScript is only supported on macOS')
565
574
  }
566
575
 
@@ -821,9 +830,9 @@ export async function executeSQL<T = unknown>(
821
830
  databasePath: string,
822
831
  query: string,
823
832
  ): Promise<T[]> {
824
- const { Database } = await import('./apis/sqlite')
833
+ const { Database } = await import('#sqlite')
825
834
 
826
- if (!fs.existsSync(databasePath)) {
835
+ if (!fileExists(databasePath)) {
827
836
  throw new Error(`Database file not found: ${databasePath}`)
828
837
  }
829
838
 
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Vim mode persistence and command execution.
3
+ * Mirrors theme.tsx pattern: global Cache storage (not per-extension),
4
+ * load/persist/initialize functions, and a command registry.
5
+ *
6
+ * Vim mode has two sub-modes (vimInputSubMode in state):
7
+ * - 'default': j/k navigate, textarea unfocused, keystrokes go to useKeyboard
8
+ * - 'search': activated by /, textarea focused, live-filtering like raycast mode
9
+ * - 'command': activated by : (from empty search bar), footer shows command input
10
+ *
11
+ * The ':' command system works in both raycast and vim modes. In raycast mode,
12
+ * ':' is intercepted when the search bar is empty via onContentChange. In vim mode,
13
+ * ':' is caught directly by useKeyboard since the textarea is unfocused.
14
+ */
15
+
16
+ import { Cache } from 'termcast/src/apis/cache'
17
+ import { useStore, type InputMode } from 'termcast/src/state'
18
+ import { logger } from 'termcast/src/logger'
19
+
20
+ const VIM_MODE_STORAGE_KEY = 'termcast.vimMode'
21
+
22
+ let globalCache: Cache | null = null
23
+ let globalCachePath: string | null = null
24
+
25
+ function getGlobalCache(): Cache {
26
+ const currentPath = useStore.getState().extensionPath
27
+ if (!globalCache || currentPath !== globalCachePath) {
28
+ globalCache = new Cache()
29
+ globalCachePath = currentPath
30
+ }
31
+ return globalCache
32
+ }
33
+
34
+ export function loadPersistedInputMode(): InputMode {
35
+ try {
36
+ const stored = getGlobalCache().get(VIM_MODE_STORAGE_KEY)
37
+ if (stored === 'vim' || stored === 'raycast') {
38
+ return stored
39
+ }
40
+ } catch {
41
+ // Ignore errors on load
42
+ }
43
+ return 'raycast'
44
+ }
45
+
46
+ export function persistInputMode(mode: InputMode): void {
47
+ try {
48
+ getGlobalCache().set(VIM_MODE_STORAGE_KEY, mode)
49
+ } catch {
50
+ // Ignore errors on save
51
+ }
52
+ }
53
+
54
+ export function initializeVimMode(): void {
55
+ const mode = loadPersistedInputMode()
56
+ useStore.setState({ inputMode: mode })
57
+ }
58
+
59
+ // Command registry for ':' commands.
60
+ // Each command has a name and an execute function.
61
+ // Some commands need special handling by the caller (e.g. opening dialogs,
62
+ // accessing component refs). These return their name from executeVimCommand()
63
+ // so the caller can handle them.
64
+ export interface VimCommand {
65
+ name: string
66
+ execute: () => void
67
+ // Only show this command when in a specific mode (undefined = show always)
68
+ showWhenMode?: InputMode
69
+ // Commands that need the caller to do something (open dialog, access refs)
70
+ // return their name from executeVimCommand() instead of handling everything here.
71
+ handledByCaller?: boolean
72
+ }
73
+
74
+ const VIM_COMMANDS: VimCommand[] = [
75
+ {
76
+ name: 'vim',
77
+ showWhenMode: 'raycast',
78
+ execute: () => {
79
+ useStore.setState({ inputMode: 'vim', vimInputSubMode: 'default', vimCommandText: '' })
80
+ persistInputMode('vim')
81
+ logger.log('Vim mode enabled')
82
+ },
83
+ },
84
+ {
85
+ name: 'theme',
86
+ handledByCaller: true,
87
+ execute: () => {
88
+ useStore.setState({ vimInputSubMode: 'default', vimCommandText: '' })
89
+ },
90
+ },
91
+ {
92
+ name: 'actions',
93
+ handledByCaller: true,
94
+ execute: () => {
95
+ useStore.setState({ vimInputSubMode: 'default', vimCommandText: '' })
96
+ },
97
+ },
98
+ {
99
+ name: 'filter',
100
+ handledByCaller: true,
101
+ execute: () => {
102
+ useStore.setState({ vimInputSubMode: 'default', vimCommandText: '' })
103
+ },
104
+ },
105
+ {
106
+ name: 'q',
107
+ execute: () => {
108
+ useStore.setState({ vimInputSubMode: 'default', vimCommandText: '' })
109
+ process.exit(0)
110
+ },
111
+ },
112
+ ]
113
+
114
+ /**
115
+ * Get commands matching the current typed text, filtered by current mode.
116
+ */
117
+ export function getMatchingCommands(text: string): VimCommand[] {
118
+ const currentMode = useStore.getState().inputMode
119
+ return VIM_COMMANDS.filter((cmd) => {
120
+ if (cmd.showWhenMode && cmd.showWhenMode !== currentMode) return false
121
+ if (text.length === 0) return true
122
+ return cmd.name.startsWith(text)
123
+ })
124
+ }
125
+
126
+ /**
127
+ * Execute a command by name.
128
+ * Returns the command name if it needs caller handling (e.g. 'theme', 'actions', 'filter').
129
+ * Returns true if the command executed fully.
130
+ * Returns false if no matching command was found.
131
+ */
132
+ export function executeVimCommand(text: string): string | boolean {
133
+ const currentMode = useStore.getState().inputMode
134
+ const command = VIM_COMMANDS.find((cmd) => {
135
+ if (cmd.showWhenMode && cmd.showWhenMode !== currentMode) return false
136
+ return cmd.name === text
137
+ })
138
+ if (!command) return false
139
+ command.execute()
140
+ if (command.handledByCaller) return command.name
141
+ return true
142
+ }
143
+
144
+ /**
145
+ * Toggle vim mode on/off. Used by the action panel "Toggle Vim Mode" action.
146
+ */
147
+ export function toggleVimMode(): void {
148
+ const currentMode = useStore.getState().inputMode
149
+ const newMode: InputMode = currentMode === 'vim' ? 'raycast' : 'vim'
150
+ useStore.setState({ inputMode: newMode, vimInputSubMode: 'default', vimCommandText: '' })
151
+ persistInputMode(newMode)
152
+ logger.log(`Input mode: ${newMode}`)
153
+ }
@@ -1,14 +0,0 @@
1
- /**
2
- * SQLite abstraction layer — swap engine by toggling the commented block.
3
- * To use libsql: `bun add libsql`, comment bun:sqlite, uncomment libsql.
4
- */
5
-
6
- // ── bun:sqlite ─────────────────────────────────────────────────────
7
- import { Database } from 'bun:sqlite'
8
- export { Database }
9
-
10
- // ── libsql (better-sqlite3 compatible, works on Node/Bun/Deno) ────
11
- // import type LibsqlType from 'libsql'
12
- // import LibsqlDatabase from 'libsql'
13
- // export type Database = LibsqlType.Database
14
- // export const Database: new (path: string, options?: LibsqlType.Options) => Database = LibsqlDatabase