termcast 1.3.54 → 1.4.1

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 (170) 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 +40 -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 +62 -10
  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 +37 -42
  121. package/src/examples/form-basic.vitest.tsx +45 -41
  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 +10 -10
  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/examples/toast-variations.vitest.tsx +5 -5
  151. package/src/extensions/dev.tsx +5 -2
  152. package/src/extensions/dev.vitest.tsx +3 -3
  153. package/src/globals.ts +2 -1
  154. package/src/internal/error-handler.tsx +19 -21
  155. package/src/internal/providers.tsx +39 -0
  156. package/src/logger.tsx +48 -41
  157. package/src/platform/browser/cache.ts +327 -0
  158. package/src/platform/browser/localstorage.ts +119 -0
  159. package/src/platform/browser/runtime.ts +209 -0
  160. package/src/platform/bun/sqlite.ts +19 -0
  161. package/src/platform/node/cache.ts +372 -0
  162. package/src/platform/node/localstorage.ts +214 -0
  163. package/src/platform/node/runtime.ts +264 -0
  164. package/src/platform/node/sqlite.ts +43 -0
  165. package/src/state.tsx +17 -28
  166. package/src/utils/file-system.ts +17 -22
  167. package/src/utils.test.tsx +1 -1
  168. package/src/utils.tsx +56 -47
  169. package/src/vim-mode.tsx +153 -0
  170. package/src/apis/sqlite.ts +0 -14
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Node/Bun LocalStorage implementation backed by SQLite.
3
+ * Moved from apis/localstorage.tsx.
4
+ */
5
+
6
+ import { Database, type DatabaseInstance } from '#sqlite'
7
+ import { joinPath, dirname, ensureDir } from '#platform/runtime'
8
+ import { logger } from '../../logger'
9
+ import { useStore } from '../../state'
10
+
11
+ let db: DatabaseInstance | null = null
12
+ let currentDbPath: string | null = null
13
+
14
+ function getCurrentDatabasePath(): string {
15
+ const { extensionPath } = useStore.getState()
16
+
17
+ if (!extensionPath) {
18
+ throw new Error('Cannot access LocalStorage - extensionPath not set')
19
+ }
20
+
21
+ return joinPath(extensionPath, '.termcast-bundle', 'data.db')
22
+ }
23
+
24
+ function getDatabase(): DatabaseInstance {
25
+ const dbPath = getCurrentDatabasePath()
26
+
27
+ // Check if we need to reconnect due to path change
28
+ if (db && currentDbPath !== dbPath) {
29
+ db.close()
30
+ db = null
31
+ currentDbPath = null
32
+ }
33
+
34
+ if (!db) {
35
+ // Ensure parent directory exists
36
+ const dbDir = dirname(dbPath)
37
+ ensureDir(dbDir)
38
+
39
+ db = new Database(dbPath, {
40
+ create: true,
41
+ readwrite: true,
42
+ })
43
+ currentDbPath = dbPath
44
+
45
+ // Use WAL mode and optimize for single file usage
46
+ db.exec('PRAGMA journal_mode = WAL')
47
+ db.exec('PRAGMA wal_autocheckpoint = 1000')
48
+ db.exec('PRAGMA synchronous = NORMAL')
49
+
50
+ db.exec(`
51
+ CREATE TABLE IF NOT EXISTS localstorage (
52
+ key TEXT PRIMARY KEY,
53
+ value TEXT NOT NULL,
54
+ type TEXT NOT NULL
55
+ )
56
+ `)
57
+ }
58
+ return db
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
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Node.js / Bun platform runtime.
3
+ *
4
+ * Re-exports native APIs behind a platform-agnostic interface so the rest of
5
+ * termcast never imports node:* directly (except CLI / build tooling which is
6
+ * excluded from the browser bundle).
7
+ */
8
+
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+ import os from 'node:os'
12
+ import util from 'node:util'
13
+ import { exec as _exec } from 'node:child_process'
14
+ import { promisify } from 'node:util'
15
+
16
+ const execAsync = promisify(_exec)
17
+
18
+ // ── filesystem ──────────────────────────────────────────────────────
19
+
20
+ export function ensureDir(dir: string): void {
21
+ if (!fs.existsSync(dir)) {
22
+ fs.mkdirSync(dir, { recursive: true })
23
+ }
24
+ }
25
+
26
+ export function fileExists(p: string): boolean {
27
+ return fs.existsSync(p)
28
+ }
29
+
30
+ export function readFileSync(p: string): string {
31
+ return fs.readFileSync(p, 'utf-8')
32
+ }
33
+
34
+ export function appendToFile(p: string, data: string): void {
35
+ fs.appendFileSync(p, data)
36
+ }
37
+
38
+ export function unlinkIfExists(p: string): void {
39
+ if (fs.existsSync(p)) {
40
+ fs.unlinkSync(p)
41
+ }
42
+ }
43
+
44
+ export function readdirSync(dir: string): Array<{ name: string; isDirectory(): boolean }> {
45
+ return fs.readdirSync(dir, { withFileTypes: true })
46
+ }
47
+
48
+ export function rmSync(p: string): void {
49
+ fs.rmSync(p, { recursive: true, force: true })
50
+ }
51
+
52
+ export function mkdirSync(dir: string): void {
53
+ fs.mkdirSync(dir, { recursive: true })
54
+ }
55
+
56
+ export function cpSync(src: string, dest: string): void {
57
+ fs.cpSync(src, dest, { recursive: true })
58
+ }
59
+
60
+ // ── async filesystem ────────────────────────────────────────────────
61
+
62
+ import fsPromises from 'node:fs/promises'
63
+
64
+ export async function readdirAsync(dir: string): Promise<Array<{ name: string; isDirectory(): boolean }>> {
65
+ return fsPromises.readdir(dir, { withFileTypes: true })
66
+ }
67
+
68
+ export async function accessAsync(p: string): Promise<boolean> {
69
+ try {
70
+ await fsPromises.access(p)
71
+ return true
72
+ } catch {
73
+ return false
74
+ }
75
+ }
76
+
77
+ // ── path ────────────────────────────────────────────────────────────
78
+
79
+ export const joinPath: (...parts: string[]) => string = path.join
80
+ export const dirname: (p: string) => string = path.dirname
81
+ export const basename: (p: string) => string = path.basename
82
+ export const resolvePath: (...parts: string[]) => string = path.resolve
83
+ export const isAbsolute: (p: string) => boolean = path.isAbsolute
84
+ export const relativePath: (from: string, to: string) => string = path.relative
85
+
86
+ // ── os ──────────────────────────────────────────────────────────────
87
+
88
+ export function homedir(): string {
89
+ return os.homedir()
90
+ }
91
+
92
+ // ── process ─────────────────────────────────────────────────────────
93
+
94
+ export const platform: string = process.platform
95
+
96
+ export function exit(code?: number): void {
97
+ process.exit(code)
98
+ }
99
+
100
+ export function getEnv(key: string): string | undefined {
101
+ return process.env[key]
102
+ }
103
+
104
+ export function setEnv(key: string, value: string): void {
105
+ process.env[key] = value
106
+ }
107
+
108
+ export function cwd(): string {
109
+ return process.cwd()
110
+ }
111
+
112
+ export function stdoutWrite(data: string): void {
113
+ process.stdout.write(data)
114
+ }
115
+
116
+ export function isTTY(): boolean {
117
+ return !!process.stdout.isTTY
118
+ }
119
+
120
+ export function getArgv(): string[] {
121
+ return process.argv
122
+ }
123
+
124
+ // ── appearance ──────────────────────────────────────────────────────
125
+
126
+ export function getSystemAppearance(): 'dark' | 'light' {
127
+ if (process.platform !== 'darwin') {
128
+ return 'light'
129
+ }
130
+ try {
131
+ const { execFileSync } = require('node:child_process') as typeof import('node:child_process')
132
+ const result = execFileSync('defaults', ['read', '-g', 'AppleInterfaceStyle'], {
133
+ encoding: 'utf-8',
134
+ timeout: 500,
135
+ stdio: ['pipe', 'pipe', 'pipe'],
136
+ })
137
+ return result.trim().toLowerCase() === 'dark' ? 'dark' : 'light'
138
+ } catch {
139
+ // "AppleInterfaceStyle" key doesn't exist when in light mode
140
+ return 'light'
141
+ }
142
+ }
143
+
144
+ // ── util ────────────────────────────────────────────────────────────
145
+
146
+ export function byteLength(str: string): number {
147
+ return Buffer.byteLength(str, 'utf-8')
148
+ }
149
+
150
+ export function inspectValue(val: unknown, depth = 3): string {
151
+ return util.inspect(val, { depth })
152
+ }
153
+
154
+ // ── error handling ──────────────────────────────────────────────────
155
+
156
+ export function setupErrorHandlers(handler: (err: Error, type: string) => void): void {
157
+ process.on('uncaughtException', (err) => {
158
+ handler(err, 'uncaughtException')
159
+ })
160
+ process.on('unhandledRejection', (reason) => {
161
+ handler(
162
+ reason instanceof Error ? reason : new Error(String(reason)),
163
+ 'unhandledRejection',
164
+ )
165
+ })
166
+ process.on('uncaughtExceptionMonitor', (err, origin) => {
167
+ handler(err, origin)
168
+ })
169
+ }
170
+
171
+ // ── shell / clipboard ───────────────────────────────────────────────
172
+
173
+ export function execWithInput(command: string, input: string): Promise<void> {
174
+ return new Promise((resolve, reject) => {
175
+ const child = _exec(command, (error) => {
176
+ if (error) {
177
+ reject(error)
178
+ } else {
179
+ resolve()
180
+ }
181
+ })
182
+ child.stdin?.write(input)
183
+ child.stdin?.end()
184
+ })
185
+ }
186
+
187
+ export async function execCommand(command: string): Promise<string> {
188
+ const { stdout } = await execAsync(command)
189
+ return stdout
190
+ }
191
+
192
+ export async function copyToClipboard(text: string): Promise<void> {
193
+ if (process.platform === 'darwin') {
194
+ await execWithInput('pbcopy', text)
195
+ } else if (process.platform === 'linux') {
196
+ await execWithInput('xclip -selection clipboard', text)
197
+ } else if (process.platform === 'win32') {
198
+ await execWithInput('clip', text)
199
+ }
200
+ }
201
+
202
+ export async function readClipboard(): Promise<string> {
203
+ if (process.platform === 'darwin') {
204
+ return (await execAsync('pbpaste')).stdout
205
+ } else if (process.platform === 'linux') {
206
+ return (await execAsync('xclip -selection clipboard -o')).stdout
207
+ } else if (process.platform === 'win32') {
208
+ return (await execAsync('powershell -command "Get-Clipboard"')).stdout
209
+ }
210
+ return ''
211
+ }
212
+
213
+ export async function openUrl(url: string): Promise<void> {
214
+ if (process.platform === 'darwin') {
215
+ await execAsync(`open "${url}"`)
216
+ } else if (process.platform === 'linux') {
217
+ await execAsync(`xdg-open "${url}"`)
218
+ } else if (process.platform === 'win32') {
219
+ await execAsync(`start "${url}"`)
220
+ }
221
+ }
222
+
223
+ export async function openFile(target: string, app?: string): Promise<void> {
224
+ if (process.platform === 'darwin') {
225
+ if (app) {
226
+ await execAsync(`open -a "${app}" "${target}"`)
227
+ } else {
228
+ await execAsync(`open "${target}"`)
229
+ }
230
+ } else if (process.platform === 'linux') {
231
+ await execAsync(`xdg-open "${target}"`)
232
+ } else if (process.platform === 'win32') {
233
+ await execAsync(`start "" "${target}"`)
234
+ }
235
+ }
236
+
237
+ export async function showInFileManager(filePath: string): Promise<void> {
238
+ if (process.platform === 'darwin') {
239
+ await execAsync(`open -R "${filePath}"`)
240
+ } else if (process.platform === 'linux') {
241
+ await execAsync(`xdg-open "$(dirname "${filePath}")"`)
242
+ } else if (process.platform === 'win32') {
243
+ await execAsync(`explorer /select,"${filePath}"`)
244
+ }
245
+ }
246
+
247
+ export async function moveToTrash(filePath: string): Promise<void> {
248
+ if (process.platform === 'darwin') {
249
+ await execAsync(
250
+ `osascript -e 'tell application "Finder" to delete POSIX file "${filePath}"'`,
251
+ )
252
+ } else if (process.platform === 'linux') {
253
+ try {
254
+ await execAsync(`gio trash "${filePath}"`)
255
+ } catch {
256
+ const trashDir = `${os.homedir()}/.local/share/Trash/files`
257
+ await execAsync(`mkdir -p "${trashDir}" && mv "${filePath}" "${trashDir}/"`)
258
+ }
259
+ } else if (process.platform === 'win32') {
260
+ await execAsync(
261
+ `powershell -command "Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('${filePath}','OnlyErrorDialogs','SendToRecycleBin')"`,
262
+ )
263
+ }
264
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * SQLite Database for Node.js — uses better-sqlite3.
3
+ *
4
+ * Translates bun:sqlite constructor options ({ create, readwrite })
5
+ * to better-sqlite3 options ({ fileMustExist, readonly }).
6
+ * All other APIs (prepare/get/all/run/exec/transaction/close) are
7
+ * identical between the two libraries.
8
+ */
9
+
10
+ import BetterSqlite3 from 'better-sqlite3'
11
+
12
+ interface DatabaseOptions {
13
+ create?: boolean
14
+ readwrite?: boolean
15
+ readonly?: boolean
16
+ }
17
+
18
+ function NodeDatabase(path: string, options?: DatabaseOptions) {
19
+ const betterOpts: { fileMustExist?: boolean; readonly?: boolean } = {}
20
+
21
+ if (options?.create === false) {
22
+ betterOpts.fileMustExist = true
23
+ }
24
+ if (options?.readonly === true || options?.readwrite === false) {
25
+ betterOpts.readonly = true
26
+ }
27
+
28
+ return new BetterSqlite3(path, betterOpts)
29
+ }
30
+
31
+ export const Database: new (path: string, options?: DatabaseOptions) => DatabaseInstance = NodeDatabase as any
32
+
33
+ /** Instance type returned by `new Database(...)` */
34
+ export type DatabaseInstance = {
35
+ exec(sql: string): void
36
+ prepare(sql: string): {
37
+ get(...params: any[]): any
38
+ all(...params: any[]): any[]
39
+ run(...params: any[]): any
40
+ }
41
+ transaction<T>(fn: () => T): () => T
42
+ close(): void
43
+ }
package/src/state.tsx CHANGED
@@ -1,4 +1,3 @@
1
- import fs from 'node:fs'
2
1
  import { create } from 'zustand'
3
2
  import { type ReactNode } from 'react'
4
3
  import type { TextareaRenderable } from '@opentui/core'
@@ -6,6 +5,7 @@ import type { RaycastPackageJson } from './package-json'
6
5
  import type { KeyboardKeyEquivalent, KeyboardKeyModifier } from 'termcast/src/keyboard'
7
6
  import { getResolvedTheme } from './themes'
8
7
  import { logger } from './logger'
8
+ import { getEnv } from '#platform/runtime'
9
9
 
10
10
  // Registered action shortcuts for global keyboard handling
11
11
  // Stored by ActionPanel, consumed by List/Detail/Form keyboard handlers
@@ -52,7 +52,17 @@ export interface NavigationStackItem {
52
52
  searchText?: string
53
53
  }
54
54
 
55
+ // Vim mode types
56
+ // inputMode: global mode persisted across sessions
57
+ // vimInputSubMode: transient sub-state within the current mode
58
+ export type InputMode = 'raycast' | 'vim'
59
+ export type VimInputSubMode = 'default' | 'search' | 'command'
60
+
55
61
  interface AppState {
62
+ // Vim mode state
63
+ inputMode: InputMode
64
+ vimInputSubMode: VimInputSubMode
65
+ vimCommandText: string
56
66
  toast: ToastData | null
57
67
  toastWithPrimaryAction: boolean
58
68
  dialogStack: DialogStackItem[]
@@ -96,6 +106,10 @@ interface AppState {
96
106
  }
97
107
 
98
108
  export const useStore = create<AppState>(() => ({
109
+ // Vim mode — initialized from persistence in initializeVimMode()
110
+ inputMode: 'raycast',
111
+ vimInputSubMode: 'default',
112
+ vimCommandText: '',
99
113
  toast: null,
100
114
  toastWithPrimaryAction: false,
101
115
  dialogStack: [],
@@ -120,36 +134,11 @@ export const useStore = create<AppState>(() => ({
120
134
  showActionsDialog: false,
121
135
  actionsPortalTarget: null,
122
136
  // Theme state — TERMCAST_DEFAULT_THEME env var is set by the app launcher
123
- currentThemeName: process.env.TERMCAST_DEFAULT_THEME || 'nerv',
137
+ currentThemeName: getEnv('TERMCAST_DEFAULT_THEME') || 'nerv',
124
138
  // Active search input ref
125
139
  activeSearchInputRef: null,
126
140
  // Registered action shortcuts
127
141
  registeredActionShortcuts: [],
128
142
  }))
129
143
 
130
- // Sync WezTerm's window background with the active termcast theme.
131
- // When the theme changes, rewrite the background color in wezterm.lua.
132
- // WezTerm auto-reloads the config on file change, updating the window edges/padding.
133
- // The config path is passed from the launcher via TERMCAST_WEZTERM_CONFIG env var.
134
- const weztermConfigPath = process.env.TERMCAST_WEZTERM_CONFIG
135
- if (weztermConfigPath) {
136
- useStore.subscribe((state, prevState) => {
137
- if (state.currentThemeName === prevState.currentThemeName) {
138
- return
139
- }
140
- try {
141
- const theme = getResolvedTheme(state.currentThemeName)
142
- const content = fs.readFileSync(weztermConfigPath, 'utf-8')
143
- // Replace the background hex in: config.colors = { background = '#xxxxxx' }
144
- const updated = content.replace(
145
- /background\s*=\s*'#[0-9a-fA-F]{6}'/,
146
- `background = '${theme.background}'`,
147
- )
148
- if (updated !== content) {
149
- fs.writeFileSync(weztermConfigPath, updated)
150
- }
151
- } catch (e) {
152
- logger.log('Failed to update wezterm config background:', e)
153
- }
154
- })
155
- }
144
+
@@ -1,6 +1,4 @@
1
- import * as fs from 'fs/promises'
2
- import * as path from 'path'
3
- import * as os from 'os'
1
+ import { homedir, isAbsolute, resolvePath, joinPath, cwd, relativePath, readdirAsync, accessAsync } from '#platform/runtime'
4
2
 
5
3
  export interface FileSystemItem {
6
4
  name: string
@@ -26,18 +24,18 @@ export async function listAllFiles({
26
24
 
27
25
  // Resolve ~ to home directory
28
26
  if (basePath.startsWith('~')) {
29
- basePath = basePath.replace('~', os.homedir())
27
+ basePath = basePath.replace('~', homedir())
30
28
  }
31
29
 
32
- const resolvedBase = path.isAbsolute(basePath)
30
+ const resolvedBase = isAbsolute(basePath)
33
31
  ? basePath
34
- : path.resolve(process.cwd(), basePath)
32
+ : resolvePath(cwd(), basePath)
35
33
 
36
34
  async function walk(dir: string, depth: number) {
37
35
  if (depth > maxDepth || results.length >= maxFiles) return
38
36
 
39
37
  try {
40
- const entries = await fs.readdir(dir, { withFileTypes: true })
38
+ const entries = await readdirAsync(dir)
41
39
 
42
40
  for (const entry of entries) {
43
41
  if (results.length >= maxFiles) break
@@ -48,16 +46,16 @@ export async function listAllFiles({
48
46
  if (entry.name === 'dist') continue
49
47
  if (entry.name === 'build') continue
50
48
 
51
- const fullPath = path.join(dir, entry.name)
52
- const relativePath = path.relative(resolvedBase, fullPath)
49
+ const fullPath = joinPath(dir, entry.name)
50
+ const relPath = relativePath(resolvedBase, fullPath)
53
51
 
54
52
  if (entry.isDirectory()) {
55
53
  if (includeDirectories) {
56
- results.push(relativePath + '/')
54
+ results.push(relPath + '/')
57
55
  }
58
56
  await walk(fullPath, depth + 1)
59
57
  } else {
60
- results.push(relativePath)
58
+ results.push(relPath)
61
59
  }
62
60
  }
63
61
  } catch {
@@ -76,23 +74,20 @@ export async function searchFiles(
76
74
  try {
77
75
  // Resolve ~ to home directory
78
76
  if (searchPath.startsWith('~')) {
79
- searchPath = searchPath.replace('~', os.homedir())
77
+ searchPath = searchPath.replace('~', homedir())
80
78
  }
81
79
 
82
80
  // Handle absolute vs relative paths
83
- const basePath = path.isAbsolute(searchPath)
81
+ const basePath = isAbsolute(searchPath)
84
82
  ? searchPath
85
- : path.resolve(process.cwd(), searchPath || '.')
83
+ : resolvePath(cwd(), searchPath || '.')
86
84
 
87
85
  // Check if directory exists
88
- try {
89
- await fs.access(basePath)
90
- } catch {
91
- return []
92
- }
86
+ const exists = await accessAsync(basePath)
87
+ if (!exists) return []
93
88
 
94
89
  // List files and directories
95
- const entries = await fs.readdir(basePath, { withFileTypes: true })
90
+ const entries = await readdirAsync(basePath)
96
91
 
97
92
  const items: FileSystemItem[] = []
98
93
 
@@ -107,7 +102,7 @@ export async function searchFiles(
107
102
  ) {
108
103
  items.push({
109
104
  name: entry.name,
110
- path: path.join(basePath, entry.name),
105
+ path: joinPath(basePath, entry.name),
111
106
  isDirectory: entry.isDirectory(),
112
107
  })
113
108
  }
@@ -130,7 +125,7 @@ export async function searchFiles(
130
125
  export function parsePath(input: string): { basePath: string; prefix: string } {
131
126
  // Handle ~ expansion
132
127
  if (input.startsWith('~')) {
133
- input = input.replace('~', os.homedir())
128
+ input = input.replace('~', homedir())
134
129
  }
135
130
 
136
131
  const lastSlashIndex = input.lastIndexOf('/')