termcast 1.3.30 → 1.3.32

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 (294) hide show
  1. package/dist/apis/cache.d.ts.map +1 -1
  2. package/dist/apis/cache.js +4 -39
  3. package/dist/apis/cache.js.map +1 -1
  4. package/dist/apis/hud.d.ts.map +1 -1
  5. package/dist/apis/hud.js +13 -31
  6. package/dist/apis/hud.js.map +1 -1
  7. package/dist/apis/localstorage.d.ts.map +1 -1
  8. package/dist/apis/localstorage.js +3 -27
  9. package/dist/apis/localstorage.js.map +1 -1
  10. package/dist/apis/toast.d.ts +16 -43
  11. package/dist/apis/toast.d.ts.map +1 -1
  12. package/dist/apis/toast.js +78 -177
  13. package/dist/apis/toast.js.map +1 -1
  14. package/dist/build.d.ts +3 -1
  15. package/dist/build.d.ts.map +1 -1
  16. package/dist/build.js +52 -2
  17. package/dist/build.js.map +1 -1
  18. package/dist/cli.d.ts +1 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +206 -25
  21. package/dist/cli.js.map +1 -1
  22. package/dist/colors.d.ts.map +1 -1
  23. package/dist/colors.js +1 -0
  24. package/dist/colors.js.map +1 -1
  25. package/dist/compile.d.ts +0 -1
  26. package/dist/compile.d.ts.map +1 -1
  27. package/dist/compile.js +18 -23
  28. package/dist/compile.js.map +1 -1
  29. package/dist/components/actions.d.ts.map +1 -1
  30. package/dist/components/actions.js +30 -15
  31. package/dist/components/actions.js.map +1 -1
  32. package/dist/components/animation-tick.d.ts +12 -0
  33. package/dist/components/animation-tick.d.ts.map +1 -0
  34. package/dist/components/animation-tick.js +63 -0
  35. package/dist/components/animation-tick.js.map +1 -0
  36. package/dist/components/detail.d.ts.map +1 -1
  37. package/dist/components/detail.js +10 -13
  38. package/dist/components/detail.js.map +1 -1
  39. package/dist/components/dropdown.d.ts +1 -0
  40. package/dist/components/dropdown.d.ts.map +1 -1
  41. package/dist/components/dropdown.js +27 -26
  42. package/dist/components/dropdown.js.map +1 -1
  43. package/dist/components/extension-preferences.d.ts.map +1 -1
  44. package/dist/components/extension-preferences.js +15 -10
  45. package/dist/components/extension-preferences.js.map +1 -1
  46. package/dist/components/footer.d.ts +13 -0
  47. package/dist/components/footer.d.ts.map +1 -0
  48. package/dist/components/footer.js +106 -0
  49. package/dist/components/footer.js.map +1 -0
  50. package/dist/components/form/file-autocomplete.d.ts +19 -4
  51. package/dist/components/form/file-autocomplete.d.ts.map +1 -1
  52. package/dist/components/form/file-autocomplete.js +56 -55
  53. package/dist/components/form/file-autocomplete.js.map +1 -1
  54. package/dist/components/form/file-picker.d.ts.map +1 -1
  55. package/dist/components/form/file-picker.js +26 -15
  56. package/dist/components/form/file-picker.js.map +1 -1
  57. package/dist/components/form/index.d.ts.map +1 -1
  58. package/dist/components/form/index.js +17 -15
  59. package/dist/components/form/index.js.map +1 -1
  60. package/dist/components/form/with-left-border.d.ts.map +1 -1
  61. package/dist/components/form/with-left-border.js +4 -12
  62. package/dist/components/form/with-left-border.js.map +1 -1
  63. package/dist/components/list.d.ts.map +1 -1
  64. package/dist/components/list.js +126 -86
  65. package/dist/components/list.js.map +1 -1
  66. package/dist/components/loading-bar.d.ts.map +1 -1
  67. package/dist/components/loading-bar.js +5 -22
  68. package/dist/components/loading-bar.js.map +1 -1
  69. package/dist/components/loading-text.d.ts.map +1 -1
  70. package/dist/components/loading-text.js +3 -22
  71. package/dist/components/loading-text.js.map +1 -1
  72. package/dist/components/theme-picker.d.ts +2 -0
  73. package/dist/components/theme-picker.d.ts.map +1 -0
  74. package/dist/components/theme-picker.js +37 -0
  75. package/dist/components/theme-picker.js.map +1 -0
  76. package/dist/descendants.d.ts +6 -0
  77. package/dist/descendants.d.ts.map +1 -1
  78. package/dist/descendants.js +74 -8
  79. package/dist/descendants.js.map +1 -1
  80. package/dist/examples/internal/descendants-rerender.d.ts +14 -0
  81. package/dist/examples/internal/descendants-rerender.d.ts.map +1 -0
  82. package/dist/examples/internal/descendants-rerender.js +145 -0
  83. package/dist/examples/internal/descendants-rerender.js.map +1 -0
  84. package/dist/examples/internal/simple-dialog.js +4 -1
  85. package/dist/examples/internal/simple-dialog.js.map +1 -1
  86. package/dist/examples/internal/simple-scrollbox.js +1 -1
  87. package/dist/examples/internal/simple-scrollbox.js.map +1 -1
  88. package/dist/examples/list-with-dropdown.js +1 -1
  89. package/dist/examples/list-with-dropdown.js.map +1 -1
  90. package/dist/examples/miscellaneous.js +1 -1
  91. package/dist/examples/miscellaneous.js.map +1 -1
  92. package/dist/examples/toast-action.d.ts +2 -0
  93. package/dist/examples/toast-action.d.ts.map +1 -0
  94. package/dist/examples/toast-action.js +76 -0
  95. package/dist/examples/toast-action.js.map +1 -0
  96. package/dist/examples/toast-variations.js +38 -36
  97. package/dist/examples/toast-variations.js.map +1 -1
  98. package/dist/extensions/dev.d.ts +1 -1
  99. package/dist/extensions/dev.d.ts.map +1 -1
  100. package/dist/extensions/dev.js +62 -30
  101. package/dist/extensions/dev.js.map +1 -1
  102. package/dist/extensions/home.d.ts.map +1 -1
  103. package/dist/extensions/home.js +4 -3
  104. package/dist/extensions/home.js.map +1 -1
  105. package/dist/extensions/react-refresh-init.d.ts +5 -0
  106. package/dist/extensions/react-refresh-init.d.ts.map +1 -0
  107. package/dist/extensions/react-refresh-init.js +52 -0
  108. package/dist/extensions/react-refresh-init.js.map +1 -0
  109. package/dist/internal/date-picker-widget.js +1 -1
  110. package/dist/internal/date-picker-widget.js.map +1 -1
  111. package/dist/internal/dialog.d.ts +8 -3
  112. package/dist/internal/dialog.d.ts.map +1 -1
  113. package/dist/internal/dialog.js +37 -53
  114. package/dist/internal/dialog.js.map +1 -1
  115. package/dist/internal/navigation.d.ts +1 -0
  116. package/dist/internal/navigation.d.ts.map +1 -1
  117. package/dist/internal/navigation.js +25 -1
  118. package/dist/internal/navigation.js.map +1 -1
  119. package/dist/internal/providers.d.ts.map +1 -1
  120. package/dist/internal/providers.js +9 -197
  121. package/dist/internal/providers.js.map +1 -1
  122. package/dist/internal/scrollbox.d.ts.map +1 -1
  123. package/dist/internal/scrollbox.js +1 -0
  124. package/dist/internal/scrollbox.js.map +1 -1
  125. package/dist/release.d.ts +1 -0
  126. package/dist/release.d.ts.map +1 -1
  127. package/dist/release.js +16 -9
  128. package/dist/release.js.map +1 -1
  129. package/dist/state.d.ts +27 -1
  130. package/dist/state.d.ts.map +1 -1
  131. package/dist/state.js +6 -0
  132. package/dist/state.js.map +1 -1
  133. package/dist/theme.d.ts +6 -19
  134. package/dist/theme.d.ts.map +1 -1
  135. package/dist/theme.js +76 -45
  136. package/dist/theme.js.map +1 -1
  137. package/dist/themes/aura.json +69 -0
  138. package/dist/themes/ayu.json +80 -0
  139. package/dist/themes/catppuccin-frappe.json +233 -0
  140. package/dist/themes/catppuccin-macchiato.json +233 -0
  141. package/dist/themes/catppuccin.json +112 -0
  142. package/dist/themes/cobalt2.json +228 -0
  143. package/dist/themes/cursor.json +249 -0
  144. package/dist/themes/dracula.json +219 -0
  145. package/dist/themes/everforest.json +241 -0
  146. package/dist/themes/flexoki.json +237 -0
  147. package/dist/themes/github-light.json +56 -0
  148. package/dist/themes/github.json +241 -0
  149. package/dist/themes/gruvbox.json +95 -0
  150. package/dist/themes/kanagawa.json +77 -0
  151. package/dist/themes/lucent-orng.json +227 -0
  152. package/dist/themes/material.json +235 -0
  153. package/dist/themes/matrix.json +77 -0
  154. package/dist/themes/mercury.json +245 -0
  155. package/dist/themes/monokai.json +221 -0
  156. package/dist/themes/nightowl.json +221 -0
  157. package/dist/themes/nord.json +223 -0
  158. package/dist/themes/one-dark.json +84 -0
  159. package/dist/themes/opencode-light.json +62 -0
  160. package/dist/themes/opencode.json +245 -0
  161. package/dist/themes/orng.json +245 -0
  162. package/dist/themes/palenight.json +222 -0
  163. package/dist/themes/rosepine.json +234 -0
  164. package/dist/themes/solarized.json +223 -0
  165. package/dist/themes/synthwave84.json +226 -0
  166. package/dist/themes/termcast.json +226 -0
  167. package/dist/themes/tokyonight.json +243 -0
  168. package/dist/themes/vercel.json +255 -0
  169. package/dist/themes/vesper.json +218 -0
  170. package/dist/themes/zenburn.json +223 -0
  171. package/dist/themes.d.ts +57 -0
  172. package/dist/themes.d.ts.map +1 -0
  173. package/dist/themes.js +181 -0
  174. package/dist/themes.js.map +1 -0
  175. package/dist/utils/run-command.d.ts +2 -1
  176. package/dist/utils/run-command.d.ts.map +1 -1
  177. package/dist/utils/run-command.js +20 -10
  178. package/dist/utils/run-command.js.map +1 -1
  179. package/dist/utils.d.ts +2 -1
  180. package/dist/utils.d.ts.map +1 -1
  181. package/dist/utils.js +90 -17
  182. package/dist/utils.js.map +1 -1
  183. package/dist/watcher.d.ts +3 -0
  184. package/dist/watcher.d.ts.map +1 -0
  185. package/dist/watcher.js +16 -0
  186. package/dist/watcher.js.map +1 -0
  187. package/package.json +16 -10
  188. package/src/apis/cache.tsx +5 -44
  189. package/src/apis/hud.tsx +17 -62
  190. package/src/apis/localstorage.tsx +3 -32
  191. package/src/apis/toast.tsx +91 -275
  192. package/src/build.test.tsx +10 -0
  193. package/src/build.tsx +61 -1
  194. package/src/cli.tsx +365 -103
  195. package/src/colors.tsx +1 -0
  196. package/src/compile.tsx +21 -29
  197. package/src/compile.vitest.tsx +300 -0
  198. package/src/components/actions.tsx +64 -45
  199. package/src/components/animation-tick.tsx +85 -0
  200. package/src/components/detail.tsx +31 -35
  201. package/src/components/dropdown.tsx +32 -21
  202. package/src/components/extension-preferences.tsx +14 -10
  203. package/src/components/footer.tsx +241 -0
  204. package/src/components/form/file-autocomplete.tsx +80 -60
  205. package/src/components/form/file-picker.tsx +37 -25
  206. package/src/components/form/index.tsx +45 -41
  207. package/src/components/form/with-left-border.tsx +4 -14
  208. package/src/components/list.tsx +181 -121
  209. package/src/components/loading-bar.tsx +5 -25
  210. package/src/components/loading-text.tsx +4 -23
  211. package/src/components/theme-picker.tsx +57 -0
  212. package/src/descendants.tsx +98 -9
  213. package/src/examples/actions-dialog-layout.vitest.tsx +112 -0
  214. package/src/examples/file-autocomplete.vitest.tsx +131 -122
  215. package/src/examples/form-basic.vitest.tsx +463 -644
  216. package/src/examples/form-dropdown.vitest.tsx +553 -571
  217. package/src/examples/form-scroll.vitest.tsx +112 -102
  218. package/src/examples/form-tagpicker.vitest.tsx +364 -338
  219. package/src/examples/internal/descendants-rerender.tsx +273 -0
  220. package/src/examples/internal/descendants-rerender.vitest.tsx +194 -0
  221. package/src/examples/internal/simple-dialog.tsx +4 -4
  222. package/src/examples/internal/simple-scrollbox.tsx +2 -2
  223. package/src/examples/internal/simple-scrollbox.vitest.tsx +43 -31
  224. package/src/examples/list-detail-metadata.vitest.tsx +34 -30
  225. package/src/examples/list-dropdown-default.vitest.tsx +84 -72
  226. package/src/examples/list-empty-view.vitest.tsx +93 -0
  227. package/src/examples/list-fetch-data.vitest.tsx +36 -30
  228. package/src/examples/list-scrollbox.vitest.tsx +59 -39
  229. package/src/examples/list-with-detail.vitest.tsx +339 -314
  230. package/src/examples/list-with-dropdown.tsx +1 -0
  231. package/src/examples/list-with-dropdown.vitest.tsx +176 -150
  232. package/src/examples/list-with-sections.vitest.tsx +289 -270
  233. package/src/examples/list-with-toast.vitest.tsx +44 -44
  234. package/src/examples/miscellaneous.tsx +10 -0
  235. package/src/examples/simple-file-picker.vitest.tsx +90 -86
  236. package/src/examples/simple-grid.vitest.tsx +275 -249
  237. package/src/examples/simple-navigation.vitest.tsx +192 -168
  238. package/src/examples/store.vitest.tsx +6 -4
  239. package/src/examples/swift-extension.vitest.tsx +31 -19
  240. package/src/examples/synonyms.vitest.tsx +93 -83
  241. package/src/examples/toast-action.tsx +160 -0
  242. package/src/examples/toast-action.vitest.tsx +404 -0
  243. package/src/examples/toast-variations.tsx +58 -57
  244. package/src/examples/toast-variations.vitest.tsx +186 -166
  245. package/src/extensions/dev.tsx +74 -33
  246. package/src/extensions/dev.vitest.tsx +162 -69
  247. package/src/extensions/home.tsx +5 -6
  248. package/src/extensions/react-refresh-init.tsx +59 -0
  249. package/src/internal/date-picker-widget.tsx +1 -1
  250. package/src/internal/dialog.tsx +59 -83
  251. package/src/internal/navigation.tsx +37 -4
  252. package/src/internal/providers.tsx +27 -315
  253. package/src/internal/scrollbox.tsx +1 -0
  254. package/src/release.tsx +16 -10
  255. package/src/state.tsx +36 -3
  256. package/src/theme.tsx +82 -51
  257. package/src/themes/aura.json +69 -0
  258. package/src/themes/ayu.json +80 -0
  259. package/src/themes/catppuccin-frappe.json +233 -0
  260. package/src/themes/catppuccin-macchiato.json +233 -0
  261. package/src/themes/catppuccin.json +112 -0
  262. package/src/themes/cobalt2.json +228 -0
  263. package/src/themes/cursor.json +249 -0
  264. package/src/themes/dracula.json +219 -0
  265. package/src/themes/everforest.json +241 -0
  266. package/src/themes/flexoki.json +237 -0
  267. package/src/themes/github-light.json +56 -0
  268. package/src/themes/github.json +241 -0
  269. package/src/themes/gruvbox.json +95 -0
  270. package/src/themes/kanagawa.json +77 -0
  271. package/src/themes/lucent-orng.json +227 -0
  272. package/src/themes/material.json +235 -0
  273. package/src/themes/matrix.json +77 -0
  274. package/src/themes/mercury.json +252 -0
  275. package/src/themes/monokai.json +221 -0
  276. package/src/themes/nightowl.json +221 -0
  277. package/src/themes/nord.json +223 -0
  278. package/src/themes/one-dark.json +84 -0
  279. package/src/themes/opencode-light.json +62 -0
  280. package/src/themes/opencode.json +245 -0
  281. package/src/themes/orng.json +245 -0
  282. package/src/themes/palenight.json +222 -0
  283. package/src/themes/rosepine.json +234 -0
  284. package/src/themes/solarized.json +223 -0
  285. package/src/themes/synthwave84.json +226 -0
  286. package/src/themes/termcast.json +227 -0
  287. package/src/themes/tokyonight.json +243 -0
  288. package/src/themes/vercel.json +255 -0
  289. package/src/themes/vesper.json +218 -0
  290. package/src/themes/zenburn.json +223 -0
  291. package/src/themes.ts +291 -0
  292. package/src/utils/run-command.tsx +23 -12
  293. package/src/utils.tsx +115 -18
  294. package/src/watcher.tsx +19 -0
