termcast 1.3.32 → 1.3.34

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 (327) hide show
  1. package/dist/action-utils.d.ts.map +1 -1
  2. package/dist/action-utils.js +8 -0
  3. package/dist/action-utils.js.map +1 -1
  4. package/dist/apis/cache.d.ts +1 -2
  5. package/dist/apis/cache.d.ts.map +1 -1
  6. package/dist/apis/cache.js +138 -54
  7. package/dist/apis/cache.js.map +1 -1
  8. package/dist/apis/clipboard.d.ts.map +1 -1
  9. package/dist/apis/clipboard.js +4 -0
  10. package/dist/apis/clipboard.js.map +1 -1
  11. package/dist/apis/oauth.d.ts.map +1 -1
  12. package/dist/apis/oauth.js +31 -4
  13. package/dist/apis/oauth.js.map +1 -1
  14. package/dist/build.d.ts +0 -1
  15. package/dist/build.d.ts.map +1 -1
  16. package/dist/build.js +30 -51
  17. package/dist/build.js.map +1 -1
  18. package/dist/cli.js +31 -14
  19. package/dist/cli.js.map +1 -1
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +5 -1
  22. package/dist/compile.js.map +1 -1
  23. package/dist/components/actions.d.ts +14 -0
  24. package/dist/components/actions.d.ts.map +1 -1
  25. package/dist/components/actions.js +151 -59
  26. package/dist/components/actions.js.map +1 -1
  27. package/dist/components/alert.d.ts.map +1 -1
  28. package/dist/components/alert.js +6 -5
  29. package/dist/components/alert.js.map +1 -1
  30. package/dist/components/animation-tick.d.ts +1 -1
  31. package/dist/components/animation-tick.js +1 -1
  32. package/dist/components/animation-tick.js.map +1 -1
  33. package/dist/components/detail.d.ts +5 -31
  34. package/dist/components/detail.d.ts.map +1 -1
  35. package/dist/components/detail.js +36 -52
  36. package/dist/components/detail.js.map +1 -1
  37. package/dist/components/dropdown.d.ts +1 -1
  38. package/dist/components/dropdown.d.ts.map +1 -1
  39. package/dist/components/dropdown.js +50 -22
  40. package/dist/components/dropdown.js.map +1 -1
  41. package/dist/components/footer.d.ts.map +1 -1
  42. package/dist/components/footer.js +19 -18
  43. package/dist/components/footer.js.map +1 -1
  44. package/dist/components/form/checkbox.d.ts.map +1 -1
  45. package/dist/components/form/checkbox.js +12 -11
  46. package/dist/components/form/checkbox.js.map +1 -1
  47. package/dist/components/form/date-picker.d.ts.map +1 -1
  48. package/dist/components/form/date-picker.js +7 -22
  49. package/dist/components/form/date-picker.js.map +1 -1
  50. package/dist/components/form/description.d.ts +1 -1
  51. package/dist/components/form/description.d.ts.map +1 -1
  52. package/dist/components/form/description.js +6 -5
  53. package/dist/components/form/description.js.map +1 -1
  54. package/dist/components/form/dropdown.d.ts.map +1 -1
  55. package/dist/components/form/dropdown.js +53 -50
  56. package/dist/components/form/dropdown.js.map +1 -1
  57. package/dist/components/form/file-autocomplete.d.ts.map +1 -1
  58. package/dist/components/form/file-autocomplete.js +5 -4
  59. package/dist/components/form/file-autocomplete.js.map +1 -1
  60. package/dist/components/form/file-picker.d.ts.map +1 -1
  61. package/dist/components/form/file-picker.js +23 -22
  62. package/dist/components/form/file-picker.js.map +1 -1
  63. package/dist/components/form/form-end.d.ts.map +1 -1
  64. package/dist/components/form/form-end.js +6 -4
  65. package/dist/components/form/form-end.js.map +1 -1
  66. package/dist/components/form/form-field-wrapper.d.ts +15 -0
  67. package/dist/components/form/form-field-wrapper.d.ts.map +1 -0
  68. package/dist/components/form/form-field-wrapper.js +29 -0
  69. package/dist/components/form/form-field-wrapper.js.map +1 -0
  70. package/dist/components/form/index.d.ts.map +1 -1
  71. package/dist/components/form/index.js +31 -30
  72. package/dist/components/form/index.js.map +1 -1
  73. package/dist/components/form/password-field.d.ts.map +1 -1
  74. package/dist/components/form/password-field.js +7 -6
  75. package/dist/components/form/password-field.js.map +1 -1
  76. package/dist/components/form/separator.d.ts.map +1 -1
  77. package/dist/components/form/separator.js +3 -2
  78. package/dist/components/form/separator.js.map +1 -1
  79. package/dist/components/form/tagpicker.d.ts.map +1 -1
  80. package/dist/components/form/tagpicker.js +2 -1
  81. package/dist/components/form/tagpicker.js.map +1 -1
  82. package/dist/components/form/text-area.d.ts.map +1 -1
  83. package/dist/components/form/text-area.js +7 -6
  84. package/dist/components/form/text-area.js.map +1 -1
  85. package/dist/components/form/text-field.d.ts.map +1 -1
  86. package/dist/components/form/text-field.js +7 -6
  87. package/dist/components/form/text-field.js.map +1 -1
  88. package/dist/components/form/use-form-navigation.d.ts.map +1 -1
  89. package/dist/components/form/use-form-navigation.js +4 -4
  90. package/dist/components/form/use-form-navigation.js.map +1 -1
  91. package/dist/components/form/with-left-border.d.ts +15 -0
  92. package/dist/components/form/with-left-border.d.ts.map +1 -1
  93. package/dist/components/form/with-left-border.js +21 -9
  94. package/dist/components/form/with-left-border.js.map +1 -1
  95. package/dist/components/icon.d.ts +14 -0
  96. package/dist/components/icon.d.ts.map +1 -1
  97. package/dist/components/icon.js +60 -0
  98. package/dist/components/icon.js.map +1 -1
  99. package/dist/components/image.d.ts +47 -2
  100. package/dist/components/image.d.ts.map +1 -1
  101. package/dist/components/image.js +46 -7
  102. package/dist/components/image.js.map +1 -1
  103. package/dist/components/list.d.ts +5 -0
  104. package/dist/components/list.d.ts.map +1 -1
  105. package/dist/components/list.js +188 -132
  106. package/dist/components/list.js.map +1 -1
  107. package/dist/components/loading-bar.d.ts.map +1 -1
  108. package/dist/components/loading-bar.js +4 -3
  109. package/dist/components/loading-bar.js.map +1 -1
  110. package/dist/components/metadata.d.ts +70 -0
  111. package/dist/components/metadata.d.ts.map +1 -0
  112. package/dist/components/metadata.js +82 -0
  113. package/dist/components/metadata.js.map +1 -0
  114. package/dist/components/theme-picker.d.ts.map +1 -1
  115. package/dist/components/theme-picker.js +3 -2
  116. package/dist/components/theme-picker.js.map +1 -1
  117. package/dist/descendants-v2.d.ts +60 -0
  118. package/dist/descendants-v2.d.ts.map +1 -0
  119. package/dist/descendants-v2.js +144 -0
  120. package/dist/descendants-v2.js.map +1 -0
  121. package/dist/examples/actions-context.d.ts +2 -0
  122. package/dist/examples/actions-context.d.ts.map +1 -0
  123. package/dist/examples/actions-context.js +33 -0
  124. package/dist/examples/actions-context.js.map +1 -0
  125. package/dist/examples/form-basic.d.ts.map +1 -1
  126. package/dist/examples/form-basic.js +1 -1
  127. package/dist/examples/form-basic.js.map +1 -1
  128. package/dist/examples/form-dropdown.js +1 -1
  129. package/dist/examples/form-dropdown.js.map +1 -1
  130. package/dist/examples/internal/custom-action-renderables.d.ts +70 -0
  131. package/dist/examples/internal/custom-action-renderables.d.ts.map +1 -0
  132. package/dist/examples/internal/custom-action-renderables.js +163 -0
  133. package/dist/examples/internal/custom-action-renderables.js.map +1 -0
  134. package/dist/examples/internal/custom-dropdown.d.ts +99 -0
  135. package/dist/examples/internal/custom-dropdown.d.ts.map +1 -0
  136. package/dist/examples/internal/custom-dropdown.js +270 -0
  137. package/dist/examples/internal/custom-dropdown.js.map +1 -0
  138. package/dist/examples/internal/custom-renderable-form.d.ts +43 -0
  139. package/dist/examples/internal/custom-renderable-form.d.ts.map +1 -0
  140. package/dist/examples/internal/custom-renderable-form.js +284 -0
  141. package/dist/examples/internal/custom-renderable-form.js.map +1 -0
  142. package/dist/examples/internal/custom-renderable-list-default-search.d.ts +2 -0
  143. package/dist/examples/internal/custom-renderable-list-default-search.d.ts.map +1 -0
  144. package/dist/examples/internal/custom-renderable-list-default-search.js +16 -0
  145. package/dist/examples/internal/custom-renderable-list-default-search.js.map +1 -0
  146. package/dist/examples/internal/custom-renderable-list-v2-default-search.d.ts +2 -0
  147. package/dist/examples/internal/custom-renderable-list-v2-default-search.d.ts.map +1 -0
  148. package/dist/examples/internal/custom-renderable-list-v2-default-search.js +24 -0
  149. package/dist/examples/internal/custom-renderable-list-v2-default-search.js.map +1 -0
  150. package/dist/examples/internal/custom-renderable-list-v2.d.ts +189 -0
  151. package/dist/examples/internal/custom-renderable-list-v2.d.ts.map +1 -0
  152. package/dist/examples/internal/custom-renderable-list-v2.js +708 -0
  153. package/dist/examples/internal/custom-renderable-list-v2.js.map +1 -0
  154. package/dist/examples/internal/custom-renderable-list.d.ts +72 -0
  155. package/dist/examples/internal/custom-renderable-list.d.ts.map +1 -0
  156. package/dist/examples/internal/custom-renderable-list.js +544 -0
  157. package/dist/examples/internal/custom-renderable-list.js.map +1 -0
  158. package/dist/examples/internal/rhf-custom-ref.js +5 -4
  159. package/dist/examples/internal/rhf-custom-ref.js.map +1 -1
  160. package/dist/examples/internal/scrollbox-with-descendants.js +4 -2
  161. package/dist/examples/internal/scrollbox-with-descendants.js.map +1 -1
  162. package/dist/examples/list-controlled-search.d.ts +2 -0
  163. package/dist/examples/list-controlled-search.d.ts.map +1 -0
  164. package/dist/examples/list-controlled-search.js +12 -0
  165. package/dist/examples/list-controlled-search.js.map +1 -0
  166. package/dist/examples/list-detail-metadata.js +1 -1
  167. package/dist/examples/list-detail-metadata.js.map +1 -1
  168. package/dist/examples/simple-image-mask.d.ts +8 -0
  169. package/dist/examples/simple-image-mask.d.ts.map +1 -0
  170. package/dist/examples/simple-image-mask.js +12 -0
  171. package/dist/examples/simple-image-mask.js.map +1 -0
  172. package/dist/examples/toast-variations.js +1 -1
  173. package/dist/examples/toast-variations.js.map +1 -1
  174. package/dist/extensions/dev.d.ts.map +1 -1
  175. package/dist/extensions/dev.js +3 -2
  176. package/dist/extensions/dev.js.map +1 -1
  177. package/dist/extensions/react-refresh-init.d.ts.map +1 -1
  178. package/dist/extensions/react-refresh-init.js +4 -3
  179. package/dist/extensions/react-refresh-init.js.map +1 -1
  180. package/dist/index.d.ts +3 -2
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +1 -1
  183. package/dist/index.js.map +1 -1
  184. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  185. package/dist/internal/date-picker-widget.js +2 -1
  186. package/dist/internal/date-picker-widget.js.map +1 -1
  187. package/dist/internal/dialog.d.ts +6 -0
  188. package/dist/internal/dialog.d.ts.map +1 -1
  189. package/dist/internal/dialog.js +59 -18
  190. package/dist/internal/dialog.js.map +1 -1
  191. package/dist/internal/navigation.d.ts.map +1 -1
  192. package/dist/internal/navigation.js +8 -1
  193. package/dist/internal/navigation.js.map +1 -1
  194. package/dist/internal/offscreen.d.ts +3 -0
  195. package/dist/internal/offscreen.d.ts.map +1 -1
  196. package/dist/internal/offscreen.js +5 -0
  197. package/dist/internal/offscreen.js.map +1 -1
  198. package/dist/internal/providers.d.ts.map +1 -1
  199. package/dist/internal/providers.js +20 -3
  200. package/dist/internal/providers.js.map +1 -1
  201. package/dist/internal/scrollbox.d.ts.map +1 -1
  202. package/dist/internal/scrollbox.js +3 -2
  203. package/dist/internal/scrollbox.js.map +1 -1
  204. package/dist/logger.d.ts.map +1 -1
  205. package/dist/logger.js +4 -0
  206. package/dist/logger.js.map +1 -1
  207. package/dist/preload.js +5 -17
  208. package/dist/preload.js.map +1 -1
  209. package/dist/state.d.ts +4 -0
  210. package/dist/state.d.ts.map +1 -1
  211. package/dist/state.js +4 -0
  212. package/dist/state.js.map +1 -1
  213. package/dist/test-border-overlay.d.ts +2 -0
  214. package/dist/test-border-overlay.d.ts.map +1 -0
  215. package/dist/test-border-overlay.js +7 -0
  216. package/dist/test-border-overlay.js.map +1 -0
  217. package/dist/test-layout-2.d.ts +2 -0
  218. package/dist/test-layout-2.d.ts.map +1 -0
  219. package/dist/test-layout-2.js +5 -0
  220. package/dist/test-layout-2.js.map +1 -0
  221. package/dist/test-layout.d.ts +2 -0
  222. package/dist/test-layout.d.ts.map +1 -0
  223. package/dist/test-layout.js +7 -0
  224. package/dist/test-layout.js.map +1 -0
  225. package/dist/theme.d.ts +1 -2
  226. package/dist/theme.d.ts.map +1 -1
  227. package/dist/theme.js +5 -9
  228. package/dist/theme.js.map +1 -1
  229. package/dist/utils/run-command.d.ts +1 -1
  230. package/dist/utils/run-command.d.ts.map +1 -1
  231. package/dist/utils/run-command.js +27 -7
  232. package/dist/utils/run-command.js.map +1 -1
  233. package/dist/utils.d.ts +1 -0
  234. package/dist/utils.d.ts.map +1 -1
  235. package/dist/utils.js +44 -23
  236. package/dist/utils.js.map +1 -1
  237. package/dist/watcher.d.ts.map +1 -1
  238. package/dist/watcher.js +24 -4
  239. package/dist/watcher.js.map +1 -1
  240. package/package.json +14 -12
  241. package/src/action-utils.tsx +10 -0
  242. package/src/apis/cache.test.ts +35 -3
  243. package/src/apis/cache.tsx +184 -59
  244. package/src/apis/clipboard.tsx +5 -0
  245. package/src/apis/oauth.tsx +33 -4
  246. package/src/build.tsx +35 -58
  247. package/src/cli.tsx +156 -134
  248. package/src/compile.tsx +6 -3
  249. package/src/compile.vitest.tsx +33 -15
  250. package/src/components/actions.tsx +230 -99
  251. package/src/components/alert.tsx +11 -10
  252. package/src/components/animation-tick.tsx +1 -1
  253. package/src/components/detail.tsx +56 -151
  254. package/src/components/dropdown.tsx +70 -36
  255. package/src/components/footer.tsx +58 -33
  256. package/src/components/form/checkbox.tsx +30 -32
  257. package/src/components/form/date-picker.tsx +27 -47
  258. package/src/components/form/description.tsx +19 -18
  259. package/src/components/form/dropdown.tsx +95 -103
  260. package/src/components/form/file-autocomplete.tsx +9 -8
  261. package/src/components/form/file-picker.tsx +46 -46
  262. package/src/components/form/form-end.tsx +6 -4
  263. package/src/components/form/index.tsx +38 -48
  264. package/src/components/form/password-field.tsx +25 -27
  265. package/src/components/form/separator.tsx +3 -2
  266. package/src/components/form/tagpicker.tsx +2 -1
  267. package/src/components/form/text-area.tsx +25 -30
  268. package/src/components/form/text-field.tsx +25 -27
  269. package/src/components/form/use-form-navigation.tsx +4 -5
  270. package/src/components/form/with-left-border.tsx +48 -10
  271. package/src/components/icon.tsx +69 -0
  272. package/src/components/image.tsx +60 -7
  273. package/src/components/list.tsx +270 -202
  274. package/src/components/loading-bar.tsx +4 -3
  275. package/src/components/metadata.tsx +217 -0
  276. package/src/components/theme-picker.tsx +3 -2
  277. package/src/examples/actions-context.tsx +63 -0
  278. package/src/examples/actions-context.vitest.tsx +110 -0
  279. package/src/examples/actions-dialog-layout.vitest.tsx +2 -1
  280. package/src/examples/file-autocomplete.vitest.tsx +15 -15
  281. package/src/examples/form-basic.tsx +12 -0
  282. package/src/examples/form-basic.vitest.tsx +74 -74
  283. package/src/examples/form-dropdown.tsx +8 -0
  284. package/src/examples/form-dropdown.vitest.tsx +364 -421
  285. package/src/examples/form-tagpicker.vitest.tsx +56 -54
  286. package/src/examples/github.vitest.tsx +252 -0
  287. package/src/examples/internal/rhf-custom-ref.tsx +16 -15
  288. package/src/examples/internal/scrollbox-with-descendants.tsx +4 -2
  289. package/src/examples/internal/simple-dialog.tsx +1 -1
  290. package/src/examples/internal/simple-scrollbox.vitest.tsx +14 -9
  291. package/src/examples/list-controlled-search.tsx +28 -0
  292. package/src/examples/list-controlled-search.vitest.tsx +49 -0
  293. package/src/examples/list-detail-metadata.tsx +8 -5
  294. package/src/examples/list-detail-metadata.vitest.tsx +22 -22
  295. package/src/examples/list-dropdown-default.vitest.tsx +12 -12
  296. package/src/examples/list-scrollbox.vitest.tsx +52 -38
  297. package/src/examples/list-with-detail.vitest.tsx +45 -41
  298. package/src/examples/list-with-dropdown.vitest.tsx +5 -5
  299. package/src/examples/list-with-sections.vitest.tsx +65 -12
  300. package/src/examples/list-with-toast.vitest.tsx +4 -4
  301. package/src/examples/simple-file-picker.vitest.tsx +12 -12
  302. package/src/examples/simple-grid.vitest.tsx +53 -53
  303. package/src/examples/simple-image-mask.tsx +58 -0
  304. package/src/examples/simple-navigation.vitest.tsx +19 -19
  305. package/src/examples/store.vitest.tsx +1 -1
  306. package/src/examples/swift-extension.vitest.tsx +4 -2
  307. package/src/examples/synonyms.vitest.tsx +31 -9
  308. package/src/examples/toast-action.vitest.tsx +8 -8
  309. package/src/examples/toast-variations.tsx +1 -1
  310. package/src/examples/toast-variations.vitest.tsx +69 -134
  311. package/src/extensions/dev.tsx +3 -2
  312. package/src/extensions/dev.vitest.tsx +65 -28
  313. package/src/extensions/react-refresh-init.tsx +4 -3
  314. package/src/index.tsx +3 -1
  315. package/src/internal/date-picker-widget.tsx +2 -1
  316. package/src/internal/dialog.tsx +100 -28
  317. package/src/internal/navigation.tsx +8 -1
  318. package/src/internal/offscreen.tsx +10 -0
  319. package/src/internal/providers.tsx +34 -8
  320. package/src/internal/scrollbox.tsx +4 -2
  321. package/src/logger.tsx +4 -0
  322. package/src/preload.tsx +5 -17
  323. package/src/state.tsx +12 -0
  324. package/src/theme.tsx +6 -9
  325. package/src/utils/run-command.tsx +32 -8
  326. package/src/utils.tsx +58 -23
  327. package/src/watcher.tsx +26 -6
