gloomberb 0.9.2 → 0.9.4
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 +24 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +20 -8
- package/src/brokers/account-cache.ts +1 -1
- package/src/brokers/sync-broker-instance.test.ts +107 -0
- package/src/brokers/sync-broker-instance.ts +22 -10
- package/src/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- package/src/components/chart/bar-chart-renderer.test.ts +108 -3
- package/src/components/chart/bar-chart-renderer.ts +226 -27
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +34 -6
- package/src/components/chart/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +40 -4
- package/src/components/layout/pane/index.tsx +3 -1
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/cursor-occlusion.ts +196 -0
- package/src/components/layout/shell/drag/runtime.ts +28 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +487 -4
- package/src/components/layout/shell/index.tsx +165 -4
- package/src/components/layout/shell/layout-state.test.ts +33 -0
- package/src/components/layout/shell/layout-state.ts +4 -9
- package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +34 -16
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +111 -1
- package/src/components/layout/status-bar.tsx +37 -7
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- package/src/components/price-performance.tsx +9 -1
- package/src/components/table-view-shared.tsx +12 -4
- package/src/components/ui/data-table/opentui/index.tsx +25 -12
- package/src/components/ui/data-table/types.ts +4 -0
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +26 -10
- package/src/components/ui/ui.test.tsx +87 -0
- package/src/core/app-services.ts +7 -4
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/data/config/store/index.ts +1 -2
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
- package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
- package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
- package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
- package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
- package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
- package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
- package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
- package/src/plugins/builtin/layout-manager/index.tsx +5 -1
- package/src/plugins/builtin/news/wire/read-state.ts +9 -32
- package/src/plugins/builtin/options/view.test.tsx +141 -4
- package/src/plugins/builtin/options/view.tsx +76 -15
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
- package/src/plugins/builtin/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager/queries.ts +8 -2
- package/src/plugins/pane-manager.test.ts +33 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +10 -4
- package/src/plugins/prediction-markets/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- package/src/plugins/prediction-markets/detail/book.tsx +141 -96
- package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
- package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
- package/src/plugins/prediction-markets/detail.test.tsx +50 -0
- package/src/plugins/prediction-markets/pane.test.tsx +75 -0
- package/src/plugins/prediction-markets/pane.tsx +71 -23
- package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/index.ts +13 -0
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
- package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/styles.css +11 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/start.tsx +13 -5
- package/src/renderers/opentui/ui-host.tsx +57 -25
- package/src/sources/provider-router/batches.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +5 -0
- package/src/sources/provider-router/financials.ts +11 -23
- package/src/sources/provider-router/index.test.ts +189 -0
- package/src/sources/provider-router/primary.ts +3 -2
- package/src/sources/sec-edgar.test.ts +83 -0
- package/src/sources/sec-edgar.ts +231 -0
- package/src/sources/yahoo-finance.test.ts +51 -0
- package/src/sources/yahoo-finance.ts +49 -1
- package/src/state/app/context/index.test.tsx +5 -8
- package/src/state/app/input-capture.ts +12 -0
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/plugin.ts +28 -0
- package/src/types/trading.ts +2 -0
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +2 -0
- package/src/utils/cli-output.ts +6 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
attachPredictionMarketsPersistence,
|
|
25
25
|
} from "./services/fetch";
|
|
26
26
|
import { normalizeKalshiMarket } from "./services/kalshi/adapter";
|
|
27
|
+
import type { PredictionDetailTab } from "./types";
|
|
27
28
|
|
|
28
29
|
export const TEST_PANE_ID = "prediction-markets:main";
|
|
29
30
|
const originalFetch = globalThis.fetch;
|
|
@@ -408,7 +409,13 @@ export function ChartHarness({
|
|
|
408
409
|
);
|
|
409
410
|
}
|
|
410
411
|
|
|
411
|
-
export function GroupedDetailHarness(
|
|
412
|
+
export function GroupedDetailHarness({
|
|
413
|
+
detailTab = "overview",
|
|
414
|
+
loading = false,
|
|
415
|
+
}: {
|
|
416
|
+
detailTab?: PredictionDetailTab;
|
|
417
|
+
loading?: boolean;
|
|
418
|
+
} = {}) {
|
|
412
419
|
const [state, dispatch] = useReducer(
|
|
413
420
|
appReducer,
|
|
414
421
|
(() => {
|
|
@@ -475,29 +482,40 @@ export function GroupedDetailHarness() {
|
|
|
475
482
|
<AppContext value={{ state, dispatch }}>
|
|
476
483
|
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
477
484
|
<PredictionMarketDetailPane
|
|
478
|
-
detail={
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
485
|
+
detail={
|
|
486
|
+
loading
|
|
487
|
+
? null
|
|
488
|
+
: {
|
|
489
|
+
summary,
|
|
490
|
+
siblings: selectedRow!.markets.map((market) => ({
|
|
491
|
+
key: market.key,
|
|
492
|
+
marketId: market.marketId,
|
|
493
|
+
label: market.marketLabel,
|
|
494
|
+
yesPrice: market.yesPrice,
|
|
495
|
+
volume24h: market.volume24h,
|
|
496
|
+
})),
|
|
497
|
+
rules: [],
|
|
498
|
+
history: [],
|
|
499
|
+
book: {
|
|
500
|
+
yesBids: [{ price: 0.47, size: 120 }],
|
|
501
|
+
yesAsks: [{ price: 0.49, size: 95 }],
|
|
502
|
+
noBids: [{ price: 0.51, size: 80 }],
|
|
503
|
+
noAsks: [{ price: 0.53, size: 105 }],
|
|
504
|
+
lastTradePrice: null,
|
|
505
|
+
},
|
|
506
|
+
trades: [{
|
|
507
|
+
id: "trade-1",
|
|
508
|
+
timestamp: Date.parse("2026-04-01T12:34:56Z"),
|
|
509
|
+
side: "buy",
|
|
510
|
+
outcome: "yes",
|
|
511
|
+
price: 0.48,
|
|
512
|
+
size: 50,
|
|
513
|
+
}],
|
|
514
|
+
}
|
|
515
|
+
}
|
|
498
516
|
detailError={null}
|
|
499
|
-
detailLoadCount={0}
|
|
500
|
-
detailTab=
|
|
517
|
+
detailLoadCount={loading ? 1 : 0}
|
|
518
|
+
detailTab={detailTab}
|
|
501
519
|
detailWidth={58}
|
|
502
520
|
focused
|
|
503
521
|
height={24}
|
|
@@ -531,6 +549,7 @@ export async function emitKeypress(
|
|
|
531
549
|
event: {
|
|
532
550
|
name?: string;
|
|
533
551
|
sequence?: string;
|
|
552
|
+
targetEditable?: boolean;
|
|
534
553
|
shift?: boolean;
|
|
535
554
|
},
|
|
536
555
|
) {
|
|
@@ -30,6 +30,15 @@ describe("applicationMenuCommand", () => {
|
|
|
30
30
|
})).toEqual({ type: "open-devtools" });
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
test("parses the native quit command", () => {
|
|
34
|
+
expect(applicationMenuCommand({
|
|
35
|
+
data: {
|
|
36
|
+
action: ELECTROBUN_APPLICATION_MENU_ACTION,
|
|
37
|
+
data: { type: "quit" },
|
|
38
|
+
},
|
|
39
|
+
})).toEqual({ type: "quit" });
|
|
40
|
+
});
|
|
41
|
+
|
|
33
42
|
test("ignores unknown command types", () => {
|
|
34
43
|
expect(applicationMenuCommand({
|
|
35
44
|
data: {
|
|
@@ -9,4 +9,13 @@ describe("desktop application menu", () => {
|
|
|
9
9
|
test("keeps the native application menu on macOS", () => {
|
|
10
10
|
expect(buildDesktopApplicationMenu("darwin")).toEqual(buildApplicationMenu());
|
|
11
11
|
});
|
|
12
|
+
|
|
13
|
+
test("routes macOS quit through an explicit command item", () => {
|
|
14
|
+
const appMenu = buildApplicationMenu()[0]?.submenu;
|
|
15
|
+
expect(appMenu?.at(-1)).toMatchObject({
|
|
16
|
+
label: "Quit Gloomberb",
|
|
17
|
+
accelerator: "CmdOrCtrl+Q",
|
|
18
|
+
data: { type: "quit" },
|
|
19
|
+
});
|
|
20
|
+
});
|
|
12
21
|
});
|
|
@@ -6,7 +6,8 @@ const GITHUB_ISSUE_URL = "https://github.com/vincelwt/gloomberb/issues/new/choos
|
|
|
6
6
|
|
|
7
7
|
export type ElectrobunApplicationMenuCommand =
|
|
8
8
|
| DesktopApplicationMenuCommand
|
|
9
|
-
| { type: "open-devtools" }
|
|
9
|
+
| { type: "open-devtools" }
|
|
10
|
+
| { type: "quit" };
|
|
10
11
|
|
|
11
12
|
function commandItem(
|
|
12
13
|
label: string,
|
|
@@ -38,7 +39,7 @@ export function buildApplicationMenu(): ApplicationMenuItemConfig[] {
|
|
|
38
39
|
{ role: "hideOthers" },
|
|
39
40
|
{ role: "showAll" },
|
|
40
41
|
{ type: "divider" },
|
|
41
|
-
{
|
|
42
|
+
commandItem("Quit Gloomberb", { type: "quit" }, { accelerator: "CmdOrCtrl+Q" }),
|
|
42
43
|
],
|
|
43
44
|
},
|
|
44
45
|
{
|
|
@@ -281,6 +281,15 @@ function closeAllDetachedWindows(): void {
|
|
|
281
281
|
detachedWindowManager.closeAll();
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
function quitDesktopApp(): void {
|
|
285
|
+
closeAllDetachedWindows();
|
|
286
|
+
teardownServices();
|
|
287
|
+
const window = mainWindow;
|
|
288
|
+
mainWindow = null;
|
|
289
|
+
window?.close();
|
|
290
|
+
Utils.quit();
|
|
291
|
+
}
|
|
292
|
+
|
|
284
293
|
function controlWindowForRpcKey(windowKey: string | undefined, action: DesktopWindowControlAction): boolean {
|
|
285
294
|
const targetWindow = windowKey === MAIN_WINDOW_RPC_KEY
|
|
286
295
|
? mainWindow
|
|
@@ -446,6 +455,10 @@ ApplicationMenu.on("application-menu-clicked", (event: unknown) => {
|
|
|
446
455
|
openMainWindowDevTools();
|
|
447
456
|
return;
|
|
448
457
|
}
|
|
458
|
+
if (command.type === "quit") {
|
|
459
|
+
quitDesktopApp();
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
449
462
|
if (!isWindowRpcReady(MAIN_WINDOW_RPC_KEY)) return;
|
|
450
463
|
getWindowRpc(MAIN_WINDOW_RPC_KEY)?.send["application-menu.select"]({ command });
|
|
451
464
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { detachedRpcKey, focusWindowForRpcKey, MAIN_WINDOW_RPC_KEY, type FocusableElectrobunWindow } from "./focus";
|
|
3
|
+
|
|
4
|
+
describe("Electrobun window focus routing", () => {
|
|
5
|
+
test("uses activate for the main window", () => {
|
|
6
|
+
const calls: string[] = [];
|
|
7
|
+
const mainWindow: FocusableElectrobunWindow = {
|
|
8
|
+
activate: () => calls.push("activate"),
|
|
9
|
+
focus: () => calls.push("focus"),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
expect(focusWindowForRpcKey(MAIN_WINDOW_RPC_KEY, mainWindow, new Map())).toBe(true);
|
|
13
|
+
expect(calls).toEqual(["activate"]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("uses activate for detached windows", () => {
|
|
17
|
+
const calls: string[] = [];
|
|
18
|
+
const detachedWindows = new Map<string, FocusableElectrobunWindow>([
|
|
19
|
+
["pane:1", { activate: () => calls.push("detached") }],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
expect(focusWindowForRpcKey(detachedRpcKey("pane:1"), null, detachedWindows)).toBe(true);
|
|
23
|
+
expect(calls).toEqual(["detached"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("falls back to focus for legacy window objects", () => {
|
|
27
|
+
const calls: string[] = [];
|
|
28
|
+
const mainWindow: FocusableElectrobunWindow = {
|
|
29
|
+
focus: () => calls.push("focus"),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
expect(focusWindowForRpcKey(MAIN_WINDOW_RPC_KEY, mainWindow, new Map())).toBe(true);
|
|
33
|
+
expect(calls).toEqual(["focus"]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -3,6 +3,7 @@ export const MAIN_WINDOW_RPC_KEY = "main";
|
|
|
3
3
|
const DETACHED_WINDOW_RPC_PREFIX = "detached:";
|
|
4
4
|
|
|
5
5
|
export interface FocusableElectrobunWindow {
|
|
6
|
+
activate?: () => void;
|
|
6
7
|
focus?: () => void;
|
|
7
8
|
}
|
|
8
9
|
|
|
@@ -31,6 +32,10 @@ export function focusWindowForRpcKey(
|
|
|
31
32
|
detachedWindows: ReadonlyMap<string, FocusableElectrobunWindow>,
|
|
32
33
|
): boolean {
|
|
33
34
|
const window = resolveWindowForRpcKey(rpcKey, mainWindow, detachedWindows);
|
|
35
|
+
if (window?.activate) {
|
|
36
|
+
window.activate();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
34
39
|
if (!window?.focus) return false;
|
|
35
40
|
window.focus();
|
|
36
41
|
return true;
|
|
@@ -7,7 +7,7 @@ import type { AppServices } from "../../../core/app-services";
|
|
|
7
7
|
import type { AppConfig } from "../../../types/config";
|
|
8
8
|
import { newsProvider } from "../../../capabilities";
|
|
9
9
|
import { debugLog } from "../../../utils/debug-log";
|
|
10
|
-
import { measurePerf } from "../../../utils/perf-marks";
|
|
10
|
+
import { measurePerf, measurePerfAsync } from "../../../utils/perf-marks";
|
|
11
11
|
import { getRendererBuiltinPlugins } from "../../../plugins/catalog-ui";
|
|
12
12
|
import { createRemoteAssetDataClient } from "./remote/asset-data-client";
|
|
13
13
|
import { RemotePersistence } from "./remote/persistence";
|
|
@@ -47,10 +47,11 @@ export function createAppServices({ config }: { config: AppConfig }): AppService
|
|
|
47
47
|
}));
|
|
48
48
|
|
|
49
49
|
const plugins = getRendererBuiltinPlugins();
|
|
50
|
+
const pluginReadyPromises: Promise<void>[] = [];
|
|
50
51
|
for (const plugin of plugins) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
pluginReadyPromises.push(measurePerfAsync("startup.services.register-plugin", () => (
|
|
53
|
+
pluginRegistry.register(plugin)
|
|
54
|
+
), { pluginId: plugin.id }));
|
|
54
55
|
}
|
|
55
56
|
measurePerf("startup.services.news-start", () => {
|
|
56
57
|
newsService.start();
|
|
@@ -65,6 +66,7 @@ export function createAppServices({ config }: { config: AppConfig }): AppService
|
|
|
65
66
|
marketData,
|
|
66
67
|
pluginRegistry,
|
|
67
68
|
newsService,
|
|
69
|
+
ready: Promise.all(pluginReadyPromises).then(() => {}),
|
|
68
70
|
destroy() {
|
|
69
71
|
setSharedMarketDataCoordinator(null);
|
|
70
72
|
setSharedNewsService(null);
|
|
@@ -20,7 +20,7 @@ import { resolvePaneBodyFrame } from "../../../components/layout/pane/sizing";
|
|
|
20
20
|
import { getPaneDisplayTitle } from "../../../components/layout/pane/title";
|
|
21
21
|
import type { AppConfig } from "../../../types/config";
|
|
22
22
|
import type { CachedFinancialsTarget, DataProvider, QuoteSubscriptionTarget } from "../../../types/data-provider";
|
|
23
|
-
import type { TickerFinancials } from "../../../types/financials";
|
|
23
|
+
import type { OptionsChain, TickerFinancials } from "../../../types/financials";
|
|
24
24
|
import type { TickerRecord } from "../../../types/ticker";
|
|
25
25
|
import type { AppState, PaneRuntimeState } from "../../../core/state/app/state";
|
|
26
26
|
import type { PaneDef } from "../../../types/plugin";
|
|
@@ -32,6 +32,7 @@ interface CliPaneShotPayload {
|
|
|
32
32
|
heightCells: number;
|
|
33
33
|
tickers: TickerRecord[];
|
|
34
34
|
financials: Array<[string, TickerFinancials]>;
|
|
35
|
+
optionsChains: Array<[string, OptionsChain]>;
|
|
35
36
|
paneState: Record<string, PaneRuntimeState>;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -56,6 +57,7 @@ const TRACKED_RESPONSE_METHODS = new Set<PropertyKey>([
|
|
|
56
57
|
|
|
57
58
|
let pendingShotWork = 0;
|
|
58
59
|
let didInstallShotFetchTracker = false;
|
|
60
|
+
let shotDataProvider: DataProvider | null = null;
|
|
59
61
|
|
|
60
62
|
const rendererHost: RendererHost = {
|
|
61
63
|
requestExit() {},
|
|
@@ -152,6 +154,7 @@ function waitForShotReadiness(): () => void {
|
|
|
152
154
|
|
|
153
155
|
function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
|
|
154
156
|
const financials = new Map(payload.financials.map(([symbol, data]) => [normalizeSymbol(symbol), data]));
|
|
157
|
+
const optionsChains = new Map((payload.optionsChains ?? []).map(([symbol, data]) => [normalizeSymbol(symbol), data]));
|
|
155
158
|
|
|
156
159
|
const getFinancials = (symbol: string) => {
|
|
157
160
|
const data = financials.get(normalizeSymbol(symbol));
|
|
@@ -187,6 +190,13 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
|
|
|
187
190
|
getExchangeRate() {
|
|
188
191
|
return resolveShotWork(1);
|
|
189
192
|
},
|
|
193
|
+
getOptionsChain(ticker) {
|
|
194
|
+
return trackShotWork(Promise.resolve().then(() => {
|
|
195
|
+
const chain = optionsChains.get(normalizeSymbol(ticker));
|
|
196
|
+
if (!chain) throw new Error(`No screenshot options data available for ${ticker}.`);
|
|
197
|
+
return chain;
|
|
198
|
+
}));
|
|
199
|
+
},
|
|
190
200
|
search(query) {
|
|
191
201
|
const normalized = normalizeSymbol(query);
|
|
192
202
|
return resolveShotWork(payload.tickers
|
|
@@ -217,6 +227,7 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
|
|
|
217
227
|
|
|
218
228
|
function installShotMarketData(payload: CliPaneShotPayload): void {
|
|
219
229
|
const provider = createShotDataProvider(payload);
|
|
230
|
+
shotDataProvider = provider;
|
|
220
231
|
const coordinator = new MarketDataCoordinator(provider);
|
|
221
232
|
coordinator.primeCachedFinancials(payload.tickers.flatMap((ticker) => {
|
|
222
233
|
const financials = payload.financials.find(([symbol]) => normalizeSymbol(symbol) === normalizeSymbol(ticker.metadata.ticker))?.[1];
|
|
@@ -236,7 +247,7 @@ function createRuntime(payload: CliPaneShotPayload): PluginRuntimeAccess {
|
|
|
236
247
|
return (payload.config.pluginConfig[pluginId]?.[key] as T | undefined) ?? null;
|
|
237
248
|
}
|
|
238
249
|
return {
|
|
239
|
-
getMarketData: () =>
|
|
250
|
+
getMarketData: () => shotDataProvider,
|
|
240
251
|
getCapability: () => null,
|
|
241
252
|
getBrokerAdapter: () => null,
|
|
242
253
|
connectBrokerInstance: async () => {},
|
|
@@ -59,6 +59,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
59
59
|
isSelected,
|
|
60
60
|
onSelect,
|
|
61
61
|
onActivate,
|
|
62
|
+
onTableMouseDown,
|
|
62
63
|
onRowMouseDown,
|
|
63
64
|
onRowContextMenu,
|
|
64
65
|
rowContextMenuSurface = false,
|
|
@@ -71,6 +72,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
71
72
|
emptyStateHint,
|
|
72
73
|
virtualize = true,
|
|
73
74
|
overscan = 3,
|
|
75
|
+
columnGap = 1,
|
|
76
|
+
horizontalPadding = 1,
|
|
77
|
+
fillAvailableWidth = true,
|
|
74
78
|
showHorizontalScrollbar = true,
|
|
75
79
|
scrollToIndex,
|
|
76
80
|
scrollToIndexAlign = "nearest",
|
|
@@ -85,10 +89,13 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
85
89
|
const bodyVertical = useScrollbarState(true);
|
|
86
90
|
const [viewportWidth, setViewportWidth] = useState(0);
|
|
87
91
|
const [scrollbarActive, markScrollbarActive] = useScrollbarActivity();
|
|
88
|
-
const tableWidth = useMemo(
|
|
92
|
+
const tableWidth = useMemo(
|
|
93
|
+
() => getTableWidth(columns, columnGap, horizontalPadding),
|
|
94
|
+
[columnGap, columns, horizontalPadding],
|
|
95
|
+
);
|
|
89
96
|
const gridTemplateColumns = useMemo(
|
|
90
|
-
() => buildTableGridTemplateColumns(columns),
|
|
91
|
-
[columns],
|
|
97
|
+
() => buildTableGridTemplateColumns(columns, fillAvailableWidth),
|
|
98
|
+
[columns, fillAvailableWidth],
|
|
92
99
|
);
|
|
93
100
|
const selectRow = useCallback((item: T, index: number) => {
|
|
94
101
|
onSelect(item, index);
|
|
@@ -136,8 +143,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
136
143
|
: WEB_CELL_HEIGHT + items.length * WEB_CELL_HEIGHT;
|
|
137
144
|
const bodyAfterHeight = bodyAfter ? WEB_CELL_HEIGHT * 6 : 0;
|
|
138
145
|
const horizontalScrollEnabled = showHorizontalScrollbar
|
|
139
|
-
&& hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth);
|
|
146
|
+
&& hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth, columnGap);
|
|
140
147
|
const scrollContentWidth = horizontalScrollEnabled
|
|
148
|
+
|| !fillAvailableWidth
|
|
141
149
|
? Math.max(1, tableWidth * WEB_CELL_WIDTH)
|
|
142
150
|
: "100%";
|
|
143
151
|
const measureViewportWidth = useCallback(() => {
|
|
@@ -247,6 +255,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
247
255
|
style={bodyScrollerStyle}
|
|
248
256
|
onMouseDown={() => {
|
|
249
257
|
focusPane();
|
|
258
|
+
onTableMouseDown?.({});
|
|
250
259
|
}}
|
|
251
260
|
onMouseLeave={() => {
|
|
252
261
|
if (hoveredIdx !== null) setHoveredIdx(null);
|
|
@@ -270,7 +279,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
270
279
|
>
|
|
271
280
|
<WebDataTableHeader
|
|
272
281
|
columns={columns}
|
|
282
|
+
columnGap={columnGap}
|
|
283
|
+
horizontalPadding={horizontalPadding}
|
|
273
284
|
focusPane={focusPane}
|
|
285
|
+
onTableMouseDown={onTableMouseDown}
|
|
274
286
|
gridTemplateColumns={gridTemplateColumns}
|
|
275
287
|
onHeaderClick={onHeaderClick}
|
|
276
288
|
sortColumnId={sortColumnId}
|
|
@@ -314,7 +326,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
314
326
|
item={item}
|
|
315
327
|
itemKey={itemKey}
|
|
316
328
|
columns={columns}
|
|
329
|
+
columnGap={columnGap}
|
|
330
|
+
horizontalPadding={horizontalPadding}
|
|
317
331
|
focusPane={focusPane}
|
|
332
|
+
onTableMouseDown={onTableMouseDown}
|
|
318
333
|
gridTemplateColumns={gridTemplateColumns}
|
|
319
334
|
onActivateRow={onActivate ? activateRow : undefined}
|
|
320
335
|
onRowContextMenu={onRowContextMenu}
|
|
@@ -44,16 +44,30 @@ function contentJustifyForAlign(align: string | undefined): CSSProperties["justi
|
|
|
44
44
|
return "flex-start";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function columnGapCss(columnGap: number): CSSProperties["columnGap"] {
|
|
48
|
+
return columnGap === 0 ? 0 : `calc(${columnGap} * var(--cell-w))`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inlinePaddingPx(horizontalPadding: number): number {
|
|
52
|
+
return TABLE_INLINE_PADDING_PX * horizontalPadding;
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
export function WebDataTableHeader<C extends DataTableColumn>({
|
|
48
56
|
columns,
|
|
57
|
+
columnGap,
|
|
58
|
+
horizontalPadding,
|
|
49
59
|
focusPane,
|
|
60
|
+
onTableMouseDown,
|
|
50
61
|
gridTemplateColumns,
|
|
51
62
|
onHeaderClick,
|
|
52
63
|
sortColumnId,
|
|
53
64
|
sortDirection,
|
|
54
65
|
}: {
|
|
55
66
|
columns: C[];
|
|
67
|
+
columnGap: number;
|
|
68
|
+
horizontalPadding: number;
|
|
56
69
|
focusPane: () => void;
|
|
70
|
+
onTableMouseDown?: (event: any) => void;
|
|
57
71
|
gridTemplateColumns: string;
|
|
58
72
|
onHeaderClick: (columnId: string) => void;
|
|
59
73
|
sortColumnId: string | null;
|
|
@@ -69,13 +83,13 @@ export function WebDataTableHeader<C extends DataTableColumn>({
|
|
|
69
83
|
zIndex: 2,
|
|
70
84
|
display: "grid",
|
|
71
85
|
gridTemplateColumns,
|
|
72
|
-
columnGap:
|
|
86
|
+
columnGap: columnGapCss(columnGap),
|
|
73
87
|
alignItems: "center",
|
|
74
88
|
width: "100%",
|
|
75
89
|
minWidth: 0,
|
|
76
90
|
height: WEB_CELL_HEIGHT,
|
|
77
|
-
paddingLeft:
|
|
78
|
-
paddingRight:
|
|
91
|
+
paddingLeft: inlinePaddingPx(horizontalPadding),
|
|
92
|
+
paddingRight: inlinePaddingPx(horizontalPadding),
|
|
79
93
|
boxSizing: "border-box",
|
|
80
94
|
backgroundColor: CSS_PANEL,
|
|
81
95
|
}}
|
|
@@ -99,6 +113,7 @@ export function WebDataTableHeader<C extends DataTableColumn>({
|
|
|
99
113
|
}}
|
|
100
114
|
onMouseDown={(event) => {
|
|
101
115
|
focusPane();
|
|
116
|
+
onTableMouseDown?.(event);
|
|
102
117
|
event.preventDefault();
|
|
103
118
|
onHeaderClick(column.id);
|
|
104
119
|
}}
|
|
@@ -128,7 +143,10 @@ function WebDataTableRowInner<
|
|
|
128
143
|
C extends DataTableColumn,
|
|
129
144
|
>({
|
|
130
145
|
columns,
|
|
146
|
+
columnGap,
|
|
147
|
+
horizontalPadding,
|
|
131
148
|
focusPane,
|
|
149
|
+
onTableMouseDown,
|
|
132
150
|
onActivateRow,
|
|
133
151
|
onRowContextMenu,
|
|
134
152
|
onRowMouseDown,
|
|
@@ -148,7 +166,10 @@ function WebDataTableRowInner<
|
|
|
148
166
|
setHoveredIdx,
|
|
149
167
|
}: {
|
|
150
168
|
columns: C[];
|
|
169
|
+
columnGap: number;
|
|
170
|
+
horizontalPadding: number;
|
|
151
171
|
focusPane: () => void;
|
|
172
|
+
onTableMouseDown?: (event: any) => void;
|
|
152
173
|
onActivateRow?: (item: T, index: number) => void;
|
|
153
174
|
onRowContextMenu?: DataTableProps<T, C>["onRowContextMenu"];
|
|
154
175
|
onRowMouseDown?: DataTableProps<T, C>["onRowMouseDown"];
|
|
@@ -177,13 +198,13 @@ function WebDataTableRowInner<
|
|
|
177
198
|
transform: `translateY(${rowStart}px)`,
|
|
178
199
|
display: "grid",
|
|
179
200
|
gridTemplateColumns,
|
|
180
|
-
columnGap:
|
|
201
|
+
columnGap: columnGapCss(columnGap),
|
|
181
202
|
alignItems: "center",
|
|
182
203
|
width: "100%",
|
|
183
204
|
minWidth: 0,
|
|
184
205
|
height: rowSize,
|
|
185
|
-
paddingLeft:
|
|
186
|
-
paddingRight:
|
|
206
|
+
paddingLeft: inlinePaddingPx(horizontalPadding),
|
|
207
|
+
paddingRight: inlinePaddingPx(horizontalPadding),
|
|
187
208
|
boxSizing: "border-box",
|
|
188
209
|
lineHeight: "var(--cell-h)",
|
|
189
210
|
};
|
|
@@ -199,6 +220,7 @@ function WebDataTableRowInner<
|
|
|
199
220
|
}}
|
|
200
221
|
onMouseDown={(event) => {
|
|
201
222
|
focusPane();
|
|
223
|
+
onTableMouseDown?.(event);
|
|
202
224
|
event.preventDefault();
|
|
203
225
|
}}
|
|
204
226
|
>
|
|
@@ -243,6 +265,7 @@ function WebDataTableRowInner<
|
|
|
243
265
|
}}
|
|
244
266
|
onMouseDown={(event) => {
|
|
245
267
|
focusPane();
|
|
268
|
+
onTableMouseDown?.(event);
|
|
246
269
|
if (onRowMouseDown?.(item, index, eventWithCellCoordinates(event)) === true) {
|
|
247
270
|
return;
|
|
248
271
|
}
|
|
@@ -274,6 +297,7 @@ function WebDataTableRowInner<
|
|
|
274
297
|
}}
|
|
275
298
|
onMouseDown={(event) => {
|
|
276
299
|
focusPane();
|
|
300
|
+
onTableMouseDown?.(event);
|
|
277
301
|
if (cell.onMouseDown) {
|
|
278
302
|
cell.onMouseDown(eventWithCellCoordinates(event));
|
|
279
303
|
return;
|
|
@@ -84,6 +84,7 @@ export function WebTextField({
|
|
|
84
84
|
inputRef,
|
|
85
85
|
onChange,
|
|
86
86
|
onSubmit,
|
|
87
|
+
onBlur,
|
|
87
88
|
hint,
|
|
88
89
|
type = "text",
|
|
89
90
|
variant = "default",
|
|
@@ -155,7 +156,12 @@ export function WebTextField({
|
|
|
155
156
|
}}
|
|
156
157
|
onInput={onChange}
|
|
157
158
|
onChange={onChange}
|
|
158
|
-
onSubmit={() => onSubmit?.(
|
|
159
|
+
onSubmit={(nextValue?: string) => onSubmit?.(
|
|
160
|
+
typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
|
|
161
|
+
)}
|
|
162
|
+
onBlur={(nextValue?: string) => onBlur?.(
|
|
163
|
+
typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
|
|
164
|
+
)}
|
|
159
165
|
/>
|
|
160
166
|
</Box>
|
|
161
167
|
{hint && (
|
|
@@ -347,14 +353,14 @@ export function WebPageStackView({
|
|
|
347
353
|
|
|
348
354
|
if (!detailOpen) {
|
|
349
355
|
return (
|
|
350
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
356
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
351
357
|
{rootContent}
|
|
352
358
|
</Box>
|
|
353
359
|
);
|
|
354
360
|
}
|
|
355
361
|
|
|
356
362
|
return (
|
|
357
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
363
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
358
364
|
<Box
|
|
359
365
|
flexDirection="row"
|
|
360
366
|
alignItems="flex-start"
|
|
@@ -370,7 +376,7 @@ export function WebPageStackView({
|
|
|
370
376
|
flexDirection="row"
|
|
371
377
|
alignItems="center"
|
|
372
378
|
backgroundColor={panelFill()}
|
|
373
|
-
onMouseDown={(event) => {
|
|
379
|
+
onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
374
380
|
event.preventDefault?.();
|
|
375
381
|
event.stopPropagation?.();
|
|
376
382
|
onBack();
|
|
@@ -413,7 +419,7 @@ export function WebPageStackView({
|
|
|
413
419
|
</Text>
|
|
414
420
|
) : null}
|
|
415
421
|
</Box>
|
|
416
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
422
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
417
423
|
{detailContent}
|
|
418
424
|
</Box>
|
|
419
425
|
</Box>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { renderAsciiText, type AsciiFontName } from "../../../../ui/ascii-font";
|
|
2
|
+
|
|
3
|
+
const WEB_GLOOMBERB_WORDMARK = [
|
|
4
|
+
" ____ _ _ _ ",
|
|
5
|
+
" / ___| | ___ ___ _ __ ___ | |__ ___ _ __| |__ ",
|
|
6
|
+
"| | _| |/ _ \\ / _ \\| '_ ` _ \\| '_ \\ / _ \\ '__| '_ \\ ",
|
|
7
|
+
"| |_| | | (_) | (_) | | | | | | |_) | __/ | | |_) |",
|
|
8
|
+
" \\____|_|\\___/ \\___/|_| |_| |_|_.__/ \\___|_| |_.__/ ",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
type WebWordmarkVariant = "legacy" | "compat" | null;
|
|
12
|
+
|
|
13
|
+
function currentDesktopPlatform(): string {
|
|
14
|
+
const navigatorLike = (globalThis as {
|
|
15
|
+
navigator?: {
|
|
16
|
+
platform?: string;
|
|
17
|
+
userAgent?: string;
|
|
18
|
+
userAgentData?: { platform?: string };
|
|
19
|
+
};
|
|
20
|
+
}).navigator;
|
|
21
|
+
return [
|
|
22
|
+
navigatorLike?.userAgentData?.platform,
|
|
23
|
+
navigatorLike?.platform,
|
|
24
|
+
navigatorLike?.userAgent,
|
|
25
|
+
].filter(Boolean).join(" ");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isMacDesktopPlatform(desktopPlatform?: string): boolean {
|
|
29
|
+
const raw = desktopPlatform?.trim() || currentDesktopPlatform();
|
|
30
|
+
return /(darwin|mac|iphone|ipad|ipod)/i.test(raw);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function webAsciiTextWordmarkVariant(
|
|
34
|
+
text: string,
|
|
35
|
+
font: AsciiFontName = "tiny",
|
|
36
|
+
desktopPlatform?: string,
|
|
37
|
+
): WebWordmarkVariant {
|
|
38
|
+
if (font !== "wordmark" || text.trim().toLowerCase() !== "gloomberb") return null;
|
|
39
|
+
return isMacDesktopPlatform(desktopPlatform) ? "legacy" : "compat";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function webAsciiTextLines(
|
|
43
|
+
text: string,
|
|
44
|
+
font: AsciiFontName = "tiny",
|
|
45
|
+
desktopPlatform?: string,
|
|
46
|
+
): string[] {
|
|
47
|
+
return webAsciiTextWordmarkVariant(text, font, desktopPlatform) === "compat"
|
|
48
|
+
? WEB_GLOOMBERB_WORDMARK
|
|
49
|
+
: renderAsciiText(text, font);
|
|
50
|
+
}
|