termcast 1.3.53 → 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 +46 -20
- 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/candle-chart.d.ts +110 -0
- package/dist/components/candle-chart.d.ts.map +1 -0
- package/dist/components/candle-chart.js +295 -0
- package/dist/components/candle-chart.js.map +1 -0
- 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/components/table.d.ts +2 -0
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +41 -4
- package/dist/components/table.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/examples/simple-candle-chart-data.d.ts +9064 -0
- package/dist/examples/simple-candle-chart-data.d.ts.map +1 -0
- package/dist/examples/simple-candle-chart-data.js +12683 -0
- package/dist/examples/simple-candle-chart-data.js.map +1 -0
- package/dist/examples/simple-candle-chart.d.ts +2 -0
- package/dist/examples/simple-candle-chart.d.ts.map +1 -0
- package/dist/examples/simple-candle-chart.js +125 -0
- package/dist/examples/simple-candle-chart.js.map +1 -0
- 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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.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 +51 -20
- package/src/cli.tsx +14 -15
- package/src/compile.vitest.tsx +2 -2
- package/src/components/actions.tsx +19 -1
- package/src/components/candle-chart.tsx +410 -0
- 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/components/table.tsx +46 -4
- 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-data.ts +12683 -0
- package/src/examples/simple-candle-chart.tsx +363 -0
- package/src/examples/simple-candle-chart.vitest.tsx +269 -0
- package/src/examples/simple-detail-markdown.vitest.tsx +8 -8
- package/src/examples/simple-detail-table.vitest.tsx +10 -10
- 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/simple-table-wrap.vitest.tsx +19 -19
- 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/table-flex-grow.vitest.tsx +8 -8
- 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/index.tsx +7 -0
- 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
package/src/apis/cache.tsx
CHANGED
|
@@ -1,137 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const CACHE_TABLE_NAME = 'cache_entries'
|
|
9
|
-
const DEFAULT_NAMESPACE = '__default__'
|
|
10
|
-
const initializedDatabasePaths = new Set<string>()
|
|
11
|
-
let logicalTimestamp = Date.now()
|
|
12
|
-
|
|
13
|
-
function nextTimestamp(): number {
|
|
14
|
-
logicalTimestamp += 1
|
|
15
|
-
return logicalTimestamp
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getCurrentDatabasePath(): string {
|
|
19
|
-
const { extensionPath } = useStore.getState()
|
|
20
|
-
const dbSuffix = process.env.TERMCAST_DB_SUFFIX?.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
21
|
-
const dbFileName = dbSuffix ? `data-${dbSuffix}.db` : 'data.db'
|
|
22
|
-
|
|
23
|
-
if (extensionPath) {
|
|
24
|
-
return path.join(extensionPath, '.termcast-bundle', dbFileName)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Fallback for examples/tests that don't set extensionPath
|
|
28
|
-
return path.join(os.homedir(), '.termcast', '.termcast-bundle', dbFileName)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getCurrentCacheDir(namespace?: string): string {
|
|
32
|
-
const { extensionPath } = useStore.getState()
|
|
33
|
-
|
|
34
|
-
const baseDir = extensionPath
|
|
35
|
-
? path.join(extensionPath, '.termcast-bundle', 'cache')
|
|
36
|
-
: path.join(os.homedir(), '.termcast', '.termcast-bundle', 'cache')
|
|
37
|
-
|
|
38
|
-
return namespace ? path.join(baseDir, namespace) : baseDir
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getNamespace(namespace?: string): string {
|
|
42
|
-
return namespace || DEFAULT_NAMESPACE
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function initializeDatabaseOnce({ db, dbPath }: { db: Database; dbPath: string }): void {
|
|
46
|
-
if (initializedDatabasePaths.has(dbPath)) {
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
db.exec('PRAGMA journal_mode = WAL')
|
|
51
|
-
db.exec('PRAGMA wal_autocheckpoint = 1000')
|
|
52
|
-
db.exec('PRAGMA synchronous = NORMAL')
|
|
53
|
-
|
|
54
|
-
db.exec(`
|
|
55
|
-
CREATE TABLE IF NOT EXISTS ${CACHE_TABLE_NAME} (
|
|
56
|
-
namespace TEXT NOT NULL,
|
|
57
|
-
key TEXT NOT NULL,
|
|
58
|
-
data TEXT NOT NULL,
|
|
59
|
-
size INTEGER NOT NULL,
|
|
60
|
-
last_accessed_at INTEGER NOT NULL,
|
|
61
|
-
updated_at INTEGER NOT NULL,
|
|
62
|
-
PRIMARY KEY(namespace, key)
|
|
63
|
-
)
|
|
64
|
-
`)
|
|
65
|
-
|
|
66
|
-
db.exec(`
|
|
67
|
-
CREATE INDEX IF NOT EXISTS idx_${CACHE_TABLE_NAME}_namespace_lru
|
|
68
|
-
ON ${CACHE_TABLE_NAME}(namespace, last_accessed_at)
|
|
69
|
-
`)
|
|
70
|
-
|
|
71
|
-
cleanupLegacyCacheTables(db)
|
|
72
|
-
initializedDatabasePaths.add(dbPath)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function cleanupLegacyCacheTables(db: Database): void {
|
|
76
|
-
const rows = db
|
|
77
|
-
.prepare(
|
|
78
|
-
`SELECT name FROM sqlite_master
|
|
79
|
-
WHERE type = 'table'
|
|
80
|
-
AND (name = 'cache' OR name LIKE 'cache_%')
|
|
81
|
-
AND name != ?`,
|
|
82
|
-
)
|
|
83
|
-
.all(CACHE_TABLE_NAME) as Array<{ name: string }>
|
|
84
|
-
|
|
85
|
-
if (rows.length === 0) {
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const tx = db.transaction(() => {
|
|
90
|
-
const upsert = db.prepare(
|
|
91
|
-
`INSERT INTO ${CACHE_TABLE_NAME} (namespace, key, data, size, last_accessed_at, updated_at)
|
|
92
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
93
|
-
ON CONFLICT(namespace, key)
|
|
94
|
-
DO UPDATE SET
|
|
95
|
-
data = excluded.data,
|
|
96
|
-
size = excluded.size,
|
|
97
|
-
last_accessed_at = excluded.last_accessed_at,
|
|
98
|
-
updated_at = excluded.updated_at`,
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
for (const { name } of rows) {
|
|
102
|
-
const namespace =
|
|
103
|
-
name === 'cache'
|
|
104
|
-
? DEFAULT_NAMESPACE
|
|
105
|
-
: name === 'cache_tanstack_query'
|
|
106
|
-
? 'tanstack-query'
|
|
107
|
-
: `legacy:${name.slice('cache_'.length)}`
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const values = db
|
|
111
|
-
.prepare(`SELECT key, data, size, rowid FROM ${name}`)
|
|
112
|
-
.all() as Array<{ key: string; data: string; size: number; rowid: number }>
|
|
113
|
-
|
|
114
|
-
values.forEach((entry) => {
|
|
115
|
-
const timestamp = entry.rowid
|
|
116
|
-
upsert.run(
|
|
117
|
-
namespace,
|
|
118
|
-
entry.key,
|
|
119
|
-
entry.data,
|
|
120
|
-
entry.size,
|
|
121
|
-
timestamp,
|
|
122
|
-
timestamp,
|
|
123
|
-
)
|
|
124
|
-
})
|
|
125
|
-
} catch {
|
|
126
|
-
// Ignore invalid legacy tables and continue cleanup.
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
db.exec(`DROP TABLE IF EXISTS ${name}`)
|
|
130
|
-
}
|
|
131
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* Cache API — platform-agnostic facade.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the platform-specific Cache class from #platform/cache
|
|
5
|
+
* (SQLite on Node/Bun, IndexedDB on browser) and provides the shared
|
|
6
|
+
* `withCache` higher-order function on top.
|
|
7
|
+
*/
|
|
132
8
|
|
|
133
|
-
|
|
134
|
-
}
|
|
9
|
+
export { Cache } from '#platform/cache'
|
|
10
|
+
import { Cache } from '#platform/cache'
|
|
135
11
|
|
|
136
12
|
function hashString(value: string): string {
|
|
137
13
|
let hash = 0
|
|
@@ -141,246 +17,6 @@ function hashString(value: string): string {
|
|
|
141
17
|
return Math.abs(hash).toString(36)
|
|
142
18
|
}
|
|
143
19
|
|
|
144
|
-
export class Cache {
|
|
145
|
-
static get STORAGE_DIRECTORY_NAME(): string {
|
|
146
|
-
const extensionPath = useStore.getState().extensionPath
|
|
147
|
-
return extensionPath ? 'cache' : '.termcast-cache'
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
static get DEFAULT_CAPACITY(): number {
|
|
151
|
-
return 10 * 1024 * 1024 // 10 MB
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private db: Database
|
|
155
|
-
private capacity: number
|
|
156
|
-
private namespace: string
|
|
157
|
-
private subscribers: Cache.Subscriber[] = []
|
|
158
|
-
private currentSize: number = 0
|
|
159
|
-
|
|
160
|
-
constructor(options?: Cache.Options) {
|
|
161
|
-
const sqliteLoadStart = Date.now()
|
|
162
|
-
this.capacity = options?.capacity || Cache.DEFAULT_CAPACITY
|
|
163
|
-
this.namespace = getNamespace(options?.namespace)
|
|
164
|
-
|
|
165
|
-
const dbPath = getCurrentDatabasePath()
|
|
166
|
-
|
|
167
|
-
// Ensure parent directory exists
|
|
168
|
-
const dbDir = path.dirname(dbPath)
|
|
169
|
-
if (!fs.existsSync(dbDir)) {
|
|
170
|
-
fs.mkdirSync(dbDir, { recursive: true })
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
this.db = new Database(dbPath, {
|
|
174
|
-
create: true,
|
|
175
|
-
readwrite: true,
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
initializeDatabaseOnce({ db: this.db, dbPath })
|
|
179
|
-
|
|
180
|
-
// Calculate initial size
|
|
181
|
-
const row = this.db
|
|
182
|
-
.prepare(
|
|
183
|
-
`SELECT COALESCE(SUM(size), 0) as total FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`,
|
|
184
|
-
)
|
|
185
|
-
.get(this.namespace) as { total: number | null } | undefined
|
|
186
|
-
this.currentSize = row?.total || 0
|
|
187
|
-
|
|
188
|
-
const sqliteLoadMs = Date.now() - sqliteLoadStart
|
|
189
|
-
if (sqliteLoadMs > 500) {
|
|
190
|
-
logger.log(
|
|
191
|
-
`[perf] sqlite cache init took ${sqliteLoadMs}ms (namespace=${this.namespace})`,
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Bind all methods to this instance
|
|
196
|
-
this.get = this.get.bind(this)
|
|
197
|
-
this.has = this.has.bind(this)
|
|
198
|
-
this.set = this.set.bind(this)
|
|
199
|
-
this.remove = this.remove.bind(this)
|
|
200
|
-
this.clear = this.clear.bind(this)
|
|
201
|
-
this.subscribe = this.subscribe.bind(this)
|
|
202
|
-
this.maintainCapacity = this.maintainCapacity.bind(this)
|
|
203
|
-
this.notifySubscribers = this.notifySubscribers.bind(this)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
get storageDirectory(): string {
|
|
207
|
-
return getCurrentCacheDir(this.namespace)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
get(key: string): string | undefined {
|
|
211
|
-
const now = nextTimestamp()
|
|
212
|
-
const row = this.db
|
|
213
|
-
.prepare(
|
|
214
|
-
`SELECT data, size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
|
|
215
|
-
)
|
|
216
|
-
.get(this.namespace, key) as { data: string; size: number } | undefined
|
|
217
|
-
|
|
218
|
-
if (row) {
|
|
219
|
-
this.db
|
|
220
|
-
.prepare(
|
|
221
|
-
`UPDATE ${CACHE_TABLE_NAME} SET last_accessed_at = ? WHERE namespace = ? AND key = ?`,
|
|
222
|
-
)
|
|
223
|
-
.run(now, this.namespace, key)
|
|
224
|
-
|
|
225
|
-
return row.data
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return undefined
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
has(key: string): boolean {
|
|
232
|
-
const row = this.db
|
|
233
|
-
.prepare(`SELECT 1 FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`)
|
|
234
|
-
.get(this.namespace, key)
|
|
235
|
-
return !!row
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
get isEmpty(): boolean {
|
|
239
|
-
const row = this.db
|
|
240
|
-
.prepare(
|
|
241
|
-
`SELECT COUNT(*) as count FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`,
|
|
242
|
-
)
|
|
243
|
-
.get(this.namespace) as { count: number }
|
|
244
|
-
return row.count === 0
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
set(key: string, data: string): void {
|
|
248
|
-
const now = nextTimestamp()
|
|
249
|
-
const dataSize = Buffer.byteLength(data, 'utf8')
|
|
250
|
-
|
|
251
|
-
// Get existing size if any
|
|
252
|
-
const existingRow = this.db
|
|
253
|
-
.prepare(
|
|
254
|
-
`SELECT size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
|
|
255
|
-
)
|
|
256
|
-
.get(this.namespace, key) as { size: number } | undefined
|
|
257
|
-
const oldSize = existingRow?.size || 0
|
|
258
|
-
const newTotalSize = this.currentSize - oldSize + dataSize
|
|
259
|
-
|
|
260
|
-
if (newTotalSize > this.capacity) {
|
|
261
|
-
this.maintainCapacity(newTotalSize - this.capacity)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Insert or update the cache entry
|
|
265
|
-
this.db
|
|
266
|
-
.prepare(
|
|
267
|
-
`INSERT INTO ${CACHE_TABLE_NAME} (namespace, key, data, size, last_accessed_at, updated_at)
|
|
268
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
269
|
-
ON CONFLICT(namespace, key)
|
|
270
|
-
DO UPDATE SET
|
|
271
|
-
data = excluded.data,
|
|
272
|
-
size = excluded.size,
|
|
273
|
-
last_accessed_at = excluded.last_accessed_at,
|
|
274
|
-
updated_at = excluded.updated_at`,
|
|
275
|
-
)
|
|
276
|
-
.run(this.namespace, key, data, dataSize, now, now)
|
|
277
|
-
|
|
278
|
-
this.currentSize = this.currentSize - oldSize + dataSize
|
|
279
|
-
this.notifySubscribers(key, data)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
remove(key: string): boolean {
|
|
283
|
-
// Check if key exists and get its size
|
|
284
|
-
const row = this.db
|
|
285
|
-
.prepare(
|
|
286
|
-
`SELECT size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
|
|
287
|
-
)
|
|
288
|
-
.get(this.namespace, key) as { size: number } | undefined
|
|
289
|
-
|
|
290
|
-
if (row) {
|
|
291
|
-
// Delete the key
|
|
292
|
-
this.db
|
|
293
|
-
.prepare(`DELETE FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`)
|
|
294
|
-
.run(this.namespace, key)
|
|
295
|
-
|
|
296
|
-
this.currentSize -= row.size
|
|
297
|
-
this.notifySubscribers(key, undefined)
|
|
298
|
-
return true
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return false
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
clear(options?: { notifySubscribers: boolean }): void {
|
|
305
|
-
this.db
|
|
306
|
-
.prepare(`DELETE FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`)
|
|
307
|
-
.run(this.namespace)
|
|
308
|
-
this.currentSize = 0
|
|
309
|
-
|
|
310
|
-
if (options?.notifySubscribers !== false) {
|
|
311
|
-
this.notifySubscribers(undefined, undefined)
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
subscribe(subscriber: Cache.Subscriber): Cache.Subscription {
|
|
316
|
-
this.subscribers.push(subscriber)
|
|
317
|
-
return () => {
|
|
318
|
-
const index = this.subscribers.indexOf(subscriber)
|
|
319
|
-
if (index > -1) {
|
|
320
|
-
this.subscribers.splice(index, 1)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private maintainCapacity(bytesToFree: number): void {
|
|
326
|
-
// Order by oldest last-access time first to evict least-recently-used rows.
|
|
327
|
-
const rows = this.db
|
|
328
|
-
.prepare(
|
|
329
|
-
`SELECT key, size FROM ${CACHE_TABLE_NAME}
|
|
330
|
-
WHERE namespace = ?
|
|
331
|
-
ORDER BY last_accessed_at ASC`,
|
|
332
|
-
)
|
|
333
|
-
.all(this.namespace) as Array<{ key: string; size: number }>
|
|
334
|
-
|
|
335
|
-
let freedBytes = 0
|
|
336
|
-
const keysToRemove: string[] = []
|
|
337
|
-
|
|
338
|
-
for (const row of rows) {
|
|
339
|
-
if (freedBytes >= bytesToFree) {
|
|
340
|
-
break
|
|
341
|
-
}
|
|
342
|
-
keysToRemove.push(row.key)
|
|
343
|
-
freedBytes += row.size
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (keysToRemove.length > 0) {
|
|
347
|
-
const placeholders = keysToRemove.map(() => '?').join(',')
|
|
348
|
-
const stmt = this.db.prepare(
|
|
349
|
-
`DELETE FROM ${CACHE_TABLE_NAME}
|
|
350
|
-
WHERE namespace = ? AND key IN (${placeholders})`,
|
|
351
|
-
)
|
|
352
|
-
stmt.run(this.namespace, ...(keysToRemove as [string, ...string[]]))
|
|
353
|
-
this.currentSize -= freedBytes
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private notifySubscribers(
|
|
358
|
-
key: string | undefined,
|
|
359
|
-
data: string | undefined,
|
|
360
|
-
): void {
|
|
361
|
-
for (const subscriber of this.subscribers) {
|
|
362
|
-
try {
|
|
363
|
-
subscriber(key, data)
|
|
364
|
-
} catch (error) {
|
|
365
|
-
logger.error('Cache subscriber error:', error)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export namespace Cache {
|
|
372
|
-
export interface Options {
|
|
373
|
-
namespace?: string
|
|
374
|
-
capacity?: number
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
export type Subscriber = (
|
|
378
|
-
key: string | undefined,
|
|
379
|
-
data: string | undefined,
|
|
380
|
-
) => void
|
|
381
|
-
export type Subscription = () => void
|
|
382
|
-
}
|
|
383
|
-
|
|
384
20
|
interface CacheMetadata {
|
|
385
21
|
timestamp: number
|
|
386
22
|
value: any
|
package/src/apis/clipboard.tsx
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
platform,
|
|
3
|
+
getEnv,
|
|
4
|
+
fileExists,
|
|
5
|
+
resolvePath,
|
|
6
|
+
copyToClipboard as platformCopy,
|
|
7
|
+
readClipboard as platformRead,
|
|
8
|
+
execCommand,
|
|
9
|
+
execWithInput,
|
|
10
|
+
} from '#platform/runtime'
|
|
6
11
|
import { copyToClipboard, pasteContent } from 'termcast/src/action-utils'
|
|
7
12
|
import { logger } from 'termcast/src/logger'
|
|
8
13
|
|
|
9
|
-
const execAsync = promisify(exec)
|
|
10
|
-
const platform = process.platform
|
|
11
|
-
|
|
12
14
|
async function copyFileToClipboard(filePath: string): Promise<void> {
|
|
13
|
-
const absolutePath =
|
|
15
|
+
const absolutePath = resolvePath(filePath)
|
|
14
16
|
|
|
15
|
-
if (
|
|
17
|
+
if (getEnv('VITEST')) {
|
|
16
18
|
logger.log(`📋 [VITEST] Skipping copy file to clipboard: ${filePath}`)
|
|
17
19
|
return
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
if (!
|
|
22
|
+
if (!fileExists(absolutePath)) {
|
|
21
23
|
throw new Error(`File not found: ${absolutePath}`)
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
try {
|
|
25
27
|
if (platform === 'darwin') {
|
|
26
|
-
// macOS: Use osascript to copy file to clipboard
|
|
27
28
|
const script = `osascript -e 'set the clipboard to (POSIX file "${absolutePath}")'`
|
|
28
|
-
await
|
|
29
|
+
await execCommand(script)
|
|
29
30
|
logger.log(`📋 Copied file to clipboard: ${filePath}`)
|
|
30
31
|
} else if (platform === 'linux') {
|
|
31
|
-
// Linux: Copy file path as text and file URI
|
|
32
32
|
const fileUri = `file://${absolutePath}`
|
|
33
|
-
await
|
|
34
|
-
|
|
33
|
+
await execWithInput(
|
|
34
|
+
'xclip -selection clipboard -t text/uri-list',
|
|
35
|
+
fileUri,
|
|
35
36
|
)
|
|
36
37
|
logger.log(`📋 Copied file to clipboard: ${filePath}`)
|
|
37
38
|
} else if (platform === 'win32') {
|
|
38
|
-
// Windows: Use PowerShell to copy file to clipboard
|
|
39
39
|
const script = `powershell -command "Set-Clipboard -Path '${absolutePath}'"`
|
|
40
|
-
await
|
|
40
|
+
await execCommand(script)
|
|
41
41
|
logger.log(`📋 Copied file to clipboard: ${filePath}`)
|
|
42
42
|
} else {
|
|
43
43
|
logger.log(`📋 File copy not supported on ${platform}: ${filePath}`)
|
|
@@ -116,24 +116,20 @@ export const Clipboard: ClipboardType = {
|
|
|
116
116
|
let file: string | undefined
|
|
117
117
|
|
|
118
118
|
if (platform === 'darwin') {
|
|
119
|
-
// Try to get file first
|
|
120
119
|
try {
|
|
121
120
|
const fileCheckScript = `osascript -e 'try' -e 'get the clipboard as «class furl»' -e 'POSIX path of result' -e 'end try'`
|
|
122
|
-
const
|
|
121
|
+
const filePath = await execCommand(fileCheckScript)
|
|
123
122
|
if (filePath && filePath.trim()) {
|
|
124
123
|
file = filePath.trim()
|
|
125
124
|
}
|
|
126
125
|
} catch {
|
|
127
|
-
// No file in clipboard
|
|
126
|
+
// No file in clipboard
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
const { stdout } = await execAsync('pbpaste')
|
|
132
|
-
text = stdout
|
|
129
|
+
text = await platformRead()
|
|
133
130
|
} else if (platform === 'linux') {
|
|
134
|
-
// Check for file URIs
|
|
135
131
|
try {
|
|
136
|
-
const
|
|
132
|
+
const fileUri = await execCommand(
|
|
137
133
|
'xclip -selection clipboard -t text/uri-list -o',
|
|
138
134
|
)
|
|
139
135
|
if (fileUri && fileUri.startsWith('file://')) {
|
|
@@ -143,24 +139,19 @@ export const Clipboard: ClipboardType = {
|
|
|
143
139
|
// No file in clipboard
|
|
144
140
|
}
|
|
145
141
|
|
|
146
|
-
// Get text content
|
|
147
142
|
try {
|
|
148
|
-
|
|
149
|
-
text = stdout
|
|
143
|
+
text = await platformRead()
|
|
150
144
|
} catch {
|
|
151
145
|
// No text in clipboard
|
|
152
146
|
}
|
|
153
147
|
} else if (platform === 'win32') {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'powershell -command "Get-Clipboard"',
|
|
157
|
-
)
|
|
158
|
-
text = stdout
|
|
159
|
-
|
|
160
|
-
// Check if it's a file path
|
|
161
|
-
if (text && fs.existsSync(text.trim())) {
|
|
148
|
+
text = await platformRead()
|
|
149
|
+
if (text && fileExists(text.trim())) {
|
|
162
150
|
file = text.trim()
|
|
163
151
|
}
|
|
152
|
+
} else {
|
|
153
|
+
// browser or other
|
|
154
|
+
text = await platformRead()
|
|
164
155
|
}
|
|
165
156
|
|
|
166
157
|
return { text, file }
|
|
@@ -188,7 +179,7 @@ export namespace Clipboard {
|
|
|
188
179
|
text: string
|
|
189
180
|
}
|
|
190
181
|
| {
|
|
191
|
-
file:
|
|
182
|
+
file: string | { href: string; toString(): string }
|
|
192
183
|
}
|
|
193
184
|
| {
|
|
194
185
|
html: string
|
package/src/apis/environment.tsx
CHANGED
|
@@ -15,10 +15,17 @@
|
|
|
15
15
|
* - System integration (selected Finder items, selected text)
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
import {
|
|
19
|
+
homedir,
|
|
20
|
+
joinPath,
|
|
21
|
+
basename,
|
|
22
|
+
platform,
|
|
23
|
+
getEnv,
|
|
24
|
+
ensureDir,
|
|
25
|
+
fileExists,
|
|
26
|
+
execCommand,
|
|
27
|
+
getSystemAppearance,
|
|
28
|
+
} from '#platform/runtime'
|
|
22
29
|
import { useStore } from '../state'
|
|
23
30
|
|
|
24
31
|
export interface Environment {
|
|
@@ -57,28 +64,16 @@ class EnvironmentImpl implements Environment {
|
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
get appearance(): 'dark' | 'light' {
|
|
60
|
-
|
|
61
|
-
if (process.platform === 'darwin') {
|
|
62
|
-
try {
|
|
63
|
-
const result = execSync(
|
|
64
|
-
'defaults read -g AppleInterfaceStyle 2>/dev/null',
|
|
65
|
-
{ encoding: 'utf8' },
|
|
66
|
-
)
|
|
67
|
-
return result.includes('Dark') ? 'dark' : 'light'
|
|
68
|
-
} catch {
|
|
69
|
-
return 'light'
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return 'light'
|
|
67
|
+
return getSystemAppearance()
|
|
73
68
|
}
|
|
74
69
|
|
|
75
70
|
get assetsPath(): string {
|
|
76
71
|
const state = useStore.getState()
|
|
77
72
|
if (state.extensionPath) {
|
|
78
|
-
return
|
|
73
|
+
return joinPath(state.extensionPath, 'assets')
|
|
79
74
|
}
|
|
80
75
|
// TODO: Fallback for non-dev mode extensions
|
|
81
|
-
return
|
|
76
|
+
return joinPath(homedir(), '.termcast', 'assets')
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
get commandMode(): 'view' | 'no-view' | 'menu-bar' {
|
|
@@ -108,7 +103,7 @@ class EnvironmentImpl implements Environment {
|
|
|
108
103
|
return state.extensionPackageJson.name
|
|
109
104
|
}
|
|
110
105
|
if (state.extensionPath) {
|
|
111
|
-
return
|
|
106
|
+
return basename(state.extensionPath)
|
|
112
107
|
}
|
|
113
108
|
return ''
|
|
114
109
|
}
|
|
@@ -143,17 +138,14 @@ class EnvironmentImpl implements Environment {
|
|
|
143
138
|
|
|
144
139
|
get supportPath(): string {
|
|
145
140
|
// Create a support directory in the user's data directory
|
|
146
|
-
const baseDir =
|
|
147
|
-
|
|
141
|
+
const baseDir = joinPath(
|
|
142
|
+
homedir(),
|
|
148
143
|
'.termcast',
|
|
149
144
|
'support',
|
|
150
145
|
this.extensionName,
|
|
151
146
|
)
|
|
152
147
|
|
|
153
|
-
|
|
154
|
-
if (!fs.existsSync(baseDir)) {
|
|
155
|
-
fs.mkdirSync(baseDir, { recursive: true })
|
|
156
|
-
}
|
|
148
|
+
ensureDir(baseDir)
|
|
157
149
|
|
|
158
150
|
return baseDir
|
|
159
151
|
}
|
|
@@ -180,18 +172,13 @@ export const environment = new EnvironmentImpl()
|
|
|
180
172
|
// Whether the TUI is running inside a standalone desktop app built with `termcast app build`.
|
|
181
173
|
// In app mode, ESC at root level does not exit the process.
|
|
182
174
|
export function isAppMode(): boolean {
|
|
183
|
-
return
|
|
175
|
+
return getEnv('TERMCAST_APP_MODE') === '1'
|
|
184
176
|
}
|
|
185
177
|
|
|
186
178
|
export async function getSelectedFinderItems(): Promise<string[]> {
|
|
187
179
|
// TODO: Improve cross-platform support
|
|
188
|
-
|
|
189
|
-
// Should add support for:
|
|
190
|
-
// 1. Windows Explorer selection (via PowerShell or COM)
|
|
191
|
-
// 2. Linux file managers (Nautilus, Dolphin, etc.)
|
|
192
|
-
if (process.platform === 'darwin') {
|
|
180
|
+
if (platform === 'darwin') {
|
|
193
181
|
try {
|
|
194
|
-
// Use AppleScript to get selected Finder items
|
|
195
182
|
const script = `
|
|
196
183
|
tell application "Finder"
|
|
197
184
|
set theSelection to selection
|
|
@@ -202,15 +189,12 @@ export async function getSelectedFinderItems(): Promise<string[]> {
|
|
|
202
189
|
return thePaths
|
|
203
190
|
end tell
|
|
204
191
|
`
|
|
205
|
-
const result =
|
|
206
|
-
encoding: 'utf8',
|
|
207
|
-
})
|
|
192
|
+
const result = await execCommand(`osascript -e '${script}'`)
|
|
208
193
|
return result.trim().split(', ').filter(Boolean)
|
|
209
194
|
} catch {
|
|
210
195
|
return []
|
|
211
196
|
}
|
|
212
197
|
}
|
|
213
|
-
// TODO: Implement for other platforms
|
|
214
198
|
return []
|
|
215
199
|
}
|
|
216
200
|
|
|
@@ -243,9 +227,9 @@ export async function launchCommand(options: LaunchOptions): Promise<void> {
|
|
|
243
227
|
throw new Error(`Command '${options.name}' not found in extension`)
|
|
244
228
|
}
|
|
245
229
|
|
|
246
|
-
const bundledPath =
|
|
230
|
+
const bundledPath = joinPath(extensionPath, '.termcast-bundle', `${options.name}.js`)
|
|
247
231
|
|
|
248
|
-
if (!
|
|
232
|
+
if (!fileExists(bundledPath)) {
|
|
249
233
|
throw new Error(`Command '${options.name}' has not been built`)
|
|
250
234
|
}
|
|
251
235
|
|
|
@@ -281,16 +265,8 @@ export async function launchCommand(options: LaunchOptions): Promise<void> {
|
|
|
281
265
|
}
|
|
282
266
|
|
|
283
267
|
export async function getSelectedText(): Promise<string> {
|
|
284
|
-
|
|
285
|
-
// Current implementation has issues:
|
|
286
|
-
// 1. Modifies the clipboard (should preserve original content)
|
|
287
|
-
// 2. Uses delay which may not be reliable
|
|
288
|
-
// 3. Only works on macOS
|
|
289
|
-
// Should add support for Windows and Linux
|
|
290
|
-
if (process.platform === 'darwin') {
|
|
268
|
+
if (platform === 'darwin') {
|
|
291
269
|
try {
|
|
292
|
-
// TODO: Save and restore clipboard contents to avoid side effects
|
|
293
|
-
// Use AppleScript to get selected text from frontmost application
|
|
294
270
|
const script = `
|
|
295
271
|
tell application "System Events"
|
|
296
272
|
keystroke "c" using command down
|
|
@@ -298,14 +274,11 @@ export async function getSelectedText(): Promise<string> {
|
|
|
298
274
|
return (the clipboard)
|
|
299
275
|
end tell
|
|
300
276
|
`
|
|
301
|
-
const result =
|
|
302
|
-
encoding: 'utf8',
|
|
303
|
-
})
|
|
277
|
+
const result = await execCommand(`osascript -e '${script}'`)
|
|
304
278
|
return result.trim()
|
|
305
279
|
} catch {
|
|
306
280
|
return ''
|
|
307
281
|
}
|
|
308
282
|
}
|
|
309
|
-
// TODO: Implement for Windows (via PowerShell) and Linux (xclip/xsel)
|
|
310
283
|
return ''
|
|
311
284
|
}
|