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
@@ -1,214 +1,8 @@
1
- import { Database } from './sqlite'
2
- import * as path from 'path'
3
- import * as fs from 'fs'
4
- import { logger } from '../logger'
5
- import { useStore } from '../state'
6
-
7
- let db: Database | null = null
8
- let currentDbPath: string | null = null
9
-
10
- function getCurrentDatabasePath(): string {
11
- const { extensionPath } = useStore.getState()
12
-
13
- if (!extensionPath) {
14
- throw new Error('Cannot access LocalStorage - extensionPath not set')
15
- }
16
-
17
- return path.join(extensionPath, '.termcast-bundle', 'data.db')
18
- }
19
-
20
- function getDatabase(): Database {
21
- const dbPath = getCurrentDatabasePath()
22
-
23
- // Check if we need to reconnect due to path change
24
- if (db && currentDbPath !== dbPath) {
25
- db.close()
26
- db = null
27
- currentDbPath = null
28
- }
29
-
30
- if (!db) {
31
- // Ensure parent directory exists
32
- const dbDir = path.dirname(dbPath)
33
- if (!fs.existsSync(dbDir)) {
34
- fs.mkdirSync(dbDir, { recursive: true })
35
- }
36
-
37
- db = new Database(dbPath, {
38
- create: true,
39
- readwrite: true,
40
- })
41
- currentDbPath = dbPath
42
-
43
- // Use WAL mode and optimize for single file usage
44
- db.exec('PRAGMA journal_mode = WAL')
45
- db.exec('PRAGMA wal_autocheckpoint = 1000')
46
- db.exec('PRAGMA synchronous = NORMAL')
47
-
48
- db.exec(`
49
- CREATE TABLE IF NOT EXISTS localstorage (
50
- key TEXT PRIMARY KEY,
51
- value TEXT NOT NULL,
52
- type TEXT NOT NULL
53
- )
54
- `)
55
- }
56
- return db
57
- }
58
-
59
-
60
-
61
- export namespace LocalStorage {
62
- export type Value = string | number | boolean
63
-
64
- export interface Values {
65
- [key: string]: any
66
- }
67
-
68
- export async function getItem<T extends Value = Value>(
69
- key: string,
70
- ): Promise<T | undefined> {
71
- return new Promise((resolve) => {
72
- try {
73
- const db = getDatabase()
74
- const row = db
75
- .prepare('SELECT value, type FROM localstorage WHERE key = ?')
76
- .get(key) as { value: string; type: string } | undefined
77
-
78
- if (!row) {
79
- resolve(undefined)
80
- return
81
- }
82
-
83
- let value: Value
84
- switch (row.type) {
85
- case 'number':
86
- value = parseFloat(row.value)
87
- break
88
- case 'boolean':
89
- value = row.value === 'true'
90
- break
91
- default:
92
- value = row.value
93
- }
94
-
95
- resolve(value as T)
96
- } catch (err) {
97
- logger.error('LocalStorage.getItem error:', err)
98
- resolve(undefined)
99
- }
100
- })
101
- }
102
-
103
- export function getItemSync<T extends Value = Value>(
104
- key: string,
105
- ): T | undefined {
106
- try {
107
- const db = getDatabase()
108
- const row = db
109
- .prepare('SELECT value, type FROM localstorage WHERE key = ?')
110
- .get(key) as { value: string; type: string } | undefined
111
-
112
- if (!row) {
113
- return undefined
114
- }
115
-
116
- let value: Value
117
- switch (row.type) {
118
- case 'number':
119
- value = parseFloat(row.value)
120
- break
121
- case 'boolean':
122
- value = row.value === 'true'
123
- break
124
- default:
125
- value = row.value
126
- }
127
-
128
- return value as T
129
- } catch (err) {
130
- logger.error('LocalStorage.getItemSync error:', err)
131
- return undefined
132
- }
133
- }
134
-
135
- export async function setItem(key: string, value: Value): Promise<void> {
136
- return new Promise((resolve, reject) => {
137
- try {
138
- const db = getDatabase()
139
- const type = typeof value
140
- const stringValue = String(value)
141
-
142
- db.prepare(
143
- 'INSERT OR REPLACE INTO localstorage (key, value, type) VALUES (?, ?, ?)',
144
- ).run(key, stringValue, type)
145
- resolve()
146
- } catch (err) {
147
- logger.error('LocalStorage.setItem error:', err)
148
- reject(err)
149
- }
150
- })
151
- }
152
-
153
- export async function removeItem(key: string): Promise<void> {
154
- return new Promise((resolve, reject) => {
155
- try {
156
- const db = getDatabase()
157
- db.prepare('DELETE FROM localstorage WHERE key = ?').run(key)
158
- resolve()
159
- } catch (err) {
160
- logger.error('LocalStorage.removeItem error:', err)
161
- reject(err)
162
- }
163
- })
164
- }
165
-
166
- export async function allItems<T extends Values = Values>(): Promise<T> {
167
- return new Promise((resolve, reject) => {
168
- try {
169
- const db = getDatabase()
170
- const rows = db
171
- .prepare('SELECT key, value, type FROM localstorage')
172
- .all() as Array<{
173
- key: string
174
- value: string
175
- type: string
176
- }>
177
-
178
- const result: Values = {}
179
- for (const row of rows) {
180
- let value: Value
181
- switch (row.type) {
182
- case 'number':
183
- value = parseFloat(row.value)
184
- break
185
- case 'boolean':
186
- value = row.value === 'true'
187
- break
188
- default:
189
- value = row.value
190
- }
191
- result[row.key] = value
192
- }
193
-
194
- resolve(result as T)
195
- } catch (err) {
196
- logger.error('LocalStorage.allItems error:', err)
197
- reject(err)
198
- }
199
- })
200
- }
201
-
202
- export async function clear(): Promise<void> {
203
- return new Promise((resolve, reject) => {
204
- try {
205
- const db = getDatabase()
206
- db.exec('DELETE FROM localstorage')
207
- resolve()
208
- } catch (err) {
209
- logger.error('LocalStorage.clear error:', err)
210
- reject(err)
211
- }
212
- })
213
- }
214
- }
1
+ /**
2
+ * LocalStorage API platform-agnostic facade.
3
+ *
4
+ * Re-exports the platform-specific LocalStorage from #platform/localstorage
5
+ * (SQLite on Node/Bun, window.localStorage on browser).
6
+ */
7
+
8
+ export { LocalStorage } from '#platform/localstorage'
package/src/app.tsx CHANGED
@@ -494,11 +494,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
494
494
  if (dir[i] == L'\\\\') { dir[i + 1] = L'\\0'; break; }
