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
|
@@ -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"
|
|
@@ -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 {
|
|
@@ -3,6 +3,11 @@ import { blendHex, colors, priceColor } from "../theme/colors";
|
|
|
3
3
|
import { displayWidth, formatPercent, padTo } from "../utils/format";
|
|
4
4
|
import type { PriceReturnField } from "../market-data/performance";
|
|
5
5
|
|
|
6
|
+
type MouseInteractionEvent = {
|
|
7
|
+
stopPropagation?: () => void;
|
|
8
|
+
preventDefault?: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
6
11
|
const STRIP_COLUMN_GAP = 1;
|
|
7
12
|
const RETURN_CELL_MIN_WIDTH = 7;
|
|
8
13
|
const SYMBOL_COLUMN_MIN_WIDTH = 6;
|
|
@@ -100,12 +105,14 @@ function chooseComparisonFieldIds(width: number, availableIds: readonly string[]
|
|
|
100
105
|
|
|
101
106
|
export function PriceReturnTable({
|
|
102
107
|
height,
|
|
108
|
+
onFocusInteraction,
|
|
103
109
|
onOpenSymbol,
|
|
104
110
|
onSelectSymbol,
|
|
105
111
|
rows,
|
|
106
112
|
width,
|
|
107
113
|
}: {
|
|
108
114
|
height: number;
|
|
115
|
+
onFocusInteraction?: (event: MouseInteractionEvent | null | undefined) => void;
|
|
109
116
|
onOpenSymbol: (symbol: string) => void;
|
|
110
117
|
onSelectSymbol: (symbol: string) => void;
|
|
111
118
|
rows: PriceReturnTableRow[];
|
|
@@ -151,7 +158,8 @@ export function PriceReturnTable({
|
|
|
151
158
|
width={width}
|
|
152
159
|
backgroundColor={row.selected ? blendHex(colors.panel, colors.borderFocused, 0.18) : colors.panel}
|
|
153
160
|
onMouseMove={() => onSelectSymbol(row.symbol)}
|
|
154
|
-
onMouseDown={() => {
|
|
161
|
+
onMouseDown={(event: any) => {
|
|
162
|
+
onFocusInteraction?.(event);
|
|
155
163
|
onSelectSymbol(row.symbol);
|
|
156
164
|
onOpenSymbol(row.symbol);
|
|
157
165
|
}}
|
package/src/core/app-services.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { assetDataProvider, newsProvider } from "../capabilities";
|
|
|
12
12
|
import type { AppConfig } from "../types/config";
|
|
13
13
|
import type { DataProvider } from "../types/data-provider";
|
|
14
14
|
import { debugLog } from "../utils/debug-log";
|
|
15
|
-
import { measurePerf } from "../utils/perf-marks";
|
|
15
|
+
import { measurePerf, measurePerfAsync } from "../utils/perf-marks";
|
|
16
16
|
import { setIbkrPortfolioPerformanceResourceStore } from "../plugins/ibkr/portfolio-performance";
|
|
17
17
|
|
|
18
18
|
const servicesLog = debugLog.createLogger("services");
|
|
@@ -25,6 +25,7 @@ export interface AppServices {
|
|
|
25
25
|
marketData: MarketDataCoordinator;
|
|
26
26
|
pluginRegistry: PluginRegistry;
|
|
27
27
|
newsService: NewsService;
|
|
28
|
+
ready: Promise<void>;
|
|
28
29
|
destroy(): void;
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -68,10 +69,11 @@ export function createAppServices({
|
|
|
68
69
|
setSharedMarketDataCoordinator(marketData);
|
|
69
70
|
|
|
70
71
|
const plugins = getLoadablePlugins(externalPlugins);
|
|
72
|
+
const pluginReadyPromises: Promise<void>[] = [];
|
|
71
73
|
for (const plugin of plugins) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
pluginReadyPromises.push(measurePerfAsync("startup.services.register-plugin", () => (
|
|
75
|
+
pluginRegistry.register(plugin)
|
|
76
|
+
), { pluginId: plugin.id }));
|
|
75
77
|
}
|
|
76
78
|
measurePerf("startup.services.news-start", () => {
|
|
77
79
|
newsService.start();
|
|
@@ -86,6 +88,7 @@ export function createAppServices({
|
|
|
86
88
|
marketData,
|
|
87
89
|
pluginRegistry,
|
|
88
90
|
newsService,
|
|
91
|
+
ready: Promise.all(pluginReadyPromises).then(() => {}),
|
|
89
92
|
destroy() {
|
|
90
93
|
setSharedMarketDataCoordinator(null);
|
|
91
94
|
setSharedNewsService(null);
|
|
@@ -21,8 +21,7 @@ export function setConfigStoreHost(host: ConfigStoreHost | null): void {
|
|
|
21
21
|
|
|
22
22
|
async function loadNodeHost(): Promise<ConfigStoreHost> {
|
|
23
23
|
if (!nodeHostPromise) {
|
|
24
|
-
|
|
25
|
-
nodeHostPromise = import(modulePath) as Promise<ConfigStoreHost>;
|
|
24
|
+
nodeHostPromise = import("./node") as Promise<ConfigStoreHost>;
|
|
26
25
|
}
|
|
27
26
|
return nodeHostPromise;
|
|
28
27
|
}
|
|
@@ -267,6 +267,7 @@ export function TwitterFeedPane({ focused, width, height }: PaneProps) {
|
|
|
267
267
|
inputRef={searchInputRef}
|
|
268
268
|
onFocus={focusSearch}
|
|
269
269
|
onBlur={blurSearch}
|
|
270
|
+
onNavigateDown={blurSearch}
|
|
270
271
|
onQueryChange={updateFeedQuery}
|
|
271
272
|
/>
|
|
272
273
|
) : null;
|
|
@@ -309,6 +310,7 @@ export function TwitterFeedPane({ focused, width, height }: PaneProps) {
|
|
|
309
310
|
load={loadActiveFeed}
|
|
310
311
|
onResult={markFeedResult}
|
|
311
312
|
onError={markFeedError}
|
|
313
|
+
onFocusSearch={focusSearch}
|
|
312
314
|
emptyStateTitle={searchEnabled ? "No tweets" : "Enter a search query"}
|
|
313
315
|
emptyStateHint={searchEnabled ? activeFeedQuery : undefined}
|
|
314
316
|
/>
|
|
@@ -15,6 +15,7 @@ export function TwitterFeedSearchBar({
|
|
|
15
15
|
inputRef,
|
|
16
16
|
onFocus,
|
|
17
17
|
onBlur,
|
|
18
|
+
onNavigateDown,
|
|
18
19
|
onQueryChange,
|
|
19
20
|
}: {
|
|
20
21
|
feed: TwitterFeed;
|
|
@@ -25,6 +26,7 @@ export function TwitterFeedSearchBar({
|
|
|
25
26
|
inputRef: RefObject<InputRenderable | null>;
|
|
26
27
|
onFocus: () => void;
|
|
27
28
|
onBlur: () => void;
|
|
29
|
+
onNavigateDown?: () => void;
|
|
28
30
|
onQueryChange: (feedId: string, query: string) => void;
|
|
29
31
|
}) {
|
|
30
32
|
const updateQuery = useCallback((value: string) => {
|
|
@@ -43,6 +45,7 @@ export function TwitterFeedSearchBar({
|
|
|
43
45
|
debounceMs={TWEET_SEARCH_DEBOUNCE_MS}
|
|
44
46
|
onFocus={onFocus}
|
|
45
47
|
onBlur={onBlur}
|
|
48
|
+
onNavigateDown={onNavigateDown}
|
|
46
49
|
onQueryChange={updateQuery}
|
|
47
50
|
/>
|
|
48
51
|
);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
usePaneFooter,
|
|
7
7
|
type DataTableCell,
|
|
8
8
|
type DataTableKeyEvent,
|
|
9
|
+
type DataTableRootKeyContext,
|
|
9
10
|
} from "../../../components";
|
|
10
11
|
import { TickerBadgeText } from "../../../components/ticker/badge/text";
|
|
11
12
|
import { RemoteImage } from "../../../components/ui";
|
|
@@ -15,6 +16,7 @@ import type { CloudTweetPayload, CloudTweetSearchResponse } from "../../../api-c
|
|
|
15
16
|
import { formatTimeAgo } from "../../../utils/format";
|
|
16
17
|
import { colors } from "../../../theme/colors";
|
|
17
18
|
import { CloudAuthNotice } from "../cloud/auth-actions";
|
|
19
|
+
import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
|
|
18
20
|
import {
|
|
19
21
|
buildTweetColumns,
|
|
20
22
|
formatMetric,
|
|
@@ -152,6 +154,7 @@ export function TweetSearchTable({
|
|
|
152
154
|
load,
|
|
153
155
|
onResult,
|
|
154
156
|
onError,
|
|
157
|
+
onFocusSearch,
|
|
155
158
|
emptyStateTitle,
|
|
156
159
|
emptyStateHint,
|
|
157
160
|
}: {
|
|
@@ -165,6 +168,7 @@ export function TweetSearchTable({
|
|
|
165
168
|
load: () => Promise<CloudTweetSearchResponse>;
|
|
166
169
|
onResult?: (result: CloudTweetSearchResponse) => void;
|
|
167
170
|
onError?: (message: string) => void;
|
|
171
|
+
onFocusSearch?: () => void;
|
|
168
172
|
emptyStateTitle?: string;
|
|
169
173
|
emptyStateHint?: string;
|
|
170
174
|
}) {
|
|
@@ -219,13 +223,21 @@ export function TweetSearchTable({
|
|
|
219
223
|
));
|
|
220
224
|
}, []);
|
|
221
225
|
|
|
222
|
-
const handleRootKeyDown = useCallback((
|
|
226
|
+
const handleRootKeyDown = useCallback((
|
|
227
|
+
event: DataTableKeyEvent,
|
|
228
|
+
context: DataTableRootKeyContext,
|
|
229
|
+
) => {
|
|
230
|
+
if (onFocusSearch && context.selectedIndex <= 0 && isPlainArrowUp(event)) {
|
|
231
|
+
stopSearchFocusNavigation(event);
|
|
232
|
+
onFocusSearch();
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
223
235
|
if (event.name !== "r") return false;
|
|
224
236
|
event.preventDefault?.();
|
|
225
237
|
event.stopPropagation?.();
|
|
226
238
|
reload();
|
|
227
239
|
return true;
|
|
228
|
-
}, [reload]);
|
|
240
|
+
}, [onFocusSearch, reload]);
|
|
229
241
|
|
|
230
242
|
const handleDetailKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
231
243
|
if (event.name !== "o") return false;
|