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
|
@@ -40,18 +40,18 @@ test('flexGrow table fills remaining space next to fixed-width label', async ()
|
|
|
40
40
|
|
|
41
41
|
With flexGrow=1 + wrapText (fills remaining space)
|
|
42
42
|
|
|
43
|
-
Config: Key
|
|
44
|
-
version
|
|
45
|
-
license
|
|
46
|
-
author
|
|
43
|
+
Config: Key Value
|
|
44
|
+
version 2.1.0
|
|
45
|
+
license MIT
|
|
46
|
+
author termcast
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
Width=auto + wrapText (content-sized, no stretch)
|
|
50
50
|
|
|
51
|
-
Config:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
Config: Key Value
|
|
52
|
+
version 2.1.0
|
|
53
|
+
license MIT
|
|
54
|
+
author termcast
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
With flexGrow=1 no wrapText (column-based)
|
|
@@ -124,7 +124,7 @@ test('pressing escape hides the toast', async () => {
|
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
↵ show toast ↑↓ navigate ^k actions
|
|
127
|
+
↵ show toast ↑↓ navigate ^k actions :vim
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
|
|
@@ -238,7 +238,7 @@ test('form toast: pressing enter triggers primary action (navigation)', async ()
|
|
|
238
238
|
|
|
239
239
|
|
|
240
240
|
|
|
241
|
-
↑↓ navigate ^k actions
|
|
241
|
+
↑↓ navigate ^k actions :vim
|
|
242
242
|
|
|
243
243
|
|
|
244
244
|
|
package/src/extensions/dev.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { TermcastProvider } from 'termcast/src/internal/providers'
|
|
|
15
15
|
import { showToast, Toast } from 'termcast/src/apis/toast'
|
|
16
16
|
import { Icon } from 'termcast'
|
|
17
17
|
import { useTheme, initializeTheme } from 'termcast/src/theme'
|
|
18
|
+
import { initializeVimMode } from 'termcast/src/vim-mode'
|
|
18
19
|
import { logger } from '../logger'
|
|
19
20
|
import { getCommandsWithFiles, CommandWithFile, RaycastPackageJson } from '../package-json'
|
|
20
21
|
import { buildExtensionCommands } from '../build'
|
|
@@ -226,8 +227,9 @@ export async function startDevMode({
|
|
|
226
227
|
devRebuildCount: 1,
|
|
227
228
|
})
|
|
228
229
|
|
|
229
|
-
// Load theme after state reset — extensionPath is now set so it reads from the correct DB
|
|
230
|
+
// Load theme and vim mode after state reset — extensionPath is now set so it reads from the correct DB
|
|
230
231
|
initializeTheme()
|
|
232
|
+
initializeVimMode()
|
|
231
233
|
|
|
232
234
|
function App(): any {
|
|
233
235
|
const devElement = useStore((state) => state.devElement)
|
|
@@ -299,8 +301,9 @@ export async function startCompiledExtension({
|
|
|
299
301
|
devRebuildCount: 1,
|
|
300
302
|
})
|
|
301
303
|
|
|
302
|
-
// Load theme after state reset — extensionPath is now set so it reads from the correct DB
|
|
304
|
+
// Load theme and vim mode after state reset — extensionPath is now set so it reads from the correct DB
|
|
303
305
|
initializeTheme()
|
|
306
|
+
initializeVimMode()
|
|
304
307
|
|
|
305
308
|
function App(): any {
|
|
306
309
|
const devElement = useStore((state) => state.devElement)
|
|
@@ -65,7 +65,7 @@ test('dev command shows extension commands list', async () => {
|
|
|
65
65
|
Show State Shows the current application state in view
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
↵ run command ↑↓ navigate ^k actions
|
|
68
|
+
↵ run command ↑↓ navigate ^k actions :vim
|
|
69
69
|
"
|
|
70
70
|
`)
|
|
71
71
|
}, 30000)
|
|
@@ -101,7 +101,7 @@ test('selecting command with arguments shows arguments form', async () => {
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
↵ run command ↑↓ navigate ^k actions
|
|
104
|
+
↵ run command ↑↓ navigate ^k actions :vim
|
|
105
105
|
"
|
|
106
106
|
`)
|
|
107
107
|
|
|
@@ -217,7 +217,7 @@ test('can fill arguments and run command', async () => {
|
|
|
217
217
|
|
|
218
218
|
|
|
219
219
|
|
|
220
|
-
↵ copy value ↑↓ navigate ^k actions
|
|
220
|
+
↵ copy value ↑↓ navigate ^k actions :vim
|
|
221
221
|
"
|
|
222
222
|
`)
|
|
223
223
|
}, 30000)
|
package/src/globals.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// Set up global references for external packages
|
|
3
3
|
//
|
|
4
4
|
import { logger } from './logger'
|
|
5
|
+
import { setEnv } from '#platform/runtime'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
setEnv('TERMCAST', 'true')
|
|
7
8
|
|
|
8
9
|
import * as opentuiCore from '@opentui/core'
|
|
9
10
|
import * as opentuiReact from '@opentui/react'
|
package/src/index.tsx
CHANGED
|
@@ -86,6 +86,13 @@ export type {
|
|
|
86
86
|
BarGraphSeriesProps,
|
|
87
87
|
} from 'termcast/src/components/bar-graph'
|
|
88
88
|
|
|
89
|
+
// Core UI Components - CandleChart
|
|
90
|
+
export { CandleChart } from 'termcast/src/components/candle-chart'
|
|
91
|
+
export type {
|
|
92
|
+
CandleChartProps,
|
|
93
|
+
CandleData,
|
|
94
|
+
} from 'termcast/src/components/candle-chart'
|
|
95
|
+
|
|
89
96
|
// Core UI Components - CalendarHeatmap
|
|
90
97
|
export { CalendarHeatmap, Heatmap } from 'termcast/src/components/heatmap'
|
|
91
98
|
export type {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { showFailureToast } from 'termcast/src/apis/toast'
|
|
2
2
|
import { logger } from 'termcast/src/logger'
|
|
3
|
+
import { setupErrorHandlers } from '#platform/runtime'
|
|
3
4
|
|
|
4
5
|
let initialized = false
|
|
5
6
|
|
|
@@ -7,26 +8,23 @@ export function initializeErrorHandlers(): void {
|
|
|
7
8
|
if (initialized) return
|
|
8
9
|
initialized = true
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
process.on('uncaughtExceptionMonitor', (err, origin) => {
|
|
30
|
-
logger.error(`Uncaught exception from ${origin}:`, err)
|
|
11
|
+
setupErrorHandlers((err, type) => {
|
|
12
|
+
if (type === 'unhandledRejection') {
|
|
13
|
+
logger.error('Unhandled rejection:', err)
|
|
14
|
+
showFailureToast(err, {
|
|
15
|
+
title: 'Unhandled Promise Rejection',
|
|
16
|
+
}).catch((toastErr) => {
|
|
17
|
+
logger.error('Failed to show toast for unhandled rejection:', toastErr)
|
|
18
|
+
})
|
|
19
|
+
} else if (type === 'uncaughtException') {
|
|
20
|
+
logger.error('Uncaught exception:', err)
|
|
21
|
+
showFailureToast(err, {
|
|
22
|
+
title: 'Uncaught Exception',
|
|
23
|
+
}).catch((toastErr) => {
|
|
24
|
+
logger.error('Failed to show toast for uncaught exception:', toastErr)
|
|
25
|
+
})
|
|
26
|
+
} else {
|
|
27
|
+
logger.error(`Uncaught exception from ${type}:`, err)
|
|
28
|
+
}
|
|
31
29
|
})
|
|
32
30
|
}
|
|
@@ -15,6 +15,7 @@ import { useTheme } from 'termcast/src/theme'
|
|
|
15
15
|
import { useStore } from 'termcast/src/state'
|
|
16
16
|
import { useKeyboard, useRenderer } from '@opentui/react'
|
|
17
17
|
import { initializeErrorHandlers } from 'termcast/src/internal/error-handler'
|
|
18
|
+
import { stdoutWrite } from '#platform/runtime'
|
|
18
19
|
|
|
19
20
|
import { InFocus } from './focus-context'
|
|
20
21
|
import { Clipboard } from '../apis/clipboard'
|
|
@@ -150,6 +151,44 @@ export function TermcastProvider(props: ProvidersProps): any {
|
|
|
150
151
|
const theme = useTheme()
|
|
151
152
|
const renderer = useRenderer()
|
|
152
153
|
|
|
154
|
+
// TODO: Remove this when opentui adds { name: "backspace", super: true, action: "delete-to-line-start" }
|
|
155
|
+
// to defaultTextareaKeybindings in packages/core/src/renderables/Textarea.ts
|
|
156
|
+
// Translate Cmd+Backspace (kitty CSI \x1b[127;9u) to Ctrl+U (\x15) so opentui's
|
|
157
|
+
// textarea keybinding for delete-to-line-start handles it. opentui doesn't have a
|
|
158
|
+
// super+backspace binding, so we remap at the input level before key dispatch.
|
|
159
|
+
React.useLayoutEffect(() => {
|
|
160
|
+
if (!renderer) return
|
|
161
|
+
const handler = (sequence: string) => {
|
|
162
|
+
if (sequence === '\x1b[127;9u') {
|
|
163
|
+
renderer.stdin.emit('data', '\x15')
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
renderer.prependInputHandler(handler)
|
|
169
|
+
return () => {
|
|
170
|
+
renderer.removeInputHandler(handler)
|
|
171
|
+
}
|
|
172
|
+
}, [renderer])
|
|
173
|
+
|
|
174
|
+
// Sync terminal background with the active termcast theme via OSC 11 (standard escape
|
|
175
|
+
// sequence to set terminal background color). This works on WezTerm, iTerm2, kitty, etc.
|
|
176
|
+
// WezTerm's set_config_overrides for colors has a bug (#5451) where it only hot-reloads
|
|
177
|
+
// non-focused windows, so we use OSC 11 instead which updates immediately.
|
|
178
|
+
// Uses renderer's realStdoutWrite to bypass opentui's stdout interception.
|
|
179
|
+
React.useLayoutEffect(() => {
|
|
180
|
+
if (!renderer) return
|
|
181
|
+
// OSC 11 ; color ST — sets terminal default background color
|
|
182
|
+
const sequence = `\x1b]11;${theme.background}\x07`
|
|
183
|
+
const realWrite = (renderer as any).realStdoutWrite as typeof process.stdout.write | undefined
|
|
184
|
+
if (realWrite) {
|
|
185
|
+
// realStdoutWrite needs process.stdout as `this` context
|
|
186
|
+
realWrite.call(process.stdout, sequence)
|
|
187
|
+
} else {
|
|
188
|
+
stdoutWrite(sequence)
|
|
189
|
+
}
|
|
190
|
+
}, [renderer, theme.background])
|
|
191
|
+
|
|
153
192
|
useKeyboard((key) => {
|
|
154
193
|
if (!renderer) return
|
|
155
194
|
if (key.ctrl && key.name === 'd') {
|
package/src/logger.tsx
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
joinPath,
|
|
3
|
+
cwd,
|
|
4
|
+
unlinkIfExists,
|
|
5
|
+
appendToFile,
|
|
6
|
+
inspectValue,
|
|
7
|
+
getEnv,
|
|
8
|
+
exit,
|
|
9
|
+
setupErrorHandlers,
|
|
10
|
+
} from '#platform/runtime'
|
|
4
11
|
import { useEffect } from 'react'
|
|
5
12
|
|
|
6
|
-
const LOG_FILE =
|
|
13
|
+
const LOG_FILE = joinPath(cwd(), 'app.log')
|
|
7
14
|
|
|
8
15
|
// Delete log file on process start
|
|
9
|
-
|
|
10
|
-
fs.unlinkSync(LOG_FILE)
|
|
11
|
-
}
|
|
16
|
+
unlinkIfExists(LOG_FILE)
|
|
12
17
|
|
|
13
18
|
function serialize(msg: any): string {
|
|
14
19
|
if (msg instanceof Error) {
|
|
@@ -17,7 +22,7 @@ function serialize(msg: any): string {
|
|
|
17
22
|
if (typeof msg === 'string') {
|
|
18
23
|
return msg
|
|
19
24
|
}
|
|
20
|
-
return
|
|
25
|
+
return inspectValue(msg, 3)
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export const logger = {
|
|
@@ -25,21 +30,21 @@ export const logger = {
|
|
|
25
30
|
const timestamp = new Date().toISOString()
|
|
26
31
|
const formattedMessages = messages.map(serialize).join(' ')
|
|
27
32
|
const logEntry = `[${timestamp}] ${formattedMessages}\n`
|
|
28
|
-
|
|
33
|
+
appendToFile(LOG_FILE, logEntry)
|
|
29
34
|
console.log(...messages)
|
|
30
35
|
},
|
|
31
36
|
error: (...messages: any[]) => {
|
|
32
37
|
const timestamp = new Date().toISOString()
|
|
33
38
|
const formattedMessages = messages.map(serialize).join(' ')
|
|
34
39
|
const logEntry = `[${timestamp}] ERROR: ${formattedMessages}\n`
|
|
35
|
-
|
|
40
|
+
appendToFile(LOG_FILE, logEntry)
|
|
36
41
|
console.error(...messages)
|
|
37
42
|
},
|
|
38
43
|
warn: (...messages: any[]) => {
|
|
39
44
|
const timestamp = new Date().toISOString()
|
|
40
45
|
const formattedMessages = messages.map(serialize).join(' ')
|
|
41
46
|
const logEntry = `[${timestamp}] WARN: ${formattedMessages}\n`
|
|
42
|
-
|
|
47
|
+
appendToFile(LOG_FILE, logEntry)
|
|
43
48
|
console.warn(...messages)
|
|
44
49
|
},
|
|
45
50
|
trace: (...messages: any[]) => {
|
|
@@ -54,41 +59,33 @@ export const logger = {
|
|
|
54
59
|
}
|
|
55
60
|
const formattedMessages = messages.map(serialize).join(' ')
|
|
56
61
|
const logEntry = `[${timestamp}] TRACE: ${formattedMessages}\n${stack}\n`
|
|
57
|
-
|
|
62
|
+
appendToFile(LOG_FILE, logEntry)
|
|
58
63
|
console.trace(...messages)
|
|
59
64
|
},
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
// Catch unhandled errors and exceptions
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
'Unhandled Rejection
|
|
81
|
-
|
|
82
|
-
'reason:',
|
|
83
|
-
reason.message,
|
|
84
|
-
reason.stack,
|
|
85
|
-
)
|
|
68
|
+
setupErrorHandlers((error, type) => {
|
|
69
|
+
if (type === 'uncaughtException') {
|
|
70
|
+
if (error instanceof Error) {
|
|
71
|
+
logger.error('Uncaught Exception:', error.message, error.stack)
|
|
72
|
+
} else {
|
|
73
|
+
logger.error('Uncaught Exception:', serialize(error))
|
|
74
|
+
}
|
|
75
|
+
// In app mode, don't exit on uncaught exceptions — the error boundary
|
|
76
|
+
// will catch React errors, and crashing the whole app is worse than
|
|
77
|
+
// a broken screen the user can recover from.
|
|
78
|
+
if (getEnv('TERMCAST_APP_MODE') !== '1') {
|
|
79
|
+
exit(1)
|
|
80
|
+
}
|
|
81
|
+
} else if (type === 'unhandledRejection') {
|
|
82
|
+
if (error instanceof Error) {
|
|
83
|
+
logger.error('Unhandled Rejection:', error.message, error.stack)
|
|
84
|
+
} else {
|
|
85
|
+
logger.error('Unhandled Rejection:', serialize(error))
|
|
86
|
+
}
|
|
86
87
|
} else {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
promise,
|
|
90
|
-
'reason:',
|
|
91
|
-
serialize(reason),
|
|
92
|
-
)
|
|
88
|
+
// uncaughtExceptionMonitor
|
|
89
|
+
logger.error(`Uncaught exception from ${type}:`, error)
|
|
93
90
|
}
|
|
94
91
|
})
|
|
@@ -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
|
+
}
|