495
495
  }
496
496
 
497
- /* Set TERMCAST_WEZTERM_CONFIG env var so the TUI can rewrite the config on theme change */
498
- WCHAR configPath[MAX_PATH * 2];
499
- wsprintfW(configPath, L"%sruntime\\\\config\\\\wezterm.lua", dir);
500
- SetEnvironmentVariableW(L"TERMCAST_WEZTERM_CONFIG", configPath);
501
-
502
497
  /* Set default theme name baked at build time */
503
498
  SetEnvironmentVariableW(L"TERMCAST_DEFAULT_THEME", L"${themeName}");
504
499
 
@@ -618,6 +613,8 @@ config.keys = {
618
613
  { key = 'RightArrow', mods = 'SUPER', action = wezterm.action.SendString('\\x1b[1;9C') },
619
614
  { key = 'UpArrow', mods = 'SUPER', action = wezterm.action.SendString('\\x1b[1;9A') },
620
615
  { key = 'DownArrow', mods = 'SUPER', action = wezterm.action.SendString('\\x1b[1;9B') },
616
+ -- Cmd+Backspace -> delete to line start (backspace codepoint = 127, super modifier = 9)
617
+ { key = 'Backspace', mods = 'SUPER', action = wezterm.action.SendString('\\x1b[127;9u') },
621
618
  }
622
619
  `
623
620
  : ''
@@ -637,9 +634,9 @@ config.window_decorations = '${platform === 'darwin' ? 'TITLE|RESIZE' : 'RESIZE'
637
634
  config.window_padding = { left = 0, right = 0, top = 0, bottom = 0 }
638
635
  config.window_close_confirmation = 'NeverPrompt'
639
636
 
640
- -- Background color matching the configured termcast theme.
641
- -- The TUI rewrites this file on theme change so WezTerm auto-reloads it,
642
- -- keeping the window edges/padding in sync with the active theme.
637
+ -- Background color: the TUI sets this dynamically via OSC 11 escape sequence
638
+ -- on startup and every theme change, so the window edges/padding always match.
639
+ -- This initial value comes from the theme configured at build time.
643
640
  config.colors = { background = '${backgroundColor}' }
644
641
 
645
642
  -- Default window size: 120x36 is comfortable for TUI apps (WezTerm default is 80x24)
@@ -681,10 +678,9 @@ function generateLaunchScript({ weztermBinaryName, themeName }: { weztermBinaryN
681
678
  return `\
