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
|
@@ -41,6 +41,7 @@ interface UseShellTerminalPointerRuntimeOptions {
|
|
|
41
41
|
selectWindowModePane: (paneId: string) => void;
|
|
42
42
|
setHoveredMenuItemId: Dispatch<SetStateAction<string | null>>;
|
|
43
43
|
setMenuState: Dispatch<SetStateAction<ActionMenuState | null>>;
|
|
44
|
+
transientFocusActive: boolean;
|
|
44
45
|
visibleFloatingPanes: VisibleFloatingPane[];
|
|
45
46
|
width: number;
|
|
46
47
|
windowMode: WindowEditState | null;
|
|
@@ -79,6 +80,7 @@ export function useShellTerminalPointerRuntime({
|
|
|
79
80
|
selectWindowModePane,
|
|
80
81
|
setHoveredMenuItemId,
|
|
81
82
|
setMenuState,
|
|
83
|
+
transientFocusActive,
|
|
82
84
|
visibleFloatingPanes,
|
|
83
85
|
width,
|
|
84
86
|
windowMode,
|
|
@@ -132,25 +134,27 @@ export function useShellTerminalPointerRuntime({
|
|
|
132
134
|
return;
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
137
|
+
if (!transientFocusActive) {
|
|
138
|
+
for (const divider of dockDividerLayouts) {
|
|
139
|
+
if (!pointInRect(divider.rect, event.x, shellY)) continue;
|
|
140
|
+
dragRef.current = {
|
|
141
|
+
type: "divider",
|
|
142
|
+
path: divider.path,
|
|
143
|
+
axis: divider.axis,
|
|
144
|
+
startX: preciseX,
|
|
145
|
+
startY: preciseShellY,
|
|
146
|
+
startRatio: divider.ratio,
|
|
147
|
+
bounds: divider.bounds,
|
|
148
|
+
};
|
|
149
|
+
updateDividerPreview({
|
|
150
|
+
pathKey: divider.path.join("."),
|
|
151
|
+
rect: divider.rect,
|
|
152
|
+
ratio: divider.ratio,
|
|
153
|
+
});
|
|
154
|
+
event.stopPropagation();
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
for (const leaf of dockLeafLayouts) {
|
|
@@ -279,6 +283,11 @@ export function useShellTerminalPointerRuntime({
|
|
|
279
283
|
event.preventDefault();
|
|
280
284
|
return;
|
|
281
285
|
}
|
|
286
|
+
if (relativeY === 0 && transientFocusActive) {
|
|
287
|
+
event.stopPropagation();
|
|
288
|
+
event.preventDefault();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
282
291
|
if (relativeY === 0) {
|
|
283
292
|
dragRef.current = {
|
|
284
293
|
type: "pane-drag",
|
|
@@ -317,6 +326,7 @@ export function useShellTerminalPointerRuntime({
|
|
|
317
326
|
selectWindowModePane,
|
|
318
327
|
setHoveredMenuItemId,
|
|
319
328
|
setMenuState,
|
|
329
|
+
transientFocusActive,
|
|
320
330
|
updateDividerPreview,
|
|
321
331
|
updateDragFloatingRect,
|
|
322
332
|
visibleFloatingPanes,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
removeUnavailablePaneTypes,
|
|
3
|
+
type PaneTypeAvailability,
|
|
4
|
+
} from "../../../plugins/pane-manager";
|
|
5
|
+
import type { LayoutConfig } from "../../../types/config";
|
|
6
|
+
|
|
7
|
+
export function resolveShellVisibleLayout(
|
|
8
|
+
layout: LayoutConfig,
|
|
9
|
+
disabledPaneIds: ReadonlySet<string>,
|
|
10
|
+
registeredPaneIds: PaneTypeAvailability,
|
|
11
|
+
): LayoutConfig {
|
|
12
|
+
return removeUnavailablePaneTypes(
|
|
13
|
+
layout,
|
|
14
|
+
registeredPaneIds,
|
|
15
|
+
{ disabledPaneIds },
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
applyWindowEditDirection,
|
|
14
14
|
cycleWindowEditFocus,
|
|
15
15
|
cycleWindowEditPane,
|
|
16
|
+
cycleWindowEditTarget,
|
|
16
17
|
directionFromWindowEditKey,
|
|
17
18
|
getWindowEditPaneIds,
|
|
18
19
|
normalizeWindowEditFocus,
|
|
@@ -72,7 +73,10 @@ export function useShellWindowMode({
|
|
|
72
73
|
);
|
|
73
74
|
const windowModePaneId = windowMode?.paneId;
|
|
74
75
|
const activeLayout = windowMode?.previewLayout ?? visibleLayout;
|
|
75
|
-
const windowModePaneIds = useMemo(
|
|
76
|
+
const windowModePaneIds = useMemo(
|
|
77
|
+
() => getWindowEditPaneIds(activeLayout, bounds, dockGeometryOptions),
|
|
78
|
+
[activeLayout, bounds, dockGeometryOptions],
|
|
79
|
+
);
|
|
76
80
|
const windowModeDockMovePreview = useMemo(
|
|
77
81
|
() => resolveWindowEditDockMovePreview(windowMode, bounds, dockGeometryOptions),
|
|
78
82
|
[bounds, dockGeometryOptions, windowMode],
|
|
@@ -210,12 +214,16 @@ export function useShellWindowMode({
|
|
|
210
214
|
}
|
|
211
215
|
} else if (name === "w") {
|
|
212
216
|
setWindowMode((current) => current
|
|
213
|
-
?
|
|
217
|
+
? current.mode === "move" && current.focus.kind === "dock-move"
|
|
218
|
+
? cycleWindowEditTarget(current, bounds, dockGeometryOptions, event.shift ? -1 : 1)
|
|
219
|
+
: cycleWindowEditPane(current, windowModePaneIds, bounds, dockGeometryOptions, event.shift ? -1 : 1)
|
|
214
220
|
: current);
|
|
215
221
|
} else if (name === "tab") {
|
|
216
222
|
setWindowMode((current) => current
|
|
217
223
|
? current.mode === "move"
|
|
218
|
-
?
|
|
224
|
+
? current.focus.kind === "dock-move"
|
|
225
|
+
? cycleWindowEditTarget(current, bounds, dockGeometryOptions, event.shift ? -1 : 1)
|
|
226
|
+
: cycleWindowEditPane(current, windowModePaneIds, bounds, dockGeometryOptions, event.shift ? -1 : 1)
|
|
219
227
|
: {
|
|
220
228
|
...current,
|
|
221
229
|
focus: cycleWindowEditFocus(
|
|
@@ -40,6 +40,13 @@ interface ShellWindowModeOverlaysProps {
|
|
|
40
40
|
windowModeDockMovePreview: WindowEditDockMovePreview | null;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function getHighlightedPaneId(windowMode: WindowEditState | null, focusedPaneId: string | null): string | null {
|
|
44
|
+
if (windowMode?.mode === "move" && windowMode.focus.kind === "dock-move") {
|
|
45
|
+
return windowMode.focus.targetId;
|
|
46
|
+
}
|
|
47
|
+
return windowMode?.paneId ?? focusedPaneId;
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
function windowModeBannerKey(windowMode: WindowEditState): string {
|
|
44
51
|
const focusKey = windowMode.focus.kind === "dock-move"
|
|
45
52
|
? `${windowMode.focus.targetId}:${windowMode.focus.position}`
|
|
@@ -69,7 +76,7 @@ export function ShellWindowModeOverlays({
|
|
|
69
76
|
windowMode,
|
|
70
77
|
windowModeDockMovePreview,
|
|
71
78
|
}: ShellWindowModeOverlaysProps) {
|
|
72
|
-
const highlightedPaneId = windowMode
|
|
79
|
+
const highlightedPaneId = getHighlightedPaneId(windowMode, focusedPaneId);
|
|
73
80
|
const highlightedFloatingPane = highlightedPaneId
|
|
74
81
|
? visibleFloatingPanes.find((entry) => entry.pane.instance.instanceId === highlightedPaneId)
|
|
75
82
|
: null;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { testRender } from "../../renderers/opentui/test-utils";
|
|
3
3
|
import { AppContext, createInitialState } from "../../state/app/context";
|
|
4
|
-
import { cloneLayout, createDefaultConfig, type LayoutConfig } from "../../types/config";
|
|
4
|
+
import { cloneLayout, createDefaultConfig, TICKER_RESEARCH_PANE_ID, type LayoutConfig } from "../../types/config";
|
|
5
5
|
import type { AppNotificationRequest } from "../../types/plugin";
|
|
6
6
|
import { StatusBar } from "./status-bar";
|
|
7
7
|
import { setSharedRegistryForTests } from "../../plugins/registry";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { TransientLayoutProvider, useTransientLayout } from "./transient-layout";
|
|
8
10
|
|
|
9
11
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
10
12
|
|
|
@@ -17,6 +19,37 @@ afterEach(() => {
|
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
describe("StatusBar", () => {
|
|
22
|
+
function SeedTransientLayout({
|
|
23
|
+
onActivate,
|
|
24
|
+
onDeactivate,
|
|
25
|
+
onExit,
|
|
26
|
+
}: {
|
|
27
|
+
onActivate?: () => void;
|
|
28
|
+
onDeactivate?: () => void;
|
|
29
|
+
onExit?: () => void;
|
|
30
|
+
}) {
|
|
31
|
+
const { setTransientLayout } = useTransientLayout();
|
|
32
|
+
const [active, setActive] = useState(true);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setTransientLayout({
|
|
35
|
+
id: "pane-focus",
|
|
36
|
+
label: "^F Focus",
|
|
37
|
+
active,
|
|
38
|
+
onActivate: () => {
|
|
39
|
+
onActivate?.();
|
|
40
|
+
setActive(true);
|
|
41
|
+
},
|
|
42
|
+
onDeactivate: () => {
|
|
43
|
+
onDeactivate?.();
|
|
44
|
+
setActive(false);
|
|
45
|
+
},
|
|
46
|
+
onExit,
|
|
47
|
+
});
|
|
48
|
+
return () => setTransientLayout(null);
|
|
49
|
+
}, [active, onActivate, onDeactivate, onExit, setTransientLayout]);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
20
53
|
test("opens the command bar from the shortcut hint", async () => {
|
|
21
54
|
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
22
55
|
config.layouts = [{ name: "Home", layout: cloneLayout(config.layout) }];
|
|
@@ -45,6 +78,78 @@ describe("StatusBar", () => {
|
|
|
45
78
|
expect(actions).toContainEqual({ type: "SET_COMMAND_BAR", open: true, query: "" });
|
|
46
79
|
});
|
|
47
80
|
|
|
81
|
+
test("shows a transient focus layout tab without replacing saved layouts", async () => {
|
|
82
|
+
const config = createDefaultConfig("/tmp/gloomberb-transient-layout-test");
|
|
83
|
+
config.layouts = [
|
|
84
|
+
{ name: "Default", layout: cloneLayout(config.layout) },
|
|
85
|
+
{ name: "Monitor", layout: cloneLayout(config.layout) },
|
|
86
|
+
];
|
|
87
|
+
const state = {
|
|
88
|
+
...createInitialState(config),
|
|
89
|
+
statusBarVisible: true,
|
|
90
|
+
};
|
|
91
|
+
const actions: Array<{ type: string; index?: number }> = [];
|
|
92
|
+
let activateCount = 0;
|
|
93
|
+
let deactivateCount = 0;
|
|
94
|
+
let exitCount = 0;
|
|
95
|
+
const handleActivate = () => { activateCount += 1; };
|
|
96
|
+
const handleDeactivate = () => { deactivateCount += 1; };
|
|
97
|
+
const handleExit = () => { exitCount += 1; };
|
|
98
|
+
|
|
99
|
+
testSetup = await testRender(
|
|
100
|
+
<AppContext value={{ state, dispatch: (action) => actions.push(action as { type: string; index?: number }) }}>
|
|
101
|
+
<TransientLayoutProvider>
|
|
102
|
+
<SeedTransientLayout
|
|
103
|
+
onActivate={handleActivate}
|
|
104
|
+
onDeactivate={handleDeactivate}
|
|
105
|
+
onExit={handleExit}
|
|
106
|
+
/>
|
|
107
|
+
<StatusBar />
|
|
108
|
+
</TransientLayoutProvider>
|
|
109
|
+
</AppContext>,
|
|
110
|
+
{ width: 120, height: 1 },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await testSetup.renderOnce();
|
|
114
|
+
await testSetup.renderOnce();
|
|
115
|
+
|
|
116
|
+
const frame = testSetup.captureCharFrame();
|
|
117
|
+
expect(frame).toContain("^1 Default");
|
|
118
|
+
expect(frame).toContain("^2 Monitor");
|
|
119
|
+
expect(frame).toContain("^F Focus");
|
|
120
|
+
|
|
121
|
+
const monitorX = frame.split("\n")[0]?.indexOf("^2 Monitor") ?? -1;
|
|
122
|
+
expect(monitorX).toBeGreaterThanOrEqual(0);
|
|
123
|
+
|
|
124
|
+
await testSetup.mockMouse.click(monitorX + 1, 0);
|
|
125
|
+
await testSetup.renderOnce();
|
|
126
|
+
await testSetup.renderOnce();
|
|
127
|
+
|
|
128
|
+
expect(deactivateCount).toBe(1);
|
|
129
|
+
expect(exitCount).toBe(0);
|
|
130
|
+
expect(actions).toContainEqual({ type: "SWITCH_LAYOUT", index: 1 });
|
|
131
|
+
|
|
132
|
+
const afterSwitchFrame = testSetup.captureCharFrame();
|
|
133
|
+
expect(afterSwitchFrame).toContain("^F Focus");
|
|
134
|
+
|
|
135
|
+
const focusX = afterSwitchFrame.split("\n")[0]?.indexOf("^F Focus") ?? -1;
|
|
136
|
+
expect(focusX).toBeGreaterThanOrEqual(0);
|
|
137
|
+
|
|
138
|
+
await testSetup.mockMouse.click(focusX + 1, 0);
|
|
139
|
+
await testSetup.renderOnce();
|
|
140
|
+
|
|
141
|
+
expect(activateCount).toBe(1);
|
|
142
|
+
|
|
143
|
+
const activeFocusFrame = testSetup.captureCharFrame();
|
|
144
|
+
const activeFocusX = activeFocusFrame.split("\n")[0]?.indexOf("^F Focus") ?? -1;
|
|
145
|
+
expect(activeFocusX).toBeGreaterThanOrEqual(0);
|
|
146
|
+
|
|
147
|
+
await testSetup.mockMouse.click(activeFocusX + 1, 0);
|
|
148
|
+
await testSetup.renderOnce();
|
|
149
|
+
|
|
150
|
+
expect(exitCount).toBe(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
48
153
|
test("shows a gridlock tip after a corner snap and runs gridlock on click", async () => {
|
|
49
154
|
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
50
155
|
const floatingLayout = cloneLayout(config.layout);
|
|
@@ -69,6 +174,10 @@ describe("StatusBar", () => {
|
|
|
69
174
|
const notifications: AppNotificationRequest[] = [];
|
|
70
175
|
|
|
71
176
|
setSharedRegistryForTests({
|
|
177
|
+
panes: new Map([
|
|
178
|
+
["portfolio-list", {}],
|
|
179
|
+
[TICKER_RESEARCH_PANE_ID, {}],
|
|
180
|
+
]),
|
|
72
181
|
getLayoutFn: () => state.config.layout,
|
|
73
182
|
getTermSizeFn: () => ({ width: 120, height: 40 }),
|
|
74
183
|
updateLayoutFn: (layout: LayoutConfig) => { updatedLayout = layout; },
|
|
@@ -128,6 +237,7 @@ describe("StatusBar", () => {
|
|
|
128
237
|
globalThis.clearTimeout = (() => {}) as typeof clearTimeout;
|
|
129
238
|
|
|
130
239
|
setSharedRegistryForTests({
|
|
240
|
+
panes: new Map(),
|
|
131
241
|
getLayoutFn: () => state.config.layout,
|
|
132
242
|
getTermSizeFn: () => ({ width: 120, height: 40 }),
|
|
133
243
|
updateLayoutFn: () => {},
|
|
@@ -16,6 +16,7 @@ import { notifyGridlockComplete } from "../../plugins/gridlock-notification";
|
|
|
16
16
|
import { PluginSlot } from "../../react/plugins/plugin-slot";
|
|
17
17
|
import type { ContextMenuItem } from "../../types/context-menu";
|
|
18
18
|
import { Tabs } from "../ui/tabs";
|
|
19
|
+
import { useTransientLayout } from "./transient-layout";
|
|
19
20
|
|
|
20
21
|
const GRIDLOCK_TIP_DURATION_MS = 60_000;
|
|
21
22
|
|
|
@@ -31,6 +32,7 @@ type LayoutTabItem = {
|
|
|
31
32
|
|
|
32
33
|
type StatusBarViewProps = {
|
|
33
34
|
activeLayoutIdx: number;
|
|
35
|
+
activeLayoutValue: string;
|
|
34
36
|
dismissGridlockTip: (event?: StatusBarEvent) => void;
|
|
35
37
|
handleGridlockTip: (event?: StatusBarEvent) => void;
|
|
36
38
|
handleLayoutSelect: (value: string) => void;
|
|
@@ -62,18 +64,40 @@ export function StatusBar() {
|
|
|
62
64
|
const statusBarVisible = useAppSelector(selectStatusBarVisible);
|
|
63
65
|
const gridlockTipVisible = useAppSelector(selectGridlockTipVisible);
|
|
64
66
|
const gridlockTipSequence = useAppSelector(selectGridlockTipSequence);
|
|
67
|
+
const { transientLayout } = useTransientLayout();
|
|
65
68
|
const [hoveredControl, setHoveredControl] = useState<string | null>(null);
|
|
66
69
|
|
|
67
|
-
const hasMultipleLayouts = layouts.length > 1;
|
|
70
|
+
const hasMultipleLayouts = layouts.length > 1 || !!transientLayout;
|
|
68
71
|
const showGridlockTip = gridlockTipVisible && !!registry;
|
|
69
|
-
const
|
|
72
|
+
const savedLayoutTabs = layouts.map((layout, index) => ({
|
|
70
73
|
label: `^${index + 1} ${truncate(layout.name, 14)}`,
|
|
71
74
|
value: String(index),
|
|
72
75
|
}));
|
|
76
|
+
const layoutTabs = transientLayout
|
|
77
|
+
? [
|
|
78
|
+
...savedLayoutTabs,
|
|
79
|
+
{
|
|
80
|
+
label: transientLayout.label,
|
|
81
|
+
value: transientLayout.id,
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
: savedLayoutTabs;
|
|
73
85
|
const layoutTabsWidth = layoutTabs.reduce((sum, tab) => sum + tab.label.length + 2, 0);
|
|
86
|
+
const activeLayoutValue = transientLayout?.active ? transientLayout.id : String(activeLayoutIdx);
|
|
74
87
|
const handleLayoutSelect = (value: string) => {
|
|
88
|
+
if (value === transientLayout?.id) {
|
|
89
|
+
if (transientLayout.active) {
|
|
90
|
+
transientLayout.onExit?.();
|
|
91
|
+
} else {
|
|
92
|
+
transientLayout.onActivate?.();
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
75
96
|
const index = Number(value);
|
|
76
97
|
if (!Number.isInteger(index) || index < 0 || index >= layouts.length) return;
|
|
98
|
+
if (transientLayout?.active) {
|
|
99
|
+
transientLayout.onDeactivate?.();
|
|
100
|
+
}
|
|
77
101
|
dispatch({ type: "SWITCH_LAYOUT", index });
|
|
78
102
|
};
|
|
79
103
|
|
|
@@ -90,7 +114,11 @@ export function StatusBar() {
|
|
|
90
114
|
event?.stopPropagation?.();
|
|
91
115
|
if (!registry) return;
|
|
92
116
|
const { width, height } = registry.getTermSizeFn();
|
|
93
|
-
registry.updateLayoutFn(gridlockAllPanes(
|
|
117
|
+
registry.updateLayoutFn(gridlockAllPanes(
|
|
118
|
+
registry.getLayoutFn(),
|
|
119
|
+
{ x: 0, y: 0, width, height },
|
|
120
|
+
registry.panes,
|
|
121
|
+
));
|
|
94
122
|
notifyGridlockComplete(registry.notify.bind(registry), () => {
|
|
95
123
|
dispatch({ type: "UNDO_LAYOUT" });
|
|
96
124
|
});
|
|
@@ -184,11 +212,12 @@ export function StatusBar() {
|
|
|
184
212
|
);
|
|
185
213
|
}, [activeLayoutIdx, layoutContextMenuItems, layouts, showContextMenu]);
|
|
186
214
|
const handleLayoutTabContextMenu = useCallback((value: string, event: any) => {
|
|
215
|
+
if (value === transientLayout?.id) return;
|
|
187
216
|
const index = Number(value);
|
|
188
217
|
if (!Number.isInteger(index) || index < 0 || index >= layouts.length) return;
|
|
189
218
|
if (event?.type !== "contextmenu" && event?.button === 2 && nativeContextMenu === true) return;
|
|
190
219
|
void openLayoutContextMenu(index, event);
|
|
191
|
-
}, [layouts.length, nativeContextMenu, openLayoutContextMenu]);
|
|
220
|
+
}, [layouts.length, nativeContextMenu, openLayoutContextMenu, transientLayout?.id]);
|
|
192
221
|
const layoutTabItems = layoutTabs.map((tab) => ({
|
|
193
222
|
...tab,
|
|
194
223
|
onContextMenu: handleLayoutTabContextMenu,
|
|
@@ -198,6 +227,7 @@ export function StatusBar() {
|
|
|
198
227
|
|
|
199
228
|
const viewProps: StatusBarViewProps = {
|
|
200
229
|
activeLayoutIdx,
|
|
230
|
+
activeLayoutValue,
|
|
201
231
|
dismissGridlockTip,
|
|
202
232
|
handleGridlockTip,
|
|
203
233
|
handleLayoutSelect,
|
|
@@ -272,7 +302,7 @@ function TerminalStatusBar({
|
|
|
272
302
|
}
|
|
273
303
|
|
|
274
304
|
function StatusBarLayoutControl({
|
|
275
|
-
|
|
305
|
+
activeLayoutValue,
|
|
276
306
|
handleLayoutSelect,
|
|
277
307
|
hasMultipleLayouts,
|
|
278
308
|
hoveredControl,
|
|
@@ -283,7 +313,7 @@ function StatusBarLayoutControl({
|
|
|
283
313
|
setHoveredControl,
|
|
284
314
|
}: Pick<
|
|
285
315
|
StatusBarViewProps,
|
|
286
|
-
| "
|
|
316
|
+
| "activeLayoutValue"
|
|
287
317
|
| "handleLayoutSelect"
|
|
288
318
|
| "hasMultipleLayouts"
|
|
289
319
|
| "hoveredControl"
|
|
@@ -303,7 +333,7 @@ function StatusBarLayoutControl({
|
|
|
303
333
|
<Box width={layoutTabsWidth} height={1}>
|
|
304
334
|
<Tabs
|
|
305
335
|
tabs={layoutTabItems}
|
|
306
|
-
activeValue={
|
|
336
|
+
activeValue={activeLayoutValue}
|
|
307
337
|
onSelect={handleLayoutSelect}
|
|
308
338
|
compact
|
|
309
339
|
variant="pill"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface TransientLayoutState {
|
|
4
|
+
id: "pane-focus";
|
|
5
|
+
label: string;
|
|
6
|
+
active: boolean;
|
|
7
|
+
onActivate?: () => void;
|
|
8
|
+
onDeactivate?: () => void;
|
|
9
|
+
onExit?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TransientLayoutContextValue {
|
|
13
|
+
transientLayout: TransientLayoutState | null;
|
|
14
|
+
setTransientLayout: (layout: TransientLayoutState | null) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const noop = () => {};
|
|
18
|
+
|
|
19
|
+
const TransientLayoutContext = createContext<TransientLayoutContextValue>({
|
|
20
|
+
transientLayout: null,
|
|
21
|
+
setTransientLayout: noop,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function sameTransientLayout(
|
|
25
|
+
left: TransientLayoutState | null,
|
|
26
|
+
right: TransientLayoutState | null,
|
|
27
|
+
): boolean {
|
|
28
|
+
if (left === right) return true;
|
|
29
|
+
if (!left || !right) return false;
|
|
30
|
+
return left.id === right.id
|
|
31
|
+
&& left.label === right.label
|
|
32
|
+
&& left.active === right.active
|
|
33
|
+
&& left.onActivate === right.onActivate
|
|
34
|
+
&& left.onDeactivate === right.onDeactivate
|
|
35
|
+
&& left.onExit === right.onExit;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function TransientLayoutProvider({ children }: { children: ReactNode }) {
|
|
39
|
+
const [transientLayout, setTransientLayoutState] = useState<TransientLayoutState | null>(null);
|
|
40
|
+
const setTransientLayout = useCallback((layout: TransientLayoutState | null) => {
|
|
41
|
+
setTransientLayoutState((current) => (sameTransientLayout(current, layout) ? current : layout));
|
|
42
|
+
}, []);
|
|
43
|
+
const value = useMemo(() => ({
|
|
44
|
+
transientLayout,
|
|
45
|
+
setTransientLayout,
|
|
46
|
+
}), [setTransientLayout, transientLayout]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<TransientLayoutContext.Provider value={value}>
|
|
50
|
+
{children}
|
|
51
|
+
</TransientLayoutContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useTransientLayout(): TransientLayoutContextValue {
|
|
56
|
+
return useContext(TransientLayoutContext);
|
|
57
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { LayoutConfig } from "../../../types/config";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
cycleWindowEditPane,
|
|
5
|
+
cycleWindowEditTarget,
|
|
6
|
+
getWindowEditPaneIds,
|
|
7
|
+
type WindowEditState,
|
|
8
|
+
} from "./mode";
|
|
9
|
+
import { resolveWindowEditDockMovePreview } from "./presentation";
|
|
4
10
|
|
|
5
11
|
const bounds = { x: 0, y: 0, width: 120, height: 40 };
|
|
6
12
|
|
|
@@ -38,18 +44,83 @@ describe("window edit mode", () => {
|
|
|
38
44
|
dirty: false,
|
|
39
45
|
};
|
|
40
46
|
|
|
41
|
-
state = cycleWindowEditPane(state, getWindowEditPaneIds(state.previewLayout), bounds, {}, 1);
|
|
47
|
+
state = cycleWindowEditPane(state, getWindowEditPaneIds(state.previewLayout, bounds, {}), bounds, {}, 1);
|
|
42
48
|
expect(state.paneId).toBe("dock-b");
|
|
43
49
|
|
|
44
|
-
state = cycleWindowEditPane(state, getWindowEditPaneIds(state.previewLayout), bounds, {}, 1);
|
|
50
|
+
state = cycleWindowEditPane(state, getWindowEditPaneIds(state.previewLayout, bounds, {}), bounds, {}, 1);
|
|
45
51
|
expect(state.paneId).toBe("float-a");
|
|
46
52
|
expect(state.previewLayout.floating.find((entry) => entry.instanceId === "float-a")?.zIndex)
|
|
47
53
|
.toBeGreaterThan(state.previewLayout.floating.find((entry) => entry.instanceId === "float-b")?.zIndex ?? 0);
|
|
48
54
|
|
|
49
|
-
state = cycleWindowEditPane(state, getWindowEditPaneIds(state.previewLayout), bounds, {}, 1);
|
|
55
|
+
state = cycleWindowEditPane(state, getWindowEditPaneIds(state.previewLayout, bounds, {}), bounds, {}, 1);
|
|
50
56
|
expect(state.paneId).toBe("float-b");
|
|
51
57
|
|
|
52
|
-
state = cycleWindowEditPane(state, getWindowEditPaneIds(state.previewLayout), bounds, {}, 1);
|
|
58
|
+
state = cycleWindowEditPane(state, getWindowEditPaneIds(state.previewLayout, bounds, {}), bounds, {}, 1);
|
|
53
59
|
expect(state.paneId).toBe("dock-a");
|
|
54
60
|
});
|
|
61
|
+
|
|
62
|
+
test("cycles dock move targets without changing the selected window", () => {
|
|
63
|
+
const layout: LayoutConfig = {
|
|
64
|
+
dockRoot: {
|
|
65
|
+
kind: "split",
|
|
66
|
+
axis: "horizontal",
|
|
67
|
+
ratio: 0.5,
|
|
68
|
+
first: { kind: "pane", instanceId: "source" },
|
|
69
|
+
second: {
|
|
70
|
+
kind: "split",
|
|
71
|
+
axis: "vertical",
|
|
72
|
+
ratio: 0.5,
|
|
73
|
+
first: { kind: "pane", instanceId: "target-a" },
|
|
74
|
+
second: { kind: "pane", instanceId: "target-b" },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
instances: [
|
|
78
|
+
pane("source"),
|
|
79
|
+
pane("target-a"),
|
|
80
|
+
pane("target-b"),
|
|
81
|
+
],
|
|
82
|
+
floating: [],
|
|
83
|
+
detached: [],
|
|
84
|
+
};
|
|
85
|
+
const state: WindowEditState = {
|
|
86
|
+
paneId: "source",
|
|
87
|
+
previewLayout: layout,
|
|
88
|
+
mode: "move",
|
|
89
|
+
focus: { kind: "dock-move", targetId: "target-a", position: "right" },
|
|
90
|
+
dirty: false,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const next = cycleWindowEditTarget(state, bounds, { reserveDividerGutters: true }, 1);
|
|
94
|
+
|
|
95
|
+
expect(next.paneId).toBe("source");
|
|
96
|
+
expect(next.focus).toEqual({ kind: "dock-move", targetId: "target-b", position: "right" });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("uses dock geometry options for dock move preview rectangles", () => {
|
|
100
|
+
const layout: LayoutConfig = {
|
|
101
|
+
dockRoot: {
|
|
102
|
+
kind: "split",
|
|
103
|
+
axis: "horizontal",
|
|
104
|
+
ratio: 0.5,
|
|
105
|
+
first: { kind: "pane", instanceId: "left" },
|
|
106
|
+
second: { kind: "pane", instanceId: "right" },
|
|
107
|
+
},
|
|
108
|
+
instances: [
|
|
109
|
+
pane("left"),
|
|
110
|
+
pane("right"),
|
|
111
|
+
],
|
|
112
|
+
floating: [],
|
|
113
|
+
detached: [],
|
|
114
|
+
};
|
|
115
|
+
const state: WindowEditState = {
|
|
116
|
+
paneId: "left",
|
|
117
|
+
previewLayout: layout,
|
|
118
|
+
mode: "move",
|
|
119
|
+
focus: { kind: "dock-move", targetId: "right", position: "right" },
|
|
120
|
+
dirty: false,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
expect(resolveWindowEditDockMovePreview(state, bounds, { reserveDividerGutters: true })?.rect)
|
|
124
|
+
.toEqual({ x: 61, y: 0, width: 59, height: 40 });
|
|
125
|
+
});
|
|
55
126
|
});
|
|
@@ -226,12 +226,57 @@ export function cycleWindowEditFocus(
|
|
|
226
226
|
return targets[(currentIndex + delta + targets.length) % targets.length] ?? targets[0]!;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
function getWindowEditDockPaneIds(
|
|
230
|
+
layout: LayoutConfig,
|
|
231
|
+
bounds: LayoutBounds,
|
|
232
|
+
dockGeometryOptions: DockGeometryOptions,
|
|
233
|
+
): string[] {
|
|
234
|
+
return getDockLeafLayouts(layout, bounds, dockGeometryOptions).map((entry) => entry.instanceId);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function getWindowEditPaneIds(
|
|
238
|
+
layout: LayoutConfig,
|
|
239
|
+
bounds: LayoutBounds,
|
|
240
|
+
dockGeometryOptions: DockGeometryOptions,
|
|
241
|
+
): string[] {
|
|
242
|
+
const dockedIds = getWindowEditDockPaneIds(layout, bounds, dockGeometryOptions);
|
|
231
243
|
const floatingIds = layout.floating.map((entry) => entry.instanceId);
|
|
232
244
|
return [...dockedIds, ...floatingIds];
|
|
233
245
|
}
|
|
234
246
|
|
|
247
|
+
export function cycleWindowEditTarget(
|
|
248
|
+
state: WindowEditState,
|
|
249
|
+
bounds: LayoutBounds,
|
|
250
|
+
dockGeometryOptions: DockGeometryOptions,
|
|
251
|
+
delta: 1 | -1,
|
|
252
|
+
): WindowEditState {
|
|
253
|
+
if (state.mode !== "move" || state.focus.kind !== "dock-move") {
|
|
254
|
+
return state;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const targetIds = getWindowEditDockPaneIds(state.previewLayout, bounds, dockGeometryOptions)
|
|
258
|
+
.filter((paneId) => paneId !== state.paneId);
|
|
259
|
+
if (targetIds.length === 0) {
|
|
260
|
+
return {
|
|
261
|
+
...state,
|
|
262
|
+
focus: normalizeWindowEditFocus({ kind: "move" }, state.previewLayout, state.paneId, state.mode, bounds, dockGeometryOptions),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const currentIndex = targetIds.indexOf(state.focus.targetId);
|
|
267
|
+
const startIndex = currentIndex >= 0
|
|
268
|
+
? currentIndex
|
|
269
|
+
: delta === 1
|
|
270
|
+
? -1
|
|
271
|
+
: 0;
|
|
272
|
+
const targetId = targetIds[(startIndex + delta + targetIds.length) % targetIds.length] ?? targetIds[0]!;
|
|
273
|
+
return {
|
|
274
|
+
...state,
|
|
275
|
+
focus: normalizeWindowEditFocus({ ...state.focus, targetId }, state.previewLayout, state.paneId, state.mode, bounds, dockGeometryOptions),
|
|
276
|
+
notice: undefined,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
235
280
|
export function raiseWindowEditPane(layout: LayoutConfig, paneId: string): LayoutConfig {
|
|
236
281
|
const floating = layout.floating.find((entry) => entry.instanceId === paneId);
|
|
237
282
|
if (!floating) return layout;
|
|
@@ -73,7 +73,7 @@ export function windowEditStatusLine(
|
|
|
73
73
|
export function windowEditHelpText(state: WindowEditState): string {
|
|
74
74
|
if (state.mode === "move") {
|
|
75
75
|
if (state.focus.kind === "dock-move") {
|
|
76
|
-
return "arrows/hjkl choose side
|
|
76
|
+
return "arrows/hjkl choose side Tab/w target m window d float r resize Enter commit/exit Esc exit/cancel";
|
|
77
77
|
}
|
|
78
78
|
return "Tab/w window d dock/float r resize arrows/hjkl target/move Shift fast Enter commit/exit Esc exit/cancel";
|
|
79
79
|
}
|
|
@@ -100,6 +100,7 @@ export function resolveWindowEditDockMovePreview(
|
|
|
100
100
|
state.paneId,
|
|
101
101
|
leafDropTarget(focus.targetId, focus.position),
|
|
102
102
|
bounds,
|
|
103
|
+
dockGeometryOptions,
|
|
103
104
|
);
|
|
104
105
|
if (!simulation.previewRect) return null;
|
|
105
106
|
return {
|