termcast 1.3.50 → 1.3.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/dist/apis/environment.d.ts +1 -0
  2. package/dist/apis/environment.d.ts.map +1 -1
  3. package/dist/apis/environment.js +5 -0
  4. package/dist/apis/environment.js.map +1 -1
  5. package/dist/app.d.ts +33 -0
  6. package/dist/app.d.ts.map +1 -0
  7. package/dist/app.js +1125 -0
  8. package/dist/app.js.map +1 -0
  9. package/dist/cli.js +80 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/components/detail.d.ts.map +1 -1
  12. package/dist/components/detail.js +20 -17
  13. package/dist/components/detail.js.map +1 -1
  14. package/dist/components/dropdown.d.ts.map +1 -1
  15. package/dist/components/dropdown.js +3 -2
  16. package/dist/components/dropdown.js.map +1 -1
  17. package/dist/components/footer.d.ts +6 -0
  18. package/dist/components/footer.d.ts.map +1 -1
  19. package/dist/components/footer.js +15 -6
  20. package/dist/components/footer.js.map +1 -1
  21. package/dist/components/form/checkbox.d.ts.map +1 -1
  22. package/dist/components/form/checkbox.js +1 -13
  23. package/dist/components/form/checkbox.js.map +1 -1
  24. package/dist/components/form/date-picker.js +2 -2
  25. package/dist/components/form/date-picker.js.map +1 -1
  26. package/dist/components/form/description.js +1 -1
  27. package/dist/components/form/description.js.map +1 -1
  28. package/dist/components/form/dropdown.d.ts.map +1 -1
  29. package/dist/components/form/dropdown.js +19 -3
  30. package/dist/components/form/dropdown.js.map +1 -1
  31. package/dist/components/form/file-picker.d.ts.map +1 -1
  32. package/dist/components/form/file-picker.js +22 -4
  33. package/dist/components/form/file-picker.js.map +1 -1
  34. package/dist/components/form/index.d.ts +3 -1
  35. package/dist/components/form/index.d.ts.map +1 -1
  36. package/dist/components/form/index.js +6 -4
  37. package/dist/components/form/index.js.map +1 -1
  38. package/dist/components/form/password-field.js +3 -3
  39. package/dist/components/form/password-field.js.map +1 -1
  40. package/dist/components/form/text-area.d.ts.map +1 -1
  41. package/dist/components/form/text-area.js +29 -6
  42. package/dist/components/form/text-area.js.map +1 -1
  43. package/dist/components/form/text-field.js +3 -3
  44. package/dist/components/form/text-field.js.map +1 -1
  45. package/dist/components/heatmap.d.ts +80 -0
  46. package/dist/components/heatmap.d.ts.map +1 -0
  47. package/dist/components/heatmap.js +405 -0
  48. package/dist/components/heatmap.js.map +1 -0
  49. package/dist/components/list.d.ts +2 -0
  50. package/dist/components/list.d.ts.map +1 -1
  51. package/dist/components/list.js +80 -52
  52. package/dist/components/list.js.map +1 -1
  53. package/dist/components/markdown.d.ts +7 -0
  54. package/dist/components/markdown.d.ts.map +1 -0
  55. package/dist/components/markdown.js +19 -0
  56. package/dist/components/markdown.js.map +1 -0
  57. package/dist/components/metadata.d.ts.map +1 -1
  58. package/dist/components/metadata.js +4 -1
  59. package/dist/components/metadata.js.map +1 -1
  60. package/dist/components/progress-bar.d.ts +37 -0
  61. package/dist/components/progress-bar.d.ts.map +1 -0
  62. package/dist/components/progress-bar.js +34 -0
  63. package/dist/components/progress-bar.js.map +1 -0
  64. package/dist/components/table.d.ts +3 -2
  65. package/dist/components/table.d.ts.map +1 -1
  66. package/dist/components/table.js +78 -63
  67. package/dist/components/table.js.map +1 -1
  68. package/dist/diagram-parser.d.ts +17 -3
  69. package/dist/diagram-parser.d.ts.map +1 -1
  70. package/dist/diagram-parser.js +17 -3
  71. package/dist/diagram-parser.js.map +1 -1
  72. package/dist/examples/list-slot.d.ts +2 -0
  73. package/dist/examples/list-slot.d.ts.map +1 -0
  74. package/dist/examples/list-slot.js +14 -0
  75. package/dist/examples/list-slot.js.map +1 -0
  76. package/dist/examples/list-with-dropdown.js +2 -4
  77. package/dist/examples/list-with-dropdown.js.map +1 -1
  78. package/dist/examples/simple-heatmap.d.ts +2 -0
  79. package/dist/examples/simple-heatmap.d.ts.map +1 -0
  80. package/dist/examples/simple-heatmap.js +37 -0
  81. package/dist/examples/simple-heatmap.js.map +1 -0
  82. package/dist/examples/simple-progress-bar.d.ts +2 -0
  83. package/dist/examples/simple-progress-bar.d.ts.map +1 -0
  84. package/dist/examples/simple-progress-bar.js +36 -0
  85. package/dist/examples/simple-progress-bar.js.map +1 -0
  86. package/dist/index.d.ts +6 -0
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +6 -0
  89. package/dist/index.js.map +1 -1
  90. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  91. package/dist/internal/date-picker-widget.js +5 -4
  92. package/dist/internal/date-picker-widget.js.map +1 -1
  93. package/dist/internal/navigation.d.ts.map +1 -1
  94. package/dist/internal/navigation.js +7 -2
  95. package/dist/internal/navigation.js.map +1 -1
  96. package/dist/internal/providers.d.ts.map +1 -1
  97. package/dist/internal/providers.js +42 -4
  98. package/dist/internal/providers.js.map +1 -1
  99. package/dist/logger.js +6 -1
  100. package/dist/logger.js.map +1 -1
  101. package/dist/state.d.ts +2 -0
  102. package/dist/state.d.ts.map +1 -1
  103. package/dist/state.js +31 -2
  104. package/dist/state.js.map +1 -1
  105. package/dist/theme.d.ts +1 -0
  106. package/dist/theme.d.ts.map +1 -1
  107. package/dist/theme.js +23 -1
  108. package/dist/theme.js.map +1 -1
  109. package/dist/utils.d.ts.map +1 -1
  110. package/dist/utils.js +6 -1
  111. package/dist/utils.js.map +1 -1
  112. package/package.json +3 -3
  113. package/src/apis/environment.tsx +6 -0
  114. package/src/app.tsx +1487 -0
  115. package/src/assets/default-app-icon.png +0 -0
  116. package/src/cli.tsx +105 -0
  117. package/src/components/detail.tsx +32 -22
  118. package/src/components/dropdown.tsx +3 -2
  119. package/src/components/footer.tsx +37 -7
  120. package/src/components/form/checkbox.tsx +2 -17
  121. package/src/components/form/date-picker.tsx +2 -2
  122. package/src/components/form/description.tsx +1 -1
  123. package/src/components/form/dropdown.tsx +22 -3
  124. package/src/components/form/file-picker.tsx +33 -10
  125. package/src/components/form/index.tsx +10 -6
  126. package/src/components/form/password-field.tsx +3 -3
  127. package/src/components/form/text-area.tsx +31 -6
  128. package/src/components/form/text-field.tsx +3 -3
  129. package/src/components/heatmap.tsx +584 -0
  130. package/src/components/list.tsx +135 -72
  131. package/src/components/markdown.tsx +30 -0
  132. package/src/components/metadata.tsx +9 -2
  133. package/src/components/progress-bar.tsx +112 -0
  134. package/src/components/table.tsx +88 -71
  135. package/src/diagram-parser.tsx +17 -3
  136. package/src/examples/bar-graph-weekly.vitest.tsx +4 -4
  137. package/src/examples/detail-metadata-showcase.vitest.tsx +12 -12
  138. package/src/examples/form-basic.vitest.tsx +117 -16
  139. package/src/examples/graph-bar-chart.vitest.tsx +2 -2
  140. package/src/examples/graph-row.vitest.tsx +10 -10
  141. package/src/examples/internal/descendants-rerender.vitest.tsx +94 -46
  142. package/src/examples/internal/simple-scrollbox.vitest.tsx +38 -14
  143. package/src/examples/list-dropdown-default.vitest.tsx +78 -58
  144. package/src/examples/list-slot.tsx +38 -0
  145. package/src/examples/list-with-detail.vitest.tsx +8 -8
  146. package/src/examples/list-with-dropdown.tsx +2 -2
  147. package/src/examples/list-with-dropdown.vitest.tsx +16 -16
  148. package/src/examples/list-with-sections.vitest.tsx +45 -32
  149. package/src/examples/simple-detail-table.vitest.tsx +2 -2
  150. package/src/examples/simple-file-picker.vitest.tsx +1 -1
  151. package/src/examples/simple-grid.vitest.tsx +27 -53
  152. package/src/examples/simple-heatmap.tsx +63 -0
  153. package/src/examples/simple-heatmap.vitest.tsx +88 -0
  154. package/src/examples/simple-progress-bar.tsx +82 -0
  155. package/src/examples/simple-progress-bar.vitest.tsx +72 -0
  156. package/src/examples/table-edge-cases.vitest.tsx +1 -1
  157. package/src/index.tsx +19 -0
  158. package/src/internal/date-picker-widget.tsx +23 -12
  159. package/src/internal/navigation.tsx +7 -2
  160. package/src/internal/providers.tsx +48 -3
  161. package/src/logger.tsx +6 -1
  162. package/src/state.tsx +38 -2
  163. package/src/theme.tsx +26 -2
  164. package/src/utils.tsx +6 -1
