gloomberb 0.9.1 → 0.9.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 +23 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app.tsx +10 -7
- 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/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/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/comparison-stock-chart/helpers.ts +0 -32
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
- package/src/components/chart/comparison-stock-chart/legend.tsx +45 -44
- 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 +56 -6
- 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 +2 -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/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 +5 -2
- package/src/components/layout/header.tsx +13 -5
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/drag/runtime.ts +21 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +395 -2
- package/src/components/layout/shell/index.tsx +128 -2
- package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +26 -14
- 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/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 +105 -0
- package/src/components/layout/status-bar.tsx +32 -6
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-controls.tsx +19 -10
- 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/price-performance.tsx +183 -0
- package/src/core/app-services.ts +7 -4
- package/src/data/config/store/index.ts +1 -2
- package/src/market-data/performance.test.ts +54 -0
- package/src/market-data/performance.ts +109 -0
- 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 +218 -15
- package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
- package/src/plugins/builtin/congress-trades/footer.ts +14 -8
- package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
- package/src/plugins/builtin/congress-trades/model.ts +29 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
- package/src/plugins/builtin/congress-trades/table.tsx +10 -1
- package/src/plugins/builtin/news/wire/read-state.ts +9 -32
- 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/pane/index.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- 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/overview/model.ts +0 -14
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/queries.ts +8 -2
- 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/pane.test.tsx +32 -0
- package/src/plugins/prediction-markets/pane.tsx +21 -1
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
- 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/desktop/detached-windows.ts +9 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +11 -2
- package/src/renderers/electrobun/bun/desktop/window-style.ts +27 -1
- package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
- package/src/renderers/electrobun/bun/desktop/windows-icons.ts +11 -64
- package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
- package/src/renderers/electrobun/bun/index.ts +22 -2
- 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/shared/protocol.ts +1 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- 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/main.tsx +2 -1
- package/src/renderers/electrobun/view/styles.css +9 -8
- package/src/renderers/electrobun/view/ui-host.tsx +102 -77
- package/src/renderers/opentui/start.tsx +13 -5
- package/src/state/app/input-capture.ts +12 -0
- package/src/types/plugin.ts +28 -0
- package/src/utils/cli-output.ts +6 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
|
@@ -2,8 +2,14 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { TestDialogProvider, testRender } from "../../../renderers/opentui/test-utils";
|
|
3
3
|
import { openTuiUiHost } from "../../../renderers/opentui/ui-host";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
|
-
import { act, useReducer } from "react";
|
|
6
|
-
import {
|
|
5
|
+
import { act, useReducer, useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
AppContext,
|
|
8
|
+
appReducer,
|
|
9
|
+
createInitialState,
|
|
10
|
+
resolveTickerForPane,
|
|
11
|
+
usePaneTicker,
|
|
12
|
+
} from "../../../state/app/context";
|
|
7
13
|
import { cloneLayout, createDefaultConfig, TICKER_RESEARCH_PANE_ID, type LayoutConfig } from "../../../types/config";
|
|
8
14
|
import type { PluginRegistry } from "../../../plugins/registry";
|
|
9
15
|
import type { PaneProps } from "../../../types/plugin";
|
|
@@ -14,6 +20,8 @@ import {
|
|
|
14
20
|
resolveAppHeaderHeightCells,
|
|
15
21
|
Shell,
|
|
16
22
|
} from "./index";
|
|
23
|
+
import { resolvePaneFocusSourceLayout } from "./fullscreen";
|
|
24
|
+
import { TransientLayoutProvider, useTransientLayout, type TransientLayoutState } from "../transient-layout";
|
|
17
25
|
|
|
18
26
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
19
27
|
|
|
@@ -133,6 +141,44 @@ async function renderShellForWindowModeTest(
|
|
|
133
141
|
return { actions, registry };
|
|
134
142
|
}
|
|
135
143
|
|
|
144
|
+
function CaptureTransientLayout({
|
|
145
|
+
controls,
|
|
146
|
+
}: {
|
|
147
|
+
controls: { transientLayout: TransientLayoutState | null };
|
|
148
|
+
}) {
|
|
149
|
+
const { transientLayout } = useTransientLayout();
|
|
150
|
+
controls.transientLayout = transientLayout;
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ShellTransientHarness({
|
|
155
|
+
initialState,
|
|
156
|
+
registry,
|
|
157
|
+
controls,
|
|
158
|
+
}: {
|
|
159
|
+
initialState: ReturnType<typeof createInitialState>;
|
|
160
|
+
registry: PluginRegistry;
|
|
161
|
+
controls: {
|
|
162
|
+
dispatch?: (action: ShellTestAction) => void;
|
|
163
|
+
state?: ReturnType<typeof createInitialState>;
|
|
164
|
+
transientLayout: TransientLayoutState | null;
|
|
165
|
+
};
|
|
166
|
+
}) {
|
|
167
|
+
const [state, dispatch] = useReducer(appReducer, initialState);
|
|
168
|
+
controls.dispatch = dispatch;
|
|
169
|
+
controls.state = state;
|
|
170
|
+
return (
|
|
171
|
+
<AppContext value={{ state, dispatch }}>
|
|
172
|
+
<TransientLayoutProvider>
|
|
173
|
+
<TestDialogProvider>
|
|
174
|
+
<Shell pluginRegistry={registry} />
|
|
175
|
+
</TestDialogProvider>
|
|
176
|
+
<CaptureTransientLayout controls={controls} />
|
|
177
|
+
</TransientLayoutProvider>
|
|
178
|
+
</AppContext>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
136
182
|
function findUpdateLayout(actions: ShellTestAction[]) {
|
|
137
183
|
return actions.find((action) => action.type === "UPDATE_LAYOUT");
|
|
138
184
|
}
|
|
@@ -417,6 +463,7 @@ describe("Shell", () => {
|
|
|
417
463
|
expect(resolvePaneManagementShortcut({ ...base, name: "o", key: "o" })).toBe("pop-out");
|
|
418
464
|
expect(resolvePaneManagementShortcut({ ...base, name: "c", key: "c" })).toBe("copy-screenshot");
|
|
419
465
|
expect(resolvePaneManagementShortcut({ ...base, name: "l", key: "l" })).toBe("layout-actions");
|
|
466
|
+
expect(resolvePaneManagementShortcut({ ...base, name: "f", key: "f" })).toBe("toggle-fullscreen");
|
|
420
467
|
expect(resolvePaneManagementShortcut({ ...base, name: "g", key: "g" })).toBe("gridlock-all");
|
|
421
468
|
expect(resolvePaneManagementShortcut({ ...base, name: "m", key: "m" })).toBe("window-mode");
|
|
422
469
|
expect(resolvePaneManagementShortcut({ ...base, name: "r", key: "r" })).toBe("window-resize-mode");
|
|
@@ -425,6 +472,352 @@ describe("Shell", () => {
|
|
|
425
472
|
expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", meta: false, super: false })).toBeNull();
|
|
426
473
|
});
|
|
427
474
|
|
|
475
|
+
test("toggles the focused pane fullscreen without persisting layout", async () => {
|
|
476
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-shortcut-test");
|
|
477
|
+
const mainPane = requireLayoutInstance(config, "portfolio-list:main");
|
|
478
|
+
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
479
|
+
const dockedLayout = {
|
|
480
|
+
dockRoot: {
|
|
481
|
+
kind: "split" as const,
|
|
482
|
+
axis: "horizontal" as const,
|
|
483
|
+
ratio: 0.5,
|
|
484
|
+
first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
485
|
+
second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
|
|
486
|
+
},
|
|
487
|
+
instances: [{ ...mainPane }, { ...detailPane }],
|
|
488
|
+
floating: [],
|
|
489
|
+
detached: [],
|
|
490
|
+
};
|
|
491
|
+
const { actions } = await renderShellForWindowModeTest(
|
|
492
|
+
createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"),
|
|
493
|
+
{ width: 80, height: 18 },
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
expect(testSetup.captureCharFrame()).toContain("Main Portfolio");
|
|
497
|
+
expect(testSetup.captureCharFrame()).toContain("Ticker Research Body");
|
|
498
|
+
|
|
499
|
+
await emitKeypress({ name: "f", ctrl: true, shift: true });
|
|
500
|
+
let frame = testSetup.captureCharFrame();
|
|
501
|
+
expect(frame).toContain("Main Portfolio");
|
|
502
|
+
expect(frame).not.toContain("Ticker Research Body");
|
|
503
|
+
expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
|
|
504
|
+
|
|
505
|
+
await emitKeypress({ name: "f", ctrl: true, shift: true });
|
|
506
|
+
await act(async () => {
|
|
507
|
+
await testSetup!.renderOnce();
|
|
508
|
+
});
|
|
509
|
+
frame = testSetup.captureCharFrame();
|
|
510
|
+
expect(frame).toContain("Main Portfolio");
|
|
511
|
+
expect(frame).toContain("Ticker Research Body");
|
|
512
|
+
expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("captures the source layout for transient pane focus", () => {
|
|
516
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-layout-test");
|
|
517
|
+
const layout = cloneLayout(config.layout);
|
|
518
|
+
layout.dockRoot = { kind: "pane", instanceId: "portfolio-list:main" };
|
|
519
|
+
layout.floating = [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 32, height: 10, zIndex: 75 }];
|
|
520
|
+
|
|
521
|
+
const focusedLayout = resolvePaneFocusSourceLayout(layout, "ticker-detail:main");
|
|
522
|
+
|
|
523
|
+
expect(focusedLayout).toMatchObject({
|
|
524
|
+
dockRoot: { kind: "pane", instanceId: "portfolio-list:main" },
|
|
525
|
+
floating: [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 32, height: 10, zIndex: 75 }],
|
|
526
|
+
});
|
|
527
|
+
expect(focusedLayout).not.toBe(layout);
|
|
528
|
+
expect(focusedLayout?.instances.map((instance) => instance.instanceId)).toEqual(
|
|
529
|
+
layout.instances.map((instance) => instance.instanceId),
|
|
530
|
+
);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("locks layout-changing mouse drags while fullscreen is active", async () => {
|
|
534
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-drag-test");
|
|
535
|
+
const mainPane = requireLayoutInstance(config, "portfolio-list:main");
|
|
536
|
+
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
537
|
+
const dockedLayout = {
|
|
538
|
+
dockRoot: {
|
|
539
|
+
kind: "split" as const,
|
|
540
|
+
axis: "horizontal" as const,
|
|
541
|
+
ratio: 0.5,
|
|
542
|
+
first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
543
|
+
second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
|
|
544
|
+
},
|
|
545
|
+
instances: [{ ...mainPane }, { ...detailPane }],
|
|
546
|
+
floating: [],
|
|
547
|
+
detached: [],
|
|
548
|
+
};
|
|
549
|
+
const { actions } = await renderShellForWindowModeTest(
|
|
550
|
+
createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"),
|
|
551
|
+
{ width: 80, height: 18 },
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
await emitKeypress({ name: "f", ctrl: true, shift: true });
|
|
555
|
+
await act(async () => {
|
|
556
|
+
await testSetup!.mockMouse.drag(4, 0, 32, 4);
|
|
557
|
+
await testSetup!.renderOnce();
|
|
558
|
+
await testSetup!.renderOnce();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const frame = testSetup.captureCharFrame();
|
|
562
|
+
expect(frame).toContain("Main Portfolio");
|
|
563
|
+
expect(frame).not.toContain("Ticker Research Body");
|
|
564
|
+
expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("keeps focused pane local detail state when maximizing a floating pane", async () => {
|
|
568
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-local-state-test");
|
|
569
|
+
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
570
|
+
let openDetail: (() => void) | null = null;
|
|
571
|
+
const registry = createShellPluginRegistry({
|
|
572
|
+
tickerDetailComponent: () => {
|
|
573
|
+
const [detailOpen, setDetailOpen] = useState(false);
|
|
574
|
+
openDetail = () => setDetailOpen(true);
|
|
575
|
+
return <text>{detailOpen ? "Detail State" : "Table State"}</text>;
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
const floatingLayout = {
|
|
579
|
+
dockRoot: null,
|
|
580
|
+
instances: [{ ...detailPane }],
|
|
581
|
+
floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 40, height: 10, zIndex: 50 }],
|
|
582
|
+
detached: [],
|
|
583
|
+
};
|
|
584
|
+
await renderShellForWindowModeTest(
|
|
585
|
+
createShellStateWithLayout(config, floatingLayout, "ticker-detail:main"),
|
|
586
|
+
{ registry, width: 80, height: 18 },
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
await act(async () => {
|
|
590
|
+
openDetail?.();
|
|
591
|
+
await testSetup!.renderOnce();
|
|
592
|
+
});
|
|
593
|
+
expect(testSetup.captureCharFrame()).toContain("etail State");
|
|
594
|
+
|
|
595
|
+
await emitKeypress({ name: "f", ctrl: true, shift: true });
|
|
596
|
+
|
|
597
|
+
const frame = testSetup.captureCharFrame();
|
|
598
|
+
expect(frame).toContain("etail State");
|
|
599
|
+
expect(frame).not.toContain("able State");
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("allows pane fullscreen shortcut while text input is captured on desktop", async () => {
|
|
603
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-captured-input-test");
|
|
604
|
+
const mainPane = requireLayoutInstance(config, "portfolio-list:main");
|
|
605
|
+
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
606
|
+
const dockedLayout = {
|
|
607
|
+
dockRoot: {
|
|
608
|
+
kind: "split" as const,
|
|
609
|
+
axis: "horizontal" as const,
|
|
610
|
+
ratio: 0.5,
|
|
611
|
+
first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
612
|
+
second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
|
|
613
|
+
},
|
|
614
|
+
instances: [{ ...mainPane }, { ...detailPane }],
|
|
615
|
+
floating: [],
|
|
616
|
+
detached: [],
|
|
617
|
+
};
|
|
618
|
+
const { actions } = await renderShellForWindowModeTest(
|
|
619
|
+
{
|
|
620
|
+
...createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"),
|
|
621
|
+
inputCaptured: true,
|
|
622
|
+
},
|
|
623
|
+
{ width: 80, height: 18 },
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
await emitKeypress({
|
|
627
|
+
name: "f",
|
|
628
|
+
key: "f",
|
|
629
|
+
meta: true,
|
|
630
|
+
super: true,
|
|
631
|
+
shift: true,
|
|
632
|
+
targetEditable: true,
|
|
633
|
+
} as any);
|
|
634
|
+
|
|
635
|
+
const frame = testSetup.captureCharFrame();
|
|
636
|
+
expect(frame).toContain("Main Portfolio");
|
|
637
|
+
expect(frame).not.toContain("Ticker Research Body");
|
|
638
|
+
expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("keeps the transient focus layout available after switching saved layouts", async () => {
|
|
642
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-transient-focus-layout-tab-test");
|
|
643
|
+
const mainPane = requireLayoutInstance(config, "portfolio-list:main");
|
|
644
|
+
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
645
|
+
const defaultLayout = {
|
|
646
|
+
dockRoot: {
|
|
647
|
+
kind: "split" as const,
|
|
648
|
+
axis: "horizontal" as const,
|
|
649
|
+
ratio: 0.5,
|
|
650
|
+
first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
651
|
+
second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
|
|
652
|
+
},
|
|
653
|
+
instances: [{ ...mainPane }, { ...detailPane }],
|
|
654
|
+
floating: [],
|
|
655
|
+
detached: [],
|
|
656
|
+
};
|
|
657
|
+
const monitorLayout = {
|
|
658
|
+
dockRoot: { kind: "pane" as const, instanceId: "ticker-detail:main" },
|
|
659
|
+
instances: [{ ...detailPane }],
|
|
660
|
+
floating: [],
|
|
661
|
+
detached: [],
|
|
662
|
+
};
|
|
663
|
+
config.layout = cloneLayout(defaultLayout);
|
|
664
|
+
config.layouts = [
|
|
665
|
+
{ name: "Default", layout: cloneLayout(defaultLayout), focusedPaneId: "portfolio-list:main" },
|
|
666
|
+
{ name: "Monitor", layout: cloneLayout(monitorLayout), focusedPaneId: "ticker-detail:main" },
|
|
667
|
+
];
|
|
668
|
+
config.activeLayoutIndex = 0;
|
|
669
|
+
|
|
670
|
+
const initialState = {
|
|
671
|
+
...createInitialState(config),
|
|
672
|
+
focusedPaneId: "portfolio-list:main",
|
|
673
|
+
statusBarVisible: true,
|
|
674
|
+
};
|
|
675
|
+
const registry = createShellPluginRegistry();
|
|
676
|
+
const controls: {
|
|
677
|
+
dispatch?: (action: ShellTestAction) => void;
|
|
678
|
+
state?: ReturnType<typeof createInitialState>;
|
|
679
|
+
transientLayout: TransientLayoutState | null;
|
|
680
|
+
} = { transientLayout: null };
|
|
681
|
+
|
|
682
|
+
testSetup = await testRender(
|
|
683
|
+
<ShellTransientHarness initialState={initialState} registry={registry} controls={controls} />,
|
|
684
|
+
{ width: 100, height: 18 },
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
await testSetup.renderOnce();
|
|
688
|
+
await emitKeypress({ name: "f", ctrl: true, shift: true });
|
|
689
|
+
|
|
690
|
+
let frame = testSetup.captureCharFrame();
|
|
691
|
+
expect(frame).toContain("Main Portfolio");
|
|
692
|
+
expect(frame).not.toContain("Ticker Research Body");
|
|
693
|
+
expect(controls.transientLayout).toMatchObject({
|
|
694
|
+
id: "pane-focus",
|
|
695
|
+
active: true,
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
await act(async () => {
|
|
699
|
+
controls.transientLayout?.onDeactivate?.();
|
|
700
|
+
controls.dispatch?.({ type: "SWITCH_LAYOUT", index: 1 });
|
|
701
|
+
await testSetup!.renderOnce();
|
|
702
|
+
await testSetup!.renderOnce();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
expect(controls.transientLayout).toMatchObject({
|
|
706
|
+
id: "pane-focus",
|
|
707
|
+
active: false,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
await act(async () => {
|
|
711
|
+
controls.transientLayout?.onActivate?.();
|
|
712
|
+
await testSetup!.renderOnce();
|
|
713
|
+
await testSetup!.renderOnce();
|
|
714
|
+
await testSetup!.renderOnce();
|
|
715
|
+
await testSetup!.renderOnce();
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
expect(controls.transientLayout).toMatchObject({
|
|
719
|
+
id: "pane-focus",
|
|
720
|
+
active: true,
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("restores the focus source layout state when reactivating transient focus", async () => {
|
|
725
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-transient-focus-state-test");
|
|
726
|
+
const mainPane = requireLayoutInstance(config, "portfolio-list:main");
|
|
727
|
+
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
728
|
+
const defaultLayout = {
|
|
729
|
+
dockRoot: {
|
|
730
|
+
kind: "split" as const,
|
|
731
|
+
axis: "horizontal" as const,
|
|
732
|
+
ratio: 0.5,
|
|
733
|
+
first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
734
|
+
second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
|
|
735
|
+
},
|
|
736
|
+
instances: [{ ...mainPane }, { ...detailPane }],
|
|
737
|
+
floating: [],
|
|
738
|
+
detached: [],
|
|
739
|
+
};
|
|
740
|
+
const monitorLayout = cloneLayout(defaultLayout);
|
|
741
|
+
config.layout = cloneLayout(defaultLayout);
|
|
742
|
+
config.layouts = [
|
|
743
|
+
{
|
|
744
|
+
name: "Default",
|
|
745
|
+
layout: cloneLayout(defaultLayout),
|
|
746
|
+
focusedPaneId: "ticker-detail:main",
|
|
747
|
+
paneState: {
|
|
748
|
+
"portfolio-list:main": { collectionId: "main", cursorSymbol: "MSTR" },
|
|
749
|
+
"ticker-detail:main": { activeTabId: "overview" },
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
name: "Monitor",
|
|
754
|
+
layout: monitorLayout,
|
|
755
|
+
focusedPaneId: "ticker-detail:main",
|
|
756
|
+
paneState: {
|
|
757
|
+
"portfolio-list:main": { collectionId: "main", cursorSymbol: null },
|
|
758
|
+
"ticker-detail:main": { activeTabId: "overview" },
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
];
|
|
762
|
+
config.activeLayoutIndex = 0;
|
|
763
|
+
const registry = createShellPluginRegistry({
|
|
764
|
+
tickerDetailComponent: () => {
|
|
765
|
+
const { symbol } = usePaneTicker();
|
|
766
|
+
return <text>{`Ticker:${symbol ?? "none"}`}</text>;
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
const controls: {
|
|
770
|
+
dispatch?: (action: ShellTestAction) => void;
|
|
771
|
+
state?: ReturnType<typeof createInitialState>;
|
|
772
|
+
transientLayout: TransientLayoutState | null;
|
|
773
|
+
} = { transientLayout: null };
|
|
774
|
+
|
|
775
|
+
testSetup = await testRender(
|
|
776
|
+
<ShellTransientHarness
|
|
777
|
+
initialState={createInitialState(config)}
|
|
778
|
+
registry={registry}
|
|
779
|
+
controls={controls}
|
|
780
|
+
/>,
|
|
781
|
+
{ width: 100, height: 18 },
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
await testSetup.renderOnce();
|
|
785
|
+
expect(testSetup.captureCharFrame()).toContain("icker:MSTR");
|
|
786
|
+
|
|
787
|
+
await emitKeypress({ name: "f", ctrl: true, shift: true });
|
|
788
|
+
expect(testSetup.captureCharFrame()).toContain("icker:MSTR");
|
|
789
|
+
|
|
790
|
+
await act(async () => {
|
|
791
|
+
controls.transientLayout?.onDeactivate?.();
|
|
792
|
+
controls.dispatch?.({ type: "SWITCH_LAYOUT", index: 1 });
|
|
793
|
+
await testSetup!.renderOnce();
|
|
794
|
+
await testSetup!.renderOnce();
|
|
795
|
+
});
|
|
796
|
+
expect(controls.transientLayout).toMatchObject({
|
|
797
|
+
id: "pane-focus",
|
|
798
|
+
active: false,
|
|
799
|
+
});
|
|
800
|
+
expect(controls.state?.config.activeLayoutIndex).toBe(1);
|
|
801
|
+
expect(controls.state?.paneState["portfolio-list:main"]?.cursorSymbol).toBeNull();
|
|
802
|
+
expect(controls.state ? resolveTickerForPane(controls.state, "ticker-detail:main") : null).toBeNull();
|
|
803
|
+
|
|
804
|
+
await act(async () => {
|
|
805
|
+
controls.transientLayout?.onActivate?.();
|
|
806
|
+
await testSetup!.renderOnce();
|
|
807
|
+
await testSetup!.renderOnce();
|
|
808
|
+
await testSetup!.renderOnce();
|
|
809
|
+
await testSetup!.renderOnce();
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
expect(controls.transientLayout).toMatchObject({
|
|
813
|
+
id: "pane-focus",
|
|
814
|
+
active: true,
|
|
815
|
+
});
|
|
816
|
+
expect(controls.state?.config.activeLayoutIndex).toBe(0);
|
|
817
|
+
expect(controls.state?.paneState["portfolio-list:main"]?.cursorSymbol).toBe("MSTR");
|
|
818
|
+
expect(controls.state ? resolveTickerForPane(controls.state, "ticker-detail:main") : null).toBe("MSTR");
|
|
819
|
+
});
|
|
820
|
+
|
|
428
821
|
test("exits window mode on Enter without changes", async () => {
|
|
429
822
|
const config = createDefaultConfig("/tmp/gloomberb-shell-window-mode-resize-shortcut-test");
|
|
430
823
|
const mainPane = requireLayoutInstance(config, "portfolio-list:main");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AsciiText, Box, Text, compactContextMenuItems, useContextMenu, useUiHost } from "../../../ui";
|
|
2
|
-
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useRendererHost, useUiCapabilities } from "../../../ui";
|
|
4
4
|
import { useViewport } from "../../../react/input";
|
|
5
5
|
import { useDialogState } from "../../../ui/dialog";
|
|
@@ -8,6 +8,7 @@ import type { DesktopDockPreviewState, DesktopWindowBridge } from "../../../type
|
|
|
8
8
|
import {
|
|
9
9
|
getDockDividerLayouts,
|
|
10
10
|
getDockLeafLayouts,
|
|
11
|
+
isPaneInLayout,
|
|
11
12
|
type DockGeometryOptions,
|
|
12
13
|
type LayoutBounds,
|
|
13
14
|
type ResolvedPane,
|
|
@@ -58,6 +59,8 @@ import {
|
|
|
58
59
|
useShellVisibleLayout,
|
|
59
60
|
} from "./layout-state";
|
|
60
61
|
import { useShellPaneActions } from "./pane/actions";
|
|
62
|
+
import { resolvePaneFocusSourceLayout } from "./fullscreen";
|
|
63
|
+
import { useTransientLayout } from "../transient-layout";
|
|
61
64
|
|
|
62
65
|
export { resolveAppHeaderHeightCells } from "./chrome";
|
|
63
66
|
export { buildNativeWindowState } from "./native/window-state";
|
|
@@ -70,6 +73,13 @@ interface ShellProps {
|
|
|
70
73
|
commandBarNativeOccluder?: LayoutBounds | null;
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
interface TransientFocusLayoutState {
|
|
77
|
+
paneId: string;
|
|
78
|
+
layout: LayoutConfig;
|
|
79
|
+
sourceLayoutIndex: number;
|
|
80
|
+
active: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
export function Shell({
|
|
74
84
|
pluginRegistry,
|
|
75
85
|
desktopWindowBridge,
|
|
@@ -86,6 +96,7 @@ export function Shell({
|
|
|
86
96
|
const inputCaptured = useAppSelector((state) => state.inputCaptured);
|
|
87
97
|
const statusBarVisible = useAppSelector(selectStatusBarVisible);
|
|
88
98
|
const rendererHost = useRendererHost();
|
|
99
|
+
const { setTransientLayout } = useTransientLayout();
|
|
89
100
|
const uiKind = useUiHost().kind;
|
|
90
101
|
const shortcutDisplayMode = getShortcutDisplayMode(uiKind);
|
|
91
102
|
const { nativePaneChrome = false, nativeContextMenu, precisePointer, titleBarOverlay, cellHeightPx } = useUiCapabilities();
|
|
@@ -103,6 +114,9 @@ export function Shell({
|
|
|
103
114
|
setHoveredPaneId((current) => (current === paneId ? current : paneId));
|
|
104
115
|
}, []);
|
|
105
116
|
const [menuState, setMenuState] = useState<ActionMenuState | null>(null);
|
|
117
|
+
const [transientFocusLayoutState, setTransientFocusLayoutState] = useState<TransientFocusLayoutState | null>(null);
|
|
118
|
+
const transientFocusLayoutStateRef = useRef<TransientFocusLayoutState | null>(null);
|
|
119
|
+
transientFocusLayoutStateRef.current = transientFocusLayoutState;
|
|
106
120
|
const [hoveredMenuItemId, setHoveredMenuItemId] = useState<string | null>(null);
|
|
107
121
|
const closePaneMenu = useCallback(() => {
|
|
108
122
|
setMenuState(null);
|
|
@@ -149,7 +163,7 @@ export function Shell({
|
|
|
149
163
|
}, [dispatch]);
|
|
150
164
|
|
|
151
165
|
const {
|
|
152
|
-
activeLayout,
|
|
166
|
+
activeLayout: windowModeLayout,
|
|
153
167
|
nativeWindowModePanelRect,
|
|
154
168
|
selectWindowModePane,
|
|
155
169
|
startWindowMode,
|
|
@@ -171,6 +185,17 @@ export function Shell({
|
|
|
171
185
|
visibleLayout,
|
|
172
186
|
width,
|
|
173
187
|
});
|
|
188
|
+
const transientFocusActive = !windowMode && transientFocusLayoutState?.active === true;
|
|
189
|
+
const activeLayout = transientFocusActive && transientFocusLayoutState
|
|
190
|
+
? transientFocusLayoutState.layout
|
|
191
|
+
: windowModeLayout;
|
|
192
|
+
const transientFocusPaneId = transientFocusActive ? transientFocusLayoutState?.paneId ?? null : null;
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!windowMode || !transientFocusLayoutState) return;
|
|
196
|
+
transientFocusLayoutStateRef.current = null;
|
|
197
|
+
setTransientFocusLayoutState(null);
|
|
198
|
+
}, [transientFocusLayoutState, windowMode]);
|
|
174
199
|
|
|
175
200
|
const {
|
|
176
201
|
dockedPanes,
|
|
@@ -211,6 +236,102 @@ export function Shell({
|
|
|
211
236
|
visibleLayout,
|
|
212
237
|
width,
|
|
213
238
|
});
|
|
239
|
+
const setTransientFocusLayout = useCallback((next: TransientFocusLayoutState | null) => {
|
|
240
|
+
transientFocusLayoutStateRef.current = next;
|
|
241
|
+
setTransientFocusLayoutState(next);
|
|
242
|
+
}, []);
|
|
243
|
+
const activateTransientFocusState = useCallback((current: TransientFocusLayoutState) => {
|
|
244
|
+
closePaneMenu();
|
|
245
|
+
setTransientFocusLayout({ ...current, active: true });
|
|
246
|
+
const sourceLayout = config.layouts[current.sourceLayoutIndex]?.layout;
|
|
247
|
+
if (
|
|
248
|
+
current.sourceLayoutIndex !== config.activeLayoutIndex
|
|
249
|
+
&& sourceLayout
|
|
250
|
+
&& isPaneInLayout(sourceLayout, current.paneId)
|
|
251
|
+
) {
|
|
252
|
+
dispatch({ type: "SWITCH_LAYOUT", index: current.sourceLayoutIndex });
|
|
253
|
+
}
|
|
254
|
+
focusPane(current.paneId);
|
|
255
|
+
}, [closePaneMenu, config.activeLayoutIndex, config.layouts, dispatch, focusPane, setTransientFocusLayout]);
|
|
256
|
+
const toggleFocusedPaneFullscreen = useCallback(() => {
|
|
257
|
+
const current = transientFocusLayoutStateRef.current;
|
|
258
|
+
if (current?.active) {
|
|
259
|
+
setTransientFocusLayout(null);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (
|
|
264
|
+
current
|
|
265
|
+
&& current.paneId === focusedPaneId
|
|
266
|
+
&& current.sourceLayoutIndex === config.activeLayoutIndex
|
|
267
|
+
) {
|
|
268
|
+
activateTransientFocusState(current);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const nextLayout = resolvePaneFocusSourceLayout(visibleLayout, focusedPaneId);
|
|
273
|
+
if (!focusedPaneId || !nextLayout) {
|
|
274
|
+
pluginRegistry.notify({ body: "Focus a pane to make it fullscreen", type: "info" });
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
closePaneMenu();
|
|
279
|
+
setTransientFocusLayout({
|
|
280
|
+
paneId: focusedPaneId,
|
|
281
|
+
layout: nextLayout,
|
|
282
|
+
sourceLayoutIndex: config.activeLayoutIndex,
|
|
283
|
+
active: true,
|
|
284
|
+
});
|
|
285
|
+
focusPane(focusedPaneId);
|
|
286
|
+
return true;
|
|
287
|
+
}, [
|
|
288
|
+
activateTransientFocusState,
|
|
289
|
+
closePaneMenu,
|
|
290
|
+
config.activeLayoutIndex,
|
|
291
|
+
focusedPaneId,
|
|
292
|
+
focusPane,
|
|
293
|
+
pluginRegistry,
|
|
294
|
+
setTransientFocusLayout,
|
|
295
|
+
visibleLayout,
|
|
296
|
+
]);
|
|
297
|
+
const activateTransientFocusLayout = useCallback(() => {
|
|
298
|
+
const current = transientFocusLayoutStateRef.current;
|
|
299
|
+
if (!current) return;
|
|
300
|
+
activateTransientFocusState(current);
|
|
301
|
+
}, [activateTransientFocusState]);
|
|
302
|
+
const deactivateTransientFocusLayout = useCallback(() => {
|
|
303
|
+
const current = transientFocusLayoutStateRef.current;
|
|
304
|
+
if (!current || !current.active) return;
|
|
305
|
+
closePaneMenu();
|
|
306
|
+
setTransientFocusLayout({ ...current, active: false });
|
|
307
|
+
}, [closePaneMenu, setTransientFocusLayout]);
|
|
308
|
+
const exitTransientFocusLayout = useCallback(() => {
|
|
309
|
+
closePaneMenu();
|
|
310
|
+
setTransientFocusLayout(null);
|
|
311
|
+
}, [closePaneMenu, setTransientFocusLayout]);
|
|
312
|
+
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
setTransientLayout(
|
|
315
|
+
transientFocusLayoutState
|
|
316
|
+
? {
|
|
317
|
+
id: "pane-focus",
|
|
318
|
+
label: "^F Focus",
|
|
319
|
+
active: transientFocusActive,
|
|
320
|
+
onActivate: activateTransientFocusLayout,
|
|
321
|
+
onDeactivate: deactivateTransientFocusLayout,
|
|
322
|
+
onExit: exitTransientFocusLayout,
|
|
323
|
+
}
|
|
324
|
+
: null,
|
|
325
|
+
);
|
|
326
|
+
return () => setTransientLayout(null);
|
|
327
|
+
}, [
|
|
328
|
+
activateTransientFocusLayout,
|
|
329
|
+
deactivateTransientFocusLayout,
|
|
330
|
+
exitTransientFocusLayout,
|
|
331
|
+
setTransientLayout,
|
|
332
|
+
transientFocusActive,
|
|
333
|
+
transientFocusLayoutState,
|
|
334
|
+
]);
|
|
214
335
|
|
|
215
336
|
useShellPaneManagementShortcuts({
|
|
216
337
|
cancelActiveDrag,
|
|
@@ -226,6 +347,7 @@ export function Shell({
|
|
|
226
347
|
overlayOpen,
|
|
227
348
|
popOutFocusedPane,
|
|
228
349
|
startWindowMode,
|
|
350
|
+
toggleFocusedPaneFullscreen,
|
|
229
351
|
toggleFocusedPaneFloating,
|
|
230
352
|
});
|
|
231
353
|
|
|
@@ -333,6 +455,7 @@ export function Shell({
|
|
|
333
455
|
closePaneMenu,
|
|
334
456
|
contentHeight,
|
|
335
457
|
dispatch,
|
|
458
|
+
dockGeometryOptions,
|
|
336
459
|
dockDividerLayouts,
|
|
337
460
|
dockLeafLayouts,
|
|
338
461
|
dragRuntime,
|
|
@@ -349,6 +472,7 @@ export function Shell({
|
|
|
349
472
|
setHoveredMenuItemId,
|
|
350
473
|
setMenuState,
|
|
351
474
|
snapGuides,
|
|
475
|
+
transientFocusActive,
|
|
352
476
|
updateWindowModePreviewLayout,
|
|
353
477
|
visibleFloatingPanes,
|
|
354
478
|
visibleLayout,
|
|
@@ -409,6 +533,8 @@ export function Shell({
|
|
|
409
533
|
startNativeDockedDrag={startNativeDockedDrag}
|
|
410
534
|
startNativeFloatingDrag={startNativeFloatingDrag}
|
|
411
535
|
startNativeFloatResize={startNativeFloatResize}
|
|
536
|
+
transientFocusActive={transientFocusActive}
|
|
537
|
+
transientFocusPaneId={transientFocusPaneId}
|
|
412
538
|
visibleFloatingPanes={visibleFloatingPanes}
|
|
413
539
|
width={width}
|
|
414
540
|
windowModeDockResizePathKey={windowModeDockResizePathKey}
|