@gloom-sh/terminal 0.19.2 → 0.19.3

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 (472) hide show
  1. package/README.md +7 -1
  2. package/package.json +1 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +20 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app.tsx +23 -1
  20. package/src/architecture/core-builtin-allowlist.json +2 -1
  21. package/src/capabilities/factories.ts +1 -0
  22. package/src/cli/ascii-output.ts +42 -0
  23. package/src/cli/base-converter.ts +10 -2
  24. package/src/cli/command-options.ts +16 -1
  25. package/src/cli/commands/crypto-hints.ts +23 -4
  26. package/src/cli/commands/market.ts +92 -23
  27. package/src/cli/commands/news-search.ts +24 -5
  28. package/src/cli/commands/overview.ts +9 -4
  29. package/src/cli/commands/search.ts +71 -12
  30. package/src/cli/commands/system.ts +22 -30
  31. package/src/cli/commands/ticker.ts +49 -9
  32. package/src/cli/commands/version.ts +33 -0
  33. package/src/cli/context.ts +5 -9
  34. package/src/cli/data-dir.ts +7 -0
  35. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  36. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  37. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  38. package/src/cli/desktop-pane-shot.ts +196 -256
  39. package/src/cli/early-command.ts +42 -0
  40. package/src/cli/earnings-calendar.ts +9 -0
  41. package/src/cli/entry.ts +19 -13
  42. package/src/cli/errors.ts +12 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/history-export.ts +22 -2
  45. package/src/cli/history-rows.ts +154 -4
  46. package/src/cli/index.ts +18 -1
  47. package/src/cli/known-gaps.ts +40 -5
  48. package/src/cli/listing-arg.ts +46 -9
  49. package/src/cli/options.ts +1 -1
  50. package/src/cli/output-file.ts +60 -13
  51. package/src/cli/pane-functions/catalog.ts +14 -3
  52. package/src/cli/pane-functions/data.ts +1 -1
  53. package/src/cli/pane-functions/dom.ts +45 -0
  54. package/src/cli/pane-functions/freshness.ts +46 -9
  55. package/src/cli/pane-functions/function-help.ts +17 -9
  56. package/src/cli/pane-functions/headless.ts +22 -14
  57. package/src/cli/pane-functions/index.ts +6 -2
  58. package/src/cli/pane-functions/options.ts +6 -2
  59. package/src/cli/pane-functions/resolver.ts +27 -5
  60. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  61. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  62. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  63. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  64. package/src/cli/quote-answer.ts +20 -0
  65. package/src/cli/registry.ts +41 -13
  66. package/src/cli/result.ts +12 -6
  67. package/src/cli/ticker-resolution.ts +2 -1
  68. package/src/cli/types.ts +2 -0
  69. package/src/components/chart/composite/format.ts +3 -0
  70. package/src/components/chart-table/header.tsx +1 -1
  71. package/src/components/chart-table/layout.ts +28 -11
  72. package/src/components/command-bar/action-runtime.ts +4 -0
  73. package/src/components/command-bar/commands/direct/index.ts +18 -33
  74. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  75. package/src/components/command-bar/commands/registry.ts +10 -1
  76. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  77. package/src/components/command-bar/routes/root/runtime.ts +24 -1
  78. package/src/components/command-bar/surface/index.tsx +9 -3
  79. package/src/components/company-logo.tsx +2 -1
  80. package/src/components/layout/header.tsx +8 -3
  81. package/src/components/layout/market-summary.ts +25 -7
  82. package/src/components/layout/pane/footer/index.tsx +7 -2
  83. package/src/components/layout/pane/footer/model.ts +40 -5
  84. package/src/components/market-board/index.tsx +3 -2
  85. package/src/components/onboarding/desks-step.tsx +12 -4
  86. package/src/components/onboarding/first-run-workspace.ts +81 -4
  87. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  88. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  89. package/src/components/onboarding/onboarding-wizard.tsx +54 -23
  90. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  91. package/src/components/onboarding/run-setup-again.ts +24 -0
  92. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  93. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  94. package/src/components/onboarding/wizard-model.ts +57 -3
  95. package/src/components/onboarding/wizard-positions.ts +1 -1
  96. package/src/components/ui/delayed-mark.tsx +56 -0
  97. package/src/components/ui/external-link.tsx +3 -6
  98. package/src/components/ui/icon.tsx +8 -1
  99. package/src/components/ui/logo-mark.tsx +8 -5
  100. package/src/components/ui/stat-grid.tsx +263 -19
  101. package/src/data/config/store/node.ts +11 -5
  102. package/src/data/config/store/normalize.ts +16 -3
  103. package/src/data/profile-permissions.ts +47 -0
  104. package/src/data/resource-store.ts +34 -31
  105. package/src/data/sqlite/access.ts +19 -8
  106. package/src/data/sqlite/checkpointer.ts +95 -8
  107. package/src/data/sqlite/database.ts +40 -7
  108. package/src/docs/snapshot.json +191 -78
  109. package/src/i18n/es.ts +18 -2
  110. package/src/i18n/ja.ts +18 -2
  111. package/src/i18n/ko.ts +18 -2
  112. package/src/i18n/zh-cn.ts +18 -2
  113. package/src/i18n/zh-tw.ts +18 -2
  114. package/src/layout/desk-keys.ts +4 -1
  115. package/src/layout/desks.ts +38 -5
  116. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  117. package/src/market-data/batched-reads.ts +16 -0
  118. package/src/market-data/coordinator/auxiliary.ts +4 -2
  119. package/src/market-data/coordinator/index.ts +11 -2
  120. package/src/market-data/futures-board.ts +1 -1
  121. package/src/market-data/futures-contract.ts +3 -1
  122. package/src/market-data/market/index-level.ts +6 -4
  123. package/src/market-data/published-apac-sessions.ts +6 -0
  124. package/src/news/aggregator.ts +7 -4
  125. package/src/news/news-model.ts +11 -0
  126. package/src/news/story-id.ts +22 -0
  127. package/src/news/ticker-symbols.ts +17 -1
  128. package/src/notifications/app-notifier.ts +35 -14
  129. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  130. package/src/plugins/builtin/account-management/index.tsx +2 -3
  131. package/src/plugins/builtin/account-management/model.ts +1 -1
  132. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  133. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  134. package/src/plugins/builtin/alerts/events.ts +10 -2
  135. package/src/plugins/builtin/alerts/index.tsx +5 -5
  136. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  137. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  138. package/src/plugins/builtin/alerts/targets.ts +4 -1
  139. package/src/plugins/builtin/attention/headless.ts +1 -1
  140. package/src/plugins/builtin/attention/index.tsx +2 -4
  141. package/src/plugins/builtin/attention/listing.ts +1 -1
  142. package/src/plugins/builtin/attention/series.ts +1 -1
  143. package/src/plugins/builtin/awards/index.tsx +1 -2
  144. package/src/plugins/builtin/backtest/client.ts +1 -0
  145. package/src/plugins/builtin/backtest/index.tsx +1 -2
  146. package/src/plugins/builtin/backtest/rules.ts +3 -1
  147. package/src/plugins/builtin/backtest/window.ts +8 -7
  148. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  149. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  150. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  151. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  152. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  153. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  154. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  155. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  156. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  157. package/src/plugins/builtin/buildout/format.ts +9 -0
  158. package/src/plugins/builtin/buildout/index.tsx +51 -5
  159. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  160. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  161. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  162. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  163. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  164. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  165. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  166. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  167. package/src/plugins/builtin/cds/headless.ts +10 -2
  168. package/src/plugins/builtin/cds/model.ts +7 -3
  169. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  170. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  171. package/src/plugins/builtin/changelog/index.tsx +3 -2
  172. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
  173. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  174. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  175. package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
  176. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  177. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  178. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  179. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  180. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  181. package/src/plugins/builtin/chat/index.tsx +5 -5
  182. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  183. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  184. package/src/plugins/builtin/cloud/index.tsx +1 -2
  185. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  186. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  187. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  188. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  189. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  190. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  191. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  192. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  193. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  194. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  195. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  196. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  197. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  198. package/src/plugins/builtin/connections/index.ts +2 -3
  199. package/src/plugins/builtin/cot/index.tsx +1 -2
  200. package/src/plugins/builtin/cpi/index.tsx +1 -2
  201. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  202. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  203. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  204. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  205. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  206. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  207. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  208. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  209. package/src/plugins/builtin/crypto-board/series.ts +2 -0
  210. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  211. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  212. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  213. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  214. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  215. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  216. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  217. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  218. package/src/plugins/builtin/debug/index.tsx +1 -1
  219. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  220. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  221. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  222. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  223. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  224. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  225. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  226. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  227. package/src/plugins/builtin/docs/index.tsx +2 -3
  228. package/src/plugins/builtin/doe/headless.ts +16 -3
  229. package/src/plugins/builtin/doe/index.tsx +5 -4
  230. package/src/plugins/builtin/earnings/board.tsx +2 -1
  231. package/src/plugins/builtin/earnings/history.tsx +2 -1
  232. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  233. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  234. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  235. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  236. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  237. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  238. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  239. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  240. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  241. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  242. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  243. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  244. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  245. package/src/plugins/builtin/exposure/index.tsx +1 -2
  246. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  247. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  248. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  249. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  250. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  251. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  252. package/src/plugins/builtin/futures/index.tsx +15 -0
  253. package/src/plugins/builtin/futures/model.ts +14 -0
  254. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  255. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  256. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  257. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  258. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  259. package/src/plugins/builtin/gpu/board.tsx +51 -12
  260. package/src/plugins/builtin/gpu/client.ts +15 -2
  261. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  262. package/src/plugins/builtin/gpu/headless.ts +14 -2
  263. package/src/plugins/builtin/gpu/index.tsx +1 -2
  264. package/src/plugins/builtin/gpu/model.ts +20 -2
  265. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  266. package/src/plugins/builtin/help/index.tsx +1 -1
  267. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  268. package/src/plugins/builtin/holders/index.tsx +2 -3
  269. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  270. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  271. package/src/plugins/builtin/jodi/index.tsx +1 -2
  272. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  273. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  274. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  275. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  276. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  277. package/src/plugins/builtin/market-movers/index.tsx +7 -1
  278. package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
  279. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  280. package/src/plugins/builtin/members/index.tsx +1 -2
  281. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  282. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  283. package/src/plugins/builtin/news/headless.ts +7 -0
  284. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  285. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  286. package/src/plugins/builtin/notes/index.tsx +3 -6
  287. package/src/plugins/builtin/openfda/index.tsx +1 -2
  288. package/src/plugins/builtin/options/view.tsx +6 -5
  289. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  290. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  291. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  292. package/src/plugins/builtin/perps/index.tsx +1 -2
  293. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  294. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  295. package/src/plugins/builtin/plugin-module.ts +16 -2
  296. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  297. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  298. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  299. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  300. package/src/plugins/builtin/power/index.tsx +1 -2
  301. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  302. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  303. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  304. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  305. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  306. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  307. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  308. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  309. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  310. package/src/plugins/builtin/research/index.tsx +5 -8
  311. package/src/plugins/builtin/research-search/index.tsx +1 -2
  312. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  313. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  314. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  315. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  316. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  317. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  318. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  319. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  320. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  321. package/src/plugins/builtin/sec/headless.ts +5 -1
  322. package/src/plugins/builtin/sec/index.tsx +4 -2
  323. package/src/plugins/builtin/sec/model.ts +2 -0
  324. package/src/plugins/builtin/sectors/index.tsx +11 -5
  325. package/src/plugins/builtin/series-inventory.ts +2 -0
  326. package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
  327. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  328. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  329. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  330. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  331. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  332. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  333. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  334. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  335. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  336. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  337. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  338. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  339. package/src/plugins/builtin/thirteenf/api.ts +116 -13
  340. package/src/plugins/builtin/thirteenf/data.ts +132 -30
  341. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  342. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  343. package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
  344. package/src/plugins/builtin/thirteenf/types.ts +12 -0
  345. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
  346. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  347. package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
  348. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  349. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  350. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  351. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  352. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  353. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  354. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  355. package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  357. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  358. package/src/plugins/builtin/volatility/index.tsx +1 -2
  359. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  360. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  361. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  362. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  363. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  364. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  365. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  366. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  367. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  368. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  369. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  370. package/src/plugins/bundle.ts +52 -2
  371. package/src/plugins/catalog-browser.ts +2 -2
  372. package/src/plugins/catalog-cli.ts +20 -0
  373. package/src/plugins/host-link.ts +13 -1
  374. package/src/plugins/installer.ts +15 -4
  375. package/src/plugins/loader.ts +43 -11
  376. package/src/plugins/peer-imports.ts +201 -0
  377. package/src/plugins/peer-retry.ts +26 -29
  378. package/src/plugins/plugin-process.ts +128 -0
  379. package/src/plugins/redirection-guard.ts +45 -0
  380. package/src/remote/endpoint.ts +16 -3
  381. package/src/renderers/browser/main.tsx +3 -2
  382. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  383. package/src/renderers/dom/desktop/icons.tsx +2 -0
  384. package/src/renderers/dom/host/image-surface.tsx +46 -21
  385. package/src/renderers/dom/styles.css +7 -0
  386. package/src/renderers/dom/toast-host.tsx +2 -0
  387. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  388. package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
  389. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  390. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  391. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  392. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  393. package/src/renderers/electrobun/bun/window/create.ts +4 -2
  394. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  395. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  396. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  397. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  398. package/src/renderers/electrobun/view/main.tsx +7 -2
  399. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  400. package/src/renderers/opentui/browser-open.ts +10 -4
  401. package/src/renderers/opentui/image/surface.tsx +1 -1
  402. package/src/renderers/opentui/start.tsx +12 -6
  403. package/src/renderers/opentui/toast-host.tsx +2 -1
  404. package/src/series/registry.ts +18 -0
  405. package/src/series/standalone.ts +10 -1
  406. package/src/series/types.ts +7 -0
  407. package/src/series/validate.ts +11 -2
  408. package/src/sources/gloomberb-cloud/index.ts +28 -5
  409. package/src/sources/gloomberb-cloud/news.ts +24 -3
  410. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  411. package/src/sources/provider-errors.ts +26 -3
  412. package/src/sources/provider-router/history.ts +5 -0
  413. package/src/sources/provider-router/index.ts +13 -0
  414. package/src/sync/core-contributors.ts +32 -0
  415. package/src/telemetry/attention-counts.ts +64 -6
  416. package/src/telemetry/attention-setting.ts +33 -0
  417. package/src/telemetry/crash-reports-node.ts +3 -1
  418. package/src/telemetry/stall-clocks.ts +93 -0
  419. package/src/telemetry/stall-reports-node.ts +12 -7
  420. package/src/telemetry/stall-reports.ts +34 -5
  421. package/src/test-support/preload.ts +9 -0
  422. package/src/tickers/company-name.ts +19 -0
  423. package/src/tickers/listing-fate.ts +14 -3
  424. package/src/tickers/saved-instrument.ts +17 -0
  425. package/src/tickers/search/home-listing.ts +13 -2
  426. package/src/tickers/search/index.ts +4 -1
  427. package/src/tickers/search/types.ts +6 -1
  428. package/src/time-series/bar-aggregation.ts +7 -1
  429. package/src/time-series/base-series.ts +3 -3
  430. package/src/time-series/chart-bounds.ts +1 -1
  431. package/src/time-series/chart-data.ts +5 -2
  432. package/src/time-series/chart-seed.ts +1 -1
  433. package/src/time-series/coin-session-close.ts +211 -0
  434. package/src/time-series/live-bars.ts +13 -2
  435. package/src/time-series/live-quotes.ts +1 -1
  436. package/src/time-series/official-close.ts +159 -0
  437. package/src/time-series/price-comparison.ts +55 -6
  438. package/src/time-series/resolve.ts +12 -3
  439. package/src/time-series/series-presentation.ts +1 -1
  440. package/src/time-series/studies/formula.ts +205 -0
  441. package/src/time-series/studies/indicators.ts +299 -0
  442. package/src/time-series/studies/leverage.ts +75 -0
  443. package/src/time-series/studies/pair.ts +138 -0
  444. package/src/time-series/studies/resolve.ts +333 -0
  445. package/src/time-series/studies/shared.ts +141 -0
  446. package/src/time-series/studies/units.ts +243 -0
  447. package/src/time-series/studies/vwap.ts +274 -0
  448. package/src/types/config.ts +18 -2
  449. package/src/types/data-provider.ts +23 -0
  450. package/src/types/financials.ts +22 -0
  451. package/src/types/headless.ts +42 -3
  452. package/src/types/plugin.ts +32 -3
  453. package/src/ui/host.tsx +11 -0
  454. package/src/ui/toast.tsx +26 -1
  455. package/src/utils/ascii-text.ts +140 -0
  456. package/src/utils/cli-output.ts +91 -18
  457. package/src/utils/concurrency.ts +24 -0
  458. package/src/utils/exchanges.ts +6 -0
  459. package/src/utils/external-url.ts +20 -7
  460. package/src/utils/futures-generic.ts +4 -4
  461. package/src/utils/http-proxy-upstream.ts +124 -0
  462. package/src/utils/main-thread-stall.ts +75 -3
  463. package/src/utils/perf-marks.ts +36 -2
  464. package/src/utils/private-files.ts +141 -0
  465. package/src/utils/public-address.ts +99 -0
  466. package/src/account/upgrade-personalization.ts +0 -57
  467. package/src/account/upgrade-tickers.ts +0 -124
  468. package/src/api-client/upgrade-experiment.ts +0 -91
  469. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  470. package/src/renderers/electrobun/launcher/preload.ts +0 -13
  471. package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
  472. package/src/time-series/studies.ts +0 -1628
