gloomberb 0.8.2 → 0.9.1
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 +9 -7
- package/package.json +18 -13
- package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
- package/patches/electrobun@1.18.1.patch +85 -0
- package/src/app/global-shortcuts.test.tsx +66 -4
- package/src/app/global-shortcuts.ts +12 -6
- package/src/assets/gloomberb-logo-windows.ico +0 -0
- package/src/assets/gloomberb-logo-windows.png +0 -0
- package/src/assets/gloomberb-logo.ico +0 -0
- package/src/components/command-bar/action-runtime.ts +1 -0
- package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
- package/src/components/command-bar/layout-items/current-layout.ts +33 -0
- package/src/components/command-bar/list/view.tsx +3 -1
- package/src/components/command-bar/pane-templates/items.ts +4 -1
- package/src/components/command-bar/panel/body.tsx +1 -0
- package/src/components/command-bar/routes/root/results.ts +0 -30
- package/src/components/command-bar/routes/root/runtime.ts +2 -20
- package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
- package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
- package/src/components/command-bar/routing/launch-request.ts +14 -7
- package/src/components/command-bar/routing/list-state.ts +1 -3
- package/src/components/command-bar/surface/index.test.tsx +76 -4
- package/src/components/command-bar/surface/index.tsx +0 -2
- package/src/components/command-bar/surface/pane.test.tsx +1 -0
- package/src/components/command-bar/surface/test-harness.tsx +1 -0
- package/src/components/command-bar/view-model.ts +3 -3
- package/src/components/layout/detached-pane-shell.tsx +8 -4
- package/src/components/layout/header.tsx +13 -6
- package/src/components/layout/shell/index.test.tsx +120 -2
- package/src/components/layout/shell/index.tsx +2 -0
- package/src/components/layout/shell/menu.ts +1 -7
- package/src/components/layout/shell/pane/actions.ts +8 -0
- package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
- package/src/components/layout/shell/shortcuts.ts +14 -6
- package/src/components/layout/shell/window-mode/index.ts +8 -3
- package/src/components/layout/titlebar-overlay.ts +16 -0
- package/src/components/layout/window-controls.tsx +90 -0
- package/src/components/layout/window-edit/presentation.ts +4 -4
- package/src/components/onboarding/onboarding-steps.tsx +3 -2
- package/src/components/ticker/list-table-view.tsx +6 -0
- package/src/components/ui/remote-image.tsx +1 -14
- package/src/components/ui/tabs.tsx +95 -80
- package/src/components/ui/ui.test.tsx +54 -0
- package/src/core/state/app/types.ts +12 -4
- package/src/plugins/builtin/chat/content/channel-navigation.ts +44 -11
- package/src/plugins/builtin/chat/content/index.tsx +5 -4
- package/src/plugins/builtin/chat/content/shortcuts.ts +7 -0
- package/src/plugins/builtin/chat/sidebar.test.tsx +71 -0
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/help/index.test.tsx +105 -30
- package/src/plugins/builtin/help/index.tsx +324 -168
- package/src/plugins/builtin/help/shortcut-model.ts +5 -6
- package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
- package/src/plugins/builtin/substack/api/auth.ts +185 -0
- package/src/plugins/builtin/substack/api/cache.ts +78 -0
- package/src/plugins/builtin/substack/api/loaders.ts +146 -0
- package/src/plugins/builtin/substack/api/store.ts +186 -0
- package/src/plugins/builtin/substack/api/types.ts +40 -0
- package/src/plugins/builtin/substack/api.test.ts +298 -0
- package/src/plugins/builtin/substack/article-detail.tsx +359 -0
- package/src/plugins/builtin/substack/article-stack.tsx +129 -0
- package/src/plugins/builtin/substack/content.ts +478 -0
- package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
- package/src/plugins/builtin/substack/index.tsx +49 -0
- package/src/plugins/builtin/substack/login-view.tsx +126 -0
- package/src/plugins/builtin/substack/model.test.ts +155 -0
- package/src/plugins/builtin/substack/normalize.ts +343 -0
- package/src/plugins/builtin/substack/pane-footer.ts +71 -0
- package/src/plugins/builtin/substack/pane-state.ts +170 -0
- package/src/plugins/builtin/substack/pane.tsx +500 -0
- package/src/plugins/builtin/substack/table.ts +111 -0
- package/src/plugins/builtin/substack/types.ts +71 -0
- package/src/plugins/builtin/substack/utils.ts +131 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/pane-manager/layout-state.ts +11 -0
- package/src/plugins/pane-manager.ts +1 -1
- package/src/react/use-throttled-commit-value.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +12 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +4 -0
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +43 -2
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +31 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.test.ts +53 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +64 -0
- package/src/renderers/electrobun/bun/desktop/window-style.ts +5 -0
- package/src/renderers/electrobun/bun/desktop/windows-icons.ts +123 -0
- package/src/renderers/electrobun/bun/index.ts +28 -6
- package/src/renderers/electrobun/bun/window/frame.test.ts +7 -0
- package/src/renderers/electrobun/bun/window/frame.ts +5 -0
- package/src/renderers/electrobun/view/build-assets.ts +2 -1
- package/src/renderers/electrobun/view/host/text.tsx +29 -9
- package/src/renderers/electrobun/view/http-fetch.ts +10 -3
- package/src/renderers/electrobun/view/styles.css +20 -0
- package/src/renderers/electrobun/view/ui-host.tsx +14 -0
- package/src/state/app/context/index.test.tsx +16 -0
- package/src/tickers/search/index.ts +1 -0
- package/src/ui/host.tsx +2 -0
- package/src/updater.test.ts +47 -2
- package/src/updater.ts +67 -10
- package/src/version.ts +1 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
rankTickerSearchItems,
|
|
7
7
|
upsertTickerFromSearchResult,
|
|
8
8
|
type TickerSearchCandidate,
|
|
9
|
+
type TickerSearchInstrumentClass,
|
|
9
10
|
} from "../../../../tickers/search";
|
|
10
11
|
import type { ResultItem } from "../../list/model";
|
|
11
12
|
import {
|
|
@@ -45,12 +46,15 @@ export function useCommandBarTickerSearchActions({
|
|
|
45
46
|
}, [tickerRepository, dispatch, pluginRegistry.events, focusTicker, closeAll]);
|
|
46
47
|
|
|
47
48
|
const mapTickerSearchCandidateToResultItem = useCallback((candidate: TickerSearchCandidate): ResultItem => {
|
|
49
|
+
const detail = formatTickerSearchDetail(candidate);
|
|
50
|
+
const right = formatTickerSearchRight(candidate);
|
|
51
|
+
|
|
48
52
|
if (candidate.kind === "ticker" && candidate.ticker) {
|
|
49
53
|
return {
|
|
50
54
|
id: candidate.id,
|
|
51
55
|
label: candidate.label,
|
|
52
|
-
detail
|
|
53
|
-
right
|
|
56
|
+
detail,
|
|
57
|
+
right,
|
|
54
58
|
category: candidate.category,
|
|
55
59
|
kind: "ticker",
|
|
56
60
|
secondaryAction: () => {
|
|
@@ -67,8 +71,8 @@ export function useCommandBarTickerSearchActions({
|
|
|
67
71
|
return {
|
|
68
72
|
id: candidate.id,
|
|
69
73
|
label: candidate.label,
|
|
70
|
-
detail
|
|
71
|
-
right
|
|
74
|
+
detail,
|
|
75
|
+
right,
|
|
72
76
|
category: candidate.category,
|
|
73
77
|
kind: "search",
|
|
74
78
|
secondaryAction: () => openTickerResearch(candidate.result!, { forceNewPane: true }),
|
|
@@ -140,3 +144,37 @@ export function useCommandBarTickerSearchActions({
|
|
|
140
144
|
writeTickerSearchCache,
|
|
141
145
|
};
|
|
142
146
|
}
|
|
147
|
+
|
|
148
|
+
function formatTickerSearchRight(candidate: TickerSearchCandidate): string | undefined {
|
|
149
|
+
const assetClass = formatInstrumentClass(candidate.instrumentClass);
|
|
150
|
+
const exchange = candidate.exchangeLabel || candidate.primaryExchangeLabel || candidate.right || "";
|
|
151
|
+
if (!exchange) return assetClass;
|
|
152
|
+
if (assetClass === "Other") return exchange;
|
|
153
|
+
return `${assetClass} ${exchange}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatTickerSearchDetail(candidate: TickerSearchCandidate): string {
|
|
157
|
+
const metadata = [
|
|
158
|
+
formatInstrumentClass(candidate.instrumentClass),
|
|
159
|
+
candidate.exchangeLabel,
|
|
160
|
+
candidate.primaryExchangeLabel && candidate.primaryExchangeLabel !== candidate.exchangeLabel
|
|
161
|
+
? candidate.primaryExchangeLabel
|
|
162
|
+
: null,
|
|
163
|
+
].filter((value): value is string => !!value);
|
|
164
|
+
return metadata.length > 0
|
|
165
|
+
? `${candidate.detail} | ${metadata.join(" | ")}`
|
|
166
|
+
: candidate.detail;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatInstrumentClass(instrumentClass: TickerSearchInstrumentClass): string {
|
|
170
|
+
switch (instrumentClass) {
|
|
171
|
+
case "equity":
|
|
172
|
+
return "Equity";
|
|
173
|
+
case "fund":
|
|
174
|
+
return "Fund";
|
|
175
|
+
case "derivative":
|
|
176
|
+
return "Derivative";
|
|
177
|
+
case "other":
|
|
178
|
+
return "Other";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -65,7 +65,10 @@ export function useTickerSearchRouteResults(options: {
|
|
|
65
65
|
|
|
66
66
|
const searchQuery = routeQuery.trim();
|
|
67
67
|
if (!searchQuery) {
|
|
68
|
-
|
|
68
|
+
searchRequestIdRef.current += 1;
|
|
69
|
+
setTickerSearchPending(false);
|
|
70
|
+
setTickerSearchResults(localTickerSearchResultItems(undefined, { limit: 8 }));
|
|
71
|
+
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
|
69
72
|
return;
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import type { PluginRegistry } from "../../../plugins/registry";
|
|
3
|
+
import type { AppState } from "../../../state/app/context";
|
|
3
4
|
import type { CommandDef } from "../../../types/plugin";
|
|
4
5
|
|
|
5
|
-
interface CommandBarPluginLaunchRequest {
|
|
6
|
-
kind: "plugin-command";
|
|
7
|
-
commandId: string;
|
|
8
|
-
sequence: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
interface UseCommandBarLaunchRequestOptions {
|
|
12
7
|
activeTickerSymbol: string | null;
|
|
13
|
-
commandBarLaunchRequest:
|
|
8
|
+
commandBarLaunchRequest: AppState["commandBarLaunchRequest"];
|
|
14
9
|
commandBarOpen: boolean;
|
|
10
|
+
openModeRoute: (
|
|
11
|
+
screen: "ticker-search" | "plugins" | "layout",
|
|
12
|
+
initialQuery?: string,
|
|
13
|
+
payload?: Record<string, unknown>,
|
|
14
|
+
) => void;
|
|
15
15
|
openPluginCommandWorkflow: (
|
|
16
16
|
command: CommandDef,
|
|
17
17
|
options?: { values?: Record<string, string> },
|
|
@@ -23,6 +23,7 @@ export function useCommandBarLaunchRequest({
|
|
|
23
23
|
activeTickerSymbol,
|
|
24
24
|
commandBarLaunchRequest,
|
|
25
25
|
commandBarOpen,
|
|
26
|
+
openModeRoute,
|
|
26
27
|
openPluginCommandWorkflow,
|
|
27
28
|
pluginRegistry,
|
|
28
29
|
}: UseCommandBarLaunchRequestOptions) {
|
|
@@ -38,6 +39,11 @@ export function useCommandBarLaunchRequest({
|
|
|
38
39
|
if (processedLaunchSequenceRef.current === launch.sequence) return;
|
|
39
40
|
processedLaunchSequenceRef.current = launch.sequence;
|
|
40
41
|
|
|
42
|
+
if (launch.kind === "ticker-search") {
|
|
43
|
+
openModeRoute("ticker-search", launch.query ?? "");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
const command = pluginRegistry.commands.get(launch.commandId);
|
|
42
48
|
if (!command?.wizard || command.wizard.length === 0) return;
|
|
43
49
|
let values: Record<string, string> | undefined;
|
|
@@ -53,6 +59,7 @@ export function useCommandBarLaunchRequest({
|
|
|
53
59
|
activeTickerSymbol,
|
|
54
60
|
commandBarLaunchRequest,
|
|
55
61
|
commandBarOpen,
|
|
62
|
+
openModeRoute,
|
|
56
63
|
openPluginCommandWorkflow,
|
|
57
64
|
pluginRegistry,
|
|
58
65
|
]);
|
|
@@ -125,9 +125,7 @@ function buildRouteListState(options: BuildRouteListStateOptions): ListScreenSta
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
case "ticker-search": {
|
|
128
|
-
const results =
|
|
129
|
-
? tickerSearchResults.map((item) => adaptTickerSearchRouteResult(item, currentRoute.payload))
|
|
130
|
-
: [];
|
|
128
|
+
const results = tickerSearchResults.map((item) => adaptTickerSearchRouteResult(item, currentRoute.payload));
|
|
131
129
|
const emptyState = getEmptyState("search", currentRoute.query, currentRoute.query);
|
|
132
130
|
return {
|
|
133
131
|
kind: "mode",
|
|
@@ -81,13 +81,35 @@ describe("CommandBar", () => {
|
|
|
81
81
|
|
|
82
82
|
const frame = testSetup.captureCharFrame();
|
|
83
83
|
expect(frame).toContain("Commands");
|
|
84
|
-
expect(frame).toContain("Tickers");
|
|
85
|
-
expect(frame).toContain("AAPL");
|
|
84
|
+
expect(frame).not.toContain("Tickers");
|
|
86
85
|
expect(frame).toContain("Panes");
|
|
87
86
|
expect(frame).toContain("Portfolio");
|
|
88
87
|
expect(frame).toContain("Help");
|
|
89
88
|
});
|
|
90
89
|
|
|
90
|
+
test("keeps generic command filtering separate from ticker search", async () => {
|
|
91
|
+
const searchQueries: string[] = [];
|
|
92
|
+
testSetup = await testRender(<CommandBarHarness
|
|
93
|
+
query="MSFT"
|
|
94
|
+
dataProvider={makeDataProvider(async (query) => {
|
|
95
|
+
searchQueries.push(query);
|
|
96
|
+
return [];
|
|
97
|
+
})}
|
|
98
|
+
/>, {
|
|
99
|
+
width: 80,
|
|
100
|
+
height: 24,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await testSetup.renderOnce();
|
|
104
|
+
await Bun.sleep(260);
|
|
105
|
+
await testSetup.renderOnce();
|
|
106
|
+
|
|
107
|
+
const frame = testSetup.captureCharFrame();
|
|
108
|
+
expect(frame).not.toContain("Tickers");
|
|
109
|
+
expect(frame).not.toContain("Microsoft Corp.");
|
|
110
|
+
expect(searchQueries).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
91
113
|
test("runs check for updates from the command bar", async () => {
|
|
92
114
|
const calls: number[] = [];
|
|
93
115
|
|
|
@@ -416,6 +438,56 @@ describe("CommandBar", () => {
|
|
|
416
438
|
expect(frame).toContain("AMD");
|
|
417
439
|
});
|
|
418
440
|
|
|
441
|
+
test("opens ticker search from a launch request with saved ticker metadata", async () => {
|
|
442
|
+
testSetup = await testRender(<CommandBarHarness
|
|
443
|
+
query=""
|
|
444
|
+
extraTickers={[makeTicker("BRK.B", "Berkshire Hathaway Inc.", {
|
|
445
|
+
exchange: "NYSE",
|
|
446
|
+
assetCategory: "STK",
|
|
447
|
+
})]}
|
|
448
|
+
configureState={(state) => ({
|
|
449
|
+
...state,
|
|
450
|
+
commandBarLaunchRequest: {
|
|
451
|
+
kind: "ticker-search",
|
|
452
|
+
query: "",
|
|
453
|
+
sequence: 1,
|
|
454
|
+
},
|
|
455
|
+
})}
|
|
456
|
+
/>, {
|
|
457
|
+
width: 100,
|
|
458
|
+
height: 24,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const frame = await waitForFrameToContain("Security Description");
|
|
462
|
+
expectSingleBackControl(frame);
|
|
463
|
+
expect(frame).toContain("BRK.B");
|
|
464
|
+
expect(frame).toContain("Equity NYSE");
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("opens ticker search when activating the Ticker Research pane item without a ticker", async () => {
|
|
468
|
+
testSetup = await testRender(<CommandBarHarness query="ticker research" />, {
|
|
469
|
+
width: 100,
|
|
470
|
+
height: 24,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
await testSetup.renderOnce();
|
|
474
|
+
const rootFrame = testSetup.captureCharFrame();
|
|
475
|
+
const tickerResearchRow = rootFrame
|
|
476
|
+
.split("\n")
|
|
477
|
+
.find((line) => line.includes("Ticker Research"));
|
|
478
|
+
expect(tickerResearchRow).toMatch(/\bT\s*$/);
|
|
479
|
+
|
|
480
|
+
await act(async () => {
|
|
481
|
+
testSetup!.mockInput.pressEnter();
|
|
482
|
+
await testSetup!.renderOnce();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const frame = testSetup.captureCharFrame();
|
|
486
|
+
expectSingleBackControl(frame);
|
|
487
|
+
expect(frame).toContain("Security Description");
|
|
488
|
+
expect(frame).toContain("Search tickers");
|
|
489
|
+
});
|
|
490
|
+
|
|
419
491
|
test("keeps typed prefixes in the root query until a result is activated", async () => {
|
|
420
492
|
testSetup = await testRender(<CommandBarHarness query="DES " />, {
|
|
421
493
|
width: 80,
|
|
@@ -763,8 +835,8 @@ describe("CommandBar", () => {
|
|
|
763
835
|
const rows = frame.split("\n");
|
|
764
836
|
const savedHeadings = frame.split("\n").filter((line) => line.trim() === "Saved");
|
|
765
837
|
const otherListingsHeadings = frame.split("\n").filter((line) => line.trim() === "Other Listings");
|
|
766
|
-
const aaplRow = rows.findIndex((line) => line.trimStart().startsWith("AAPL")
|
|
767
|
-
const appRow = rows.findIndex((line) => line.trimStart().startsWith("APP")
|
|
838
|
+
const aaplRow = rows.findIndex((line) => line.trimStart().startsWith("AAPL"));
|
|
839
|
+
const appRow = rows.findIndex((line) => line.trimStart().startsWith("APP"));
|
|
768
840
|
expect(savedHeadings).toHaveLength(1);
|
|
769
841
|
expect(otherListingsHeadings).toHaveLength(1);
|
|
770
842
|
expect(aaplRow).toBeGreaterThanOrEqual(0);
|
|
@@ -110,7 +110,6 @@ export function CommandBar({
|
|
|
110
110
|
getWorkflowFieldStringValue,
|
|
111
111
|
getWorkflowInputRef,
|
|
112
112
|
localTickerSearchResultItems,
|
|
113
|
-
mapTickerSearchCandidateToResultItem,
|
|
114
113
|
moveWorkflowFocus,
|
|
115
114
|
nonShortcutPaneTemplateItems,
|
|
116
115
|
openInlineConfirm,
|
|
@@ -193,7 +192,6 @@ export function CommandBar({
|
|
|
193
192
|
getTickers: getTickerSearchTickers,
|
|
194
193
|
hasPaneSettings,
|
|
195
194
|
localTickerSearchResultItems,
|
|
196
|
-
mapTickerSearchCandidateToResultItem,
|
|
197
195
|
nativeListScrollRef,
|
|
198
196
|
nonShortcutPaneTemplateItems,
|
|
199
197
|
openModeRoute,
|
|
@@ -158,6 +158,7 @@ describe("CommandBar pane and layout routes", () => {
|
|
|
158
158
|
expect(frame).toContain("Float Pane");
|
|
159
159
|
expect(frame).toContain("Undo Layout Change");
|
|
160
160
|
expect(frame).toContain("Current Layout");
|
|
161
|
+
expect(frame).toContain("Close All Floating Panes");
|
|
161
162
|
});
|
|
162
163
|
|
|
163
164
|
test("runs layout actions directly from root search", async () => {
|
|
@@ -174,6 +174,7 @@ function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () =>
|
|
|
174
174
|
paneId: TICKER_RESEARCH_PANE_ID,
|
|
175
175
|
label: "Ticker Research",
|
|
176
176
|
description: "Open another research pane",
|
|
177
|
+
shortcut: { prefix: "T", argPlaceholder: "ticker", argKind: "ticker" },
|
|
177
178
|
}],
|
|
178
179
|
["new-chat-pane", {
|
|
179
180
|
id: "new-chat-pane",
|
|
@@ -40,7 +40,7 @@ export function resolveCommandBarMode(query: string, commandList?: Command[]): C
|
|
|
40
40
|
const match = matchPrefix(query, commandList);
|
|
41
41
|
|
|
42
42
|
if (!query.trim()) {
|
|
43
|
-
return { kind: "default", badge: "BROWSE", hint: "Type a command
|
|
43
|
+
return { kind: "default", badge: "BROWSE", hint: "Type a command or prefix" };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (!match) {
|
|
@@ -100,9 +100,9 @@ export function getEmptyState(mode: CommandBarMode, query: string, searchQuery?:
|
|
|
100
100
|
return { label: "No layout actions match", detail: query.trim() || "Focused-pane and layout actions will appear here" };
|
|
101
101
|
default:
|
|
102
102
|
if (query.trim()) {
|
|
103
|
-
return { label: `No matches for "${query.trim()}"`, detail: "Try a
|
|
103
|
+
return { label: `No matches for "${query.trim()}"`, detail: "Try a command name or prefix. Use T for ticker search" };
|
|
104
104
|
}
|
|
105
|
-
return { label: "No results yet", detail: "
|
|
105
|
+
return { label: "No results yet", detail: "Suggested commands will appear here" };
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -12,7 +12,8 @@ import { PaneBodyFrame, getPaneWindowAttributes } from "./pane/frame";
|
|
|
12
12
|
import { PaneContent } from "./pane/content";
|
|
13
13
|
import { resolvePaneBodyFrame } from "./pane/sizing";
|
|
14
14
|
import { getPaneDisplayTitle } from "./pane/title";
|
|
15
|
-
import { TITLEBAR_OVERLAY_HEIGHT_PX,
|
|
15
|
+
import { TITLEBAR_OVERLAY_HEIGHT_PX, getTitlebarLeadingInset } from "./titlebar-overlay";
|
|
16
|
+
import { WindowControls } from "./window-controls";
|
|
16
17
|
import {
|
|
17
18
|
createDoubleEscapeCloseState,
|
|
18
19
|
recordDoubleEscapeClose,
|
|
@@ -41,7 +42,9 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
41
42
|
typeof document === "undefined" ? true : document.hasFocus()
|
|
42
43
|
));
|
|
43
44
|
const { width, height } = useViewport();
|
|
44
|
-
const { cellHeightPx = 18, nativePaneChrome, titleBarOverlay } = useUiCapabilities();
|
|
45
|
+
const { cellHeightPx = 18, nativePaneChrome, titleBarOverlay, windowControls } = useUiCapabilities();
|
|
46
|
+
const showWindowControls = windowControls === "windows";
|
|
47
|
+
const titlebarLeadingInset = titleBarOverlay ? getTitlebarLeadingInset() : 0;
|
|
45
48
|
const instance = useAppSelector((state) => findPaneInstance(state.config.layout, desktopWindowBridge.paneId) ?? null);
|
|
46
49
|
const paneDef = instance ? pluginRegistry.panes.get(instance.paneId) ?? null : null;
|
|
47
50
|
const hasPaneSettings = !!instance && pluginRegistry.hasPaneSettings(instance.instanceId);
|
|
@@ -171,8 +174,8 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
171
174
|
alignItems="center"
|
|
172
175
|
flexGrow={1}
|
|
173
176
|
minWidth={0}
|
|
174
|
-
paddingLeft={titleBarOverlay ?
|
|
175
|
-
paddingRight={1}
|
|
177
|
+
paddingLeft={titleBarOverlay ? titlebarLeadingInset : 1}
|
|
178
|
+
paddingRight={showWindowControls ? 0 : 1}
|
|
176
179
|
>
|
|
177
180
|
<Box flexGrow={1} minWidth={0} overflow="hidden">
|
|
178
181
|
<Text fg={paneTitleText(focused, true)} selectable={false} data-gloom-role="pane-title">{title}</Text>
|
|
@@ -189,6 +192,7 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
189
192
|
{" ... "}
|
|
190
193
|
</Text>
|
|
191
194
|
)}
|
|
195
|
+
{showWindowControls ? <WindowControls /> : null}
|
|
192
196
|
</Box>
|
|
193
197
|
</Box>
|
|
194
198
|
<PaneBodyFrame layoutProps={bodyFrame.layoutProps} backgroundColor={background}>
|
|
@@ -17,7 +17,8 @@ import { formatPercentRaw } from "../../utils/format";
|
|
|
17
17
|
import { formatMarketPrice } from "../../market-data/market/format";
|
|
18
18
|
import { marketStateLabel, marketStateColor, getActiveQuoteDisplay } from "../../market-data/market/status";
|
|
19
19
|
import { VERSION } from "../../version";
|
|
20
|
-
import {
|
|
20
|
+
import { getTitlebarLeadingInset } from "./titlebar-overlay";
|
|
21
|
+
import { WindowControls } from "./window-controls";
|
|
21
22
|
|
|
22
23
|
const SPY_REFRESH_MS = 5 * 60_000; // 5 min
|
|
23
24
|
const UPDATE_NOTICE_DURATION_MS = 5_000;
|
|
@@ -125,7 +126,9 @@ export function Header() {
|
|
|
125
126
|
useThemeColors();
|
|
126
127
|
const baseCurrency = useAppSelector(selectBaseCurrency);
|
|
127
128
|
const appActive = useAppActive();
|
|
128
|
-
const { titleBarOverlay } = useUiCapabilities();
|
|
129
|
+
const { titleBarOverlay, windowControls } = useUiCapabilities();
|
|
130
|
+
const showWindowControls = windowControls === "windows";
|
|
131
|
+
const titlebarLeadingInset = titleBarOverlay ? getTitlebarLeadingInset() : 0;
|
|
129
132
|
const spyQuoteEntry = useQuoteEntry("SPY", null);
|
|
130
133
|
const spyQuote = useResolvedEntryValue(spyQuoteEntry);
|
|
131
134
|
|
|
@@ -165,10 +168,10 @@ export function Header() {
|
|
|
165
168
|
style={{
|
|
166
169
|
boxShadow: `0 -1px 0 ${colors.header}, inset 0 1px 0 ${colors.header}`,
|
|
167
170
|
paddingLeft: 8,
|
|
168
|
-
paddingRight: 12,
|
|
171
|
+
paddingRight: showWindowControls ? 0 : 12,
|
|
169
172
|
}}
|
|
170
173
|
>
|
|
171
|
-
<Box paddingLeft={
|
|
174
|
+
<Box paddingLeft={titlebarLeadingInset} flexDirection="row" alignItems="center" gap={1}>
|
|
172
175
|
<Text attributes={TextAttributes.BOLD} fg={colors.headerText}>
|
|
173
176
|
Gloomberb
|
|
174
177
|
</Text>
|
|
@@ -195,7 +198,10 @@ export function Header() {
|
|
|
195
198
|
<Box paddingRight={1}>
|
|
196
199
|
<Text fg={spyColor}>{spyText}</Text>
|
|
197
200
|
</Box>
|
|
198
|
-
<
|
|
201
|
+
<Box paddingRight={showWindowControls ? 1 : 0}>
|
|
202
|
+
<Text fg={colors.headerText}>{baseCurrency}</Text>
|
|
203
|
+
</Box>
|
|
204
|
+
{showWindowControls ? <WindowControls /> : null}
|
|
199
205
|
</Box>
|
|
200
206
|
);
|
|
201
207
|
}
|
|
@@ -209,7 +215,7 @@ export function Header() {
|
|
|
209
215
|
data-titlebar-overlay={titleBarOverlay ? "true" : undefined}
|
|
210
216
|
className={titleBarOverlay ? "electrobun-webkit-app-region-drag" : undefined}
|
|
211
217
|
>
|
|
212
|
-
<Box paddingLeft={titleBarOverlay ?
|
|
218
|
+
<Box paddingLeft={titleBarOverlay ? titlebarLeadingInset : 1}>
|
|
213
219
|
<Text attributes={TextAttributes.BOLD} fg={colors.headerText}>
|
|
214
220
|
Gloomberb v{VERSION}
|
|
215
221
|
</Text>
|
|
@@ -230,6 +236,7 @@ export function Header() {
|
|
|
230
236
|
{baseCurrency}
|
|
231
237
|
</Text>
|
|
232
238
|
</Box>
|
|
239
|
+
{showWindowControls ? <WindowControls /> : null}
|
|
233
240
|
</Box>
|
|
234
241
|
);
|
|
235
242
|
}
|
|
@@ -61,10 +61,11 @@ function createShellPluginRegistry(options?: {
|
|
|
61
61
|
} as unknown as PluginRegistry;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
async function emitKeypress(event: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; shift?: boolean }) {
|
|
64
|
+
async function emitKeypress(event: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; shift?: boolean; alt?: boolean }) {
|
|
65
65
|
await act(async () => {
|
|
66
66
|
const keyEvent = {
|
|
67
67
|
ctrl: false,
|
|
68
|
+
alt: false,
|
|
68
69
|
meta: false,
|
|
69
70
|
option: false,
|
|
70
71
|
shift: false,
|
|
@@ -312,6 +313,7 @@ describe("Shell", () => {
|
|
|
312
313
|
const frame = testSetup.captureCharFrame();
|
|
313
314
|
expect(frame).toContain("Settings");
|
|
314
315
|
expect(frame).toContain("Ctrl+,");
|
|
316
|
+
expect(frame).not.toContain("Layout Actions");
|
|
315
317
|
expect(frame).not.toContain("CmdOrCtrl");
|
|
316
318
|
});
|
|
317
319
|
|
|
@@ -400,24 +402,92 @@ describe("Shell", () => {
|
|
|
400
402
|
});
|
|
401
403
|
await testSetup.renderOnce();
|
|
402
404
|
|
|
403
|
-
|
|
405
|
+
const frame = testSetup.captureCharFrame();
|
|
406
|
+
expect(frame).toContain("Dock Pane");
|
|
407
|
+
expect(frame).not.toContain("Layout Actions");
|
|
404
408
|
});
|
|
405
409
|
|
|
406
410
|
test("resolves pane management shortcuts", () => {
|
|
407
411
|
const base = { ctrl: false, meta: true, super: true, shift: true, alt: false };
|
|
408
412
|
expect(resolvePaneManagementShortcut({ ...base, name: ",", key: ",", shift: false })).toBe("settings");
|
|
409
413
|
expect(resolvePaneManagementShortcut({ ...base, name: "w", key: "w", ctrl: true, meta: false, super: false, shift: false })).toBe("close");
|
|
414
|
+
expect(resolvePaneManagementShortcut({ ...base, name: "w", key: "w", shift: false, alt: true })).toBe("close-all-floating");
|
|
415
|
+
expect(resolvePaneManagementShortcut({ ...base, name: "W", key: "W", shift: false })).toBeNull();
|
|
410
416
|
expect(resolvePaneManagementShortcut({ ...base, name: "D", key: "D" })).toBe("toggle-floating");
|
|
411
417
|
expect(resolvePaneManagementShortcut({ ...base, name: "o", key: "o" })).toBe("pop-out");
|
|
412
418
|
expect(resolvePaneManagementShortcut({ ...base, name: "c", key: "c" })).toBe("copy-screenshot");
|
|
413
419
|
expect(resolvePaneManagementShortcut({ ...base, name: "l", key: "l" })).toBe("layout-actions");
|
|
414
420
|
expect(resolvePaneManagementShortcut({ ...base, name: "g", key: "g" })).toBe("gridlock-all");
|
|
415
421
|
expect(resolvePaneManagementShortcut({ ...base, name: "m", key: "m" })).toBe("window-mode");
|
|
422
|
+
expect(resolvePaneManagementShortcut({ ...base, name: "r", key: "r" })).toBe("window-resize-mode");
|
|
416
423
|
expect(resolvePaneManagementShortcut({ ...base, name: "n", key: "n" })).toBeNull();
|
|
417
424
|
expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", alt: true })).toBeNull();
|
|
418
425
|
expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", meta: false, super: false })).toBeNull();
|
|
419
426
|
});
|
|
420
427
|
|
|
428
|
+
test("exits window mode on Enter without changes", async () => {
|
|
429
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-window-mode-resize-shortcut-test");
|
|
430
|
+
const mainPane = requireLayoutInstance(config, "portfolio-list:main");
|
|
431
|
+
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
432
|
+
const dockedLayout = {
|
|
433
|
+
dockRoot: {
|
|
434
|
+
kind: "split" as const,
|
|
435
|
+
axis: "horizontal" as const,
|
|
436
|
+
ratio: 0.5,
|
|
437
|
+
first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
438
|
+
second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
|
|
439
|
+
},
|
|
440
|
+
instances: [{ ...mainPane }, { ...detailPane }],
|
|
441
|
+
floating: [],
|
|
442
|
+
detached: [],
|
|
443
|
+
};
|
|
444
|
+
const { actions } = await renderShellForWindowModeTest(
|
|
445
|
+
createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
await emitKeypress({ name: "m", ctrl: true, shift: true });
|
|
449
|
+
expect(testSetup.captureCharFrame()).toContain("WINDOW MOVE");
|
|
450
|
+
|
|
451
|
+
await emitKeypress({ name: "enter" });
|
|
452
|
+
await act(async () => {
|
|
453
|
+
await testSetup!.renderOnce();
|
|
454
|
+
});
|
|
455
|
+
expect(testSetup.captureCharFrame()).not.toContain("WINDOW MOVE");
|
|
456
|
+
expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("starts resize mode directly from the resize shortcut", async () => {
|
|
460
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-window-mode-resize-shortcut-test");
|
|
461
|
+
const mainPane = requireLayoutInstance(config, "portfolio-list:main");
|
|
462
|
+
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
463
|
+
const dockedLayout = {
|
|
464
|
+
dockRoot: {
|
|
465
|
+
kind: "split" as const,
|
|
466
|
+
axis: "horizontal" as const,
|
|
467
|
+
ratio: 0.5,
|
|
468
|
+
first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
469
|
+
second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
|
|
470
|
+
},
|
|
471
|
+
instances: [{ ...mainPane }, { ...detailPane }],
|
|
472
|
+
floating: [],
|
|
473
|
+
detached: [],
|
|
474
|
+
};
|
|
475
|
+
const { actions } = await renderShellForWindowModeTest(
|
|
476
|
+
createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
await emitKeypress({ name: "r", ctrl: true, shift: true });
|
|
480
|
+
expect(testSetup.captureCharFrame()).toContain("WINDOW RESIZE");
|
|
481
|
+
|
|
482
|
+
await emitKeypress({ name: "enter" });
|
|
483
|
+
await act(async () => {
|
|
484
|
+
await testSetup!.renderOnce();
|
|
485
|
+
});
|
|
486
|
+
const frame = testSetup.captureCharFrame();
|
|
487
|
+
expect(frame).not.toContain("WINDOW RESIZE");
|
|
488
|
+
expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
|
|
489
|
+
});
|
|
490
|
+
|
|
421
491
|
test("moves a floating pane in window mode and commits once", async () => {
|
|
422
492
|
const config = createDefaultConfig("/tmp/gloomberb-shell-window-mode-test");
|
|
423
493
|
const floatingLayout = cloneLayout(config.layout);
|
|
@@ -785,4 +855,52 @@ describe("Shell", () => {
|
|
|
785
855
|
expect(updateLayout?.layout.floating).toEqual([]);
|
|
786
856
|
});
|
|
787
857
|
|
|
858
|
+
test("closes all floating panes with Ctrl+Alt+W", async () => {
|
|
859
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-test");
|
|
860
|
+
const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
|
|
861
|
+
const firstDetailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
|
|
862
|
+
if (!mainPane || !firstDetailPane) throw new Error("missing default panes");
|
|
863
|
+
const secondDetailPane = {
|
|
864
|
+
...firstDetailPane,
|
|
865
|
+
instanceId: "ticker-detail:secondary",
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
const mixedLayout = {
|
|
869
|
+
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
870
|
+
instances: [{ ...mainPane }, { ...firstDetailPane }, secondDetailPane],
|
|
871
|
+
floating: [
|
|
872
|
+
{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 },
|
|
873
|
+
{ instanceId: "ticker-detail:secondary", x: 8, y: 3, width: 30, height: 8 },
|
|
874
|
+
],
|
|
875
|
+
detached: [],
|
|
876
|
+
};
|
|
877
|
+
const state = {
|
|
878
|
+
...createInitialState({
|
|
879
|
+
...config,
|
|
880
|
+
layout: cloneLayout(mixedLayout),
|
|
881
|
+
layouts: [{ name: "Default", layout: cloneLayout(mixedLayout) }],
|
|
882
|
+
}),
|
|
883
|
+
focusedPaneId: "portfolio-list:main",
|
|
884
|
+
};
|
|
885
|
+
const actions: Array<any> = [];
|
|
886
|
+
|
|
887
|
+
testSetup = await testRender(
|
|
888
|
+
<AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
|
|
889
|
+
<TestDialogProvider>
|
|
890
|
+
<Shell pluginRegistry={createShellPluginRegistry()} />
|
|
891
|
+
</TestDialogProvider>
|
|
892
|
+
</AppContext>,
|
|
893
|
+
{ width: 40, height: 12 },
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
await testSetup.renderOnce();
|
|
897
|
+
await emitKeypress({ name: "w", ctrl: true, alt: true });
|
|
898
|
+
|
|
899
|
+
const updateLayout = actions.find((action) => action.type === "UPDATE_LAYOUT");
|
|
900
|
+
expect(actions).toContainEqual({ type: "PUSH_LAYOUT_HISTORY" });
|
|
901
|
+
expect(updateLayout?.layout.instances.map((instance: { instanceId: string }) => instance.instanceId)).toEqual(["portfolio-list:main"]);
|
|
902
|
+
expect(updateLayout?.layout.floating).toEqual([]);
|
|
903
|
+
expect(updateLayout?.layout.dockRoot).toEqual({ kind: "pane", instanceId: "portfolio-list:main" });
|
|
904
|
+
});
|
|
905
|
+
|
|
788
906
|
});
|
|
@@ -186,6 +186,7 @@ export function Shell({
|
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
const {
|
|
189
|
+
closeAllFloatingPanes,
|
|
189
190
|
closeFocusedPane,
|
|
190
191
|
copyFocusedPaneScreenshot,
|
|
191
192
|
copyPaneScreenshot,
|
|
@@ -213,6 +214,7 @@ export function Shell({
|
|
|
213
214
|
|
|
214
215
|
useShellPaneManagementShortcuts({
|
|
215
216
|
cancelActiveDrag,
|
|
217
|
+
closeAllFloatingPanes,
|
|
216
218
|
closeFocusedPane,
|
|
217
219
|
copyFocusedPaneScreenshot,
|
|
218
220
|
focusedPaneId,
|
|
@@ -101,15 +101,9 @@ export function menuForPane(
|
|
|
101
101
|
{
|
|
102
102
|
id: "window-resize-mode",
|
|
103
103
|
label: "Resize Window...",
|
|
104
|
-
accelerator:
|
|
104
|
+
accelerator: PANE_MANAGEMENT_ACCELERATORS.windowResizeMode,
|
|
105
105
|
onSelect: () => pluginRegistry.openWindowMode(pane.instance.instanceId, "resize"),
|
|
106
106
|
},
|
|
107
|
-
{
|
|
108
|
-
id: "layout-actions",
|
|
109
|
-
label: "Layout Actions...",
|
|
110
|
-
accelerator: PANE_MANAGEMENT_ACCELERATORS.layoutActions,
|
|
111
|
-
onSelect: () => pluginRegistry.openCommandBar("LAY "),
|
|
112
|
-
},
|
|
113
107
|
);
|
|
114
108
|
|
|
115
109
|
return baseActions;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
floatPane,
|
|
6
6
|
gridlockAllPanes,
|
|
7
7
|
isPaneInLayout,
|
|
8
|
+
removeFloatingPanes,
|
|
8
9
|
removePane,
|
|
9
10
|
type ResolvedPane,
|
|
10
11
|
} from "../../../../plugins/pane-manager";
|
|
@@ -73,6 +74,12 @@ export function useShellPaneActions({
|
|
|
73
74
|
return true;
|
|
74
75
|
}, [focusedPaneId, persistLayout, visibleLayout]);
|
|
75
76
|
|
|
77
|
+
const closeAllFloatingPanes = useCallback(() => {
|
|
78
|
+
if (visibleLayout.floating.length === 0) return false;
|
|
79
|
+
persistLayout(removeFloatingPanes(visibleLayout));
|
|
80
|
+
return true;
|
|
81
|
+
}, [persistLayout, visibleLayout]);
|
|
82
|
+
|
|
76
83
|
const copyFocusedPaneScreenshot = useCallback(() => {
|
|
77
84
|
if (!focusedPaneId || !nativePaneChrome || !rendererHost.copyPngImage) return false;
|
|
78
85
|
if (!paneMap.has(focusedPaneId)) return false;
|
|
@@ -115,6 +122,7 @@ export function useShellPaneActions({
|
|
|
115
122
|
}, [persistLayout, visibleLayout]);
|
|
116
123
|
|
|
117
124
|
return {
|
|
125
|
+
closeAllFloatingPanes,
|
|
118
126
|
closeFocusedPane,
|
|
119
127
|
copyFocusedPaneScreenshot,
|
|
120
128
|
copyPaneScreenshot,
|