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
|
@@ -20,6 +20,7 @@ interface UseShellNativePointerRuntimeOptions {
|
|
|
20
20
|
selectWindowModePane: (paneId: string) => void;
|
|
21
21
|
setHoveredMenuItemId: Dispatch<SetStateAction<string | null>>;
|
|
22
22
|
setMenuState: Dispatch<SetStateAction<ActionMenuState | null>>;
|
|
23
|
+
transientFocusActive: boolean;
|
|
23
24
|
windowMode: WindowEditState | null;
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -35,6 +36,7 @@ export function useShellNativePointerRuntime({
|
|
|
35
36
|
selectWindowModePane,
|
|
36
37
|
setHoveredMenuItemId,
|
|
37
38
|
setMenuState,
|
|
39
|
+
transientFocusActive,
|
|
38
40
|
windowMode,
|
|
39
41
|
}: UseShellNativePointerRuntimeOptions) {
|
|
40
42
|
const {
|
|
@@ -69,6 +71,7 @@ export function useShellNativePointerRuntime({
|
|
|
69
71
|
const startNativeFloatingDrag = useCallback((paneId: string, rect: FloatingRect, event: ShellMouseEvent) => {
|
|
70
72
|
if (!nativePaneChrome) return;
|
|
71
73
|
if (windowMode) return;
|
|
74
|
+
if (transientFocusActive) return;
|
|
72
75
|
if (event.button === 2) return;
|
|
73
76
|
|
|
74
77
|
const pointer = getShellPointer(event);
|
|
@@ -83,11 +86,12 @@ export function useShellNativePointerRuntime({
|
|
|
83
86
|
};
|
|
84
87
|
updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
|
|
85
88
|
event.preventDefault();
|
|
86
|
-
}, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, updateDragFloatingRect, windowMode]);
|
|
89
|
+
}, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, transientFocusActive, updateDragFloatingRect, windowMode]);
|
|
87
90
|
|
|
88
91
|
const startNativeDockedDrag = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
|
|
89
92
|
if (!nativePaneChrome) return;
|
|
90
93
|
if (windowMode) return;
|
|
94
|
+
if (transientFocusActive) return;
|
|
91
95
|
if (event.button === 2) return;
|
|
92
96
|
|
|
93
97
|
const pointer = getShellPointer(event);
|
|
@@ -101,10 +105,11 @@ export function useShellNativePointerRuntime({
|
|
|
101
105
|
origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
102
106
|
};
|
|
103
107
|
event.preventDefault();
|
|
104
|
-
}, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, windowMode]);
|
|
108
|
+
}, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, transientFocusActive, windowMode]);
|
|
105
109
|
|
|
106
110
|
const startNativeFloatResize = useCallback((paneId: string, rect: FloatingRect, event: ShellMouseEvent) => {
|
|
107
111
|
if (!nativePaneChrome) return;
|
|
112
|
+
if (transientFocusActive) return;
|
|
108
113
|
|
|
109
114
|
const pointer = getShellPointer(event);
|
|
110
115
|
if (windowMode) {
|
|
@@ -122,10 +127,11 @@ export function useShellNativePointerRuntime({
|
|
|
122
127
|
updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
|
|
123
128
|
event.stopPropagation();
|
|
124
129
|
event.preventDefault();
|
|
125
|
-
}, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, selectWindowModePane, updateDragFloatingRect, windowMode]);
|
|
130
|
+
}, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, selectWindowModePane, transientFocusActive, updateDragFloatingRect, windowMode]);
|
|
126
131
|
|
|
127
132
|
const startNativeDividerDrag = useCallback((divider: DockDividerLayout, event: ShellMouseEvent) => {
|
|
128
133
|
if (!nativePaneChrome) return;
|
|
134
|
+
if (transientFocusActive) return;
|
|
129
135
|
|
|
130
136
|
const pointer = getShellPointer(event);
|
|
131
137
|
if (menuState) {
|
|
@@ -148,7 +154,7 @@ export function useShellNativePointerRuntime({
|
|
|
148
154
|
});
|
|
149
155
|
event.stopPropagation();
|
|
150
156
|
event.preventDefault();
|
|
151
|
-
}, [dragRef, getShellPointer, menuState, nativePaneChrome, setHoveredMenuItemId, setMenuState, updateDividerPreview]);
|
|
157
|
+
}, [dragRef, getShellPointer, menuState, nativePaneChrome, setHoveredMenuItemId, setMenuState, transientFocusActive, updateDividerPreview]);
|
|
152
158
|
|
|
153
159
|
const handlePaneAction = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
|
|
154
160
|
if (windowMode) return;
|
|
@@ -48,6 +48,8 @@ interface ShellPaneLayersProps {
|
|
|
48
48
|
startNativeDockedDrag: (paneId: string, rect: LayoutBounds, event: any) => void;
|
|
49
49
|
startNativeFloatingDrag: (paneId: string, rect: FloatingRect, event: any) => void;
|
|
50
50
|
startNativeFloatResize: (paneId: string, rect: FloatingRect, event: any) => void;
|
|
51
|
+
transientFocusActive: boolean;
|
|
52
|
+
transientFocusPaneId: string | null;
|
|
51
53
|
visibleFloatingPanes: VisibleFloatingPane[];
|
|
52
54
|
width: number;
|
|
53
55
|
windowModeDockResizePathKey: string | null;
|
|
@@ -79,6 +81,8 @@ export function ShellPaneLayers({
|
|
|
79
81
|
startNativeDockedDrag,
|
|
80
82
|
startNativeFloatingDrag,
|
|
81
83
|
startNativeFloatResize,
|
|
84
|
+
transientFocusActive,
|
|
85
|
+
transientFocusPaneId,
|
|
82
86
|
visibleFloatingPanes,
|
|
83
87
|
width,
|
|
84
88
|
windowModeDockResizePathKey,
|
|
@@ -87,8 +91,12 @@ export function ShellPaneLayers({
|
|
|
87
91
|
return (
|
|
88
92
|
<>
|
|
89
93
|
{dockLeafLayouts.map((leaf) => {
|
|
94
|
+
if (transientFocusActive && leaf.instanceId !== transientFocusPaneId) return null;
|
|
90
95
|
const pane = paneMap.get(leaf.instanceId);
|
|
91
96
|
if (!pane) return null;
|
|
97
|
+
const rect = transientFocusActive
|
|
98
|
+
? { x: 0, y: 0, width, height: contentHeight }
|
|
99
|
+
: leaf.rect;
|
|
92
100
|
const focused = focusedPaneId === leaf.instanceId && (!overlayOpen || menuPaneId === leaf.instanceId);
|
|
93
101
|
const windowModeSelected = windowModePaneId === leaf.instanceId;
|
|
94
102
|
const showActions = focused || hoveredPaneId === leaf.instanceId || menuPaneId === leaf.instanceId;
|
|
@@ -96,17 +104,17 @@ export function ShellPaneLayers({
|
|
|
96
104
|
<Box
|
|
97
105
|
key={`dock:${leaf.instanceId}`}
|
|
98
106
|
position="absolute"
|
|
99
|
-
left={
|
|
100
|
-
top={
|
|
101
|
-
width={
|
|
102
|
-
height={
|
|
107
|
+
left={rect.x}
|
|
108
|
+
top={rect.y}
|
|
109
|
+
width={rect.width}
|
|
110
|
+
height={rect.height}
|
|
103
111
|
>
|
|
104
112
|
<PaneFooterProvider>
|
|
105
113
|
{(footer) => {
|
|
106
114
|
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, hasPaneFooterContent(footer));
|
|
107
115
|
const bodyFrame = resolvePaneBodyFrame({
|
|
108
|
-
width:
|
|
109
|
-
height:
|
|
116
|
+
width: rect.width,
|
|
117
|
+
height: rect.height,
|
|
110
118
|
nativePaneChrome,
|
|
111
119
|
reserveFooter,
|
|
112
120
|
});
|
|
@@ -115,18 +123,18 @@ export function ShellPaneLayers({
|
|
|
115
123
|
paneId={leaf.instanceId}
|
|
116
124
|
title={getPaneTitle(pane)}
|
|
117
125
|
focused={focused}
|
|
118
|
-
width={
|
|
119
|
-
height={
|
|
126
|
+
width={rect.width}
|
|
127
|
+
height={rect.height}
|
|
120
128
|
showActions={showActions}
|
|
121
129
|
windowModeSelected={windowModeSelected}
|
|
122
130
|
footer={footer}
|
|
123
131
|
onMouseDown={nativePaneChrome ? (event) => handleNativePaneMouseDown(leaf.instanceId, event) : undefined}
|
|
124
132
|
onMouseMove={() => setHoveredPaneIfChanged(leaf.instanceId)}
|
|
125
|
-
onHeaderMouseDown={nativePaneChrome ? (event) => startNativeDockedDrag(leaf.instanceId,
|
|
126
|
-
onHeaderMouseDrag={nativePaneChrome ? handleNativeDrag : undefined}
|
|
127
|
-
onHeaderMouseDragEnd={nativePaneChrome ? handleNativeDrag : undefined}
|
|
128
|
-
onHeaderContextMenu={nativePaneChrome && nativeContextMenu === true ? (event) => handleNativePaneContextMenu(leaf.instanceId,
|
|
129
|
-
onActionMouseDown={(event) => handlePaneAction(leaf.instanceId,
|
|
133
|
+
onHeaderMouseDown={nativePaneChrome && !transientFocusActive ? (event) => startNativeDockedDrag(leaf.instanceId, rect, event) : undefined}
|
|
134
|
+
onHeaderMouseDrag={nativePaneChrome && !transientFocusActive ? handleNativeDrag : undefined}
|
|
135
|
+
onHeaderMouseDragEnd={nativePaneChrome && !transientFocusActive ? handleNativeDrag : undefined}
|
|
136
|
+
onHeaderContextMenu={nativePaneChrome && nativeContextMenu === true ? (event) => handleNativePaneContextMenu(leaf.instanceId, rect, event) : undefined}
|
|
137
|
+
onActionMouseDown={(event) => handlePaneAction(leaf.instanceId, rect, event)}
|
|
130
138
|
>
|
|
131
139
|
<PaneContent
|
|
132
140
|
component={pane.def.component}
|
|
@@ -145,7 +153,10 @@ export function ShellPaneLayers({
|
|
|
145
153
|
})}
|
|
146
154
|
|
|
147
155
|
{visibleFloatingPanes.map(({ pane, rect }) => {
|
|
148
|
-
|
|
156
|
+
if (transientFocusActive && pane.instance.instanceId !== transientFocusPaneId) return null;
|
|
157
|
+
const preview = transientFocusActive
|
|
158
|
+
? { x: 0, y: 0, width, height: contentHeight }
|
|
159
|
+
: dragFloatingRect?.paneId === pane.instance.instanceId
|
|
149
160
|
? constrainFloatingRectToBounds(dragFloatingRect.rect, width, contentHeight)
|
|
150
161
|
: rect;
|
|
151
162
|
const focused = focusedPaneId === pane.instance.instanceId && (!overlayOpen || menuPaneId === pane.instance.instanceId);
|
|
@@ -203,6 +214,7 @@ export function ShellPaneLayers({
|
|
|
203
214
|
})}
|
|
204
215
|
|
|
205
216
|
{dockDividerLayouts.map((divider) => {
|
|
217
|
+
if (transientFocusActive) return null;
|
|
206
218
|
const dividerPathKey = pathKey(divider.path);
|
|
207
219
|
const previewActive = dividerPreview?.pathKey === dividerPathKey;
|
|
208
220
|
const active = previewActive || windowModeDockResizePathKey === dividerPathKey;
|
|
@@ -25,6 +25,7 @@ interface ShellPaneManagementShortcutOptions {
|
|
|
25
25
|
overlayOpen: boolean;
|
|
26
26
|
popOutFocusedPane(): boolean;
|
|
27
27
|
startWindowMode(paneId?: string, mode?: WindowEditMode): void;
|
|
28
|
+
toggleFocusedPaneFullscreen(): boolean;
|
|
28
29
|
toggleFocusedPaneFloating(): boolean;
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -42,6 +43,7 @@ export function useShellPaneManagementShortcuts({
|
|
|
42
43
|
overlayOpen,
|
|
43
44
|
popOutFocusedPane,
|
|
44
45
|
startWindowMode,
|
|
46
|
+
toggleFocusedPaneFullscreen,
|
|
45
47
|
toggleFocusedPaneFloating,
|
|
46
48
|
}: ShellPaneManagementShortcutOptions): void {
|
|
47
49
|
const doubleEscapeCloseRef = useRef(createDoubleEscapeCloseState());
|
|
@@ -53,6 +55,20 @@ export function useShellPaneManagementShortcuts({
|
|
|
53
55
|
}, [overlayOpen]);
|
|
54
56
|
|
|
55
57
|
useShortcut((event) => {
|
|
58
|
+
const shortcut = resolvePaneManagementShortcut(event);
|
|
59
|
+
if (shortcut === "toggle-fullscreen" && !overlayOpen) {
|
|
60
|
+
if (!inputCaptured || inputCaptureAllowsPaneManagementShortcut(shortcut, event)) {
|
|
61
|
+
if (hasActiveDrag()) {
|
|
62
|
+
cancelActiveDrag();
|
|
63
|
+
}
|
|
64
|
+
if (toggleFocusedPaneFullscreen()) {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
const isEscape = event.name === "escape" || event.name === "esc";
|
|
57
73
|
if (isEscape) {
|
|
58
74
|
const doubleEscapeState = doubleEscapeCloseRef.current;
|
|
@@ -91,6 +107,9 @@ export function useShellPaneManagementShortcuts({
|
|
|
91
107
|
case "settings":
|
|
92
108
|
handled = openFocusedPaneSettings();
|
|
93
109
|
break;
|
|
110
|
+
case "toggle-fullscreen":
|
|
111
|
+
handled = toggleFocusedPaneFullscreen();
|
|
112
|
+
break;
|
|
94
113
|
case "toggle-floating":
|
|
95
114
|
handled = toggleFocusedPaneFloating();
|
|
96
115
|
break;
|
|
@@ -2,6 +2,7 @@ import type { KeyEventLike } from "../../../react/input";
|
|
|
2
2
|
|
|
3
3
|
export const PANE_MANAGEMENT_ACCELERATORS = {
|
|
4
4
|
settings: "CmdOrCtrl+,",
|
|
5
|
+
fullscreen: "CmdOrCtrl+Shift+F",
|
|
5
6
|
toggleFloating: "CmdOrCtrl+Shift+D",
|
|
6
7
|
popOut: "CmdOrCtrl+Shift+O",
|
|
7
8
|
copyScreenshot: "CmdOrCtrl+Shift+C",
|
|
@@ -15,6 +16,7 @@ export const PANE_MANAGEMENT_ACCELERATORS = {
|
|
|
15
16
|
|
|
16
17
|
export type PaneManagementShortcut =
|
|
17
18
|
| "settings"
|
|
19
|
+
| "toggle-fullscreen"
|
|
18
20
|
| "toggle-floating"
|
|
19
21
|
| "pop-out"
|
|
20
22
|
| "copy-screenshot"
|
|
@@ -38,6 +40,7 @@ export function resolvePaneManagementShortcut(
|
|
|
38
40
|
if (!shifted || event.alt) return null;
|
|
39
41
|
if (name === "c") return "copy-screenshot";
|
|
40
42
|
if (name === "d") return "toggle-floating";
|
|
43
|
+
if (name === "f") return "toggle-fullscreen";
|
|
41
44
|
if (name === "o") return "pop-out";
|
|
42
45
|
if (name === "l") return "layout-actions";
|
|
43
46
|
if (name === "g") return "gridlock-all";
|
|
@@ -50,6 +53,7 @@ export function inputCaptureAllowsPaneManagementShortcut(
|
|
|
50
53
|
shortcut: PaneManagementShortcut,
|
|
51
54
|
event: Pick<KeyEventLike, "meta" | "super" | "targetEditable">,
|
|
52
55
|
): boolean {
|
|
56
|
+
if (shortcut === "toggle-fullscreen") return event.meta || event.super === true;
|
|
53
57
|
if (shortcut !== "close" && shortcut !== "close-all-floating") return false;
|
|
54
58
|
return event.meta || event.super || event.targetEditable !== true;
|
|
55
59
|
}
|
|
@@ -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,
|
|
@@ -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;
|
|
@@ -5,6 +5,8 @@ import { cloneLayout, createDefaultConfig, type LayoutConfig } from "../../types
|
|
|
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);
|
|
@@ -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
|
|
|
@@ -184,11 +208,12 @@ export function StatusBar() {
|
|
|
184
208
|
);
|
|
185
209
|
}, [activeLayoutIdx, layoutContextMenuItems, layouts, showContextMenu]);
|
|
186
210
|
const handleLayoutTabContextMenu = useCallback((value: string, event: any) => {
|
|
211
|
+
if (value === transientLayout?.id) return;
|
|
187
212
|
const index = Number(value);
|
|
188
213
|
if (!Number.isInteger(index) || index < 0 || index >= layouts.length) return;
|
|
189
214
|
if (event?.type !== "contextmenu" && event?.button === 2 && nativeContextMenu === true) return;
|
|
190
215
|
void openLayoutContextMenu(index, event);
|
|
191
|
-
}, [layouts.length, nativeContextMenu, openLayoutContextMenu]);
|
|
216
|
+
}, [layouts.length, nativeContextMenu, openLayoutContextMenu, transientLayout?.id]);
|
|
192
217
|
const layoutTabItems = layoutTabs.map((tab) => ({
|
|
193
218
|
...tab,
|
|
194
219
|
onContextMenu: handleLayoutTabContextMenu,
|
|
@@ -198,6 +223,7 @@ export function StatusBar() {
|
|
|
198
223
|
|
|
199
224
|
const viewProps: StatusBarViewProps = {
|
|
200
225
|
activeLayoutIdx,
|
|
226
|
+
activeLayoutValue,
|
|
201
227
|
dismissGridlockTip,
|
|
202
228
|
handleGridlockTip,
|
|
203
229
|
handleLayoutSelect,
|
|
@@ -272,7 +298,7 @@ function TerminalStatusBar({
|
|
|
272
298
|
}
|
|
273
299
|
|
|
274
300
|
function StatusBarLayoutControl({
|
|
275
|
-
|
|
301
|
+
activeLayoutValue,
|
|
276
302
|
handleLayoutSelect,
|
|
277
303
|
hasMultipleLayouts,
|
|
278
304
|
hoveredControl,
|
|
@@ -283,7 +309,7 @@ function StatusBarLayoutControl({
|
|
|
283
309
|
setHoveredControl,
|
|
284
310
|
}: Pick<
|
|
285
311
|
StatusBarViewProps,
|
|
286
|
-
| "
|
|
312
|
+
| "activeLayoutValue"
|
|
287
313
|
| "handleLayoutSelect"
|
|
288
314
|
| "hasMultipleLayouts"
|
|
289
315
|
| "hoveredControl"
|
|
@@ -303,7 +329,7 @@ function StatusBarLayoutControl({
|
|
|
303
329
|
<Box width={layoutTabsWidth} height={1}>
|
|
304
330
|
<Tabs
|
|
305
331
|
tabs={layoutTabItems}
|
|
306
|
-
activeValue={
|
|
332
|
+
activeValue={activeLayoutValue}
|
|
307
333
|
onSelect={handleLayoutSelect}
|
|
308
334
|
compact
|
|
309
335
|
variant="pill"
|