@@ -127,6 +127,8 @@ export interface HeadlessPaneContext {
127
127
  capabilities?: CapabilityInvoker;
128
128
  /** The catalog's name for the series charted as an expression (`FRED:DFII10`); undefined for one it does not list. */
129
129
  catalogSeriesName?: (expression: string) => string | undefined;
130
+ /** The catalog series a bare word is an alias of (`hashrate` is CAP:bitcoin:hashrate); undefined for any other word. */
131
+ catalogSeriesNamed?: (word: string) => { expression: string; name: string } | undefined;
130
132
  /** Locally registered broker adapters. Reports may read account data; they never submit orders. */
131
133
  resolveBroker?: (brokerType: string) => import("./broker").BrokerAdapter | null;
132
134
  /** Read one local portfolio without passing holdings through a remote endpoint. */
@@ -149,7 +151,9 @@ export interface HeadlessPaneContext {
149
151
  * `coinNote` is what `quote` says of a bare coin ticker read as a listing
150
152
  * that is not the coin (ETH, the Grayscale fund), for the report's warnings.
151
153
  */
152
- resolveListing?: (symbol: string, argumentWith: (key: string) => string) => Promise<{ symbol: string; exchange?: string; name?: string; coinNote?: string } | null>;
154
+ resolveListing?: (symbol: string, argumentWith: (key: string) => string) => Promise<{ symbol: string; exchange?: string; name?: string; coinNote?: string;
155
+ /** What became of the company that had the symbol, as `quote` says it ("GOGL was Golden Ocean Group until 2025, now Cmb.Tech: CMBT"). */
156
+ successorNote?: string } | null>;
153
157
  /**
154
158
  * What a plugin keeps on this device only (its `ctx.resume` state, never
155
159
  * synced), such as ALRT's log of the alerts the app fired; null when none.
@@ -191,6 +195,13 @@ export interface HeadlessPaneColumn {
191
195
  stackRows?: number;
192
196
  /** The text table leaves the column out when fitted to fewer terminal columns than this; exports and piped output keep it. */
193
197
  minTableWidth?: number;
198
+ /**
199
+ * The narrow text table drops the column, rightmost first, before it moves any column under its rows: a figure
200
+ * the others imply (RETURN's interval $ beside its prices). Exports and a table that fits keep it.
201
+ */
202
+ optional?: boolean;
203
+ /** Among optional columns, a higher priority is dropped sooner. */
204
+ dropPriority?: number;
194
205
  description?: string;
195
206
  format?: (value: unknown, row: HeadlessPaneRow) => string;
196
207
  /** What the text report prints when it can say more than an export should (a unit word beside a number); CSV and NDJSON keep `format`. */
@@ -209,9 +220,10 @@ export interface HeadlessPaneColumn {
209
220
  /**
210
221
  * The text report prints the value on its own lines under the row instead
211
222
  * of in a column, for a footnote that belongs to that row; an empty value
212
- * prints nothing. Exports keep it as a column.
223
+ * prints nothing. Exports keep it as a column. `"line"`: one line cut to
224
+ * the width, never wrapped (a story's summary under its headline).
213
225
  */
214
- underRow?: boolean;
226
+ underRow?: boolean | "line";
215
227
  }
216
228
 
217
229
  export interface HeadlessPaneEntry {
@@ -284,6 +296,13 @@ export interface HeadlessPaneFreshness {
284
296
  delayMinutes?: number;
285
297
  /** What not-a-feed data is, in a few words: "filed data", "monthly release", "your inputs". */
286
298
  basis?: string;
299
+ /**
300
+ * The pane shows nothing from a market or a feed: the user's own notes and
301
+ * rules, the app's connections, documentation, an account. A screenshot's
302
+ * footer then carries no status line, which would date nothing or call the
303
+ * app's own state "not a live feed". `fn` still cites `source` and `basis`.
304
+ */
305
+ local?: boolean;
287
306
  /**
288
307
  * How often the data is published, when its dates mark the start of a period
289
308
  * (a monthly statistic is dated the 1st). The report then names the month,
@@ -403,12 +422,24 @@ export interface HeadlessPaneFreshness {
403
422
  * `newest story` dates a news list `newest story Tue 29 Sep 2026 20:17 UTC`.
404
423
  */
405
424
  asOfLabel?: string;
425
+ /**
426
+ * The newest observation is a bar of this many minutes that opens at the
427
+ * as-of: the line names its span, `last bar 15:00–16:00 UTC`, so the bar's
428
+ * open is not read as how far the data runs.
429
+ */
430
+ barMinutes?: number;
406
431
  /**
407
432
  * Say when the rows were retrieved too, after the newest one's date: a list
408
433
  * dated by its newest filing was not read then (`newest filing Tue 27 Oct
409
434
  * 2026 · retrieved Wed 28 Oct 2026 19:07 UTC`).
410
435
  */
411
436
  retrievedToo?: boolean;
437
+ /**
438
+ * When the source last read the data, when that is not now: a page the
439
+ * server reads hourly is retrieved when it read it, not when the report
440
+ * asked the server. An ISO instant; ignored when in the future.
441
+ */
442
+ retrievedAt?: string;
412
443
  /** What the figures are, said after the status: `funding = current estimate, pays 08:00 UTC`. */
413
444
  note?: string;
414
445
  /**
@@ -504,6 +535,13 @@ export interface HeadlessRowsResult extends HeadlessPaneResultBase {
504
535
  export type HeadlessBundleSection =
505
536
  | {
506
537
  title: string;
538
+ /**
539
+ * The heading the text report prints in place of `title`, when it can say
540
+ * more than an export's section name should (the scale its amounts are
541
+ * printed in, "Total revenue, USD millions"). JSON, CSV and NDJSON keep
542
+ * `title`, so a section read by its name stays where it was.
543
+ */
544
+ textTitle?: string;
507
545
  columns?: HeadlessPaneColumn[];
508
546
  rows: HeadlessPaneRow[];
509
547
  /** What the report says when `rows` is empty, in place of "No data.": why there are none, and what to run instead. */
@@ -576,6 +614,7 @@ export type HeadlessBundleSection =
576
614
  /** In a report with `exportDated`: the period the figures cover, and when they were released (an ISO date or instant), which each exported line names in `period` and `released`. */
577
615
  period?: string;
578
616
  released?: string;
617
+ textTitle?: never;
579
618
  columns?: never;
580
619
  rows?: never;
581
620
  textColumns?: never;
@@ -183,7 +183,7 @@ export interface PaneDef {
183
183
  * `headless` model) cites, when "Gloom Cloud" with the state the footer
184
184
  * shows would be wrong: local data, a calculator, filed records.
185
185
  */
186
- reportFreshness?: Pick<HeadlessPaneFreshness, "source" | "status" | "basis">;
186
+ reportFreshness?: Pick<HeadlessPaneFreshness, "source" | "status" | "basis" | "local">;
187
187
  /**
188
188
  * Options a rendered-view report takes (`fn OMON AAPL --strikes 10`):
189
189
  * each sets the pane setting of its key, is checked by its `normalize`, and
@@ -203,7 +203,14 @@ export interface PaneDef {
203
203
  * catalog lists it, and `fn` refuses an argument from a function that
204
204
  * declares none here, in its shortcut or in a `headless` model.
205
205
  */
206
- reportArgument?: Pick<HeadlessPaneArgumentDef, "kind" | "placeholder" | "description" | "optional">;
206
+ reportArgument?: Pick<HeadlessPaneArgumentDef, "kind" | "placeholder" | "description" | "optional"> & {
207
+ /**
208
+ * The argument names something the pane opens by its own id (the story
209
+ * pane's story id), never a symbol: `fn` does not look it up as a ticker.
210
+ * A query (X Feed's) leaves it unset and keeps its ticker reading.
211
+ */
212
+ identifier?: boolean;
213
+ };
207
214
  /**
208
215
  * Lines a rendered-view report prints above its table, from the settings it
209
216
  * ran with: what the default view leaves out and how to see more. `--json`
@@ -220,6 +227,12 @@ export interface PaneDef {
220
227
  * for the symbol (`lock.context` reads it), so the note may resolve later.
221
228
  */
222
229
  reportLockNote?(lock: PaneLock): string | Promise<string>;
230
+ /**
231
+ * The sign-in wall covers the whole pane whenever there is no Gloom Cloud
232
+ * session (ASKG): a rendered-view report without one prints its
233
+ * `reportLockNote` at once rather than render the wall to read it.
234
+ */
235
+ signInWall?: boolean;
223
236
  /**
224
237
  * What a rendered-view report needs before it shows anything, such as a
225
238
  * sign-in: `gloom catalog` lists it under Requires, as it does a
@@ -540,6 +553,13 @@ export interface PaneTemplateDef {
540
553
  listed?: boolean;
541
554
  /** Template-specific headless model. Takes precedence over the pane-level model. */
542
555
  headless?: HeadlessPaneDefinition;
556
+ /**
557
+ * A one-ticker template whose pane also opens on several at once (DVD's
558
+ * ranking): `fn` and `shot` pass a typed list (`shot DVD KO JNJ`) to
559
+ * createInstance as `symbols`. Any other one-ticker template gets the list
560
+ * as it always has, in `symbol` and `arg`.
561
+ */
562
+ opensTickerList?: boolean;
543
563
  /**
544
564
  * Source and status of the view this template opens, for a template that
545
565
  * opens one view of a pane hosting several (Fixed Income's Issuer Debt).
@@ -547,9 +567,16 @@ export interface PaneTemplateDef {
547
567
  * `reportFreshness`: each field set here wins, the rest are the pane's. So
548
568
  * they date the filings on screen, not the issuer's stock quote.
549
569
  */
550
- reportFreshness?: Pick<HeadlessPaneFreshness, "source" | "status" | "basis">;
570
+ reportFreshness?: Pick<HeadlessPaneFreshness, "source" | "status" | "basis" | "local">;
551
571
  wizard?: WizardStep[];
552
572
  canCreate?: (context: PaneTemplateContext, options?: PaneTemplateCreateOptions) => boolean;
573
+ /**
574
+ * Settings shown by catalog discovery without running createInstance. Supply
575
+ * these when opening a pane needs live data or has side effects. An empty
576
+ * object means there are no settings to describe before the pane is opened.
577
+ * When omitted, discovery derives settings from createInstance as before.
578
+ */
579
+ catalogSettings?: Record<string, unknown>;
553
580
  createInstance?: (
554
581
  context: PaneTemplateContext,
555
582
  options?: PaneTemplateCreateOptions,
@@ -962,6 +989,8 @@ export interface AppNotificationRequest {
962
989
  label: string;
963
990
  onClick: () => void;
964
991
  };
992
+ /** Runs once, when the in-app toast is first drawn. Not called when `toast` is false. */
993
+ onShown?: () => void;
965
994
  }
966
995
 
967
996
  export interface BrokerInstanceUpdateOptions {
package/src/ui/host.tsx CHANGED
@@ -375,6 +375,17 @@ export interface ImageSurfaceProps extends BoxProps {
375
375
  src?: string;
376
376
  alt?: string;
377
377
  objectFit?: "contain" | "cover";
378
+ /**
379
+ * Draw `children` until the image has loaded, not only without a `src` or
380
+ * once it fails, so a box that waits on a picture shows its stand-in rather
381
+ * than an empty one. The terminal always does.
382
+ */
383
+ fallbackWhileLoading?: boolean;
384
+ /**
385
+ * A colour behind the picture once it has loaded (a logo made for a light
386
+ * page), never behind `children`. The terminal draws none.
387
+ */
388
+ imageBackground?: string;
378
389
  }
379
390
  export interface MediaSurfaceProps extends BoxProps {
380
391
  src?: string;
package/src/ui/toast.tsx CHANGED
@@ -1,4 +1,5 @@
1
- import { createContext, useContext, type ComponentType, type ReactNode } from "react";
1
+ import { createContext, useContext, useEffect, useRef, type ComponentType, type ReactNode } from "react";
2
+ import { logFailure } from "../utils/log-failure";
2
3
 
3
4
  /** @public */
4
5
  export interface ToastAction {
@@ -14,6 +15,8 @@ export interface ToastOptions {
14
15
  action?: ToastAction;
15
16
  /** Rendered next to `action`. Use for a dismissing counterpart such as snooze. */
16
17
  secondaryAction?: ToastAction;
18
+ /** Runs once, after a viewport first draws the toast. */
19
+ onShown?(): void;
17
20
  }
18
21
 
19
22
  export interface ToastHost {
@@ -141,6 +144,28 @@ export function useToastHost(): ToastHost {
141
144
  return host;
142
145
  }
143
146
 
147
+ /**
148
+ * Every viewport calls this with the toasts it draws: each toast's `onShown`
149
+ * runs once, after the commit that first puts it on screen.
150
+ */
151
+ export function useToastsShown(toasts: readonly { id: number; options?: ToastOptions }[]): void {
152
+ const shown = useRef(new Set<number>());
153
+ useEffect(() => {
154
+ const current = new Set<number>();
155
+ for (const toast of toasts) {
156
+ current.add(toast.id);
157
+ if (shown.current.has(toast.id)) continue;
158
+ try {
159
+ toast.options?.onShown?.();
160
+ } catch (error) {
161
+ // One failing callback must not keep the others from running.
162
+ logFailure("toast", "A toast's shown callback failed")(error);
163
+ }
164
+ }
165
+ shown.current = current;
166
+ }, [toasts]);
167
+ }
168
+
144
169
  export function ToastViewport(props: { position?: string }) {
145
170
  const { Viewport } = useToastHost();
146
171
  return <Viewport {...props} />;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Text for a terminal that shows only ASCII: a C locale, or a Windows console
3
+ * on a legacy code page (437, 850, 1252), where UTF-8 bytes print as `—`.
4
+ * What the CLI writes itself keeps its meaning: a dash is `-`, the `·`
5
+ * between a source line's parts `|`, a price in euros `EUR211.25` (as one in
6
+ * kroner is `NOK393.90`) and one in US cents `447.50c`.
7
+ */
8
+
9
+ /** Spelled out before single characters, so the yuan's `CN¥` is not read as the yen's `¥`. */
10
+ const ASCII_SEQUENCES: ReadonlyArray<readonly [string, string]> = [
11
+ ["CN¥", "CNY"],
12
+ ["JP¥", "JPY"],
13
+ ];
14
+
15
+ const ASCII_SPELLINGS: Readonly<Record<string, string>> = {
16
+ // Dashes and the minus sign.
17
+ "\u2012": "-", "\u2013": "-", "\u2014": "-", "\u2015": "-", "\u2212": "-",
18
+ // Separators and marks.
19
+ "\u00B7": "|", "\u2022": "*", "\u2026": "...", "\u00A6": "|",
20
+ // Currencies, as their codes; a minor unit as its letter.
21
+ "\u20AC": "EUR", "\u00A3": "GBP", "\u00A5": "JPY", "\u00A2": "c", "\u20B9": "INR", "\u20A9": "KRW", "\u20AA": "ILS",
22
+ "\u20AB": "VND", "\u20B1": "PHP", "\u20BA": "TRY", "\u20BD": "RUB", "\u20A6": "NGN", "\u20B4": "UAH", "\u20B8": "KZT",
23
+ "\u20BF": "BTC", "\u0E3F": "THB",
24
+ // Quotes and primes.
25
+ "\u2018": "'", "\u2019": "'", "\u201A": "'", "\u201C": "\"", "\u201D": "\"", "\u201E": "\"", "\u2032": "'", "\u2033": "\"",
26
+ "\u00AB": "\"", "\u00BB": "\"",
27
+ // Arithmetic.
28
+ "\u00D7": "x", "\u00F7": "/", "\u00B1": "+/-", "\u2248": "~", "\u2264": "<=", "\u2265": ">=", "\u2260": "!=", "\u00B0": "deg",
29
+ "\u00BC": "1/4", "\u00BD": "1/2", "\u00BE": "3/4", "\u2030": "per mille", "\u00B5": "u",
30
+ // Arrows and markers.
31
+ "\u2192": "->", "\u2190": "<-", "\u2191": "^", "\u2193": "v", "\u25B2": "^", "\u25BC": "v", "\u21B5": "Enter",
32
+ "\u2713": "ok", "\u2714": "ok", "\u2717": "x", "\u2715": "x",
33
+ // Box drawing.
34
+ "\u2502": "|", "\u2500": "-", "\u2503": "|", "\u2501": "-",
35
+ // Spaces.
36
+ "\u00A0": " ", "\u2009": " ", "\u202F": " ", "\u2007": " ", "\u2002": " ", "\u2003": " ",
37
+ // Letters that do not decompose.
38
+ "\u00DF": "ss", "\u00E6": "ae", "\u00C6": "AE", "\u0153": "oe", "\u0152": "OE", "\u00F8": "o", "\u00D8": "O",
39
+ "\u0111": "d", "\u0110": "D", "\u0142": "l", "\u0141": "L", "\u00FE": "th", "\u00DE": "Th", "\u0131": "i",
40
+ // Signs.
41
+ "\u2122": "(TM)", "\u00AE": "(R)", "\u00A9": "(C)",
42
+ };
43
+
44
+ const NON_ASCII = /[^\x00-\x7F]/;
45
+ const COMBINING_MARKS = /[\u0300-\u036F]/g;
46
+
47
+ /**
48
+ * `text` in ASCII: each character above with its spelling, an accented letter
49
+ * without its accent (Société Générale is Societe Generale), a box or block
50
+ * drawing character as `|`, `-`, `+` or `#`, and anything else as `?`.
51
+ * Escape sequences (colour) are ASCII and pass through.
52
+ */
53
+ export function asciiText(text: string): string {
54
+ if (!NON_ASCII.test(text)) return text;
55
+ let folded = text;
56
+ for (const [sequence, spelling] of ASCII_SEQUENCES) folded = folded.split(sequence).join(spelling);
57
+ let out = "";
58
+ for (const character of folded) {
59
+ if (character.charCodeAt(0) < 0x80) {
60
+ out += character;
61
+ continue;
62
+ }
63
+ const spelled = ASCII_SPELLINGS[character];
64
+ if (spelled !== undefined) {
65
+ out += spelled;
66
+ continue;
67
+ }
68
+ const bare = character.normalize("NFD").replace(COMBINING_MARKS, "");
69
+ if (bare && !NON_ASCII.test(bare)) {
70
+ out += bare;
71
+ continue;
72
+ }
73
+ const code = character.codePointAt(0)!;
74
+ // Box drawing (U+2500-257F): corners and joins as +; block elements (U+2580-259F), a bar or sparkline's, as #.
75
+ out += code >= 0x2500 && code <= 0x257F ? "+" : code >= 0x2580 && code <= 0x259F ? "#" : "?";
76
+ }
77
+ return out;
78
+ }
79
+
80
+ type LocaleEnv = Readonly<Record<string, string | undefined>>;
81
+
82
+ /**
83
+ * The locale text is written in, by the variable POSIX reads first (LC_ALL,
84
+ * then LC_CTYPE, then LANG; an empty one is unset), or null when none is set.
85
+ */
86
+ function localeName(env: LocaleEnv): string | null {
87
+ return env.LC_ALL || env.LC_CTYPE || env.LANG || null;
88
+ }
89
+
90
+ /**
91
+ * Whether the terminal may not read UTF-8: a locale that names no UTF-8
92
+ * (`C`, `POSIX`, `en_US.ISO-8859-1`), or a Windows shell that sets none, whose
93
+ * console code page then decides (`consoleReadsUtf8`). A locale that is UTF-8,
94
+ * or none set elsewhere, is taken to read it.
95
+ */
96
+ export function localeMayNotReadUtf8(env: LocaleEnv = process.env, platform: string = process.platform): boolean {
97
+ const locale = localeName(env);
98
+ if (locale) return !/utf-?8/i.test(locale);
99
+ return platform === "win32";
100
+ }
101
+
102
+ /**
103
+ * Where Windows keeps `chcp.com`. Windows looks a bare program name up in the
104
+ * current folder before the system's, so a `chcp.com` left in the folder a
105
+ * command runs in would run in its place; the full path never does.
106
+ */
107
+ export function windowsChcpPath(env: Readonly<Record<string, string | undefined>> = process.env): string {
108
+ const root = [env.SystemRoot, env.SYSTEMROOT, env.windir, env.WINDIR]
109
+ .find((dir): dir is string => !!dir && /^[A-Za-z]:[\\/]/.test(dir)) ?? "C:\\Windows";
110
+ return `${root.replace(/[\\/]+$/, "")}\\System32\\chcp.com`;
111
+ }
112
+
113
+ /** The console's code page as `chcp` prints it ("Active code page: 437"), or null when it cannot be read. */
114
+ function consoleCodePage(): number | null {
115
+ try {
116
+ if (typeof Bun === "undefined" || process.platform !== "win32") return null;
117
+ const answer = Bun.spawnSync([windowsChcpPath()], { stdout: "pipe", stderr: "ignore" });
118
+ const page = /(\d+)\D*$/.exec(answer.stdout.toString().trim());
119
+ return page ? Number(page[1]) : null;
120
+ } catch {
121
+ // Intentional ignore: a console whose code page cannot be read is left to show what it gets.
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Whether the terminal reads UTF-8: by the locale when one is set, else on
128
+ * Windows by the console's code page (65001 is UTF-8). A code page that
129
+ * cannot be read is taken to read it, so nothing changes where it is unknown.
130
+ */
131
+ export function localeReadsUtf8(
132
+ env: LocaleEnv = process.env,
133
+ platform: string = process.platform,
134
+ codePage: () => number | null = consoleCodePage,
135
+ ): boolean {
136
+ if (!localeMayNotReadUtf8(env, platform)) return true;
137
+ if (localeName(env)) return false;
138
+ const page = codePage();
139
+ return page == null || page === 65001;
140
+ }
@@ -1,3 +1,4 @@
1
+ import { asciiText, localeReadsUtf8 } from "./ascii-text";
1
2
  type CliAlign = "left" | "right" | "center";
2
3
 
3
4
  export interface CliTableColumn {
@@ -45,6 +46,8 @@ export interface CliTableOptions {
45
46
  indent?: number;
46
47
  /** Lines printed under each row, by row index, indented and muted: a footnote that belongs to the row. */
47
48
  below?: ReadonlyArray<readonly string[] | undefined>;
49
+ /** Each line under a row stays one line, cut to the table's width, rather than wrapping: a story's summary. */
50
+ cutBelow?: boolean;
48
51
  }
49
52
 
50
53
  export type CliStatEntry = readonly [label: string, value: string];
@@ -73,7 +76,12 @@ const NUMBER_JOINER = /^(?:\/|·|→|↑|↓|[-–]|to|up|down)$/;
73
76
  const PLAIN_EMPTY_CELL = "—";
74
77
  let colorEnabledOverride: boolean | null = null;
75
78
  let widthOverride: number | null = null;
79
+ /** The widths `COLUMNS` is taken at for piped output, as `--width` takes them; anything else is ignored. */
80
+ const MIN_COLUMNS_VARIABLE = 20;
81
+ const MAX_COLUMNS_VARIABLE = 1000;
76
82
  let plainOverride: boolean | null = null;
83
+ /** Whether the terminal reads only ASCII (`localeReadsUtf8`); undefined until first asked. */
84
+ let asciiLocale: boolean | undefined;
77
85
  /** Output is being rendered for a file (`renderForFile`), which has no terminal width. */
78
86
  let renderingForFile = false;
79
87
 
@@ -143,6 +151,23 @@ export function cliPlainEnabled(): boolean {
143
151
  return plainOverride === true;
144
152
  }
145
153
 
154
+ /** Tests say whether the terminal reads only ASCII; null asks the environment again. */
155
+ export function setCliAsciiLocaleOverride(value: boolean | null): void {
156
+ asciiLocale = value ?? undefined;
157
+ }
158
+
159
+ /**
160
+ * Plain output to a terminal that cannot show UTF-8 (a C locale, a Windows
161
+ * code page such as 437): it is written in ASCII (`asciiText`), so a dash, a
162
+ * euro or a cent sign does not print as `—`. Output rendered for a file
163
+ * keeps its UTF-8.
164
+ */
165
+ export function cliAsciiEnabled(): boolean {
166
+ if (plainOverride !== true || renderingForFile) return false;
167
+ asciiLocale ??= !localeReadsUtf8();
168
+ return asciiLocale;
169
+ }
170
+
146
171
  /**
147
172
  * Whether output is plain without `--plain` or `--no-plain`: NO_COLOR is set
148
173
  * to a value (an empty one is unset, as the convention says), or stdout is
@@ -185,14 +210,20 @@ export function plainWords(text: string): string {
185
210
  }
186
211
 
187
212
  /** A table header's sort mark in words: `MKT VAL ▼` is "MKT VAL (sorted descending)". */
188
- function plainHeader(header: string): string {
213
+ function plainHeader(header: string, ascii: boolean): string {
189
214
  const sorted = header.replace(/\s*([▲▼])\s*$/, (_match, mark: string) => ` (sorted ${mark === "▲" ? "ascending" : "descending"})`);
190
- return plainWords(sorted.trimStart());
215
+ const worded = plainWords(sorted.trimStart());
216
+ return ascii ? asciiText(worded) : worded;
191
217
  }
192
218
 
193
- /** A plain table's cell: arrows in words, and "—" for no value. */
194
- function plainCell(cell: string): string {
195
- return EMPTY_CELL.test(stripAnsi(cell).trim()) ? PLAIN_EMPTY_CELL : plainWords(cell);
219
+ /**
220
+ * A plain table's cell: arrows in words, and "—" for no value. In ASCII
221
+ * (`cliAsciiEnabled`) before the columns are measured, so `EUR211.25` lines
222
+ * up as `€211.25` did.
223
+ */
224
+ function plainCell(cell: string, ascii: boolean): string {
225
+ const plain = EMPTY_CELL.test(stripAnsi(cell).trim()) ? PLAIN_EMPTY_CELL : plainWords(cell);
226
+ return ascii ? asciiText(plain) : plain;
196
227
  }
197
228
 
198
229
  function applyAnsi(text: string, code: string): string {
@@ -217,11 +248,24 @@ export function setCliWidthOverride(value: number | null): void {
217
248
  /** Columns the output is fitted to: `--width`, else the terminal's, or null when output is piped or redirected. */
218
249
  export function cliTerminalWidth(): number | null {
219
250
  if (widthOverride != null) return widthOverride;
220
- if (renderingForFile || !process.stdout.isTTY) return null;
251
+ if (renderingForFile) return null;
252
+ if (!process.stdout.isTTY) return columnsVariable();
221
253
  const columns = process.stdout.columns;
222
254
  return typeof columns === "number" && columns > 0 ? columns : null;
223
255
  }
224
256
 
257
+ /**
258
+ * `COLUMNS=60 gloom ... | less`: piped output is fitted to the width the
259
+ * variable sets, as `--width` does, so a screen reader or a pager gets the
260
+ * same narrow layout a terminal does. Unset or not a width, output stays whole.
261
+ */
262
+ function columnsVariable(): number | null {
263
+ const value = process.env.COLUMNS?.trim();
264
+ if (!value || !/^\d+$/.test(value)) return null;
265
+ const columns = Number(value);
266
+ return columns >= MIN_COLUMNS_VARIABLE && columns <= MAX_COLUMNS_VARIABLE ? columns : null;
267
+ }
268
+
225
269
  function padDisplay(text: string, width: number, align: CliAlign = "left"): string {
226
270
  const padding = Math.max(0, width - visibleLength(text));
227
271
  if (padding === 0) return text;
@@ -263,7 +307,8 @@ export function truncateDisplay(text: string, width: number): string {
263
307
 
264
308
  /**
265
309
  * Word-wraps text to `width` cells. Lines that already fit are kept verbatim, so aligned
266
- * columns survive; continuation lines keep the paragraph's leading indent.
310
+ * columns survive; continuation lines keep the paragraph's leading indent, and words on one
311
+ * line keep the spaces between them (a heading's ` · `).
267
312
  */
268
313
  export function wrapText(text: string, width: number): string[] {
269
314
  const lines: string[] = [];
@@ -273,7 +318,13 @@ export function wrapText(text: string, width: number): string[] {
273
318
  continue;
274
319
  }
275
320
  const indent = /^ */.exec(paragraph)![0];
276
- const words = paragraph.slice(indent.length).split(/ +/).filter(Boolean);
321
+ // Each word with the spaces before it, which a line break drops. A separator (`·`) ends its line, never starts one.
322
+ const words: { gap: string; word: string }[] = [];
323
+ for (const [, gap, word] of paragraph.slice(indent.length).matchAll(/( *)([^ ]+)/g)) {
324
+ const last = words.at(-1);
325
+ if (last && word === "·") last.word += `${gap}${word}`;
326
+ else words.push({ gap: gap!, word: word! });
327
+ }
277
328
  if (words.length === 0) {
278
329
  lines.push("");
279
330
  continue;
@@ -281,16 +332,16 @@ export function wrapText(text: string, width: number): string[] {
281
332
  let line = indent;
282
333
  let lineWidth = indent.length;
283
334
  let lineHasWord = false;
284
- for (const word of words) {
335
+ for (const { gap, word } of words) {
285
336
  const wordWidth = visibleLength(word);
286
- if (lineHasWord && lineWidth + 1 + wordWidth > width) {
337
+ if (lineHasWord && lineWidth + gap.length + wordWidth > width) {
287
338
  lines.push(line);
288
339
  line = indent;
289
340
  lineWidth = indent.length;
290
341
  lineHasWord = false;
291
342
  }
292
- line += lineHasWord ? ` ${word}` : word;
293
- lineWidth += (lineHasWord ? 1 : 0) + wordWidth;
343
+ line += lineHasWord ? `${gap}${word}` : word;
344
+ lineWidth += (lineHasWord ? gap.length : 0) + wordWidth;
294
345
  lineHasWord = true;
295
346
  }
296
347
  lines.push(line);
@@ -512,6 +563,22 @@ function wrapCell(text: string, width: number): string[] {
512
563
  });
513
564
  }
514
565
 
566
+ /**
567
+ * The columns that say what a row is: the leading ones up to the first figure
568
+ * after a word (`#`, Coin and Name before Price; Symbol and Name before Last).
569
+ */
570
+ function rowIdentityColumns(columns: readonly CliTableColumn[]): Set<number> {
571
+ const identity = new Set<number>();
572
+ let named = false;
573
+ for (const [index, column] of columns.entries()) {
574
+ const figure = column.align === "right";
575
+ if (figure && named) break;
576
+ identity.add(index);
577
+ named ||= !figure;
578
+ }
579
+ return identity;
580
+ }
581
+
515
582
  interface TableLayout {
516
583
  kept: TableEntry[];
517
584
  stacked: TableEntry[];
@@ -532,13 +599,18 @@ function movesSooner(left: readonly number[], right: readonly number[]): boolean
532
599
  * (a name, a title) before dates and figures, then rightmost first. The
533
600
  * first column stays. The list ends at the first layout that keeps every
534
601
  * column it shows whole on one line: moving more only adds lines.
602
+ *
603
+ * Plain output (`--plain`, read aloud by a screen reader) keeps the columns
604
+ * that say what a row is on its first line, after only the ones too wide to
605
+ * sit there: a row is heard as "BTC Bitcoin" before its figures.
535
606
  */
536
- function tableLayouts(entries: TableEntry[], available: number): TableLayout[] {
607
+ function tableLayouts(entries: TableEntry[], available: number, identity: ReadonlySet<number> = new Set()): TableLayout[] {
537
608
  const fit = (kept: TableEntry[]) => fitColumnWidths(kept.map((entry) => entry.width), kept.map((entry) => entry.floor), available);
538
609
  const first = entries[0]!;
539
610
  const room = available - first.floor - COLUMN_GAP;
540
611
  const order = (entry: TableEntry) => [
541
612
  entry.floor > room ? 1 : 0,
613
+ identity.has(entry.index) ? 0 : 1,
542
614
  entry.column.stackPriority ?? 0,
543
615
  entry.text && entry.width > SOFT_MIN_COLUMN_WIDTH ? 1 : 0,
544
616
  entry.index,
@@ -601,7 +673,7 @@ function renderTableLayout(
601
673
  const belowIndent = `${indent}${" ".repeat(COLUMN_GAP)}`;
602
674
  const belowWidth = Math.max(1, (available ?? 0) - COLUMN_GAP);
603
675
  const renderBelow = (index: number) => (options.below?.[index] ?? []).flatMap((line) => (
604
- available == null ? [line] : wrapText(line, belowWidth)
676
+ available == null ? [line] : options.cutBelow ? [truncateDisplay(line, belowWidth)] : wrapText(line, belowWidth)
605
677
  )).map((line) => `${belowIndent}${cliStyles.muted(line)}`.trimEnd());
606
678
  // Columns moved under the rows print there as `Header value`, as many to a line as fit.
607
679
  const renderStacked = (cells: string[], rowIndex: number): string[] => {
@@ -645,9 +717,10 @@ function renderTableLayout(
645
717
  */
646
718
  export function renderTable(columns: CliTableColumn[], rows: string[][], options: CliTableOptions = {}): string {
647
719
  if (cliPlainEnabled()) {
648
- columns = columns.map((column) => ({ ...column, header: plainHeader(column.header) }));
649
- rows = rows.map((row) => columns.map((_, index) => plainCell(row[index] ?? "")));
650
- if (options.below) options = { ...options, below: options.below.map((lines) => lines?.map(plainWords)) };
720
+ const ascii = cliAsciiEnabled();
721
+ columns = columns.map((column) => ({ ...column, header: plainHeader(column.header, ascii) }));
722
+ rows = rows.map((row) => columns.map((_, index) => plainCell(row[index] ?? "", ascii)));
723
+ if (options.below) options = { ...options, below: options.below.map((lines) => lines?.map((line) => ascii ? asciiText(plainWords(line)) : plainWords(line))) };
651
724
  }
652
725
  const maxWidth = options.maxWidth === undefined ? cliTerminalWidth() : options.maxWidth;
653
726
  const available = maxWidth == null ? null : maxWidth - (options.indent ?? 0);
@@ -674,7 +747,7 @@ export function renderTable(columns: CliTableColumn[], rows: string[][], options
674
747
  });
675
748
  let best = "";
676
749
  let bestLines = Infinity;
677
- for (const layout of tableLayouts(entries, available)) {
750
+ for (const layout of tableLayouts(entries, available, cliPlainEnabled() ? rowIdentityColumns(columns) : undefined)) {
678
751
  const stacked = layout.stacked.filter((entry) => entry.column.stackRows !== 0).sort((left, right) => left.index - right.index);
679
752
  const text = renderTableLayout(columns, rows, options, layout.kept, stacked, available, true);
680
753
  const lines = text.split("\n").length;
@@ -23,3 +23,27 @@ export function chunked<T>(items: readonly T[], size: number): T[][] {
23
23
  for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
24
24
  return chunks;
25
25
  }
26
+
27
+ /**
28
+ * One gate that every caller shares: at most `limit` tasks run at once, and
29
+ * the rest wait in the order they came. Unlike mapWithConcurrency, separate
30
+ * callers (two panes, a pane and a report) count against the same limit. A
31
+ * finished task hands its slot straight to the next waiting one, so a caller
32
+ * arriving in between never takes it.
33
+ */
34
+ export function createConcurrencyLimiter(limit: number): <T>(task: () => Promise<T>) => Promise<T> {
35
+ const slots = Math.max(1, Math.floor(limit));
36
+ let active = 0;
37
+ const waiting: Array<() => void> = [];
38
+ return async (task) => {
39
+ if (active < slots) active += 1;
40
+ else await new Promise<void>((resolve) => waiting.push(resolve));
41
+ try {
42
+ return await task();
43
+ } finally {
44
+ const next = waiting.shift();
45
+ if (next) next();
46
+ else active -= 1;
47
+ }
48
+ };
49
+ }
@@ -93,6 +93,12 @@ export function isKnownExchangeCode(value?: string): boolean {
93
93
  return !!normalized && KNOWN_EXCHANGE_CODES.has(normalized);
94
94
  }
95
95
 
96
+ /** "Warsaw Stock Exchange" for a venue with a name, the canonical code otherwise (NASDAQ, whose name is its code). */
97
+ export function exchangeName(value?: string): string {
98
+ const code = canonicalExchange(value);
99
+ return EXCHANGE_NAMES[code] ?? code;
100
+ }
101
+
96
102
  /** "Euronext Paris (EPA)" for a venue with a name, the canonical code otherwise. */
97
103
  export function exchangeLabel(value?: string): string {
98
104
  const code = canonicalExchange(value);