package/src/index.tsx CHANGED
@@ -97,11 +97,13 @@ export type {
97
97
  // Icons and Images
98
98
  export { Icon, getIconEmoji, getIconShape, IconComponent } from 'termcast/src/components/icon'
99
99
  export { Image, ImageMask } from 'termcast/src/components/image'
100
+ export type { ImageType } from 'termcast/src/components/image'
100
101
  export type {
101
102
  ImageProps,
102
103
  ImageSource,
103
104
  FileIcon,
104
105
  ImageLike,
106
+ ImageFallback,
105
107
  } from 'termcast/src/components/image'
106
108
 
107
109
  // Alerts
@@ -208,7 +210,7 @@ export type { PreferenceValues } from 'termcast/src/apis/preferences'
208
210
  export type { CommonProps } from 'termcast/src/utils'
209
211
 
210
212
  // Theme
211
- export { Theme } from 'termcast/src/theme'
213
+ export { useTheme } from 'termcast/src/theme'
212
214
 
213
215
  // Logger
214
216
  export { logger } from 'termcast/src/logger'
@@ -1,6 +1,6 @@
1
1
  import { useKeyboard } from '@opentui/react'
2
2
  import { useMemo, useState, useRef } from 'react'
3
- import Theme from '../theme'
3
+ import { useTheme } from '../theme'
4
4
 
5
5
  // ----- Helpers -----
6
6
  type Focus = 'year' | 'month' | 'grid'
@@ -76,6 +76,7 @@ export function DatePickerWidget({
76
76
  onFirstRowUpKey?: () => void
77
77
  onLastRowDownKey?: () => void
78
78
  }) {
79
+ const Theme = useTheme()
79
80
  const today = useMemo(() => new Date(), [])
80
81
  const [focus, setFocus] = useState<Focus>('grid') // can be "year" | "month" | "grid"
81
82
  const [selected, setSelected] = useState<Date>(initialValue || new Date()) // focused day
@@ -1,6 +1,6 @@
1
1
  import { useKeyboard } from '@opentui/react'
2
- import React, { type ReactNode, useRef, useContext } from 'react'
3
- import { Theme } from 'termcast/src/theme'
2
+ import React, { type ReactNode, useRef, useContext, useCallback } from 'react'
3
+ import { useTheme } from 'termcast/src/theme'
4
4
  import { InFocus, useIsInFocus } from 'termcast/src/internal/focus-context'
5
5
  import { CommonProps } from 'termcast/src/utils'
6
6
  import { useStore, type DialogPosition } from 'termcast/src/state'
@@ -20,6 +20,7 @@ export function Dialog({
20
20
  position = 'center',
21
21
  onClickOutside,
22
22
  }: DialogProps): any {
23
+ const theme = useTheme()
23
24
  const inFocus = useIsInFocus()
24
25
  const clickedInsideDialog = useRef(false)
25
26
 
@@ -69,7 +70,7 @@ export function Dialog({
69
70
  alignItems={positionStyles.alignItems}
70
71
  justifyContent={positionStyles.justifyContent}
71
72
  padding={positionStyles.padding}
72
- // backgroundColor={Theme.background}
73
+ // backgroundColor={theme.background}
73
74
  onMouseDown={handleBackdropClick}
74
75
  >
75
76
  <box
@@ -77,8 +78,8 @@ export function Dialog({
77
78
  borderStyle='rounded'
78
79
  width={76}
79
80
  maxWidth='95%'
80
- backgroundColor={Theme.backgroundPanel}
81
- borderColor={Theme.accent}
81
+ backgroundColor={theme.backgroundPanel}
82
+ borderColor={theme.accent}
82
83
  paddingTop={1}
83
84
  onMouseDown={handleDialogClick}
84
85
  >
@@ -94,13 +95,34 @@ interface DialogProviderProps {
94
95
 
95
96
  export function DialogProvider(props: DialogProviderProps): any {
96
97
  const dialogStack = useStore((state) => state.dialogStack)
98
+ const showActionsDialog = useStore((state) => state.showActionsDialog)
97
99
  const inFocus = useIsInFocus()
98
100
 
99
101
  useKeyboard((evt) => {
100
102
  if (!inFocus) return
101
103
  if (evt.name === 'escape') {
102
104
  const state = useStore.getState()
105
+
106
+ // Handle actions dialog first
107
+ if (state.showActionsDialog) {
108
+ // Check if there's a search input with text that should be cleared first
109
+ const activeSearchInputRef = state.activeSearchInputRef
110
+ if (activeSearchInputRef && activeSearchInputRef.plainText) {
111
+ activeSearchInputRef.setText('')
112
+ return
113
+ }
114
+ useStore.setState({ showActionsDialog: false })
115
+ return
116
+ }
117
+
103
118
  if (state.dialogStack.length > 0) {
119
+ // Check if there's a search input with text that should be cleared first
120
+ const activeSearchInputRef = state.activeSearchInputRef
121
+ if (activeSearchInputRef && activeSearchInputRef.plainText) {
122
+ // Clear the search text instead of closing dialog
123
+ activeSearchInputRef.setText('')
124
+ return
125
+ }
104
126
  useStore.setState({
105
127
  dialogStack: state.dialogStack.slice(0, -1),
106
128
  })
@@ -108,8 +130,8 @@ export function DialogProvider(props: DialogProviderProps): any {
108
130
  }
109
131
  })
110
132
 
111
- // Children lose focus only when there's a dialog (toast uses unique shortcuts so no focus stealing needed)
112
- const childrenInFocus = !dialogStack?.length
133
+ // Children lose focus only when there's a dialog or actions dialog
134
+ const childrenInFocus = !dialogStack?.length && !showActionsDialog
113
135
 
114
136
  return (
115
137
  <>
@@ -119,38 +141,88 @@ export function DialogProvider(props: DialogProviderProps): any {
119
141
  )
120
142
  }
121
143
 
144
+ /**
145
+ * DialogOverlay renders dialog stack items and provides a portal target for
146
+ * ActionPanel. The portal target is always mounted so ActionPanel can use
147
+ * createPortal to render its Dropdown here while keeping its React context
148
+ * (FormSubmitContext, NavigationContext, etc.) from the original tree.
149
+ */
122
150
  export function DialogOverlay(): any {
123
151
  const dialogStack = useStore((state) => state.dialogStack)
152
+ const showActionsDialog = useStore((state) => state.showActionsDialog)
124
153
  const navContext = useContext(NavigationContext)
154
+ const theme = useTheme()
155
+
156
+ const setActionsPortalTargetRef = useCallback((node: any) => {
157
+ if (!node) {
158
+ useStore.setState({ actionsPortalTarget: null })
159
+ return
160
+ }
161
+
162
+ useStore.setState({ actionsPortalTarget: node })
125
163
 
126
- if (dialogStack.length === 0) {
164
+ return () => {
165
+ if (useStore.getState().actionsPortalTarget === node) {
166
+ useStore.setState({ actionsPortalTarget: null })
167
+ }
168
+ }
169
+ }, [])
170
+
171
+ if (dialogStack.length === 0 && !showActionsDialog) {
127
172
  return null
128
173
  }
129
174
 
130
- // Only render the topmost dialog
131
175
  const topIndex = dialogStack.length - 1
132
- const item = dialogStack[topIndex]
176
+ const item = topIndex >= 0 ? dialogStack[topIndex] : undefined
133
177
 
134
178
  return (
135
- <box position='absolute' width='100%' height='100%' flexDirection='column'>
136
- <InFocus inFocus={true}>
137
- <Dialog
138
- position={item.position}
139
- onClickOutside={() => {
140
- const state = useStore.getState()
141
- if (state.dialogStack.length > 0) {
142
- useStore.setState({
143
- dialogStack: state.dialogStack.slice(0, -1),
144
- })
145
- }
146
- }}
179
+ <>
180
+ {showActionsDialog && (
181
+ <box
182
+ position='absolute'
183
+ width='100%'
184
+ height='100%'
185
+ flexDirection='column'
186
+ backgroundColor={theme.background}
147
187
  >
148
- <NavigationContext.Provider value={navContext}>
149
- {item.element}
150
- </NavigationContext.Provider>
151
- </Dialog>
152
- </InFocus>
153
- </box>
188
+ <InFocus inFocus={true}>
189
+ <Dialog
190
+ position='center'
191
+ onClickOutside={() => {
192
+ useStore.setState({ showActionsDialog: false })
193
+ }}
194
+ >
195
+ <box
196
+ ref={setActionsPortalTargetRef}
197
+ flexDirection='column'
198
+ flexGrow={1}
199
+ />
200
+ </Dialog>
201
+ </InFocus>
202
+ </box>
203
+ )}
204
+ {item && (
205
+ <box position='absolute' width='100%' height='100%' flexDirection='column'>
206
+ <InFocus inFocus={true}>
207
+ <Dialog
208
+ position={item.position}
209
+ onClickOutside={() => {
210
+ const state = useStore.getState()
211
+ if (state.dialogStack.length > 0) {
212
+ useStore.setState({
213
+ dialogStack: state.dialogStack.slice(0, -1),
214
+ })
215
+ }
216
+ }}
217
+ >
218
+ <NavigationContext.Provider value={navContext}>
219
+ {item.element}
220
+ </NavigationContext.Provider>
221
+ </Dialog>
222
+ </InFocus>
223
+ </box>
224
+ )}
225
+ </>
154
226
  )
155
227
  }
156
228
 
@@ -165,7 +165,14 @@ export function NavigationProvider(props: NavigationProviderProps): any {
165
165
  )
166
166
  pop()
167
167
  } else {
168
- // At root with no dialogs - exit the CLI
168
+ // At root - check if there's a search input with text that should be cleared first
169
+ const activeSearchInputRef = useStore.getState().activeSearchInputRef
170
+ if (activeSearchInputRef && activeSearchInputRef.plainText) {
171
+ // Clear the search text instead of exiting
172
+ activeSearchInputRef.setText('')
173
+ return
174
+ }
175
+ // At root with no dialogs and no search text - exit the CLI
169
176
  renderer.destroy()
170
177
  }
171
178
  }
@@ -13,3 +13,13 @@ export function Offscreen({ children }: { children: ReactNode }): any {
13
13
  </OffscreenContext.Provider>
14
14
  )
15
15
  }
16
+
17
+ // Resets the offscreen context to false. Used by portals that render content
18
+ // from an offscreen tree into a visible overlay area.
19
+ export function Onscreen({ children }: { children: ReactNode }): any {
20
+ return (
21
+ <OffscreenContext.Provider value={false}>
22
+ {children}
23
+ </OffscreenContext.Provider>
24
+ )
25
+ }
@@ -8,15 +8,16 @@ import { QueryClient } from '@tanstack/react-query'
8
8
  import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
9
9
  import { DialogProvider, DialogOverlay } from 'termcast/src/internal/dialog'
10
10
  import { NavigationProvider } from 'termcast/src/internal/navigation'
11
- import { CommonProps } from 'termcast/src/utils'
11
+ import { CommonProps, termcastMaxContentWidth } from 'termcast/src/utils'
12
12
  import { Cache } from 'termcast/src/apis/cache'
13
13
  import { logger } from 'termcast/src/logger'
14
- import { Theme, initializeTheme } from 'termcast/src/theme'
14
+ import { useTheme, initializeTheme } from 'termcast/src/theme'
15
15
  import { useStore } from 'termcast/src/state'
16
- import { useTerminalDimensions } from '@opentui/react'
16
+ import { useKeyboard, useRenderer } from '@opentui/react'
17
17
  import { initializeErrorHandlers } from 'termcast/src/internal/error-handler'
18
18
 
19
19
  import { InFocus } from './focus-context'
20
+ import { Clipboard } from '../apis/clipboard'
20
21
 
21
22
  // Initialize error handlers at module load time
22
23
  initializeErrorHandlers()
@@ -99,9 +100,12 @@ class ErrorBoundaryClass extends Component<
99
100
  }
100
101
 
101
102
  function ErrorDisplay({ error }: { error: Error | null }): any {
103
+ const theme = useTheme()
102
104
  return (
103
105
  <box padding={2}>
104
- <text fg={Theme.error} wrapMode='none'>{error?.stack}</text>
106
+ <text fg={theme.error} wrapMode='none'>
107
+ {error?.stack}
108
+ </text>
105
109
  </box>
106
110
  )
107
111
  }
@@ -109,7 +113,18 @@ function ErrorDisplay({ error }: { error: Error | null }): any {
109
113
  const ErrorBoundary = ErrorBoundaryClass as any
110
114
 
111
115
  export function TermcastProvider(props: ProvidersProps): any {
112
-
116
+ const theme = useTheme()
117
+ const renderer = useRenderer()
118
+ useKeyboard((key) => {
119
+ if (!renderer) return
120
+ if (key.ctrl && key.name === 'd') {
121
+ renderer.console.onCopySelection = (text: any) => {
122
+ Clipboard.copy(text)
123
+ }
124
+ renderer?.toggleDebugOverlay()
125
+ renderer?.console.toggle()
126
+ }
127
+ })
113
128
 
114
129
  return (
115
130
  <ErrorBoundary>
@@ -124,15 +139,26 @@ export function TermcastProvider(props: ProvidersProps): any {
124
139
  <box
125
140
  minHeight={'100%'}
126
141
  justifyContent='flex-start'
127
- backgroundColor={Theme.background}
142
+ backgroundColor={theme.background}
143
+ width='100%'
144
+ flexGrow={1}
145
+ alignItems='center'
128
146
  // borderColor={Theme.border}
129
147
  // fg={Theme.text}
130
148
  >
131
- <box padding={2}>
149
+ <box
150
+ padding={2}
151
+ width='100%'
152
+ maxWidth={termcastMaxContentWidth}
153
+ // flexShrink={1}
154
+ // flexGrow={1}
155
+ >
132
156
  <DialogProvider>
133
157
  {/* NavigationProvider must be last to ensure parent providers remain in the tree when navigation changes */}
134
158
  <NavigationProvider overlay={<DialogOverlay />}>
135
- <box>{props.children}</box>
159
+ <box width='100%' flexGrow={1} flexShrink={1}>
160
+ {props.children}
161
+ </box>
136
162
  </NavigationProvider>
137
163
  </DialogProvider>
138
164
  </box>
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import Theme from '../theme'
2
+ import { useTheme } from '../theme'
3
3
  import { MacOSScrollAccel } from '@opentui/core'
4
4
  import { ScrollBoxProps } from '@opentui/react'
5
5
 
@@ -13,6 +13,8 @@ export function ScrollBox({
13
13
  ref,
14
14
  ...props
15
15
  }: ScrollBoxProps): any {
16
+ const theme = useTheme()
17
+
16
18
  return (
17
19
  <scrollbox
18
20
  ref={ref}
@@ -41,7 +43,7 @@ export function ScrollBox({
41
43
  // visible: true,
42
44
  // showArrows: true,
43
45
  trackOptions: {
44
- foregroundColor: Theme.textMuted,
46
+ foregroundColor: theme.textMuted,
45
47
 
46
48
  // backgroundColor: '#414868',
47
49
  },
package/src/logger.tsx CHANGED
@@ -25,18 +25,21 @@ export const logger = {
25
25
  const formattedMessages = messages.map(serialize).join(' ')
26
26
  const logEntry = `[${timestamp}] ${formattedMessages}\n`
27
27
  fs.appendFileSync(LOG_FILE, logEntry)
28
+ console.log(...messages)
28
29
  },
29
30
  error: (...messages: any[]) => {
30
31
  const timestamp = new Date().toISOString()
31
32
  const formattedMessages = messages.map(serialize).join(' ')
32
33
  const logEntry = `[${timestamp}] ERROR: ${formattedMessages}\n`
33
34
  fs.appendFileSync(LOG_FILE, logEntry)
35
+ console.error(...messages)
34
36
  },
35
37
  warn: (...messages: any[]) => {
36
38
  const timestamp = new Date().toISOString()
37
39
  const formattedMessages = messages.map(serialize).join(' ')
38
40
  const logEntry = `[${timestamp}] WARN: ${formattedMessages}\n`
39
41
  fs.appendFileSync(LOG_FILE, logEntry)
42
+ console.warn(...messages)
40
43
  },
41
44
  trace: (...messages: any[]) => {
42
45
  const timestamp = new Date().toISOString()
@@ -51,6 +54,7 @@ export const logger = {
51
54
  const formattedMessages = messages.map(serialize).join(' ')
52
55
  const logEntry = `[${timestamp}] TRACE: ${formattedMessages}\n${stack}\n`
53
56
  fs.appendFileSync(LOG_FILE, logEntry)
57
+ console.trace(...messages)
54
58
  },
55
59
  }
56
60
 
package/src/preload.tsx CHANGED
@@ -2,31 +2,19 @@ import { plugin } from 'bun'
2
2
  import path from 'node:path'
3
3
  import { logger } from './logger'
4
4
 
5
+ // Path to our forked raycast-utils with termcast OAuth proxy URLs
6
+ const RAYCAST_UTILS_PATH = path.resolve(__dirname, '../../raycast-utils/src/index.ts')
7
+
5
8
  plugin({
6
9
  name: 'alias-raycast-to-termcast',
7
10
  setup(build) {
8
- build.onResolve({ filter: /@raycast\/api/ }, () => {
11
+ // Redirect @raycast/api to termcast
12
+ build.onResolve({ filter: /^@raycast\/api$/ }, () => {
9
13
  return {
10
14
  path: require.resolve('termcast'),
11
15
  }
12
16
  })
13
- // build.onResolve({ filter: /@raycast\/utils/ }, (args) => {
14
- // return {
15
- // path: require.resolve('@raycast/utils', {
16
- // paths: [args.importer],
17
- // }),
18
17
 
19
- // }
20
- // })
21
- // build.onLoad({ filter: /@raycast\/utils/ }, (args) => {
22
- // const filePath = require
23
- // .resolve(args.path.replace('file:', ''))
24
- // .replace('file:', '')
25
- // return {
26
- // contents: require('fs').readFileSync(filePath, 'utf8'),
27
18
 
28
- // loader: 'js',
29
- // }
30
- // })
31
19
  },
32
20
  })
package/src/state.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import { create } from 'zustand'
2
2
  import { type ReactNode } from 'react'
3
+ import type { TextareaRenderable } from '@opentui/core'
3
4
  import type { RaycastPackageJson } from './package-json'
4
5
 
5
6
  // Toast action keyboard shortcuts (ctrl+t for primary, ctrl+g for secondary)
@@ -62,8 +63,15 @@ interface AppState {
62
63
  shouldAutoExecuteFirstAction: boolean
63
64
  // First action title for footer display (set by offscreen ActionPanel)
64
65
  firstActionTitle: string
66
+ // Flag to show actions dialog via portal
67
+ showActionsDialog: boolean
68
+ // Portal target node for rendering ActionPanel dialog in the overlay area.
69
+ // Set by DialogOverlay, consumed by ActionPanel via createPortal.
70
+ actionsPortalTarget: any
65
71
  // Theme state
66
72
  currentThemeName: string
73
+ // Active search input ref - used to clear search before exiting on ESC
74
+ activeSearchInputRef: TextareaRenderable | null
67
75
  }
68
76
 
69
77
  export const useStore = create<AppState>(() => ({
@@ -86,6 +94,10 @@ export const useStore = create<AppState>(() => ({
86
94
  // Actions state
87
95
  shouldAutoExecuteFirstAction: false,
88
96
  firstActionTitle: '',
97
+ showActionsDialog: false,
98
+ actionsPortalTarget: null,
89
99
  // Theme state
90
100
  currentThemeName: 'termcast',
101
+ // Active search input ref
102
+ activeSearchInputRef: null,
91
103
  }))
package/src/theme.tsx CHANGED
@@ -41,14 +41,11 @@ export function initializeTheme(): void {
41
41
  useStore.setState({ currentThemeName: themeName })
42
42
  }
43
43
 
44
- // Proxy-based Theme object that reads from zustand state
45
- export const Theme: ResolvedTheme = new Proxy({} as ResolvedTheme, {
46
- get(_, prop: string) {
47
- const themeName = useStore.getState().currentThemeName
48
- const resolved = getResolvedTheme(themeName)
49
- return resolved[prop as keyof ResolvedTheme]
50
- },
51
- })
44
+ // Reactive hook for theme - use this in React components
45
+ export function useTheme(): ResolvedTheme {
46
+ const themeName = useStore((state) => state.currentThemeName)
47
+ return getResolvedTheme(themeName)
48
+ }
52
49
 
53
50
  export function getMarkdownSyntaxStyle(): SyntaxStyle {
54
51
  const themeName = useStore.getState().currentThemeName
@@ -84,4 +81,4 @@ export const markdownSyntaxStyle = new Proxy({} as SyntaxStyle, {
84
81
  },
85
82
  })
86
83
 
87
- export default Theme
84
+
@@ -1,11 +1,14 @@
1
1
  import React from 'react'
2
- import { cac } from 'cac'
2
+ import { goke } from 'goke'
3
3
  import { useStore } from 'termcast/src/state'
4
4
  import { showToast, Toast } from 'termcast/src/apis/toast'
5
5
  import { LocalStorage } from 'termcast/src/apis/localstorage'
6
6
  import { CommandArguments } from 'termcast/src/components/command-arguments'
7
7
  import { ExtensionPreferences } from 'termcast/src/components/extension-preferences'
8
- import type { RaycastArgument, RaycastPackageJson } from 'termcast/src/package-json'
8
+ import type {
9
+ RaycastArgument,
10
+ RaycastPackageJson,
11
+ } from 'termcast/src/package-json'
9
12
  import type { LaunchProps } from 'termcast/src/apis/environment'
10
13
  import { logger } from '../logger'
11
14
 
@@ -25,14 +28,16 @@ export interface ParsedExtensionArgs {
25
28
  * Parse CLI args to extract command name and help flag.
26
29
  * @param skipArgv - Number of subcommand args to skip (e.g. 1 for "dev" in "termcast dev")
27
30
  */
28
- export function parseExtensionArgs({ skipArgv = 0 }: { skipArgv?: number } = {}): ParsedExtensionArgs {
29
- // Build argv for cac: keep first 2 (binary + script), skip subcommand args, keep the rest
31
+ export function parseExtensionArgs({
32
+ skipArgv = 0,
33
+ }: { skipArgv?: number } = {}): ParsedExtensionArgs {
34
+ // Build argv for goke: keep first 2 (binary + script), skip subcommand args, keep the rest
30
35
  const argv = [
31
36
  process.argv[0],
32
37
  process.argv[1],
33
38
  ...process.argv.slice(2 + skipArgv),
34
39
  ]
35
- const parsed = cac().parse(argv, { run: false })
40
+ const parsed = goke().parse(argv, { run: false })
36
41
  return {
37
42
  commandName: parsed.args[0] as string | undefined,
38
43
  showHelp: Boolean(parsed.options.help || parsed.options.h),
@@ -183,9 +188,26 @@ export async function runCommand(options: RunCommandOptions): Promise<void> {
183
188
  const devRebuildCount = state.devRebuildCount + 1
184
189
  useStore.setState({ devRebuildCount })
185
190
  const importPath = `${bundledPath}?rebuild=${devRebuildCount}`
186
- // logger.log(`importing ${importPath}`)
187
- const module = await import(importPath)
188
- CommandComponent = module.default
191
+ logger.log(`importing ${importPath}`)
192
+ try {
193
+ const module = await import(importPath)
194
+ CommandComponent = module.default
195
+ } catch (error) {
196
+ logger.error('Failed to import command module:', {
197
+ importPath,
198
+ error: error instanceof Error ? error.message : String(error),
199
+ stack: error instanceof Error ? error.stack : undefined,
200
+ })
201
+ await showToast({
202
+ style: Toast.Style.Failure,
203
+ title: 'Failed to load command',
204
+ message:
205
+ error instanceof Error
206
+ ? error.message
207
+ : 'Unknown error ' + String(error).slice(0, 300),
208
+ })
209
+ return
210
+ }
189
211
 
190
212
  if (!CommandComponent) {
191
213
  await showToast({
@@ -230,7 +252,9 @@ export async function runCommand(options: RunCommandOptions): Promise<void> {
230
252
  }
231
253
 
232
254
  // For view commands, push the component
255
+ logger.log('pushing WrappedComponent')
233
256
  push(<CommandComponent {...launchProps} />)
257
+ logger.log('pushed WrappedComponent')
234
258
  }
235
259
 
236
260
  async function checkRequiredPreferences({
package/src/utils.tsx CHANGED
@@ -19,9 +19,12 @@ export async function renderWithProviders(element: ReactNode): Promise<void> {
19
19
  process.exit(0)
20
20
  },
21
21
  })
22
+
22
23
  createRoot(renderer).render(<TermcastProvider>{element}</TermcastProvider>)
23
24
  }
24
25
 
26
+ export const termcastMaxContentWidth = 140
27
+
25
28
  export type CommonProps = {
26
29
  key?: any
27
30
  }
@@ -41,29 +44,61 @@ export function sleep(ms: number): Promise<void> {
41
44
  }
42
45
 
43
46
  export async function getApplications(path?: PathLike): Promise<Application[]> {
44
- // TODO: Implement system call to get applications
45
- // For now, return common applications
46
- const apps: Application[] = [
47
- {
48
- name: 'Finder',
49
- localizedName: 'Finder',
50
- path: '/System/Library/CoreServices/Finder.app',
51
- bundleId: 'com.apple.finder',
52
- },
53
- {
54
- name: 'Terminal',
55
- localizedName: 'Terminal',
56
- path: '/System/Applications/Utilities/Terminal.app',
57
- bundleId: 'com.apple.Terminal',
58
- },
59
- {
60
- name: 'Visual Studio Code',
61
- localizedName: 'Visual Studio Code',
62
- path: '/Applications/Visual Studio Code.app',
63
- bundleId: 'com.microsoft.VSCode',
64
- },
65
- ]
66
- return Promise.resolve(apps)
47
+ if (process.platform !== 'darwin') {
48
+ // Windows/Linux: return empty for now
49
+ return []
50
+ }
51
+
52
+ const { execSync } = await import('node:child_process')
53
+ const plist = await import('simple-plist')
54
+
55
+ const apps: Application[] = []
56
+
57
+ try {
58
+ // Use mdfind (Spotlight) to find all .app bundles
59
+ // This is much faster than scanning directories manually
60
+ const appPaths = execSync(
61
+ 'mdfind "kMDItemContentType == \'com.apple.application-bundle\'" 2>/dev/null || find /Applications /System/Applications -name "*.app" -maxdepth 3 2>/dev/null',
62
+ { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 },
63
+ )
64
+ .trim()
65
+ .split('\n')
66
+ .filter((p) => p && p.endsWith('.app'))
67
+
68
+ for (const appPath of appPaths) {
69
+ try {
70
+ const infoPlistPath = `${appPath}/Contents/Info.plist`
71
+
72
+ if (!fs.existsSync(infoPlistPath)) {
73
+ continue
74
+ }
75
+
76
+ const plistData = plist.default.readFileSync(infoPlistPath) as Record<
77
+ string,
78
+ unknown
79
+ >
80
+ const bundleId = plistData.CFBundleIdentifier as string | undefined
81
+ const name = (plistData.CFBundleName ||
82
+ plistData.CFBundleDisplayName ||
83
+ appPath.split('/').pop()?.replace('.app', '')) as string
84
+ const localizedName = (plistData.CFBundleDisplayName || name) as string
85
+
86
+ apps.push({
87
+ name,
88
+ localizedName,
89
+ path: appPath,
90
+ bundleId,
91
+ })
92
+ } catch {
93
+ // Skip apps with unreadable plist files
94
+ continue
95
+ }
96
+ }
97
+ } catch (error) {
98
+ logger.error('Failed to get applications:', error)
99
+ }
100
+
101
+ return apps
67
102
  }
68
103
 
69
104
  export async function getDefaultApplication(