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.
- package/dist/action-utils.d.ts.map +1 -1
- package/dist/action-utils.js +17 -132
- package/dist/action-utils.js.map +1 -1
- package/dist/apis/cache.d.ts +8 -30
- package/dist/apis/cache.d.ts.map +1 -1
- package/dist/apis/cache.js +9 -271
- package/dist/apis/cache.js.map +1 -1
- package/dist/apis/clipboard.d.ts +4 -2
- package/dist/apis/clipboard.d.ts.map +1 -1
- package/dist/apis/clipboard.js +18 -31
- package/dist/apis/clipboard.js.map +1 -1
- package/dist/apis/environment.d.ts.map +1 -1
- package/dist/apis/environment.js +14 -49
- package/dist/apis/environment.js.map +1 -1
- package/dist/apis/localstorage.d.ts +7 -12
- package/dist/apis/localstorage.d.ts.map +1 -1
- package/dist/apis/localstorage.js +7 -184
- package/dist/apis/localstorage.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +16 -15
- package/dist/app.js.map +1 -1
- package/dist/cli.js +7 -6
- package/dist/cli.js.map +1 -1
- package/dist/components/actions.d.ts.map +1 -1
- package/dist/components/actions.js +13 -2
- package/dist/components/actions.js.map +1 -1
- package/dist/components/extension-preferences.d.ts.map +1 -1
- package/dist/components/extension-preferences.js +7 -8
- package/dist/components/extension-preferences.js.map +1 -1
- package/dist/components/form/file-autocomplete.js +2 -2
- package/dist/components/form/file-autocomplete.js.map +1 -1
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +242 -14
- package/dist/components/list.js.map +1 -1
- package/dist/e2e-node.d.ts.map +1 -1
- package/dist/e2e-node.js +5 -4
- package/dist/e2e-node.js.map +1 -1
- package/dist/extensions/dev.d.ts.map +1 -1
- package/dist/extensions/dev.js +5 -2
- package/dist/extensions/dev.js.map +1 -1
- package/dist/globals.d.ts.map +1 -1
- package/dist/globals.js +2 -1
- package/dist/globals.js.map +1 -1
- package/dist/internal/error-handler.d.ts.map +1 -1
- package/dist/internal/error-handler.js +21 -19
- package/dist/internal/error-handler.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +41 -1
- package/dist/internal/providers.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +31 -29
- package/dist/logger.js.map +1 -1
- package/dist/platform/browser/cache.d.ts +41 -0
- package/dist/platform/browser/cache.d.ts.map +1 -0
- package/dist/platform/browser/cache.js +262 -0
- package/dist/platform/browser/cache.js.map +1 -0
- package/dist/platform/browser/localstorage.d.ts +20 -0
- package/dist/platform/browser/localstorage.d.ts.map +1 -0
- package/dist/platform/browser/localstorage.js +102 -0
- package/dist/platform/browser/localstorage.js.map +1 -0
- package/dist/platform/browser/runtime.d.ts +51 -0
- package/dist/platform/browser/runtime.d.ts.map +1 -0
- package/dist/platform/browser/runtime.js +164 -0
- package/dist/platform/browser/runtime.js.map +1 -0
- package/dist/platform/bun/sqlite.d.ts +17 -0
- package/dist/platform/bun/sqlite.d.ts.map +1 -0
- package/dist/platform/bun/sqlite.js +6 -0
- package/dist/platform/bun/sqlite.js.map +1 -0
- package/dist/platform/node/cache.d.ts +35 -0
- package/dist/platform/node/cache.d.ts.map +1 -0
- package/dist/platform/node/cache.js +269 -0
- package/dist/platform/node/cache.js.map +1 -0
- package/dist/platform/node/localstorage.d.ts +17 -0
- package/dist/platform/node/localstorage.d.ts.map +1 -0
- package/dist/platform/node/localstorage.js +186 -0
- package/dist/platform/node/localstorage.js.map +1 -0
- package/dist/platform/node/runtime.d.ts +52 -0
- package/dist/platform/node/runtime.d.ts.map +1 -0
- package/dist/platform/node/runtime.js +230 -0
- package/dist/platform/node/runtime.js.map +1 -0
- package/dist/platform/node/sqlite.d.ts +27 -0
- package/dist/platform/node/sqlite.d.ts.map +1 -0
- package/dist/platform/node/sqlite.js +21 -0
- package/dist/platform/node/sqlite.js.map +1 -0
- package/dist/state.d.ts +5 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +6 -28
- package/dist/state.js.map +1 -1
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +17 -22
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +42 -47
- package/dist/utils.js.map +1 -1
- package/dist/vim-mode.d.ts +40 -0
- package/dist/vim-mode.d.ts.map +1 -0
- package/dist/vim-mode.js +135 -0
- package/dist/vim-mode.js.map +1 -0
- package/fonts/Inconsolata.otf +0 -0
- package/fonts/SIL Open Font License.txt +41 -0
- package/package.json +60 -8
- package/src/action-utils.tsx +27 -124
- package/src/apis/cache.test.ts +1 -1
- package/src/apis/cache.tsx +9 -373
- package/src/apis/clipboard.tsx +29 -38
- package/src/apis/environment.tsx +25 -52
- package/src/apis/localstorage.tsx +8 -214
- package/src/app.tsx +16 -15
- package/src/cli.tsx +14 -15
- package/src/compile.vitest.tsx +2 -2
- package/src/components/actions.tsx +19 -1
- package/src/components/extension-preferences.tsx +7 -8
- package/src/components/form/file-autocomplete.tsx +2 -2
- package/src/components/list.tsx +279 -14
- package/src/e2e-node.tsx +7 -7
- package/src/examples/action-shortcut.vitest.tsx +2 -2
- package/src/examples/actions-context.vitest.tsx +1 -1
- package/src/examples/bar-graph-weekly.vitest.tsx +10 -36
- package/src/examples/detail-metadata-showcase.vitest.tsx +36 -36
- package/src/examples/form-basic.vitest.tsx +21 -17
- package/src/examples/github.vitest.tsx +4 -4
- package/src/examples/graph-bar-chart.vitest.tsx +13 -11
- package/src/examples/graph-polymarket.vitest.tsx +2 -2
- package/src/examples/graph-row.vitest.tsx +66 -66
- package/src/examples/graph-styles.vitest.tsx +12 -12
- package/src/examples/internal/simple-scrollbox.vitest.tsx +14 -48
- package/src/examples/list-detail-metadata.vitest.tsx +5 -5
- package/src/examples/list-fetch-data.vitest.tsx +3 -3
- package/src/examples/list-item-accessories.vitest.tsx +2 -2
- package/src/examples/list-loading-empty-view.vitest.tsx +1 -1
- package/src/examples/list-no-actions.vitest.tsx +2 -2
- package/src/examples/list-scrollbox.vitest.tsx +5 -5
- package/src/examples/list-spacing-mode.vitest.tsx +3 -3
- package/src/examples/list-with-detail.vitest.tsx +68 -68
- package/src/examples/list-with-dropdown.vitest.tsx +5 -5
- package/src/examples/list-with-sections.vitest.tsx +27 -27
- package/src/examples/simple-candle-chart.vitest.tsx +7 -7
- package/src/examples/simple-detail-markdown.vitest.tsx +8 -8
- package/src/examples/simple-detail-table.vitest.tsx +8 -8
- package/src/examples/simple-graph.vitest.tsx +3 -3
- package/src/examples/simple-grid.vitest.tsx +14 -14
- package/src/examples/simple-heatmap.vitest.tsx +1 -1
- package/src/examples/simple-navigation.vitest.tsx +17 -17
- package/src/examples/simple-progress-bar.vitest.tsx +1 -1
- package/src/examples/store.vitest.tsx +1 -1
- package/src/examples/swift-extension.vitest.tsx +2 -2
- package/src/examples/table-edge-cases.vitest.tsx +18 -18
- package/src/examples/toast-action.vitest.tsx +2 -2
- package/src/extensions/dev.tsx +5 -2
- package/src/extensions/dev.vitest.tsx +3 -3
- package/src/globals.ts +2 -1
- package/src/internal/error-handler.tsx +19 -21
- package/src/internal/providers.tsx +39 -0
- package/src/logger.tsx +38 -41
- package/src/platform/browser/cache.ts +327 -0
- package/src/platform/browser/localstorage.ts +119 -0
- package/src/platform/browser/runtime.ts +209 -0
- package/src/platform/bun/sqlite.ts +19 -0
- package/src/platform/node/cache.ts +372 -0
- package/src/platform/node/localstorage.ts +214 -0
- package/src/platform/node/runtime.ts +264 -0
- package/src/platform/node/sqlite.ts +43 -0
- package/src/state.tsx +17 -28
- package/src/utils/file-system.ts +17 -22
- package/src/utils.test.tsx +1 -1
- package/src/utils.tsx +56 -47
- package/src/vim-mode.tsx +153 -0
- package/src/apis/sqlite.ts +0 -14
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Cache implementation backed by IndexedDB with in-memory Map for sync reads.
|
|
3
|
+
*
|
|
4
|
+
* On construction, all entries for the namespace are loaded into a Map.
|
|
5
|
+
* Sync reads (get, has, isEmpty) hit the Map.
|
|
6
|
+
* Writes update both the Map and IndexedDB (fire-and-forget).
|
|
7
|
+
* LRU eviction is done in-memory.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { byteLength } from '#platform/runtime'
|
|
11
|
+
|
|
12
|
+
const DB_NAME = 'termcast-cache'
|
|
13
|
+
const STORE_NAME = 'cache_entries'
|
|
14
|
+
const DB_VERSION = 1
|
|
15
|
+
const DEFAULT_NAMESPACE = '__default__'
|
|
16
|
+
|
|
17
|
+
interface CacheEntry {
|
|
18
|
+
namespace: string
|
|
19
|
+
key: string
|
|
20
|
+
data: string
|
|
21
|
+
size: number
|
|
22
|
+
last_accessed_at: number
|
|
23
|
+
updated_at: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Shared IDB connection — lazily opened, reused across Cache instances
|
|
27
|
+
let dbPromise: Promise<IDBDatabase> | null = null
|
|
28
|
+
|
|
29
|
+
function openDB(): Promise<IDBDatabase> {
|
|
30
|
+
if (dbPromise) return dbPromise
|
|
31
|
+
|
|
32
|
+
dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
|
|
33
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
|
34
|
+
|
|
35
|
+
request.onupgradeneeded = () => {
|
|
36
|
+
const db = request.result
|
|
37
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
38
|
+
const store = db.createObjectStore(STORE_NAME, {
|
|
39
|
+
keyPath: ['namespace', 'key'],
|
|
40
|
+
})
|
|
41
|
+
store.createIndex('by_namespace', 'namespace', { unique: false })
|
|
42
|
+
store.createIndex('by_lru', ['namespace', 'last_accessed_at'], {
|
|
43
|
+
unique: false,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
request.onsuccess = () => {
|
|
49
|
+
resolve(request.result)
|
|
50
|
+
}
|
|
51
|
+
request.onerror = () => {
|
|
52
|
+
reject(request.error)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return dbPromise
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Load all entries for a namespace into memory (called once per Cache instance)
|
|
60
|
+
async function loadNamespace(namespace: string): Promise<Map<string, CacheEntry>> {
|
|
61
|
+
const db = await openDB()
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const tx = db.transaction(STORE_NAME, 'readonly')
|
|
64
|
+
const store = tx.objectStore(STORE_NAME)
|
|
65
|
+
const index = store.index('by_namespace')
|
|
66
|
+
const request = index.getAll(namespace)
|
|
67
|
+
|
|
68
|
+
request.onsuccess = () => {
|
|
69
|
+
const map = new Map<string, CacheEntry>()
|
|
70
|
+
for (const entry of request.result as CacheEntry[]) {
|
|
71
|
+
map.set(entry.key, entry)
|
|
72
|
+
}
|
|
73
|
+
resolve(map)
|
|
74
|
+
}
|
|
75
|
+
request.onerror = () => {
|
|
76
|
+
reject(request.error)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function idbPut(entry: CacheEntry): Promise<void> {
|
|
82
|
+
const db = await openDB()
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const tx = db.transaction(STORE_NAME, 'readwrite')
|
|
85
|
+
const store = tx.objectStore(STORE_NAME)
|
|
86
|
+
const request = store.put(entry)
|
|
87
|
+
request.onsuccess = () => {
|
|
88
|
+
resolve()
|
|
89
|
+
}
|
|
90
|
+
request.onerror = () => {
|
|
91
|
+
reject(request.error)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function idbDelete(namespace: string, key: string): Promise<void> {
|
|
97
|
+
const db = await openDB()
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const tx = db.transaction(STORE_NAME, 'readwrite')
|
|
100
|
+
const store = tx.objectStore(STORE_NAME)
|
|
101
|
+
const request = store.delete([namespace, key])
|
|
102
|
+
request.onsuccess = () => {
|
|
103
|
+
resolve()
|
|
104
|
+
}
|
|
105
|
+
request.onerror = () => {
|
|
106
|
+
reject(request.error)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function idbClearNamespace(namespace: string): Promise<void> {
|
|
112
|
+
const db = await openDB()
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const tx = db.transaction(STORE_NAME, 'readwrite')
|
|
115
|
+
const store = tx.objectStore(STORE_NAME)
|
|
116
|
+
const index = store.index('by_namespace')
|
|
117
|
+
const request = index.openCursor(namespace)
|
|
118
|
+
|
|
119
|
+
request.onsuccess = () => {
|
|
120
|
+
const cursor = request.result
|
|
121
|
+
if (cursor) {
|
|
122
|
+
cursor.delete()
|
|
123
|
+
cursor.continue()
|
|
124
|
+
} else {
|
|
125
|
+
resolve()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
request.onerror = () => {
|
|
129
|
+
reject(request.error)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let logicalTimestamp = Date.now()
|
|
135
|
+
function nextTimestamp(): number {
|
|
136
|
+
logicalTimestamp += 1
|
|
137
|
+
return logicalTimestamp
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class Cache {
|
|
141
|
+
static get STORAGE_DIRECTORY_NAME(): string {
|
|
142
|
+
return 'cache'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static get DEFAULT_CAPACITY(): number {
|
|
146
|
+
return 10 * 1024 * 1024 // 10 MB
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private entries: Map<string, CacheEntry> = new Map()
|
|
150
|
+
private capacity: number
|
|
151
|
+
private namespace: string
|
|
152
|
+
private subscribers: Cache.Subscriber[] = []
|
|
153
|
+
private currentSize: number = 0
|
|
154
|
+
private initialized: boolean = false
|
|
155
|
+
|
|
156
|
+
constructor(options?: Cache.Options) {
|
|
157
|
+
this.capacity = options?.capacity || Cache.DEFAULT_CAPACITY
|
|
158
|
+
this.namespace = options?.namespace || DEFAULT_NAMESPACE
|
|
159
|
+
|
|
160
|
+
// Kick off async load — sync reads return undefined until loaded
|
|
161
|
+
this.init()
|
|
162
|
+
|
|
163
|
+
// Bind all methods
|
|
164
|
+
this.get = this.get.bind(this)
|
|
165
|
+
this.has = this.has.bind(this)
|
|
166
|
+
this.set = this.set.bind(this)
|
|
167
|
+
this.remove = this.remove.bind(this)
|
|
168
|
+
this.clear = this.clear.bind(this)
|
|
169
|
+
this.subscribe = this.subscribe.bind(this)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async init(): Promise<void> {
|
|
173
|
+
try {
|
|
174
|
+
const loaded = await loadNamespace(this.namespace)
|
|
175
|
+
// Merge: keep in-memory writes that happened during async load,
|
|
176
|
+
// preferring the newer entry (by updated_at) on key conflicts.
|
|
177
|
+
for (const [key, inMemoryEntry] of this.entries) {
|
|
178
|
+
const loadedEntry = loaded.get(key)
|
|
179
|
+
if (!loadedEntry || inMemoryEntry.updated_at >= loadedEntry.updated_at) {
|
|
180
|
+
loaded.set(key, inMemoryEntry)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
this.entries = loaded
|
|
184
|
+
this.currentSize = 0
|
|
185
|
+
for (const entry of this.entries.values()) {
|
|
186
|
+
this.currentSize += entry.size
|
|
187
|
+
}
|
|
188
|
+
this.initialized = true
|
|
189
|
+
} catch {
|
|
190
|
+
// IndexedDB might not be available (e.g. incognito with restrictions)
|
|
191
|
+
this.initialized = true
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
get storageDirectory(): string {
|
|
196
|
+
return `/termcast/cache/${this.namespace}`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get(key: string): string | undefined {
|
|
200
|
+
const entry = this.entries.get(key)
|
|
201
|
+
if (!entry) return undefined
|
|
202
|
+
|
|
203
|
+
// Update LRU timestamp in-memory and async in IDB
|
|
204
|
+
const now = nextTimestamp()
|
|
205
|
+
entry.last_accessed_at = now
|
|
206
|
+
idbPut(entry).catch(() => {})
|
|
207
|
+
|
|
208
|
+
return entry.data
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
has(key: string): boolean {
|
|
212
|
+
return this.entries.has(key)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
get isEmpty(): boolean {
|
|
216
|
+
return this.entries.size === 0
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
set(key: string, data: string): void {
|
|
220
|
+
const now = nextTimestamp()
|
|
221
|
+
const dataSize = byteLength(data)
|
|
222
|
+
|
|
223
|
+
const existing = this.entries.get(key)
|
|
224
|
+
const oldSize = existing?.size || 0
|
|
225
|
+
const newTotalSize = this.currentSize - oldSize + dataSize
|
|
226
|
+
|
|
227
|
+
if (newTotalSize > this.capacity) {
|
|
228
|
+
this.maintainCapacity(newTotalSize - this.capacity)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const entry: CacheEntry = {
|
|
232
|
+
namespace: this.namespace,
|
|
233
|
+
key,
|
|
234
|
+
data,
|
|
235
|
+
size: dataSize,
|
|
236
|
+
last_accessed_at: now,
|
|
237
|
+
updated_at: now,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.entries.set(key, entry)
|
|
241
|
+
this.currentSize = this.currentSize - oldSize + dataSize
|
|
242
|
+
|
|
243
|
+
// Persist async
|
|
244
|
+
idbPut(entry).catch(() => {})
|
|
245
|
+
|
|
246
|
+
this.notifySubscribers(key, data)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
remove(key: string): boolean {
|
|
250
|
+
const entry = this.entries.get(key)
|
|
251
|
+
if (!entry) return false
|
|
252
|
+
|
|
253
|
+
this.entries.delete(key)
|
|
254
|
+
this.currentSize -= entry.size
|
|
255
|
+
|
|
256
|
+
// Persist async
|
|
257
|
+
idbDelete(this.namespace, key).catch(() => {})
|
|
258
|
+
|
|
259
|
+
this.notifySubscribers(key, undefined)
|
|
260
|
+
return true
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
clear(options?: { notifySubscribers: boolean }): void {
|
|
264
|
+
this.entries.clear()
|
|
265
|
+
this.currentSize = 0
|
|
266
|
+
|
|
267
|
+
// Persist async
|
|
268
|
+
idbClearNamespace(this.namespace).catch(() => {})
|
|
269
|
+
|
|
270
|
+
if (options?.notifySubscribers !== false) {
|
|
271
|
+
this.notifySubscribers(undefined, undefined)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
subscribe(subscriber: Cache.Subscriber): Cache.Subscription {
|
|
276
|
+
this.subscribers.push(subscriber)
|
|
277
|
+
return () => {
|
|
278
|
+
const index = this.subscribers.indexOf(subscriber)
|
|
279
|
+
if (index > -1) {
|
|
280
|
+
this.subscribers.splice(index, 1)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private maintainCapacity(bytesToFree: number): void {
|
|
286
|
+
// Sort by LRU — oldest first
|
|
287
|
+
const sorted = [...this.entries.values()].sort(
|
|
288
|
+
(a, b) => a.last_accessed_at - b.last_accessed_at,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
let freedBytes = 0
|
|
292
|
+
for (const entry of sorted) {
|
|
293
|
+
if (freedBytes >= bytesToFree) break
|
|
294
|
+
this.entries.delete(entry.key)
|
|
295
|
+
freedBytes += entry.size
|
|
296
|
+
// Persist async
|
|
297
|
+
idbDelete(this.namespace, entry.key).catch(() => {})
|
|
298
|
+
}
|
|
299
|
+
this.currentSize -= freedBytes
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private notifySubscribers(
|
|
303
|
+
key: string | undefined,
|
|
304
|
+
data: string | undefined,
|
|
305
|
+
): void {
|
|
306
|
+
for (const subscriber of this.subscribers) {
|
|
307
|
+
try {
|
|
308
|
+
subscriber(key, data)
|
|
309
|
+
} catch {
|
|
310
|
+
// Ignore subscriber errors in browser
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export namespace Cache {
|
|
317
|
+
export interface Options {
|
|
318
|
+
namespace?: string
|
|
319
|
+
capacity?: number
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export type Subscriber = (
|
|
323
|
+
key: string | undefined,
|
|
324
|
+
data: string | undefined,
|
|
325
|
+
) => void
|
|
326
|
+
export type Subscription = () => void
|
|
327
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser LocalStorage implementation backed by window.localStorage.
|
|
3
|
+
*
|
|
4
|
+
* Values are stored as JSON objects with type metadata to preserve
|
|
5
|
+
* number/boolean types across serialization, matching the SQLite
|
|
6
|
+
* implementation's behavior.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const PREFIX = 'termcast:'
|
|
10
|
+
|
|
11
|
+
interface StoredEntry {
|
|
12
|
+
value: string
|
|
13
|
+
type: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export namespace LocalStorage {
|
|
17
|
+
export type Value = string | number | boolean
|
|
18
|
+
|
|
19
|
+
export interface Values {
|
|
20
|
+
[key: string]: any
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getItem<T extends Value = Value>(
|
|
24
|
+
key: string,
|
|
25
|
+
): Promise<T | undefined> {
|
|
26
|
+
return getItemSync<T>(key)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getItemSync<T extends Value = Value>(
|
|
30
|
+
key: string,
|
|
31
|
+
): T | undefined {
|
|
32
|
+
try {
|
|
33
|
+
const raw = window.localStorage.getItem(PREFIX + key)
|
|
34
|
+
if (raw === null) return undefined
|
|
35
|
+
|
|
36
|
+
const entry: StoredEntry = JSON.parse(raw)
|
|
37
|
+
|
|
38
|
+
let value: Value
|
|
39
|
+
switch (entry.type) {
|
|
40
|
+
case 'number':
|
|
41
|
+
value = parseFloat(entry.value)
|
|
42
|
+
break
|
|
43
|
+
case 'boolean':
|
|
44
|
+
value = entry.value === 'true'
|
|
45
|
+
break
|
|
46
|
+
default:
|
|
47
|
+
value = entry.value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return value as T
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function setItem(key: string, value: Value): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
const entry: StoredEntry = {
|
|
59
|
+
value: String(value),
|
|
60
|
+
type: typeof value,
|
|
61
|
+
}
|
|
62
|
+
window.localStorage.setItem(PREFIX + key, JSON.stringify(entry))
|
|
63
|
+
} catch {
|
|
64
|
+
// localStorage might be full or disabled
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function removeItem(key: string): Promise<void> {
|
|
69
|
+
window.localStorage.removeItem(PREFIX + key)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function allItems<T extends Values = Values>(): Promise<T> {
|
|
73
|
+
const result: Values = {}
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
76
|
+
const fullKey = window.localStorage.key(i)
|
|
77
|
+
if (!fullKey?.startsWith(PREFIX)) continue
|
|
78
|
+
|
|
79
|
+
const key = fullKey.slice(PREFIX.length)
|
|
80
|
+
const raw = window.localStorage.getItem(fullKey)
|
|
81
|
+
if (raw === null) continue
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const entry: StoredEntry = JSON.parse(raw)
|
|
85
|
+
|
|
86
|
+
let value: Value
|
|
87
|
+
switch (entry.type) {
|
|
88
|
+
case 'number':
|
|
89
|
+
value = parseFloat(entry.value)
|
|
90
|
+
break
|
|
91
|
+
case 'boolean':
|
|
92
|
+
value = entry.value === 'true'
|
|
93
|
+
break
|
|
94
|
+
default:
|
|
95
|
+
value = entry.value
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
result[key] = value
|
|
99
|
+
} catch {
|
|
100
|
+
// Skip corrupted entries
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result as T
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function clear(): Promise<void> {
|
|
108
|
+
const keysToRemove: string[] = []
|
|
109
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
110
|
+
const fullKey = window.localStorage.key(i)
|
|
111
|
+
if (fullKey?.startsWith(PREFIX)) {
|
|
112
|
+
keysToRemove.push(fullKey)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const key of keysToRemove) {
|
|
116
|
+
window.localStorage.removeItem(key)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser platform runtime.
|
|
3
|
+
*
|
|
4
|
+
* Provides the same exports as the Node runtime but backed by browser APIs
|
|
5
|
+
* or sensible no-ops for features that don't exist in the browser.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── filesystem (no-ops / stubs) ─────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export function ensureDir(_dir: string): void {
|
|
11
|
+
// no-op: browser storage (IndexedDB) doesn't need directory creation
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function fileExists(_p: string): boolean {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readFileSync(_p: string): string {
|
|
19
|
+
throw new Error('readFileSync is not available in the browser')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function appendToFile(_p: string, _data: string): void {
|
|
23
|
+
// no-op: browser logger uses console only
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function unlinkIfExists(_p: string): void {
|
|
27
|
+
// no-op
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readdirSync(_dir: string): Array<{ name: string; isDirectory(): boolean }> {
|
|
31
|
+
return []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function rmSync(_p: string): void {
|
|
35
|
+
// no-op
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function mkdirSync(_dir: string): void {
|
|
39
|
+
// no-op
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function cpSync(_src: string, _dest: string): void {
|
|
43
|
+
throw new Error('cpSync is not available in the browser')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── async filesystem ────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export async function readdirAsync(_dir: string): Promise<Array<{ name: string; isDirectory(): boolean }>> {
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function accessAsync(_p: string): Promise<boolean> {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── path (pure string ops) ──────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export function joinPath(...parts: string[]): string {
|
|
59
|
+
return parts
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join('/')
|
|
62
|
+
.replace(/\/+/g, '/')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function dirname(p: string): string {
|
|
66
|
+
const parts = p.split('/')
|
|
67
|
+
parts.pop()
|
|
68
|
+
return parts.join('/') || '/'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function basename(p: string): string {
|
|
72
|
+
return p.split('/').pop() || ''
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolvePath(...parts: string[]): string {
|
|
76
|
+
return joinPath(...parts)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isAbsolute(p: string): boolean {
|
|
80
|
+
return p.startsWith('/')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function relativePath(from: string, to: string): string {
|
|
84
|
+
const fromParts = from.split('/').filter(Boolean)
|
|
85
|
+
const toParts = to.split('/').filter(Boolean)
|
|
86
|
+
let common = 0
|
|
87
|
+
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
|
|
88
|
+
common++
|
|
89
|
+
}
|
|
90
|
+
const ups = fromParts.length - common
|
|
91
|
+
return [...Array(ups).fill('..'), ...toParts.slice(common)].join('/')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── os ──────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
export function homedir(): string {
|
|
97
|
+
return '/home'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── process ─────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export const platform = 'browser'
|
|
103
|
+
|
|
104
|
+
export function exit(_code?: number): void {
|
|
105
|
+
// no-op: can't exit the browser
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getEnv(key: string): string | undefined {
|
|
109
|
+
// Browser env can be populated by the host app before boot
|
|
110
|
+
return (globalThis as any).__termcast_env?.[key]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function setEnv(key: string, value: string): void {
|
|
114
|
+
const g = globalThis as any
|
|
115
|
+
if (!g.__termcast_env) {
|
|
116
|
+
g.__termcast_env = {}
|
|
117
|
+
}
|
|
118
|
+
g.__termcast_env[key] = value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function cwd(): string {
|
|
122
|
+
return '/'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function stdoutWrite(_data: string): void {
|
|
126
|
+
// no-op: browser renderer handles output
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isTTY(): boolean {
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getArgv(): string[] {
|
|
134
|
+
return []
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── appearance ──────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
export function getSystemAppearance(): 'dark' | 'light' {
|
|
140
|
+
if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
|
|
141
|
+
return 'dark'
|
|
142
|
+
}
|
|
143
|
+
return 'light'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── util ────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export function byteLength(str: string): number {
|
|
149
|
+
return new TextEncoder().encode(str).length
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function inspectValue(val: unknown, _depth = 3): string {
|
|
153
|
+
if (typeof val === 'string') {
|
|
154
|
+
return val
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
return JSON.stringify(val, null, 2)
|
|
158
|
+
} catch {
|
|
159
|
+
return String(val)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── error handling ──────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
export function setupErrorHandlers(handler: (err: Error, type: string) => void): void {
|
|
166
|
+
window.addEventListener('error', (event) => {
|
|
167
|
+
handler(event.error ?? new Error(event.message), 'uncaughtException')
|
|
168
|
+
})
|
|
169
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
170
|
+
const err = event.reason instanceof Error
|
|
171
|
+
? event.reason
|
|
172
|
+
: new Error(String(event.reason))
|
|
173
|
+
handler(err, 'unhandledRejection')
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── shell / clipboard ───────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
export function execWithInput(_command: string, _input: string): Promise<void> {
|
|
180
|
+
return Promise.reject(new Error('execWithInput is not available in the browser'))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function execCommand(_command: string): Promise<string> {
|
|
184
|
+
throw new Error('execCommand is not available in the browser')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function copyToClipboard(text: string): Promise<void> {
|
|
188
|
+
await navigator.clipboard.writeText(text)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function readClipboard(): Promise<string> {
|
|
192
|
+
return navigator.clipboard.readText()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function openUrl(url: string): Promise<void> {
|
|
196
|
+
window.open(url, '_blank')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function openFile(_target: string, _app?: string): Promise<void> {
|
|
200
|
+
throw new Error('openFile is not supported in the browser')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function showInFileManager(_filePath: string): Promise<void> {
|
|
204
|
+
throw new Error('showInFileManager is not supported in the browser')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function moveToTrash(_filePath: string): Promise<void> {
|
|
208
|
+
throw new Error('moveToTrash is not supported in the browser')
|
|
209
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Database for Bun — re-exports the built-in bun:sqlite.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Database as BunDatabase } from 'bun:sqlite'
|
|
6
|
+
|
|
7
|
+
export const Database = BunDatabase
|
|
8
|
+
|
|
9
|
+
/** Instance type returned by `new Database(...)` */
|
|
10
|
+
export type DatabaseInstance = {
|
|
11
|
+
exec(sql: string): void
|
|
12
|
+
prepare(sql: string): {
|
|
13
|
+
get(...params: any[]): any
|
|
14
|
+
all(...params: any[]): any[]
|
|
15
|
+
run(...params: any[]): any
|
|
16
|
+
}
|
|
17
|
+
transaction<T>(fn: () => T): () => T
|
|
18
|
+
close(): void
|
|
19
|
+
}
|