682
679
  #!/bin/bash
683
680
  DIR="$(cd "$(dirname "$0")" && pwd)"
684
- export TERMCAST_WEZTERM_CONFIG="$DIR/../Resources/wezterm.lua"
685
681
  export TERMCAST_DEFAULT_THEME="${themeName}"
686
682
  export TERMCAST_APP_MODE=1
687
- exec "$DIR/${weztermBinaryName}" --config-file "$TERMCAST_WEZTERM_CONFIG"
683
+ exec "$DIR/${weztermBinaryName}" --config-file "$DIR/../Resources/wezterm.lua"
688
684
  `
689
685
  }
690
686
 
@@ -694,10 +690,9 @@ function generateLinuxLaunchScript({ themeName }: { themeName: string }): string
694
690
  return `\
695
691
  #!/bin/bash
696
692
  DIR="$(cd "$(dirname "$0")" && pwd)"
697
- export TERMCAST_WEZTERM_CONFIG="$DIR/runtime/config/wezterm.lua"
698
693
  export TERMCAST_DEFAULT_THEME="${themeName}"
699
694
  export TERMCAST_APP_MODE=1
700
- exec "$DIR/runtime/wezterm-gui" --config-file "$TERMCAST_WEZTERM_CONFIG"
695
+ exec "$DIR/runtime/wezterm-gui" --config-file "$DIR/runtime/config/wezterm.lua"
701
696
  `
702
697
  }
703
698
 
@@ -1121,8 +1116,9 @@ async function resolveBuildContext({
1121
1116
  // because PowerShell splits unquoted paths on whitespace.
1122
1117
  const safeName = appName.replace(/[/\\\s]+/g, '-').replace(/^-+|-+$/g, '')
1123
1118
 
1124
- // Resolve font directory: --font-dir flag, or fonts/ in extension root.
1125
- // --font-dir is resolved relative to the extension path (not cwd) for consistency.
1119
+ // Resolve font directory: --font-dir flag, fonts/ in extension root, or termcast's
1120
+ // built-in fonts/ as fallback. The built-in Inconsolata font ships with termcast so
1121
+ // every app has a guaranteed monospace font without depending on system fonts.
1126
1122
  const fontDirPath = (() => {
1127
1123
  if (fontDir) {
1128
1124
  const resolved = path.isAbsolute(fontDir)
@@ -1141,11 +1137,16 @@ async function resolveBuildContext({
1141
1137
  if (fs.existsSync(defaultFontDir) && fs.statSync(defaultFontDir).isDirectory()) {
1142
1138
  return defaultFontDir
1143
1139
  }
1140
+ // Fall back to termcast's built-in fonts/ directory (contains Inconsolata)
1141
+ const builtinFontDir = path.join(__dirname, '..', 'fonts')
1142
+ if (fs.existsSync(builtinFontDir) && fs.statSync(builtinFontDir).isDirectory()) {
1143
+ return builtinFontDir
1144
+ }
1144
1145
  return undefined
1145
1146
  })()
1146
1147
 
1147
1148
  const fontOptions: WeztermFontOptions = {
1148
- fontFamily,
1149
+ fontFamily: fontFamily ?? 'Inconsolata',
1149
1150
  fontSize,
1150
1151
  lineHeight,
1151
1152
  hasBundledFonts: !!fontDirPath,
package/src/cli.tsx CHANGED
@@ -239,13 +239,13 @@ cli
239
239
  .command('release [path]', 'Build and publish extension to GitHub releases')
240
240
  .option('--single', 'Only compile for the current platform')
241
241
  .option('--entry <file>', 'Custom entry file (instead of auto-generated one)')
242
- .action(async (extensionPath: string, options: { single?: boolean; entry?: string }) => {
243
- extensionPath = path.resolve(extensionPath || process.cwd())
242
+ .action(async (extensionPath: string | undefined, options: { single?: boolean; entry?: string }) => {
243
+ const resolvedPath = path.resolve(extensionPath || process.cwd())
244
244
 
245
245
  console.log('Building and releasing extension...')
246
246
  try {
247
247
  const result = await releaseExtension({
248
- extensionPath,
248
+ extensionPath: resolvedPath,
249
249
  single: options.single,
250
250
  entry: options.entry,
251
251
  })
@@ -279,7 +279,7 @@ cli
279
279
  .option('--theme <name>', 'Default theme name (default: nerv)')
280
280
  .action(
281
281
  async (
282
- extensionPath: string,
282
+ extensionPath: string | undefined,
283
283
  options: {
284
284
  name?: string
285
285
  icon?: string
@@ -294,6 +294,8 @@ cli
294
294
  fontSize?: string
295
295
  lineHeight?: string
296
296
  theme?: string
297
+ // `--no-installer` adds noInstaller to the inferred type
298
+ noInstaller?: boolean
297
299
  },
298
300
  ) => {
299
301
  extensionPath = path.resolve(extensionPath || process.cwd())
@@ -597,7 +599,7 @@ cli
597
599
  'Search for extensions in the Raycast store',
598
600
  )
599
601
  .option('-n, --limit [number]', 'Number of results to show (default: 10)')
600
- .action(async (query: string, options: { limit?: string }) => {
602
+ .action(async (query, options) => {
601
603
  try {
602
604
  const limit = parseInt(options.limit || '10', 10)
603
605
  const result = await searchStoreListings({ query, perPage: limit })
@@ -642,15 +644,12 @@ cli
642
644
  '--no-dir',
643
645
  'Put files directly in output directory instead of creating extension subdirectory',
644
646
  )
645
- .action(
646
- async (
647
- extensionName: string,
648
- options: { output?: string; dir: boolean },
649
- ) => {
647
+ .action(async (extensionName, options) => {
650
648
  try {
651
649
  const destPath = path.resolve(options.output || '.')
652
- // When --no-dir is passed, dir is false; put files directly in destPath
653
- const extensionDir = options.dir
650
+ // When --no-dir is passed, noDir is true; put files directly in destPath
651
+ const useSubdir = !options.noDir
652
+ const extensionDir = useSubdir
654
653
  ? path.join(destPath, extensionName)
655
654
  : destPath
656
655
  const tempCloneDir = path.join(
@@ -662,7 +661,7 @@ cli
662
661
  `Downloading extension '${extensionName}' from raycast/extensions...`,
663
662
  )
664
663
 
665
- if (options.dir && fs.existsSync(extensionDir)) {
664
+ if (useSubdir && fs.existsSync(extensionDir)) {
666
665
  console.log(`Removing existing directory: ${extensionDir}`)
667
666
  fs.rmSync(extensionDir, { recursive: true, force: true })
668
667
  }
@@ -724,7 +723,7 @@ cli
724
723
  }
725
724
 
726
725
  // Move files to final destination
727
- if (options.dir) {
726
+ if (useSubdir) {
728
727
  fs.mkdirSync(extensionDir, { recursive: true })
729
728
  }
730
729
  const filesToMove = fs.readdirSync(extensionPath)
@@ -755,7 +754,7 @@ cli
755
754
 
756
755
  cli
757
756
  .command('new [name]', 'Create a new termcast extension')
758
- .action(async (name: string) => {
757
+ .action(async (name: string | undefined) => {
759
758
  if (!name) {
760
759
  console.log('Usage: termcast new <extension-name>\n')
761
760
  console.log('Create a new termcast extension with the given name.\n')
@@ -96,7 +96,7 @@ test('compile extension and run executable', async () => {
96
96
  Show State Shows the current application state in view
97
97
 
98
98
 
99
- ↵ run command ↑↓ navigate ^k actions
99
+ ↵ run command ↑↓ navigate ^k actions :vim
100
100
  "
101
101
  `)