@@ -0,0 +1,72 @@
1
+ // E2E tests for ProgressBar example.
2
+ // Verifies ProgressBar in List.Item.Detail.Metadata updates per selected item.
3
+
4
+ import { test, expect, afterEach, beforeEach } from 'vitest'
5
+ import { launchTerminal, Session } from 'tuistory/src'
6
+
7
+ let session: Session
8
+
9
+ beforeEach(async () => {
10
+ session = await launchTerminal({
11
+ command: 'bun',
12
+ args: ['src/examples/simple-progress-bar.tsx'],
13
+ cols: 80,
14
+ rows: 24,
15
+ })
16
+ })
17
+
18
+ afterEach(() => {
19
+ session?.close()
20
+ })
21
+
22
+ test('progress bars render in detail metadata and update on selection', async () => {
23
+ const initial = await session.text({
24
+ waitFor: (text) => {
25
+ return text.includes('OpenAI account') && text.includes('37% used')
26
+ },
27
+ timeout: 10000,
28
+ })
29
+
30
+ expect(initial).toMatchInlineSnapshot(`
31
+ "
32
+
33
+
34
+ ProgressBar Metadata ─────────────────────────────────────────────────────
35
+
36
+ > Search...
37
+
38
+ ›OpenAI account default workspace │ Current session
39
+ Anthropic account research workspace │ █████████░░░░░░░░░░░░░░░░ 37% used
40
+ Google account sandbox workspace │ Resets 9pm (Asia/Bangkok)
41
+
42
+ │ Current week (all models)
43
+ │ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 7% used
44
+ │ Resets Feb 27, 1pm (Asia/Bangkok)
45
+
46
+ ↑↓ navigate ^k actions │
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+ "
56
+ `)
57
+
58
+ expect(initial).toContain('37% used')
59
+ expect(initial).toContain('7% used')
60
+
61
+ await session.press('down')
62
+
63
+ const second = await session.text({
64
+ waitFor: (text) => {
65
+ return text.includes('›Anthropic account') && text.includes('82% used')
66
+ },
67
+ timeout: 10000,
68
+ })
69
+
70
+ expect(second).toContain('46% used')
71
+ expect(second).toContain('Europe/Rome')
72
+ }, 30000)
@@ -86,6 +86,7 @@ test('inline formatting table renders all rows', async () => {
86
86
  Done.
87
87
 
88
88
 
89
+ esc go back ^k actions powered by termcast.app
89
90
 
90
91
 
91
92
 
@@ -106,7 +107,6 @@ test('inline formatting table renders all rows', async () => {
106
107
 
107
108
 
108
109
 
109
- esc go back ^k actions powered by termcast.app
110
110
 
111
111
  "
112
112
  `)
package/src/index.tsx CHANGED
@@ -45,6 +45,10 @@ export type {
45
45
  ActionPanelSectionProps,
46
46
  } from 'termcast/src/components/actions'
47
47
 
48
+ // Core UI Components - Markdown
49
+ export { Markdown } from 'termcast/src/components/markdown'
50
+ export type { MarkdownProps } from 'termcast/src/components/markdown'
51
+
48
52
  // Core UI Components - Detail
49
53
  export { Detail } from 'termcast/src/components/detail'
50
54
  export type {
@@ -82,6 +86,21 @@ export type {
82
86
  BarGraphSeriesProps,
83
87
  } from 'termcast/src/components/bar-graph'
84
88
 
89
+ // Core UI Components - CalendarHeatmap
90
+ export { CalendarHeatmap, Heatmap } from 'termcast/src/components/heatmap'
91
+ export type {
92
+ CalendarHeatmapProps,
93
+ CalendarHeatmapData,
94
+ CalendarHeatmapCellChar,
95
+ HeatmapProps,
96
+ HeatmapData,
97
+ HeatmapCellChar,
98
+ } from 'termcast/src/components/heatmap'
99
+
100
+ // Core UI Components - ProgressBar
101
+ export { ProgressBar } from 'termcast/src/components/progress-bar'
102
+ export type { ProgressBarProps } from 'termcast/src/components/progress-bar'
103
+
85
104
  // Form Components
86
105
  import {
87
106
  Form as FormComponent,
@@ -377,7 +377,6 @@ export function DatePickerWidget({
377
377
  enableColors && focus === 'year' ? Theme.primary : undefined,
378
378
  marginBottom: 0,
379
379
  }}
380
- onMouseDown={() => setFocus('year')}
381
380
  >
382
381
  <box
383
382
  style={{
@@ -387,11 +386,17 @@ export function DatePickerWidget({
387
386
  width: headerWidth,
388
387
  }}
389
388
  >
390
- <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>←</text>
391
- <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>
392
- {String(y)}
393
- </text>
394
- <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>→</text>
389
+ <box onMouseDown={() => { changeYear(-1); setFocus('year') }}>
390
+ <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>←</text>
391
+ </box>
392
+ <box onMouseDown={() => { setFocus('year') }}>
393
+ <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>
394
+ {String(y)}
395
+ </text>
396
+ </box>
397
+ <box onMouseDown={() => { changeYear(+1); setFocus('year') }}>
398
+ <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>→</text>
399
+ </box>
395
400
  </box>
396
401
  </box>
397
402
 
@@ -404,7 +409,6 @@ export function DatePickerWidget({
404
409
  enableColors && focus === 'month' ? Theme.primary : undefined,
405
410
  marginBottom: 1,
406
411
  }}
407
- onMouseDown={() => setFocus('month')}
408
412
  >
409
413
  <box
410
414
  style={{
@@ -414,11 +418,17 @@ export function DatePickerWidget({
414
418
  width: headerWidth,
415
419
  }}
416
420
  >
417
- <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>←</text>
418
- <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>
419
- {MONTHS[m]}
420
- </text>
421
- <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>→</text>
421
+ <box onMouseDown={() => { changeMonth(-1); setFocus('month') }}>
422
+ <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>←</text>
423
+ </box>
424
+ <box onMouseDown={() => { setFocus('month') }}>
425
+ <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>
426
+ {MONTHS[m]}
427
+ </text>
428
+ </box>
429
+ <box onMouseDown={() => { changeMonth(+1); setFocus('month') }}>
430
+ <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>→</text>
431
+ </box>
422
432
  </box>
423
433
  </box>
424
434
 
@@ -485,6 +495,7 @@ export function DatePickerWidget({
485
495
  setSelected(d)
486
496
  setFocus('grid')
487
497
  ensureVisibleFor(d)
498
+ onChange?.(d)
488
499
  }}
489
500
  >
490
501
  <text
@@ -13,6 +13,7 @@ import { CommonProps } from 'termcast/src/utils'
13
13
  import { useStore, type NavigationStackItem } from 'termcast/src/state'
14
14
  import { useIsInFocus } from 'termcast/src/internal/focus-context'
15
15
  import { logger } from '../logger'
16
+ import { isAppMode } from '../apis/environment'
16
17
 
17
18
  interface Navigation {
18
19
  push: (element: ReactNode, onPop?: () => void) => void
@@ -172,8 +173,12 @@ export function NavigationProvider(props: NavigationProviderProps): any {
172
173
  activeSearchInputRef.setText('')
173
174
  return
174
175
  }
175
- // At root with no dialogs and no search text - exit the CLI
176
- renderer.destroy()
176
+ // At root with no dialogs and no search text - exit the CLI.
177
+ // In app mode (standalone desktop app), ESC at root is a no-op
178
+ // since destroying the renderer would kill the entire application.
179
+ if (!isAppMode()) {
180
+ renderer.destroy()
181
+ }
177
182
  }
178
183
  }
179
184
  })
@@ -86,6 +86,7 @@ class ErrorBoundaryClass extends Component<
86
86
  constructor(props: { children: ReactNode }) {
87
87
  super(props)
88
88
  this.state = { hasError: false, error: null }
89
+ this.reset = this.reset.bind(this)
89
90
  }
90
91
 
91
92
  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
@@ -100,22 +101,45 @@ class ErrorBoundaryClass extends Component<
100
101
  })
101
102
  }
102
103
 
104
+ reset(): void {
105
+ // Clear navigation and dialog stacks so the app returns to the root view
106
+ // instead of re-rendering the same crashed component
107
+ useStore.setState({
108
+ navigationStack: [],
109
+ dialogStack: [],
110
+ toast: null,
111
+ toastWithPrimaryAction: false,
112
+ showActionsDialog: false,
113
+ })
114
+ this.setState({ hasError: false, error: null })
115
+ }
116
+
103
117
  render(): any {
104
118
  if (this.state.hasError) {
105
- return <ErrorDisplay error={this.state.error} />
119
+ return <ErrorDisplay error={this.state.error} onRetry={this.reset} />
106
120
  }
107
121
 
108
122
  return this.props.children
109
123
  }
110
124
  }
111
125
 
112
- function ErrorDisplay({ error }: { error: Error | null }): any {
126
+ function ErrorDisplay({ error, onRetry }: { error: Error | null; onRetry: () => void }): any {
113
127
  const theme = useTheme()
128
+
129
+ useKeyboard((evt) => {
130
+ if (evt.name === 'return') {
131
+ onRetry()
132
+ }
133
+ })
134
+
114
135
  return (
115
- <box padding={2}>
136
+ <box padding={2} flexDirection="column" gap={1}>
116
137
  <text fg={theme.error} wrapMode='none'>
117
138
  {error?.stack}
118
139
  </text>
140
+ <text fg={theme.textMuted}>
141
+ Press Enter to retry
142
+ </text>
119
143
  </box>
120
144
  )
121
145
  }
@@ -125,6 +149,7 @@ const ErrorBoundary = ErrorBoundaryClass as any
125
149
  export function TermcastProvider(props: ProvidersProps): any {
126
150
  const theme = useTheme()
127
151
  const renderer = useRenderer()
152
+
128
153
  useKeyboard((key) => {
129
154
  if (!renderer) return
130
155
  if (key.ctrl && key.name === 'd') {
@@ -136,6 +161,26 @@ export function TermcastProvider(props: ProvidersProps): any {
136
161
  }
137
162
  })
138
163
 
164
+ // Cmd+C (super+c): if there's an active selection, copy it to clipboard and clear.
165
+ // Otherwise let the key propagate to the TUI for other handlers.
166
+ // In standalone apps, WezTerm forwards Cmd+C via SendKey so it arrives as super modifier.
167
+ useKeyboard((key) => {
168
+ if (!renderer) return
169
+ if (key.super && key.name === 'c') {
170
+ if (renderer.hasSelection) {
171
+ const selection = renderer.getSelection()
172
+ if (selection) {
173
+ const text = selection.getSelectedText()
174
+ if (text) {
175
+ Clipboard.copy(text)
176
+ renderer.clearSelection()
177
+ key.stopPropagation()
178
+ }
179
+ }
180
+ }
181
+ }
182
+ })
183
+
139
184
  return (
140
185
  <ErrorBoundary>
141
186
  <Suspense fallback={<LoadingFallback />}>
package/src/logger.tsx CHANGED
@@ -66,7 +66,12 @@ process.on('uncaughtException', (error: Error) => {
66
66
  } else {
67
67
  logger.error('Uncaught Exception:', serialize(error))
68
68
  }
69
- process.exit(1)
69
+ // In app mode, don't exit on uncaught exceptions — the error boundary
70
+ // will catch React errors, and crashing the whole app is worse than
71
+ // a broken screen the user can recover from.
72
+ if (process.env.TERMCAST_APP_MODE !== '1') {
73
+ process.exit(1)
74
+ }
70
75
  })
71
76
 
72
77
  process.on('unhandledRejection', async (reason: any, promise: Promise<any>) => {
package/src/state.tsx CHANGED
@@ -1,8 +1,11 @@
1
+ import fs from 'node:fs'
1
2
  import { create } from 'zustand'
2
3
  import { type ReactNode } from 'react'
3
4
  import type { TextareaRenderable } from '@opentui/core'
4
5
  import type { RaycastPackageJson } from './package-json'
5
6
  import type { KeyboardKeyEquivalent, KeyboardKeyModifier } from 'termcast/src/keyboard'
7
+ import { getResolvedTheme } from './themes'
8
+ import { logger } from './logger'
6
9
 
7
10
  // Registered action shortcuts for global keyboard handling
8
11
  // Stored by ActionPanel, consumed by List/Detail/Form keyboard handlers
@@ -73,6 +76,10 @@ interface AppState {
73
76
  shouldAutoExecuteFirstAction: boolean
74
77
  // First action title for footer display (set by offscreen ActionPanel)
75
78
  firstActionTitle: string
79
+ // Selected List.Dropdown item title shown in List footer (^p label)
80
+ dropdownFooterLabel: string
81
+ // List.Dropdown tooltip shown in footer as ^p label (preferred over dropdownFooterLabel)
82
+ dropdownTooltip: string
76
83
  // Flag to show actions dialog via portal
77
84
  showActionsDialog: boolean
78
85
  // Portal target node for rendering ActionPanel dialog in the overlay area.
@@ -107,12 +114,41 @@ export const useStore = create<AppState>(() => ({
107
114
  // Actions state
108
115
  shouldAutoExecuteFirstAction: false,
109
116
  firstActionTitle: '',
117
+ dropdownFooterLabel: '',
118
+ dropdownTooltip: '',
110
119
  showActionsDialog: false,
111
120
  actionsPortalTarget: null,
112
- // Theme state
113
- currentThemeName: 'nerv',
121
+ // Theme state — TERMCAST_DEFAULT_THEME env var is set by the app launcher
122
+ currentThemeName: process.env.TERMCAST_DEFAULT_THEME || 'nerv',
114
123
  // Active search input ref
115
124
  activeSearchInputRef: null,
116
125
  // Registered action shortcuts
117
126
  registeredActionShortcuts: [],
118
127
  }))
128
+
129
+ // Sync WezTerm's window background with the active termcast theme.
130
+ // When the theme changes, rewrite the background color in wezterm.lua.
131
+ // WezTerm auto-reloads the config on file change, updating the window edges/padding.
132
+ // The config path is passed from the launcher via TERMCAST_WEZTERM_CONFIG env var.
133
+ const weztermConfigPath = process.env.TERMCAST_WEZTERM_CONFIG
134
+ if (weztermConfigPath) {
135
+ useStore.subscribe((state, prevState) => {
136
+ if (state.currentThemeName === prevState.currentThemeName) {
137
+ return
138
+ }
139
+ try {
140
+ const theme = getResolvedTheme(state.currentThemeName)
141
+ const content = fs.readFileSync(weztermConfigPath, 'utf-8')
142
+ // Replace the background hex in: config.colors = { background = '#xxxxxx' }
143
+ const updated = content.replace(
144
+ /background\s*=\s*'#[0-9a-fA-F]{6}'/,
145
+ `background = '${theme.background}'`,
146
+ )
147
+ if (updated !== content) {
148
+ fs.writeFileSync(weztermConfigPath, updated)
149
+ }
150
+ } catch (e) {
151
+ logger.log('Failed to update wezterm config background:', e)
152
+ }
153
+ })
154
+ }
package/src/theme.tsx CHANGED
@@ -28,7 +28,8 @@ export function loadPersistedTheme(): string {
28
28
  } catch {
29
29
  // Ignore errors on load
30
30
  }
31
- return defaultThemeName
31
+ // TERMCAST_DEFAULT_THEME is set by the app launcher to override the default theme
32
+ return process.env.TERMCAST_DEFAULT_THEME || defaultThemeName
32
33
  }
33
34
 
34
35
  export function persistTheme(name: string): void {
@@ -58,6 +59,30 @@ export function getMarkdownSyntaxStyle(): SyntaxStyle {
58
59
  return SyntaxStyle.fromStyles(getSyntaxTheme(themeName))
59
60
  }
60
61
 
62
+ // Resolve a visible but subtle hover background for interactive rows.
63
+ // Some themes map background and backgroundPanel to the same value.
64
+ export function getInteractiveHoverBackground(theme: ResolvedTheme): string {
65
+ const normalize = (color: string): string => {
66
+ return color.toLowerCase()
67
+ }
68
+ const background = normalize(theme.background)
69
+
70
+ if (normalize(theme.backgroundElement) !== background) {
71
+ return theme.backgroundElement
72
+ }
73
+ if (normalize(theme.backgroundPanel) !== background) {
74
+ return theme.backgroundPanel
75
+ }
76
+ if (normalize(theme.borderSubtle) !== background) {
77
+ return theme.borderSubtle
78
+ }
79
+ if (normalize(theme.border) !== background) {
80
+ return theme.border
81
+ }
82
+
83
+ return theme.primary
84
+ }
85
+
61
86
  // Shared color palette for all chart components (Graph, BarChart, BarGraph).
62
87
  // Order: accent, info, success, warning, error, secondary, primary (cycles with %).
63
88
  export function getThemePalette(theme: ResolvedTheme): string[] {
@@ -80,4 +105,3 @@ export const markdownSyntaxStyle = new Proxy({} as SyntaxStyle, {
80
105
  },
81
106
  })
82
107
 
83
-
package/src/utils.tsx CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  import { logger } from './logger'
15
15
  import { useStore } from './state'
16
16
  import { initializeTheme } from './theme'
17
+ import { isAppMode } from './apis/environment'
17
18
 
18
19
  export interface RenderWithProvidersOptions {
19
20
  extensionName?: string
@@ -59,7 +60,11 @@ export async function renderWithProviders(
59
60
 
60
61
  const renderer = await createCliRenderer({
61
62
  onDestroy: () => {
62
- process.exit(0)
63
+ // In app mode, destroying the renderer should not kill the process.
64
+ // The app launcher manages the process lifecycle.
65
+ if (!isAppMode()) {
66
+ process.exit(0)
67
+ }
63
68
  },
64
69
  })
65
70