@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.
- package/README.md +7 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +108 -1
- package/src/account/upgrade-dialog.tsx +4 -23
- package/src/api-client/coalesce.ts +118 -0
- package/src/api-client/credit-boards.ts +13 -0
- package/src/api-client/crypto-etf-flows.ts +6 -0
- package/src/api-client/crypto-markets.ts +40 -0
- package/src/api-client/data.ts +201 -14
- package/src/api-client/equity-screener.ts +10 -0
- package/src/api-client/errors.ts +20 -0
- package/src/api-client/gpu.ts +29 -0
- package/src/api-client/index.ts +5 -2
- package/src/api-client/plan-access.ts +10 -1
- package/src/api-client/plan-rules.ts +35 -0
- package/src/api-client/research-activity.ts +1 -32
- package/src/api-client/types.ts +21 -1
- package/src/app/attention-notice.tsx +94 -0
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/factories.ts +1 -0
- package/src/cli/ascii-output.ts +42 -0
- package/src/cli/base-converter.ts +10 -2
- package/src/cli/command-options.ts +16 -1
- package/src/cli/commands/crypto-hints.ts +23 -4
- package/src/cli/commands/market.ts +92 -23
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +9 -4
- package/src/cli/commands/search.ts +71 -12
- package/src/cli/commands/system.ts +22 -30
- package/src/cli/commands/ticker.ts +49 -9
- package/src/cli/commands/version.ts +33 -0
- package/src/cli/context.ts +5 -9
- package/src/cli/data-dir.ts +7 -0
- package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
- package/src/cli/desktop-pane-shot-browser.ts +320 -0
- package/src/cli/desktop-pane-shot-routes.ts +26 -1
- package/src/cli/desktop-pane-shot.ts +196 -256
- package/src/cli/early-command.ts +42 -0
- package/src/cli/earnings-calendar.ts +9 -0
- package/src/cli/entry.ts +19 -13
- package/src/cli/errors.ts +12 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +154 -4
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +40 -5
- package/src/cli/listing-arg.ts +46 -9
- package/src/cli/options.ts +1 -1
- package/src/cli/output-file.ts +60 -13
- package/src/cli/pane-functions/catalog.ts +14 -3
- package/src/cli/pane-functions/data.ts +1 -1
- package/src/cli/pane-functions/dom.ts +45 -0
- package/src/cli/pane-functions/freshness.ts +46 -9
- package/src/cli/pane-functions/function-help.ts +17 -9
- package/src/cli/pane-functions/headless.ts +22 -14
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +6 -2
- package/src/cli/pane-functions/resolver.ts +27 -5
- package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
- package/src/cli/pane-functions/screenshot-http.ts +70 -0
- package/src/cli/pane-functions/screenshot-render.ts +19 -5
- package/src/cli/quote-answer.ts +20 -0
- package/src/cli/registry.ts +41 -13
- package/src/cli/result.ts +12 -6
- package/src/cli/ticker-resolution.ts +2 -1
- package/src/cli/types.ts +2 -0
- package/src/components/chart/composite/format.ts +3 -0
- package/src/components/chart-table/header.tsx +1 -1
- package/src/components/chart-table/layout.ts +28 -11
- package/src/components/command-bar/action-runtime.ts +4 -0
- package/src/components/command-bar/commands/direct/index.ts +18 -33
- package/src/components/command-bar/commands/direct/runtime.ts +5 -0
- package/src/components/command-bar/commands/registry.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -4
- package/src/components/command-bar/routes/root/runtime.ts +24 -1
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/header.tsx +8 -3
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +7 -2
- package/src/components/layout/pane/footer/model.ts +40 -5
- package/src/components/market-board/index.tsx +3 -2
- package/src/components/onboarding/desks-step.tsx +12 -4
- package/src/components/onboarding/first-run-workspace.ts +81 -4
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +54 -23
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
- package/src/components/onboarding/run-setup-again.ts +24 -0
- package/src/components/onboarding/use-onboarding-progress.ts +33 -15
- package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
- package/src/components/onboarding/wizard-model.ts +57 -3
- package/src/components/onboarding/wizard-positions.ts +1 -1
- package/src/components/ui/delayed-mark.tsx +56 -0
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +8 -1
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +34 -31
- package/src/data/sqlite/access.ts +19 -8
- package/src/data/sqlite/checkpointer.ts +95 -8
- package/src/data/sqlite/database.ts +40 -7
- package/src/docs/snapshot.json +191 -78
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/layout/desk-keys.ts +4 -1
- package/src/layout/desks.ts +38 -5
- package/src/layout-marketplace/gallery-desktop.tsx +45 -16
- package/src/market-data/batched-reads.ts +16 -0
- package/src/market-data/coordinator/auxiliary.ts +4 -2
- package/src/market-data/coordinator/index.ts +11 -2
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +3 -1
- package/src/market-data/market/index-level.ts +6 -4
- package/src/market-data/published-apac-sessions.ts +6 -0
- package/src/news/aggregator.ts +7 -4
- package/src/news/news-model.ts +11 -0
- package/src/news/story-id.ts +22 -0
- package/src/news/ticker-symbols.ts +17 -1
- package/src/notifications/app-notifier.ts +35 -14
- package/src/plugins/builtin/account-management/form-components.tsx +1 -1
- package/src/plugins/builtin/account-management/index.tsx +2 -3
- package/src/plugins/builtin/account-management/model.ts +1 -1
- package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
- package/src/plugins/builtin/alerts/events.ts +10 -2
- package/src/plugins/builtin/alerts/index.tsx +5 -5
- package/src/plugins/builtin/alerts/research-builder.ts +5 -1
- package/src/plugins/builtin/alerts/research-rules.ts +18 -0
- package/src/plugins/builtin/alerts/targets.ts +4 -1
- package/src/plugins/builtin/attention/headless.ts +1 -1
- package/src/plugins/builtin/attention/index.tsx +2 -4
- package/src/plugins/builtin/attention/listing.ts +1 -1
- package/src/plugins/builtin/attention/series.ts +1 -1
- package/src/plugins/builtin/awards/index.tsx +1 -2
- package/src/plugins/builtin/backtest/client.ts +1 -0
- package/src/plugins/builtin/backtest/index.tsx +1 -2
- package/src/plugins/builtin/backtest/rules.ts +3 -1
- package/src/plugins/builtin/backtest/window.ts +8 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
- package/src/plugins/builtin/broker-manager/index.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +5 -0
- package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
- package/src/plugins/builtin/buildout/company/id.ts +2 -0
- package/src/plugins/builtin/buildout/company/model.ts +247 -0
- package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
- package/src/plugins/builtin/buildout/facilities.ts +309 -0
- package/src/plugins/builtin/buildout/format.ts +9 -0
- package/src/plugins/builtin/buildout/index.tsx +51 -5
- package/src/plugins/builtin/buildout/model/index.ts +62 -10
- package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
- package/src/plugins/builtin/buildout/model/types.ts +11 -2
- package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
- package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
- package/src/plugins/builtin/buildout/table-model.ts +26 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
- package/src/plugins/builtin/cds/headless.ts +10 -2
- package/src/plugins/builtin/cds/model.ts +7 -3
- package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
- package/src/plugins/builtin/chart-composer/headless.ts +88 -21
- package/src/plugins/builtin/chart-composer/index.tsx +2 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/controller/index.ts +1 -1
- package/src/plugins/builtin/chat/controller/messages.ts +15 -4
- package/src/plugins/builtin/chat/ghost-user.ts +8 -0
- package/src/plugins/builtin/chat/index.tsx +5 -5
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
- package/src/plugins/builtin/cloud/index.tsx +1 -2
- package/src/plugins/builtin/cloud/plugin.tsx +6 -4
- package/src/plugins/builtin/cloud/team/module.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
- package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
- package/src/plugins/builtin/comment-letters/index.tsx +1 -2
- package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
- package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
- package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
- package/src/plugins/builtin/congress-trades/headless.ts +2 -0
- package/src/plugins/builtin/congress-trades/model.ts +92 -64
- package/src/plugins/builtin/congress-trades/table.tsx +14 -2
- package/src/plugins/builtin/connections/index.ts +2 -3
- package/src/plugins/builtin/cot/index.tsx +1 -2
- package/src/plugins/builtin/cpi/index.tsx +1 -2
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/client.ts +7 -2
- package/src/plugins/builtin/credit-boards/headless.ts +25 -12
- package/src/plugins/builtin/credit-boards/model.ts +29 -0
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/headless.ts +4 -3
- package/src/plugins/builtin/crypto-board/index.tsx +4 -2
- package/src/plugins/builtin/crypto-board/series.ts +2 -0
- package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
- package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
- package/src/plugins/builtin/debt-maturities/model.ts +17 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
- package/src/plugins/builtin/debug/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/client.ts +32 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
- package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
- package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
- package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
- package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
- package/src/plugins/builtin/dividend-yield/view.ts +21 -0
- package/src/plugins/builtin/docs/index.tsx +2 -3
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
- package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
- package/src/plugins/builtin/econ/calendar-model.ts +29 -6
- package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +1 -0
- package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
- package/src/plugins/builtin/equity-screener/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/model.ts +28 -4
- package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
- package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
- package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cli.ts +44 -0
- package/src/plugins/builtin/fear-greed/index.tsx +5 -53
- package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
- package/src/plugins/builtin/fixed-income/index.tsx +1 -2
- package/src/plugins/builtin/futures/board-reads.ts +25 -0
- package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
- package/src/plugins/builtin/futures/index.tsx +15 -0
- package/src/plugins/builtin/futures/model.ts +14 -0
- package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
- package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
- package/src/plugins/builtin/futures-curve/headless.ts +6 -1
- package/src/plugins/builtin/futures-curve/index.tsx +1 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +51 -12
- package/src/plugins/builtin/gpu/client.ts +15 -2
- package/src/plugins/builtin/gpu/equities.tsx +5 -2
- package/src/plugins/builtin/gpu/headless.ts +14 -2
- package/src/plugins/builtin/gpu/index.tsx +1 -2
- package/src/plugins/builtin/gpu/model.ts +20 -2
- package/src/plugins/builtin/gpu/pane.tsx +1 -1
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/index-holders.ts +24 -12
- package/src/plugins/builtin/holders/index.tsx +2 -3
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
- package/src/plugins/builtin/iv-history/index.tsx +1 -2
- package/src/plugins/builtin/jodi/index.tsx +1 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
- package/src/plugins/builtin/layout-manager/index.tsx +9 -8
- package/src/plugins/builtin/macro-day/pane.tsx +4 -2
- package/src/plugins/builtin/market-halts/index.tsx +1 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
- package/src/plugins/builtin/market-movers/index.tsx +7 -1
- package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
- package/src/plugins/builtin/market-valuation/index.tsx +1 -2
- package/src/plugins/builtin/members/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/pane.tsx +2 -1
- package/src/plugins/builtin/news/headless.ts +7 -0
- package/src/plugins/builtin/news/wire/index.tsx +1 -1
- package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
- package/src/plugins/builtin/notes/index.tsx +3 -6
- package/src/plugins/builtin/openfda/index.tsx +1 -2
- package/src/plugins/builtin/options/view.tsx +6 -5
- package/src/plugins/builtin/options-calculator/index.tsx +1 -2
- package/src/plugins/builtin/options-positioning/index.tsx +1 -2
- package/src/plugins/builtin/options-scenario/index.tsx +2 -4
- package/src/plugins/builtin/perps/index.tsx +1 -2
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
- package/src/plugins/builtin/plugin-module.ts +16 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/rate-path/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/model.ts +16 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
- package/src/plugins/builtin/research/analyst-headless.ts +33 -6
- package/src/plugins/builtin/research/analyst-model.ts +37 -2
- package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
- package/src/plugins/builtin/research/index.tsx +5 -8
- package/src/plugins/builtin/research-search/index.tsx +1 -2
- package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
- package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
- package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
- package/src/plugins/builtin/roadmap/index.tsx +2 -3
- package/src/plugins/builtin/roadmap/pane.tsx +49 -2
- package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
- package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
- package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
- package/src/plugins/builtin/sec/filing-content.ts +38 -6
- package/src/plugins/builtin/sec/headless.ts +5 -1
- package/src/plugins/builtin/sec/index.tsx +4 -2
- package/src/plugins/builtin/sec/model.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +11 -5
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
- package/src/plugins/builtin/shared/forming-bars.ts +29 -0
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/supply-chain/client.ts +22 -1
- package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
- package/src/plugins/builtin/supply-chain/index.tsx +1 -2
- package/src/plugins/builtin/thirteenf/api.ts +116 -13
- package/src/plugins/builtin/thirteenf/data.ts +132 -30
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
- package/src/plugins/builtin/thirteenf/types.ts +12 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
- package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
- package/src/plugins/builtin/time-sales/index.tsx +1 -2
- package/src/plugins/builtin/time-sales/pane.tsx +3 -1
- package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
- package/src/plugins/builtin/trace-bonds/model.ts +44 -10
- package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
- package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
- package/src/plugins/builtin/vol-surface/client.ts +18 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/world-indices/footer.ts +6 -1
- package/src/plugins/builtin/world-venue-map/client.ts +10 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -2
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
- package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
- package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
- package/src/plugins/builtin/yield-curve/curves.ts +16 -2
- package/src/plugins/builtin/yield-curve/headless.ts +3 -3
- package/src/plugins/bundle.ts +52 -2
- package/src/plugins/catalog-browser.ts +2 -2
- package/src/plugins/catalog-cli.ts +20 -0
- package/src/plugins/host-link.ts +13 -1
- package/src/plugins/installer.ts +15 -4
- package/src/plugins/loader.ts +43 -11
- package/src/plugins/peer-imports.ts +201 -0
- package/src/plugins/peer-retry.ts +26 -29
- package/src/plugins/plugin-process.ts +128 -0
- package/src/plugins/redirection-guard.ts +45 -0
- package/src/remote/endpoint.ts +16 -3
- package/src/renderers/browser/main.tsx +3 -2
- package/src/renderers/cloudflare/http-proxy.ts +7 -123
- package/src/renderers/dom/desktop/icons.tsx +2 -0
- package/src/renderers/dom/host/image-surface.tsx +46 -21
- package/src/renderers/dom/styles.css +7 -0
- package/src/renderers/dom/toast-host.tsx +2 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
- package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
- package/src/renderers/electrobun/bun/window/create.ts +4 -2
- package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
- package/src/renderers/electrobun/view/main.tsx +7 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/renderers/opentui/browser-open.ts +10 -4
- package/src/renderers/opentui/image/surface.tsx +1 -1
- package/src/renderers/opentui/start.tsx +12 -6
- package/src/renderers/opentui/toast-host.tsx +2 -1
- package/src/series/registry.ts +18 -0
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +7 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/index.ts +28 -5
- package/src/sources/gloomberb-cloud/news.ts +24 -3
- package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
- package/src/sources/provider-errors.ts +26 -3
- package/src/sources/provider-router/history.ts +5 -0
- package/src/sources/provider-router/index.ts +13 -0
- package/src/sync/core-contributors.ts +32 -0
- package/src/telemetry/attention-counts.ts +64 -6
- package/src/telemetry/attention-setting.ts +33 -0
- package/src/telemetry/crash-reports-node.ts +3 -1
- package/src/telemetry/stall-clocks.ts +93 -0
- package/src/telemetry/stall-reports-node.ts +12 -7
- package/src/telemetry/stall-reports.ts +34 -5
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name.ts +19 -0
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +13 -2
- package/src/tickers/search/index.ts +4 -1
- package/src/tickers/search/types.ts +6 -1
- package/src/time-series/bar-aggregation.ts +7 -1
- package/src/time-series/base-series.ts +3 -3
- package/src/time-series/chart-bounds.ts +1 -1
- package/src/time-series/chart-data.ts +5 -2
- package/src/time-series/chart-seed.ts +1 -1
- package/src/time-series/coin-session-close.ts +211 -0
- package/src/time-series/live-bars.ts +13 -2
- package/src/time-series/live-quotes.ts +1 -1
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +55 -6
- package/src/time-series/resolve.ts +12 -3
- package/src/time-series/series-presentation.ts +1 -1
- package/src/time-series/studies/formula.ts +205 -0
- package/src/time-series/studies/indicators.ts +299 -0
- package/src/time-series/studies/leverage.ts +75 -0
- package/src/time-series/studies/pair.ts +138 -0
- package/src/time-series/studies/resolve.ts +333 -0
- package/src/time-series/studies/shared.ts +141 -0
- package/src/time-series/studies/units.ts +243 -0
- package/src/time-series/studies/vwap.ts +274 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +22 -0
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +11 -0
- package/src/ui/toast.tsx +26 -1
- package/src/utils/ascii-text.ts +140 -0
- package/src/utils/cli-output.ts +91 -18
- package/src/utils/concurrency.ts +24 -0
- package/src/utils/exchanges.ts +6 -0
- package/src/utils/external-url.ts +20 -7
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/main-thread-stall.ts +75 -3
- package/src/utils/perf-marks.ts +36 -2
- package/src/utils/private-files.ts +141 -0
- package/src/utils/public-address.ts +99 -0
- package/src/account/upgrade-personalization.ts +0 -57
- package/src/account/upgrade-tickers.ts +0 -124
- package/src/api-client/upgrade-experiment.ts +0 -91
- package/src/plugins/builtin/cloud/browser.tsx +0 -3
- package/src/renderers/electrobun/launcher/preload.ts +0 -13
- package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
- package/src/time-series/studies.ts +0 -1628
package/src/types/headless.ts
CHANGED
|
@@ -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
|
|
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;
|
package/src/types/plugin.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/cli-output.ts
CHANGED
|
@@ -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
|
-
|
|
215
|
+
const worded = plainWords(sorted.trimStart());
|
|
216
|
+
return ascii ? asciiText(worded) : worded;
|
|
191
217
|
}
|
|
192
218
|
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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
|
-
|
|
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 +
|
|
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 ?
|
|
293
|
-
lineWidth += (lineHasWord ?
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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;
|
package/src/utils/concurrency.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/exchanges.ts
CHANGED
|
@@ -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);
|