102
102
  }, 60000)
@@ -198,7 +198,7 @@ test('compiled executable can navigate back', async () => {
198
198
  ○ Fifth Item This is the fifth item
199
199
 
200
200
 
201
- ↵ copy item title ↑↓ navigate ^k actions
201
+ ↵ copy item title ↑↓ navigate ^k actions :vim
202
202
  "
203
203
  `)
204
204
  }, 60000)
@@ -1,3 +1,4 @@
1
+ import { platform } from '#platform/runtime'
1
2
  import React, {
2
3
  type ReactNode,
3
4
  type ReactElement,
@@ -22,6 +23,7 @@ import { Dropdown } from 'termcast/src/components/dropdown'
22
23
  import { ExtensionPreferences } from 'termcast/src/components/extension-preferences'
23
24
  import { ThemePicker } from 'termcast/src/components/theme-picker'
24
25
  import { useStore } from 'termcast/src/state'
26
+ import { toggleVimMode } from 'termcast/src/vim-mode'
25
27
  import { InFocus } from 'termcast/src/internal/focus-context'
26
28
  import { Onscreen, useIsOffscreen } from 'termcast/src/internal/offscreen'
27
29
  import { CommonProps } from 'termcast/src/utils'
@@ -587,7 +589,7 @@ Action.ToggleQuickLook = (props) => {
587
589
  return
588
590
  }
589
591
 
590
- if (process.platform !== 'darwin') {
592
+ if (platform !== 'darwin') {
591
593
  showToast({
592
594
  title: 'Quick Look',
593
595
  message: 'Quick Look is only supported on macOS',
@@ -743,6 +745,21 @@ export function matchesShortcut(
743
745
  return true
744
746
  }
745
747
 
748
+ // Separate component so the inputMode selector is reactive and the label
749
+ // updates when vim mode is toggled without reopening the action panel.
750
+ function VimModeAction(): any {
751
+ const inputMode = useStore((s) => s.inputMode)
752
+ return (
753
+ <Action
754
+ title={inputMode === 'vim' ? 'Disable Vim Mode' : 'Enable Vim Mode'}
755
+ onAction={() => {
756
+ useStore.setState({ showActionsDialog: false })
757
+ toggleVimMode()
758
+ }}
759
+ />
760
+ )
761
+ }
762
+
746
763
  /**
747
764
  * ActionPanel uses React portals to render its dialog content in the overlay
748
765
  * area while staying in the original React tree. This preserves all React
@@ -916,6 +933,7 @@ const ActionPanel: ActionPanelType = (props) => {
916
933
  dialog.push({ element: <ThemePicker /> })
917
934
  }}
918
935
  />
936
+ <VimModeAction />
919
937
  <Action
920
938
  title="Toggle Console Logs"
921
939
  onAction={() => {
@@ -1,6 +1,5 @@
1
1
  import React from 'react'
2
- import fs from 'node:fs'
3
- import path from 'node:path'
2
+ import { fileExists, readFileSync, joinPath, dirname } from '#platform/runtime'
4
3
  import { useQuery } from '@tanstack/react-query'
5
4
  import { Form } from './form'
6
5
  import { showToast, Toast } from '../apis/toast'
@@ -55,20 +54,20 @@ export function ExtensionPreferences({
55
54
  if (extensionPackageJson?.name === extensionName) {
56
55
  // Dev mode or compiled extension - use package.json from state
57
56
  packageJson = extensionPackageJson
58
- } else if (extensionPath && fs.existsSync(path.join(extensionPath, 'package.json'))) {
57
+ } else if (extensionPath && fileExists(joinPath(extensionPath, 'package.json'))) {
59
58
  // Dev mode with extensionPath - read from disk
60
- packageJson = JSON.parse(fs.readFileSync(path.join(extensionPath, 'package.json'), 'utf-8'))
59
+ packageJson = JSON.parse(readFileSync(joinPath(extensionPath, 'package.json')))
61
60
  } else {
62
61
  // Store extension - read from store directory
63
62
  const storeDir = getStoreDirectory()
64
- const extensionDir = path.join(storeDir, extensionName)
65
- const packageJsonPath = path.join(extensionDir, 'package.json')
63
+ const extensionDir = joinPath(storeDir, extensionName)
64
+ const packageJsonPath = joinPath(extensionDir, 'package.json')
66
65
 
67
- if (!fs.existsSync(packageJsonPath)) {
66
+ if (!fileExists(packageJsonPath)) {
68
67
  throw new Error(`Extension ${extensionName} not found`)
69
68
  }
70
69
 
71
- packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
70
+ packageJson = JSON.parse(readFileSync(packageJsonPath))
72
71
  }
73
72
 
74
73
  let prefsToUse: PreferenceManifest[] = []
@@ -5,9 +5,9 @@ import { searchFiles, parsePath } from '../../utils/file-system'
5
5
  import { useKeyboard } from '@opentui/react'
6
6
  import { useIsInFocus } from 'termcast/src/internal/focus-context'
7
7
  import { createStore, useStore } from 'zustand'
8
- import os from 'os'
8
+ import { homedir as getHomedir } from '#platform/runtime'
9
9
 
10
- const homedir = os.homedir()
10
+ const homedir = getHomedir()
11
11
 
12
12
  interface FileAutocompleteState {
13
13
  filter: string