@@ -1,9 +1,4 @@
1
- import React, { useEffect, useState } from 'react'
2
- import { Theme } from 'termcast/src/theme'
3
- import { TextAttributes } from '@opentui/core'
4
- import { useStore } from 'termcast/src/state'
5
- import { useKeyboard, useTerminalDimensions } from '@opentui/react'
6
- import { useIsInFocus } from 'termcast/src/internal/focus-context'
1
+ import { useStore, ToastData, ToastStyle } from 'termcast/src/state'
7
2
 
8
3
  export namespace Toast {
9
4
  export interface Options {
@@ -20,302 +15,149 @@ export namespace Toast {
20
15
  onAction: (toast: Toast) => void
21
16
  }
22
17
 
23
- export type Style = 'SUCCESS' | 'FAILURE' | 'ANIMATED'
18
+ export type Style = ToastStyle
24
19
  }
25
20
 
26
21
  export class Toast {
27
- private options: Toast.Options
28
- private id: string
29
- private callbacks: {
30
- onUpdate?: (toast: Toast) => void
31
- onHide?: () => void
32
- } = {}
22
+ private _id: string
23
+ private _title: string
24
+ private _message?: string
25
+ private _style: ToastStyle
26
+ private _primaryAction?: Toast.ActionOptions
27
+ private _secondaryAction?: Toast.ActionOptions
33
28
 
34
29
  static Style = {
35
- Success: 'SUCCESS' as Toast.Style,
36
- Failure: 'FAILURE' as Toast.Style,
37
- Animated: 'ANIMATED' as Toast.Style,
30
+ Success: 'SUCCESS' as ToastStyle,
31
+ Failure: 'FAILURE' as ToastStyle,
32
+ Animated: 'ANIMATED' as ToastStyle,
38
33
  }
39
34
 
40
35
  constructor(props: Toast.Options) {
41
- this.options = { ...props }
42
- this.id = Math.random().toString(36).substring(7)
36
+ this._id = Math.random().toString(36).substring(7)
37
+ this._title = props.title
38
+ this._message = props.message
39
+ this._style = props.style || 'SUCCESS'
40
+ this._primaryAction = props.primaryAction
41
+ this._secondaryAction = props.secondaryAction
42
+ }
43
+
44
+ private _isShowing(): boolean {
45
+ return useStore.getState().toast?.id === this._id
46
+ }
43
47
 
44
- // Bind all methods to this instance
45
- this.show = this.show.bind(this)
46
- this.hide = this.hide.bind(this)
47
- this.update = this.update.bind(this)
48
- this._setCallbacks = this._setCallbacks.bind(this)
49
- this._getId = this._getId.bind(this)
48
+ private _updateState(): void {
49
+ if (this._isShowing()) {
50
+ useStore.setState({
51
+ toast: this._toData(),
52
+ toastWithPrimaryAction: Boolean(this._primaryAction),
53
+ })
54
+ }
55
+ }
56
+
57
+ private _toData(): ToastData {
58
+ return {
59
+ id: this._id,
60
+ title: this._title,
61
+ message: this._message,
62
+ style: this._style,
63
+ primaryAction: this._primaryAction
64
+ ? {
65
+ title: this._primaryAction.title,
66
+ onAction: () => {
67
+ this.hide()
68
+ this._primaryAction?.onAction(this)
69
+ },
70
+ }
71
+ : undefined,
72
+ secondaryAction: this._secondaryAction
73
+ ? {
74
+ title: this._secondaryAction.title,
75
+ onAction: () => {
76
+ this.hide()
77
+ this._secondaryAction?.onAction(this)
78
+ },
79
+ }
80
+ : undefined,
81
+ onHide: () => {
82
+ this.hide()
83
+ },
84
+ }
50
85
  }
51
86
 
52
- get style(): Toast.Style {
53
- return this.options.style || Toast.Style.Success
87
+ get style(): ToastStyle {
88
+ return this._style
54
89
  }
55
90
 
56
- set style(style: Toast.Style) {
57
- this.options.style = style
58
- this.update()
91
+ set style(style: ToastStyle) {
92
+ this._style = style
93
+ this._updateState()
59
94
  }
60
95
 
61
96
  get title(): string {
62
- return this.options.title
97
+ return this._title
63
98
  }
64
99
 
65
100
  set title(title: string) {
66
- this.options.title = title
67
- this.update()
101
+ this._title = title
102
+ this._updateState()
68
103
  }
69
104
 
70
105
  get message(): string | undefined {
71
- return this.options.message
106
+ return this._message
72
107
  }
73
108
 
74
109
  set message(message: string | undefined) {
75
- this.options.message = message
76
- this.update()
110
+ this._message = message
111
+ this._updateState()
77
112
  }
78
113
 
79
114
  get primaryAction(): Toast.ActionOptions | undefined {
80
- return this.options.primaryAction
115
+ return this._primaryAction
81
116
  }
82
117
 
83
118
  set primaryAction(action: Toast.ActionOptions | undefined) {
84
- this.options.primaryAction = action
85
- this.update()
119
+ this._primaryAction = action
120
+ this._updateState()
86
121
  }
87
122
 
88
123
  get secondaryAction(): Toast.ActionOptions | undefined {
89
- return this.options.secondaryAction
124
+ return this._secondaryAction
90
125
  }
91
126
 
92
127
  set secondaryAction(action: Toast.ActionOptions | undefined) {
93
- this.options.secondaryAction = action
94
- this.update()
128
+ this._secondaryAction = action
129
+ this._updateState()
95
130
  }
96
131
 
97
132
  async show(): Promise<void> {
98
- showToastInternal(this)
133
+ useStore.setState({
134
+ toast: this._toData(),
135
+ toastWithPrimaryAction: Boolean(this._primaryAction),
136
+ })
99
137
  }
100
138
 
101
139
  async hide(): Promise<void> {
102
- useStore.setState({ toast: null })
103
- if (this.callbacks.onHide) {
104
- this.callbacks.onHide()
140
+ if (this._isShowing()) {
141
+ useStore.setState({ toast: null, toastWithPrimaryAction: false })
105
142
  }
106
143
  }
107
-
108
- private update(): void {
109
- if (this.callbacks.onUpdate) {
110
- this.callbacks.onUpdate(this)
111
- }
112
- }
113
-
114
- _setCallbacks(callbacks: typeof this.callbacks): void {
115
- this.callbacks = callbacks
116
- }
117
-
118
- _getId(): string {
119
- return this.id
120
- }
121
- }
122
-
123
- export interface ToastContentProps {
124
- toast: Toast
125
- onHide: () => void
126
- }
127
-
128
- export function ToastContent({ toast, onHide }: ToastContentProps): any {
129
- const [, forceUpdate] = useState(0)
130
- const inFocus = useIsInFocus()
131
-
132
- useEffect(() => {
133
- const onUpdate = () => {
134
- forceUpdate((n) => n + 1)
135
- }
136
- toast._setCallbacks({ onUpdate, onHide })
137
- return () => {
138
- toast._setCallbacks({})
139
- }
140
- }, [toast, onHide])
141
-
142
- useKeyboard((evt) => {
143
- if (!inFocus) return
144
-
145
- if (evt.name === 'escape') {
146
- onHide()
147
- } else if (toast.primaryAction && evt.name === 'return') {
148
- toast.primaryAction.onAction(toast)
149
- } else if (toast.secondaryAction && evt.name === 'tab') {
150
- toast.secondaryAction.onAction(toast)
151
- }
152
- })
153
-
154
- const getIcon = () => {
155
- switch (toast.style) {
156
- case Toast.Style.Success:
157
- return '✓'
158
- case Toast.Style.Failure:
159
- return '✗'
160
- case Toast.Style.Animated:
161
- return '⣾⣽⣻⢿⡿⣟⣯⣷'
162
- default:
163
- return '✓'
164
- }
165
- }
166
-
167
- const getIconColor = () => {
168
- switch (toast.style) {
169
- case Toast.Style.Success:
170
- return Theme.success
171
- case Toast.Style.Failure:
172
- return Theme.error
173
- case Toast.Style.Animated:
174
- return Theme.primary
175
- default:
176
- return Theme.success
177
- }
178
- }
179
-
180
- const [animationFrame, setAnimationFrame] = useState(0)
181
-
182
- useEffect(() => {
183
- if (toast.style === Toast.Style.Animated) {
184
- const interval = setInterval(() => {
185
- setAnimationFrame((prev) => (prev + 1) % 8)
186
- }, 100)
187
- return () => clearInterval(interval)
188
- }
189
- }, [toast.style])
190
-
191
- useEffect(() => {
192
- if (toast.style !== Toast.Style.Animated) {
193
- const duration = toast.style === Toast.Style.Failure ? 8000 : 5000
194
- const timer = setTimeout(() => {
195
- onHide()
196
- }, duration)
197
- return () => clearTimeout(timer)
198
- }
199
- }, [toast.style, onHide])
200
-
201
- const icon =
202
- toast.style === Toast.Style.Animated ? getIcon()[animationFrame] : getIcon()
203
-
204
- return (
205
- <box
206
- borderColor={Theme.border}
207
- backgroundColor={Theme.background}
208
- paddingLeft={1}
209
- paddingRight={1}
210
- flexDirection='column'
211
- >
212
- <box flexDirection='row' alignItems='center'>
213
- <text flexShrink={0} fg={getIconColor()}>
214
- {icon}{' '}
215
- </text>
216
- <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
217
- {toast.title}
218
- </text>
219
- {toast.primaryAction && (
220
- <box
221
- flexShrink={0}
222
- onMouseDown={() => {
223
- toast.primaryAction?.onAction(toast)
224
- }}
225
- >
226
- <text fg={Theme.primary}>
227
- {' '}
228
- [{toast.primaryAction.title} ↵]
229
- </text>
230
- </box>
231
- )}
232
- {toast.secondaryAction && (
233
- <box
234
- flexShrink={0}
235
- onMouseDown={() => {
236
- toast.secondaryAction?.onAction(toast)
237
- }}
238
- >
239
- <text fg={Theme.textMuted}>
240
- {' '}
241
- [{toast.secondaryAction.title} ⇥]
242
- </text>
243
- </box>
244
- )}
245
- </box>
246
- {toast.message && (
247
- <box paddingLeft={2}>
248
- <text flexShrink={0} fg={Theme.textMuted}>
249
- {toast.message}
250
- </text>
251
- </box>
252
- )}
253
- </box>
254
- )
255
- }
256
-
257
- export function ToastOverlay(): any {
258
- const dimensions = useTerminalDimensions()
259
- const toastElement = useStore((state) => state.toast)
260
-
261
- if (!toastElement) {
262
- return null
263
- }
264
-
265
- return (
266
- <box
267
- position='absolute'
268
- left={0}
269
- bottom={0}
270
- width={dimensions.width}
271
- maxHeight={10}
272
- justifyContent='flex-end'
273
- alignItems='center'
274
- >
275
- {toastElement}
276
- </box>
277
- )
278
- }
279
-
280
- let currentToastInstance: Toast | null = null
281
-
282
- export function showToastInternal(toast: Toast): void {
283
- currentToastInstance = toast
284
- useStore.setState({
285
- toast: (
286
- <ToastContent
287
- toast={toast}
288
- onHide={() => {
289
- useStore.setState({ toast: null })
290
- currentToastInstance = null
291
- }}
292
- />
293
- ),
294
- })
295
144
  }
296
145
 
297
146
  export async function showToast(options: Toast.Options): Promise<Toast>
298
147
  export async function showToast(
299
148
  style: Toast.Style,
300
149
  title: string,
301
- message?: string,
150
+ message?: string
302
151
  ): Promise<Toast>
303
152
  export async function showToast(
304
153
  optionsOrStyle: Toast.Options | Toast.Style,
305
154
  title?: string,
306
- message?: string,
155
+ message?: string
307
156
  ): Promise<Toast> {
308
- let options: Toast.Options
309
-
310
- if (typeof optionsOrStyle === 'string') {
311
- options = {
312
- style: optionsOrStyle,
313
- title: title!,
314
- message,
315
- }
316
- } else {
317
- options = optionsOrStyle
318
- }
157
+ const options: Toast.Options =
158
+ typeof optionsOrStyle === 'string'
159
+ ? { style: optionsOrStyle, title: title!, message }
160
+ : optionsOrStyle
319
161
 
320
162
  const toast = new Toast(options)
321
163
  await toast.show()
@@ -324,37 +166,13 @@ export async function showToast(
324
166
 
325
167
  /**
326
168
  * Creates and shows a failure toast for error scenarios.
327
- *
328
- * This is a convenience function that automatically creates a toast with failure styling
329
- * and extracts error messages from various error types.
330
- *
331
- * @param error - The error to display. Can be an Error object, string, or any object with a message property
332
- * @param options - Optional configuration
333
- * @param options.title - Custom title for the toast (defaults to error name or "Something went wrong")
334
- * @param options.primaryAction - Optional action button to display on the toast
335
- * @returns A Promise that resolves to the created Toast instance
336
- *
337
- * @example
338
- * ```typescript
339
- * try {
340
- * await riskyOperation()
341
- * } catch (error) {
342
- * await showFailureToast(error, {
343
- * title: "Operation Failed",
344
- * primaryAction: {
345
- * title: "Retry",
346
- * onAction: () => retryOperation()
347
- * }
348
- * })
349
- * }
350
- * ```
351
169
  */
352
170
  export async function showFailureToast(
353
171
  error: unknown,
354
172
  options?: {
355
173
  title?: string
356
174
  primaryAction?: Toast.ActionOptions
357
- },
175
+ }
358
176
  ): Promise<Toast> {
359
177
  let errorMessage: string
360
178
  let errorTitle: string
@@ -373,12 +191,10 @@ export async function showFailureToast(
373
191
  errorTitle = options?.title || 'Something went wrong'
374
192
  }
375
193
 
376
- const toastOptions: Toast.Options = {
194
+ return showToast({
377
195
  title: errorTitle,
378
196
  message: errorMessage,
379
197
  style: Toast.Style.Failure,
380
198
  primaryAction: options?.primaryAction,
381
- }
382
-
383
- return showToast(toastOptions)
199
+ })
384
200
  }
@@ -121,6 +121,16 @@ describe('buildExtensionCommands', () => {
121
121
  "filePath": "/BASE_PATH/fixtures/simple-extension/src/quick-action.tsx",
122
122
  "exists": true,
123
123
  "bundledPath": "/BASE_PATH/fixtures/simple-extension/.termcast-bundle/quick-action.js"
124
+ },
125
+ {
126
+ "name": "throw-error",
127
+ "title": "Throw Error",
128
+ "subtitle": "Test error handling",
129
+ "description": "Command that throws an error at root scope",
130
+ "mode": "view",
131
+ "filePath": "/BASE_PATH/fixtures/simple-extension/src/throw-error.tsx",
132
+ "exists": true,
133
+ "bundledPath": "/BASE_PATH/fixtures/simple-extension/.termcast-bundle/throw-error.js"
124
134
  }
125
135
  ],
126
136
  "bundleDir": "/BASE_PATH/fixtures/simple-extension/.termcast-bundle"
package/src/build.tsx CHANGED
@@ -2,6 +2,7 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { execSync } from 'node:child_process'
4
4
  import type { BunPlugin } from 'bun'
5
+ import * as swc from '@swc/core'
5
6
  import { logger } from './logger'
6
7
  import { getCommandsWithFiles, CommandWithFile } from './package-json'
7
8
  import * as termcastApi from './index'
@@ -179,6 +180,58 @@ export const aliasPlugin: BunPlugin = {
179
180
  },
180
181
  }
181
182
 
183
+ // React Refresh transform plugin for hot reloading using SWC (Rust-based, ~20x faster than Babel)
184
+ // Transforms extension source files to inject $RefreshReg$ and $RefreshSig$ calls
185
+ export const reactRefreshPlugin: BunPlugin = {
186
+ name: 'react-refresh-transform',
187
+ async setup(build) {
188
+ build.onLoad({ filter: /\.[tj]sx?$/ }, async (args) => {
189
+ // Skip node_modules
190
+ if (args.path.includes('node_modules')) {
191
+ return undefined
192
+ }
193
+
194
+ const code = await Bun.file(args.path).text()
195
+
196
+ const isTypeScript =
197
+ args.path.endsWith('.ts') || args.path.endsWith('.tsx')
198
+ const hasJsx = args.path.endsWith('.tsx') || args.path.endsWith('.jsx')
199
+
200
+ const result = await swc.transform(code, {
201
+ filename: args.path,
202
+ jsc: {
203
+ parser: isTypeScript
204
+ ? {
205
+ syntax: 'typescript',
206
+ tsx: hasJsx,
207
+ }
208
+ : {
209
+ syntax: 'ecmascript',
210
+ jsx: hasJsx,
211
+ },
212
+ transform: {
213
+ react: {
214
+ development: true,
215
+ refresh: true, // Built-in React Refresh support in SWC
216
+ runtime: 'automatic',
217
+ },
218
+ },
219
+ },
220
+ sourceMaps: 'inline',
221
+ })
222
+
223
+ if (!result?.code) {
224
+ return undefined
225
+ }
226
+
227
+ return {
228
+ contents: result.code,
229
+ loader: 'js', // SWC transforms JSX to JS
230
+ }
231
+ })
232
+ },
233
+ }
234
+
182
235
  interface BundledCommand extends CommandWithFile {
183
236
  bundledPath: string
184
237
  }
@@ -192,10 +245,12 @@ export async function buildExtensionCommands({
192
245
  extensionPath,
193
246
  format = 'cjs',
194
247
  target,
248
+ hotReload = false,
195
249
  }: {
196
250
  extensionPath: string
197
251
  format?: 'cjs' | 'esm'
198
252
  target?: 'node' | 'bun'
253
+ hotReload?: boolean
199
254
  }): Promise<BuildResult> {
200
255
  const resolvedPath = path.resolve(extensionPath)
201
256
  const bundleDir = path.join(resolvedPath, '.termcast-bundle')
@@ -221,13 +276,18 @@ export async function buildExtensionCommands({
221
276
 
222
277
  logger.log(`Building ${entrypoints.length} commands...`)
223
278
 
279
+ const plugins = [aliasPlugin, swiftLoaderPlugin]
280
+ if (hotReload) {
281
+ plugins.push(reactRefreshPlugin)
282
+ }
283
+
224
284
  const result = await Bun.build({
225
285
  entrypoints,
226
286
  outdir: bundleDir,
227
287
  target: target || (format === 'cjs' ? 'node' : 'bun'),
228
288
  format,
229
289
  // external: [],
230
- plugins: [aliasPlugin, swiftLoaderPlugin],
290
+ plugins,
231
291
  naming: '[name].js',
232
292
  throw: false,
233
293
  })