gloomberb 0.9.2 → 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/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 +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/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-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 +9 -1
- package/src/core/app-services.ts +7 -4
- package/src/data/config/store/index.ts +1 -2
- 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/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/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/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/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/ui-host.tsx +1 -1
- 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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState, type RefObject } from "react";
|
|
2
2
|
import { Box, Input, Text, type InputRenderable } from "../ui";
|
|
3
3
|
import { colors } from "../theme/colors";
|
|
4
|
+
import { useAppInputCapture } from "../state/app/input-capture";
|
|
5
|
+
import { useShortcut } from "../react/input";
|
|
6
|
+
import { isPlainArrowDown, stopSearchFocusNavigation } from "../utils/search-focus-navigation";
|
|
4
7
|
|
|
5
8
|
export function InputSearchBar({
|
|
6
9
|
value,
|
|
@@ -12,6 +15,7 @@ export function InputSearchBar({
|
|
|
12
15
|
placeholder,
|
|
13
16
|
debounceMs,
|
|
14
17
|
normalizeValue = identity,
|
|
18
|
+
onNavigateDown,
|
|
15
19
|
onFocus,
|
|
16
20
|
onBlur,
|
|
17
21
|
onQueryChange,
|
|
@@ -25,11 +29,23 @@ export function InputSearchBar({
|
|
|
25
29
|
placeholder: string;
|
|
26
30
|
debounceMs: number;
|
|
27
31
|
normalizeValue?: (value: string) => string;
|
|
32
|
+
onNavigateDown?: () => void;
|
|
28
33
|
onFocus: () => void;
|
|
29
34
|
onBlur: () => void;
|
|
30
35
|
onQueryChange: (query: string) => void;
|
|
31
36
|
}) {
|
|
32
37
|
const [draft, setDraft] = useState(value);
|
|
38
|
+
useAppInputCapture(focused && active);
|
|
39
|
+
|
|
40
|
+
useShortcut((event) => {
|
|
41
|
+
if (!onNavigateDown || !isPlainArrowDown(event)) return;
|
|
42
|
+
stopSearchFocusNavigation(event);
|
|
43
|
+
onNavigateDown();
|
|
44
|
+
}, {
|
|
45
|
+
allowEditable: true,
|
|
46
|
+
enabled: focused && active && !!onNavigateDown,
|
|
47
|
+
phase: "before",
|
|
48
|
+
});
|
|
33
49
|
|
|
34
50
|
useEffect(() => {
|
|
35
51
|
setDraft(value);
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getRememberedFloatingRect,
|
|
5
5
|
resizeSplitAtPath,
|
|
6
6
|
simulateDrop,
|
|
7
|
+
type DockGeometryOptions,
|
|
7
8
|
type DockLeafLayout,
|
|
8
9
|
type DropTarget,
|
|
9
10
|
type LayoutBounds,
|
|
@@ -32,6 +33,7 @@ interface UseShellActiveDragOptions {
|
|
|
32
33
|
bounds: LayoutBounds;
|
|
33
34
|
contentHeight: number;
|
|
34
35
|
dispatch: Dispatch<AppAction>;
|
|
36
|
+
dockGeometryOptions: DockGeometryOptions;
|
|
35
37
|
dockLeafLayouts: DockLeafLayout[];
|
|
36
38
|
dragRuntime: ShellDragRuntimeState;
|
|
37
39
|
focusPane: (paneId: string) => void;
|
|
@@ -51,6 +53,7 @@ export function useShellActiveDrag({
|
|
|
51
53
|
bounds,
|
|
52
54
|
contentHeight,
|
|
53
55
|
dispatch,
|
|
56
|
+
dockGeometryOptions,
|
|
54
57
|
dockLeafLayouts,
|
|
55
58
|
dragRuntime,
|
|
56
59
|
focusPane,
|
|
@@ -117,7 +120,7 @@ export function useShellActiveDrag({
|
|
|
117
120
|
const hoveredCell = hoveredOverlay.cells.find((cell) => pointInRect(cell.rect, hitX, hitShellY));
|
|
118
121
|
if (hoveredCell) {
|
|
119
122
|
const target: DropTarget = { kind: "leaf", targetId: hoveredOverlay.targetId, position: hoveredCell.position };
|
|
120
|
-
const simulation = simulateDrop(baseLayout, drag.paneId, target, bounds);
|
|
123
|
+
const simulation = simulateDrop(baseLayout, drag.paneId, target, bounds, dockGeometryOptions);
|
|
121
124
|
if (simulation.previewRect) {
|
|
122
125
|
updateDockPreview({ kind: "dock", target, rect: simulation.previewRect });
|
|
123
126
|
} else {
|
|
@@ -199,6 +202,7 @@ export function useShellActiveDrag({
|
|
|
199
202
|
bounds,
|
|
200
203
|
contentHeight,
|
|
201
204
|
dispatch,
|
|
205
|
+
dockGeometryOptions,
|
|
202
206
|
dividerPreviewRef,
|
|
203
207
|
dockLeafLayouts,
|
|
204
208
|
dockPreviewRef,
|
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
type MutableRefObject,
|
|
7
7
|
type SetStateAction,
|
|
8
8
|
} from "react";
|
|
9
|
+
import { useRafCallback } from "../../../../react/use-raf-callback";
|
|
9
10
|
import {
|
|
10
11
|
type DockDividerLayout,
|
|
12
|
+
type DockGeometryOptions,
|
|
11
13
|
type DockLeafLayout,
|
|
12
14
|
type FloatingRect,
|
|
13
15
|
type LayoutBounds,
|
|
@@ -91,17 +93,26 @@ export function useShellDragRuntimeState({
|
|
|
91
93
|
}): ShellDragRuntimeState {
|
|
92
94
|
const dragRef = useRef<DragMode | null>(null);
|
|
93
95
|
const [dragFloatingRect, setDragFloatingRect] = useState<{ paneId: string; rect: FloatingRect } | null>(null);
|
|
96
|
+
const pendingDragFloatingRectRef = useRef<{ paneId: string; rect: FloatingRect } | null>(null);
|
|
94
97
|
const [dragCursor, setDragCursor] = useState<{ x: number; y: number } | null>(null);
|
|
95
98
|
const [dividerPreview, setDividerPreview] = useState<DividerPreviewState | null>(null);
|
|
96
99
|
const [dockPreview, setDockPreview] = useState<DragPreview | null>(null);
|
|
97
100
|
const dividerPreviewRef = useRef<DividerPreviewState | null>(null);
|
|
98
101
|
const dockPreviewRef = useRef<DragPreview | null>(null);
|
|
102
|
+
const flushDragFloatingRect = useRafCallback(() => {
|
|
103
|
+
setDragFloatingRect(pendingDragFloatingRectRef.current);
|
|
104
|
+
});
|
|
99
105
|
|
|
100
106
|
const updateDragFloatingRect = useCallback((next: { paneId: string; rect: FloatingRect } | null) => {
|
|
101
|
-
|
|
107
|
+
pendingDragFloatingRectRef.current = next
|
|
102
108
|
? { paneId: next.paneId, rect: constrainFloatingRectToBounds(next.rect, width, contentHeight) }
|
|
103
|
-
: null
|
|
104
|
-
|
|
109
|
+
: null;
|
|
110
|
+
if (!next) {
|
|
111
|
+
setDragFloatingRect(null);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
flushDragFloatingRect();
|
|
115
|
+
}, [contentHeight, flushDragFloatingRect, width]);
|
|
105
116
|
|
|
106
117
|
const updateDividerPreview = useCallback((next: DividerPreviewState | null) => {
|
|
107
118
|
dividerPreviewRef.current = next;
|
|
@@ -146,6 +157,7 @@ interface UseShellPointerRuntimeOptions {
|
|
|
146
157
|
closePaneMenu: () => void;
|
|
147
158
|
contentHeight: number;
|
|
148
159
|
dispatch: Dispatch<AppAction>;
|
|
160
|
+
dockGeometryOptions: DockGeometryOptions;
|
|
149
161
|
dockDividerLayouts: DockDividerLayout[];
|
|
150
162
|
dockLeafLayouts: DockLeafLayout[];
|
|
151
163
|
dragRuntime: ShellDragRuntimeState;
|
|
@@ -166,6 +178,7 @@ interface UseShellPointerRuntimeOptions {
|
|
|
166
178
|
setHoveredMenuItemId: Dispatch<SetStateAction<string | null>>;
|
|
167
179
|
setMenuState: Dispatch<SetStateAction<ActionMenuState | null>>;
|
|
168
180
|
snapGuides: ReturnType<typeof makeSnapGuides>;
|
|
181
|
+
transientFocusActive: boolean;
|
|
169
182
|
updateWindowModePreviewLayout: (nextLayout: LayoutConfig, paneId?: string) => void;
|
|
170
183
|
visibleFloatingPanes: VisibleFloatingPane[];
|
|
171
184
|
visibleLayout: LayoutConfig;
|
|
@@ -179,6 +192,7 @@ export function useShellPointerRuntime({
|
|
|
179
192
|
closePaneMenu,
|
|
180
193
|
contentHeight,
|
|
181
194
|
dispatch,
|
|
195
|
+
dockGeometryOptions,
|
|
182
196
|
dockDividerLayouts,
|
|
183
197
|
dockLeafLayouts,
|
|
184
198
|
dragRuntime,
|
|
@@ -195,6 +209,7 @@ export function useShellPointerRuntime({
|
|
|
195
209
|
setHoveredMenuItemId,
|
|
196
210
|
setMenuState,
|
|
197
211
|
snapGuides,
|
|
212
|
+
transientFocusActive,
|
|
198
213
|
updateWindowModePreviewLayout,
|
|
199
214
|
visibleFloatingPanes,
|
|
200
215
|
visibleLayout,
|
|
@@ -206,6 +221,7 @@ export function useShellPointerRuntime({
|
|
|
206
221
|
bounds,
|
|
207
222
|
contentHeight,
|
|
208
223
|
dispatch,
|
|
224
|
+
dockGeometryOptions,
|
|
209
225
|
dockLeafLayouts,
|
|
210
226
|
focusPane,
|
|
211
227
|
dragRuntime,
|
|
@@ -237,6 +253,7 @@ export function useShellPointerRuntime({
|
|
|
237
253
|
selectWindowModePane,
|
|
238
254
|
setHoveredMenuItemId,
|
|
239
255
|
setMenuState,
|
|
256
|
+
transientFocusActive,
|
|
240
257
|
visibleFloatingPanes,
|
|
241
258
|
width,
|
|
242
259
|
windowMode,
|
|
@@ -254,6 +271,7 @@ export function useShellPointerRuntime({
|
|
|
254
271
|
selectWindowModePane,
|
|
255
272
|
setHoveredMenuItemId,
|
|
256
273
|
setMenuState,
|
|
274
|
+
transientFocusActive,
|
|
257
275
|
windowMode,
|
|
258
276
|
});
|
|
259
277
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { isPaneInLayout } from "../../../plugins/pane-manager";
|
|
2
|
+
import { cloneLayout, type LayoutConfig } from "../../../types/config";
|
|
3
|
+
|
|
4
|
+
export function resolvePaneFocusSourceLayout(
|
|
5
|
+
layout: LayoutConfig,
|
|
6
|
+
paneId: string | null,
|
|
7
|
+
): LayoutConfig | null {
|
|
8
|
+
if (!paneId || !isPaneInLayout(layout, paneId)) return null;
|
|
9
|
+
return cloneLayout(layout);
|
|
10
|
+
}
|
|
@@ -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");
|