gloomberb 0.9.4 → 0.9.6
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 +49 -8
- package/package.json +4 -3
- package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
- package/patches/react-reconciler@0.33.0.patch +10 -0
- package/src/app/global-shortcuts.test.tsx +20 -1
- package/src/app/global-shortcuts.ts +2 -0
- package/src/app/pane-runtime/index.tsx +11 -4
- package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
- package/src/app.tsx +41 -17
- package/src/cli/commands/remote.ts +206 -0
- package/src/cli/entry.ts +8 -1
- package/src/cli/index.ts +2 -0
- package/src/cli/native-smoke.ts +5 -0
- package/src/components/chart/core/keyboard.ts +10 -1
- package/src/components/chart/native/bitmap-support.test.ts +27 -0
- package/src/components/chart/native/bitmap-support.ts +55 -0
- package/src/components/chart/native/kitty/index.test.ts +47 -0
- package/src/components/chart/native/surface/visibility.ts +8 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
- package/src/components/chart/static/chart/bitmap.ts +16 -9
- package/src/components/chart/static/chart/surface.test.tsx +59 -4
- package/src/components/chart/static/chart/surface.tsx +4 -0
- package/src/components/chart/stock-chart/index.test.ts +7 -0
- package/src/components/command-bar/list/view.tsx +48 -1
- package/src/components/command-bar/multi-select-picker.tsx +7 -0
- package/src/components/command-bar/routing/navigation-state.ts +10 -1
- package/src/components/command-bar/surface/index.test.tsx +14 -0
- package/src/components/command-bar/theme-picker.tsx +91 -79
- package/src/components/command-bar/workflow/field-row.tsx +30 -5
- package/src/components/data-table/view.tsx +0 -12
- package/src/components/index.ts +7 -0
- package/src/components/layout/floating-pane.tsx +3 -3
- package/src/components/layout/pane/footer/index.tsx +23 -0
- package/src/components/layout/pane/header.tsx +5 -0
- package/src/components/layout/pane/index.tsx +3 -3
- package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
- package/src/components/layout/shell/cursor-occlusion.ts +9 -1
- package/src/components/layout/shell/index.test.tsx +2 -0
- package/src/components/layout/shell/index.tsx +16 -4
- package/src/components/layout/shell/pane/actions.ts +21 -7
- package/src/components/layout/shell/pane/layers.tsx +2 -2
- package/src/components/layout/status-bar.tsx +5 -5
- package/src/components/metric-treemap/index.tsx +317 -0
- package/src/components/metric-treemap/layout.test.ts +77 -0
- package/src/components/metric-treemap/layout.ts +476 -0
- package/src/components/pane-settings-dialog/desktop.tsx +1 -1
- package/src/components/price-performance.tsx +1 -1
- package/src/components/price-sparkline/view.tsx +15 -14
- package/src/components/speedometer-gauge/terminal.tsx +18 -15
- package/src/components/table-view-shared.tsx +0 -10
- package/src/components/ticker/list-table-view.tsx +1 -7
- package/src/components/ticker/list-table.test.tsx +0 -4
- package/src/components/toggle-list.tsx +10 -0
- package/src/components/ui/button.tsx +12 -0
- package/src/components/ui/data-table/index.tsx +51 -0
- package/src/components/ui/data-table/opentui/index.tsx +4 -10
- package/src/components/ui/data-table/types.ts +6 -4
- package/src/components/ui/fields.tsx +18 -0
- package/src/components/ui/list-view.tsx +84 -4
- package/src/components/ui/message-composer.tsx +22 -0
- package/src/components/ui/tabs.tsx +46 -2
- package/src/components/ui/ui.test.tsx +0 -9
- package/src/core/state/app/layout-reducer.ts +7 -1
- package/src/core/state/app/layout.ts +6 -0
- package/src/core/state/app/state.ts +23 -1
- package/src/core/state/app/types.ts +4 -2
- package/src/plugins/builtin/account-management/form-components.tsx +2 -2
- package/src/plugins/builtin/alerts/pane.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
- package/src/plugins/builtin/chat/channels.ts +5 -18
- package/src/plugins/builtin/chat/chat.test.tsx +1 -7
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
- package/src/plugins/builtin/chat/sidebar.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +2 -8
- package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
- package/src/plugins/builtin/cloud/plugin.tsx +4 -13
- package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
- package/src/plugins/builtin/debug/index.tsx +3 -4
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/holders/treemap.tsx +28 -297
- package/src/plugins/builtin/holders/types.ts +0 -34
- package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
- package/src/plugins/builtin/market-heatmap/data.ts +296 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
- package/src/plugins/builtin/options/table.ts +5 -5
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
- package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
- package/src/plugins/ibkr/gateway/service/index.ts +6 -9
- package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
- package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
- package/src/remote/app-host.tsx +77 -0
- package/src/remote/client.ts +34 -0
- package/src/remote/command-bar.ts +98 -0
- package/src/remote/controller-utils.ts +111 -0
- package/src/remote/controller.test.ts +365 -0
- package/src/remote/controller.ts +434 -0
- package/src/remote/endpoint.ts +41 -0
- package/src/remote/json-patch.test.ts +31 -0
- package/src/remote/json-patch.ts +89 -0
- package/src/remote/layout-helpers.ts +62 -0
- package/src/remote/resources.ts +186 -0
- package/src/remote/revision.ts +18 -0
- package/src/remote/schema.ts +143 -0
- package/src/remote/semantic-helpers.ts +161 -0
- package/src/remote/semantic-tree.tsx +112 -0
- package/src/remote/server.test.ts +50 -0
- package/src/remote/server.ts +121 -0
- package/src/remote/types.ts +119 -0
- package/src/renderers/electrobun/bun/index.ts +55 -1
- package/src/renderers/electrobun/shared/protocol.ts +11 -1
- package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
- package/src/renderers/electrobun/view/build-assets.ts +1 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -8
- package/src/renderers/electrobun/view/data-table/row.tsx +2 -12
- package/src/renderers/electrobun/view/host/box.tsx +11 -1
- package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
- package/src/renderers/electrobun/view/host/mouse.ts +2 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -0
- package/src/renderers/electrobun/view/main.tsx +5 -1
- package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
- package/src/renderers/electrobun/view/styles.css +7 -0
- package/src/renderers/opentui/chart-surface.test.tsx +101 -0
- package/src/renderers/opentui/chart-surface.tsx +15 -20
- package/src/renderers/opentui/native-smoke.ts +1 -0
- package/src/renderers/opentui/start.tsx +32 -0
- package/src/sources/yahoo-finance/http.ts +31 -4
- package/src/state/app/context/index.test.tsx +47 -0
- package/src/state/app/context/index.tsx +48 -18
- package/src/ui/host.tsx +1 -0
- package/src/ui/index.tsx +183 -7
- package/src/version.ts +1 -1
- package/src/plugins/builtin/holders/treemap-layout.ts +0 -231
package/src/app.tsx
CHANGED
|
@@ -39,6 +39,8 @@ import { bindPluginRegistryRuntimeAccess } from "./app/runtime/plugin-bindings";
|
|
|
39
39
|
import { useAppStartupRuntime } from "./app/runtime/startup";
|
|
40
40
|
import { useTickerRefreshRuntime } from "./app/runtime/ticker-refresh";
|
|
41
41
|
import { useAppUpdateRuntime } from "./app/runtime/update";
|
|
42
|
+
import { RemoteControlHost, type RemoteControlAdapter } from "./remote/app-host";
|
|
43
|
+
import { RemoteUiRegistryProvider } from "./remote/semantic-tree";
|
|
42
44
|
import {
|
|
43
45
|
resolveAppSessionSnapshot,
|
|
44
46
|
resolveCliLaunchConfig,
|
|
@@ -55,6 +57,7 @@ interface AppInnerProps {
|
|
|
55
57
|
sessionSnapshot?: AppSessionSnapshot | null;
|
|
56
58
|
desktopWindowBridge?: DesktopWindowBridge;
|
|
57
59
|
desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
|
|
60
|
+
remoteControlAdapter?: RemoteControlAdapter;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
function ThemedAppRoot({ children }: { children: ReactNode }) {
|
|
@@ -83,9 +86,11 @@ function AppInner({
|
|
|
83
86
|
sessionSnapshot = null,
|
|
84
87
|
desktopWindowBridge,
|
|
85
88
|
desktopApplicationMenuBridge,
|
|
89
|
+
remoteControlAdapter,
|
|
86
90
|
}: AppInnerProps) {
|
|
87
91
|
const dispatch = useAppDispatch();
|
|
88
92
|
const stateRef = useAppStateRef();
|
|
93
|
+
const getRemoteState = useCallback(() => stateRef.current, [stateRef]);
|
|
89
94
|
const config = useAppSelector((state) => state.config);
|
|
90
95
|
const tickers = useAppSelector((state) => state.tickers);
|
|
91
96
|
const paneState = useAppSelector((state) => state.paneState);
|
|
@@ -288,6 +293,13 @@ function AppInner({
|
|
|
288
293
|
if (desktopWindowBridge?.kind === "detached" && desktopWindowBridge.paneId) {
|
|
289
294
|
return (
|
|
290
295
|
<ContextMenuProvider pluginRegistry={pluginRegistry}>
|
|
296
|
+
<RemoteControlHost
|
|
297
|
+
adapter={remoteControlAdapter}
|
|
298
|
+
dispatch={dispatch}
|
|
299
|
+
getState={getRemoteState}
|
|
300
|
+
pluginRegistry={pluginRegistry}
|
|
301
|
+
desktopWindowBridge={desktopWindowBridge}
|
|
302
|
+
/>
|
|
291
303
|
<ThemedAppRoot>
|
|
292
304
|
<DetachedPaneShell
|
|
293
305
|
pluginRegistry={pluginRegistry}
|
|
@@ -301,6 +313,13 @@ function AppInner({
|
|
|
301
313
|
|
|
302
314
|
return (
|
|
303
315
|
<ContextMenuProvider pluginRegistry={pluginRegistry}>
|
|
316
|
+
<RemoteControlHost
|
|
317
|
+
adapter={remoteControlAdapter}
|
|
318
|
+
dispatch={dispatch}
|
|
319
|
+
getState={getRemoteState}
|
|
320
|
+
pluginRegistry={pluginRegistry}
|
|
321
|
+
desktopWindowBridge={desktopWindowBridge}
|
|
322
|
+
/>
|
|
304
323
|
<ThemedAppRoot>
|
|
305
324
|
<Header />
|
|
306
325
|
<TransientLayoutProvider>
|
|
@@ -336,6 +355,7 @@ interface AppProps {
|
|
|
336
355
|
desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
|
|
337
356
|
desktopSnapshot?: DesktopSharedStateSnapshot | null;
|
|
338
357
|
desktopThemePreview?: DesktopThemePreviewState | null;
|
|
358
|
+
remoteControlAdapter?: RemoteControlAdapter;
|
|
339
359
|
}
|
|
340
360
|
|
|
341
361
|
export function App({
|
|
@@ -346,6 +366,7 @@ export function App({
|
|
|
346
366
|
desktopApplicationMenuBridge,
|
|
347
367
|
desktopSnapshot = null,
|
|
348
368
|
desktopThemePreview = null,
|
|
369
|
+
remoteControlAdapter,
|
|
349
370
|
}: AppProps) {
|
|
350
371
|
const renderer = useNativeRenderer();
|
|
351
372
|
const effectiveInitialConfig = useMemo(() => {
|
|
@@ -413,23 +434,26 @@ export function App({
|
|
|
413
434
|
}
|
|
414
435
|
|
|
415
436
|
return (
|
|
416
|
-
<
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
desktopBridge={desktopWindowBridge}
|
|
421
|
-
desktopSnapshot={desktopSnapshot}
|
|
422
|
-
initialThemePreview={desktopThemePreview}
|
|
423
|
-
>
|
|
424
|
-
<AppInner
|
|
425
|
-
pluginRegistry={services.pluginRegistry}
|
|
426
|
-
tickerRepository={services.tickerRepository}
|
|
427
|
-
dataProvider={services.dataProvider}
|
|
428
|
-
marketData={services.marketData}
|
|
437
|
+
<RemoteUiRegistryProvider>
|
|
438
|
+
<AppProvider
|
|
439
|
+
config={config}
|
|
440
|
+
sessionStore={desktopWindowBridge?.kind === "detached" ? undefined : services.persistence.sessions}
|
|
429
441
|
sessionSnapshot={sessionSnapshot}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
442
|
+
desktopBridge={desktopWindowBridge}
|
|
443
|
+
desktopSnapshot={desktopSnapshot}
|
|
444
|
+
initialThemePreview={desktopThemePreview}
|
|
445
|
+
>
|
|
446
|
+
<AppInner
|
|
447
|
+
pluginRegistry={services.pluginRegistry}
|
|
448
|
+
tickerRepository={services.tickerRepository}
|
|
449
|
+
dataProvider={services.dataProvider}
|
|
450
|
+
marketData={services.marketData}
|
|
451
|
+
sessionSnapshot={sessionSnapshot}
|
|
452
|
+
desktopWindowBridge={desktopWindowBridge}
|
|
453
|
+
desktopApplicationMenuBridge={desktopApplicationMenuBridge}
|
|
454
|
+
remoteControlAdapter={remoteControlAdapter}
|
|
455
|
+
/>
|
|
456
|
+
</AppProvider>
|
|
457
|
+
</RemoteUiRegistryProvider>
|
|
434
458
|
);
|
|
435
459
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { getDataDir } from "../../data/config/store";
|
|
2
|
+
import type { CliCommandContext, CliCommandDef } from "../../types/plugin";
|
|
3
|
+
import { sendRemoteControlRequest } from "../../remote/client";
|
|
4
|
+
import type { RemoteAppKind, RemoteControlRequest, RemoteControlResponse } from "../../remote/types";
|
|
5
|
+
|
|
6
|
+
interface RemoteArgs {
|
|
7
|
+
args: string[];
|
|
8
|
+
appKind?: RemoteAppKind;
|
|
9
|
+
expectRev?: string;
|
|
10
|
+
intervalMs: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const remoteCliCommand: CliCommandDef = {
|
|
14
|
+
name: "remote",
|
|
15
|
+
description: "Control a running app through the semantic remote API",
|
|
16
|
+
help: {
|
|
17
|
+
usage: [
|
|
18
|
+
"remote schema [--app tui|desktop]",
|
|
19
|
+
"remote help [--app tui|desktop]",
|
|
20
|
+
"remote get <resource> [--app tui|desktop]",
|
|
21
|
+
"remote call <operation> [json] [--dry-run] [--app tui|desktop]",
|
|
22
|
+
"remote patch <resource> <json-patch> [--expect-rev rev] [--dry-run] [--app tui|desktop]",
|
|
23
|
+
"remote batch <json> [--dry-run] [--app tui|desktop]",
|
|
24
|
+
"remote watch <resource> [--interval ms] [--limit n] [--app tui|desktop]",
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
execute: async (rawArgs, ctx) => {
|
|
28
|
+
const parsed = parseRemoteArgs(rawArgs);
|
|
29
|
+
const action = parsed.args[0];
|
|
30
|
+
if (!action) {
|
|
31
|
+
ctx.fail("Usage: gloomberb remote <schema|help|get|call|patch|batch|watch>");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dataDir = await getDataDir();
|
|
36
|
+
if (!dataDir) {
|
|
37
|
+
ctx.fail("No data directory configured.", "Run gloomberb once before using remote control.");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (action === "watch") {
|
|
42
|
+
const resource = parsed.args[1];
|
|
43
|
+
if (!resource) {
|
|
44
|
+
ctx.fail("Usage: gloomberb remote watch <resource>");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const limit = ctx.cliOptions.limit ?? Number.POSITIVE_INFINITY;
|
|
48
|
+
for (let index = 0; index < limit; index += 1) {
|
|
49
|
+
const response = await sendRemoteControlRequest({ type: "get", resource }, {
|
|
50
|
+
dataDir,
|
|
51
|
+
appKind: parsed.appKind,
|
|
52
|
+
});
|
|
53
|
+
printRemoteResponse(response, ctx);
|
|
54
|
+
if (index < limit - 1) await new Promise((resolve) => setTimeout(resolve, parsed.intervalMs));
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const request = buildRemoteRequest(action, parsed, ctx.cliOptions.dryRun);
|
|
60
|
+
const response = await sendRemoteControlRequest(request, {
|
|
61
|
+
dataDir,
|
|
62
|
+
appKind: parsed.appKind,
|
|
63
|
+
});
|
|
64
|
+
printRemoteResponse(response, ctx);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function parseRemoteArgs(rawArgs: string[]): RemoteArgs {
|
|
69
|
+
const args: string[] = [];
|
|
70
|
+
let appKind: RemoteAppKind | undefined;
|
|
71
|
+
let expectRev: string | undefined;
|
|
72
|
+
let intervalMs = 1_000;
|
|
73
|
+
|
|
74
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
75
|
+
const arg = rawArgs[index]!;
|
|
76
|
+
if (arg === "--app") {
|
|
77
|
+
index += 1;
|
|
78
|
+
appKind = parseAppKind(rawArgs[index]);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg.startsWith("--app=")) {
|
|
82
|
+
appKind = parseAppKind(arg.slice("--app=".length));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (arg === "--expect-rev") {
|
|
86
|
+
index += 1;
|
|
87
|
+
expectRev = rawArgs[index];
|
|
88
|
+
if (!expectRev) throw new Error("Missing value for --expect-rev.");
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg.startsWith("--expect-rev=")) {
|
|
92
|
+
expectRev = arg.slice("--expect-rev=".length);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (arg === "--interval") {
|
|
96
|
+
index += 1;
|
|
97
|
+
intervalMs = parseInterval(rawArgs[index]);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg.startsWith("--interval=")) {
|
|
101
|
+
intervalMs = parseInterval(arg.slice("--interval=".length));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
args.push(arg);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { args, appKind, expectRev, intervalMs };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildRemoteRequest(action: string, parsed: RemoteArgs, dryRun: boolean): RemoteControlRequest {
|
|
111
|
+
switch (action) {
|
|
112
|
+
case "schema":
|
|
113
|
+
return { type: "schema" };
|
|
114
|
+
case "help":
|
|
115
|
+
return { type: "help" };
|
|
116
|
+
case "get": {
|
|
117
|
+
const resource = parsed.args[1];
|
|
118
|
+
if (!resource) throw new Error("Usage: gloomberb remote get <resource>");
|
|
119
|
+
return { type: "get", resource };
|
|
120
|
+
}
|
|
121
|
+
case "call": {
|
|
122
|
+
const operation = parsed.args[1];
|
|
123
|
+
if (!operation) throw new Error("Usage: gloomberb remote call <operation> [json]");
|
|
124
|
+
return {
|
|
125
|
+
type: "call",
|
|
126
|
+
operation,
|
|
127
|
+
input: parseJsonValue(parsed.args[2], {}),
|
|
128
|
+
dryRun,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
case "patch": {
|
|
132
|
+
const resource = parsed.args[1];
|
|
133
|
+
const patchRaw = parsed.args[2];
|
|
134
|
+
if (!resource || !patchRaw) throw new Error("Usage: gloomberb remote patch <resource> <json-patch>");
|
|
135
|
+
const patch = parseJsonValue(patchRaw, []);
|
|
136
|
+
if (!Array.isArray(patch)) throw new Error("Remote patch payload must be a JSON array.");
|
|
137
|
+
return {
|
|
138
|
+
type: "patch",
|
|
139
|
+
resource,
|
|
140
|
+
patch,
|
|
141
|
+
expectRev: parsed.expectRev,
|
|
142
|
+
dryRun,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
case "batch": {
|
|
146
|
+
const payload = parseJsonValue(parsed.args[1], null);
|
|
147
|
+
if (!payload) throw new Error("Usage: gloomberb remote batch <json>");
|
|
148
|
+
if (Array.isArray(payload)) return { type: "batch", requests: payload as RemoteControlRequest[], dryRun };
|
|
149
|
+
return withCliDryRun(payload as RemoteControlRequest, dryRun);
|
|
150
|
+
}
|
|
151
|
+
default:
|
|
152
|
+
throw new Error(`Unknown remote action "${action}".`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function withCliDryRun(request: RemoteControlRequest, dryRun: boolean): RemoteControlRequest {
|
|
157
|
+
if (!dryRun) return request;
|
|
158
|
+
if (request.type === "batch") return { ...request, dryRun: true };
|
|
159
|
+
if (request.type === "call" || request.type === "patch") return { ...request, dryRun: true };
|
|
160
|
+
return request;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseAppKind(value: string | undefined): RemoteAppKind {
|
|
164
|
+
if (value === "tui" || value === "desktop") return value;
|
|
165
|
+
throw new Error("--app must be either tui or desktop.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseInterval(value: string | undefined): number {
|
|
169
|
+
const parsed = Number(value);
|
|
170
|
+
if (!Number.isInteger(parsed) || parsed < 100) {
|
|
171
|
+
throw new Error("--interval must be an integer >= 100.");
|
|
172
|
+
}
|
|
173
|
+
return parsed;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseJsonValue<T>(raw: string | undefined, fallback: T): unknown {
|
|
177
|
+
if (raw == null) return fallback;
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(raw);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw new Error(`Invalid JSON payload: ${error instanceof Error ? error.message : String(error)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function printRemoteResponse(
|
|
186
|
+
response: RemoteControlResponse,
|
|
187
|
+
ctx: CliCommandContext,
|
|
188
|
+
): void {
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
const details = response.error.details == null
|
|
191
|
+
? undefined
|
|
192
|
+
: typeof response.error.details === "string"
|
|
193
|
+
? response.error.details
|
|
194
|
+
: JSON.stringify(response.error.details, null, 2);
|
|
195
|
+
ctx.fail(response.error.message, details);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const data = response.state ? { data: response.data, state: response.state } : response.data;
|
|
199
|
+
ctx.printResult({
|
|
200
|
+
data,
|
|
201
|
+
metadata: response.rev ? { rev: response.rev } : undefined,
|
|
202
|
+
warnings: response.warnings,
|
|
203
|
+
}, {
|
|
204
|
+
text: (data) => JSON.stringify(data, null, 2),
|
|
205
|
+
});
|
|
206
|
+
}
|
package/src/cli/entry.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { dispatchCli } from "./index";
|
|
|
2
2
|
import { fail, inferCliErrorOptions, printCliError } from "./errors";
|
|
3
3
|
import { loadExternalPlugins } from "../plugins/loader";
|
|
4
4
|
import type { CliLaunchRequest } from "../types/plugin";
|
|
5
|
+
import { OPEN_TUI_NATIVE_SMOKE_COMMAND, smokeOpenTuiNative } from "./native-smoke";
|
|
5
6
|
|
|
6
7
|
async function launchOpenTuiApp(options: {
|
|
7
8
|
externalPlugins: Awaited<ReturnType<typeof loadExternalPlugins>>;
|
|
@@ -18,9 +19,15 @@ async function launchOpenTuiApp(options: {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export async function runCliEntrypoint(rawArgs = process.argv.slice(2)): Promise<void> {
|
|
21
|
-
const externalPlugins = await loadExternalPlugins();
|
|
22
22
|
const command = rawArgs[0];
|
|
23
23
|
|
|
24
|
+
if (command === OPEN_TUI_NATIVE_SMOKE_COMMAND) {
|
|
25
|
+
await smokeOpenTuiNative();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const externalPlugins = await loadExternalPlugins();
|
|
30
|
+
|
|
24
31
|
if (!command) {
|
|
25
32
|
await launchOpenTuiApp({ externalPlugins });
|
|
26
33
|
return;
|
package/src/cli/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { ticker, buildTickerReport } from "./commands/ticker";
|
|
|
18
18
|
import { apiCliCommand } from "./commands/api";
|
|
19
19
|
import { marketDataCliCommands } from "./commands/market";
|
|
20
20
|
import { overviewCliCommands } from "./commands/overview";
|
|
21
|
+
import { remoteCliCommand } from "./commands/remote";
|
|
21
22
|
import { createSystemCliCommands } from "./commands/system";
|
|
22
23
|
import {
|
|
23
24
|
aiCliCommand,
|
|
@@ -188,6 +189,7 @@ function createCoreCliCommands(renderHelp: () => string): CliCommandDef[] {
|
|
|
188
189
|
apiCliCommand,
|
|
189
190
|
...marketDataCliCommands,
|
|
190
191
|
...overviewCliCommands,
|
|
192
|
+
remoteCliCommand,
|
|
191
193
|
...createSystemCliCommands(() => commands),
|
|
192
194
|
brokerCliCommand,
|
|
193
195
|
ibkrCliCommand,
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
export function resolveChartKeyboardKey(event: {
|
|
1
|
+
export function resolveChartKeyboardKey(event: {
|
|
2
|
+
name?: string;
|
|
3
|
+
sequence?: string;
|
|
4
|
+
ctrl?: boolean;
|
|
5
|
+
meta?: boolean;
|
|
6
|
+
alt?: boolean;
|
|
7
|
+
super?: boolean;
|
|
8
|
+
}): string {
|
|
9
|
+
if (event.ctrl || event.meta || event.alt || event.super) return "";
|
|
10
|
+
|
|
2
11
|
const name = event.name ?? "";
|
|
3
12
|
const sequence = event.sequence ?? "";
|
|
4
13
|
const candidates = [name, sequence];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { NativeRendererHost } from "../../../ui";
|
|
3
|
+
import { shouldRenderNativeBitmap } from "./bitmap-support";
|
|
4
|
+
|
|
5
|
+
function rendererWithCapabilities(capabilities: unknown): NativeRendererHost {
|
|
6
|
+
return {
|
|
7
|
+
terminalWidth: 80,
|
|
8
|
+
terminalHeight: 24,
|
|
9
|
+
resolution: null,
|
|
10
|
+
capabilities,
|
|
11
|
+
on() {},
|
|
12
|
+
off() {},
|
|
13
|
+
requestRender() {},
|
|
14
|
+
registerLifecyclePass() {},
|
|
15
|
+
unregisterLifecyclePass() {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("shouldRenderNativeBitmap", () => {
|
|
20
|
+
test("skips native bitmaps only when kitty graphics are known unsupported", () => {
|
|
21
|
+
expect(shouldRenderNativeBitmap(rendererWithCapabilities({ kitty_graphics: false }), true)).toBe(false);
|
|
22
|
+
expect(shouldRenderNativeBitmap(rendererWithCapabilities({ kitty_graphics: true }), true)).toBe(true);
|
|
23
|
+
expect(shouldRenderNativeBitmap(rendererWithCapabilities(null), true)).toBe(true);
|
|
24
|
+
expect(shouldRenderNativeBitmap(rendererWithCapabilities({}), true)).toBe(true);
|
|
25
|
+
expect(shouldRenderNativeBitmap(rendererWithCapabilities({ kitty_graphics: true }), false)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { NativeRendererHost, PixelResolution } from "../../../ui";
|
|
2
|
+
import { computeBitmapSize } from "./chart-rasterizer";
|
|
3
|
+
|
|
4
|
+
interface NativeBitmapRendererCapabilities {
|
|
5
|
+
kitty_graphics?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface NativeBitmapSize {
|
|
9
|
+
pixelWidth: number;
|
|
10
|
+
pixelHeight: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasKnownUnsupportedKittyGraphics(renderer: NativeRendererHost): boolean {
|
|
14
|
+
const capabilities = renderer.capabilities as NativeBitmapRendererCapabilities | null | undefined;
|
|
15
|
+
return capabilities?.kitty_graphics === false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shouldRenderNativeBitmap(renderer: NativeRendererHost, nativeCharts: boolean): boolean {
|
|
19
|
+
return nativeCharts && !hasKnownUnsupportedKittyGraphics(renderer);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveNativeBitmapSize({
|
|
23
|
+
width,
|
|
24
|
+
height,
|
|
25
|
+
resolution,
|
|
26
|
+
terminalWidth,
|
|
27
|
+
terminalHeight,
|
|
28
|
+
cellWidthPx,
|
|
29
|
+
cellHeightPx,
|
|
30
|
+
pixelRatio,
|
|
31
|
+
}: {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
resolution: PixelResolution | null;
|
|
35
|
+
terminalWidth: number;
|
|
36
|
+
terminalHeight: number;
|
|
37
|
+
cellWidthPx: number;
|
|
38
|
+
cellHeightPx: number;
|
|
39
|
+
pixelRatio: number;
|
|
40
|
+
}): NativeBitmapSize {
|
|
41
|
+
if (resolution && terminalWidth > 0 && terminalHeight > 0) {
|
|
42
|
+
return computeBitmapSize(
|
|
43
|
+
{ x: 0, y: 0, width, height },
|
|
44
|
+
resolution,
|
|
45
|
+
terminalWidth,
|
|
46
|
+
terminalHeight,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const scale = Math.max(1, pixelRatio);
|
|
51
|
+
return {
|
|
52
|
+
pixelWidth: Math.max(1, Math.round(width * cellWidthPx * scale)),
|
|
53
|
+
pixelHeight: Math.max(1, Math.round(height * cellHeightPx * scale)),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -143,6 +143,53 @@ describe("resolveNativeSurfaceVisibleRect", () => {
|
|
|
143
143
|
|
|
144
144
|
expect(resolveNativeSurfaceVisibleRect(child, 100, 30)).toBeNull();
|
|
145
145
|
});
|
|
146
|
+
|
|
147
|
+
test("stops at repeated ancestors in cyclic native renderable trees", () => {
|
|
148
|
+
const root: NativeSurfaceRenderableNode = {
|
|
149
|
+
x: 0,
|
|
150
|
+
y: 0,
|
|
151
|
+
width: 100,
|
|
152
|
+
height: 30,
|
|
153
|
+
parent: null,
|
|
154
|
+
};
|
|
155
|
+
const child: NativeSurfaceRenderableNode = {
|
|
156
|
+
x: 2,
|
|
157
|
+
y: 3,
|
|
158
|
+
width: 20,
|
|
159
|
+
height: 8,
|
|
160
|
+
parent: root,
|
|
161
|
+
};
|
|
162
|
+
root.parent = child;
|
|
163
|
+
|
|
164
|
+
expect(resolveNativeSurfaceVisibleRect(child, 100, 30)).toEqual({
|
|
165
|
+
x: 2,
|
|
166
|
+
y: 3,
|
|
167
|
+
width: 20,
|
|
168
|
+
height: 8,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("fails closed when native renderable ancestors exceed a sane depth", () => {
|
|
173
|
+
let node: NativeSurfaceRenderableNode = {
|
|
174
|
+
x: 0,
|
|
175
|
+
y: 0,
|
|
176
|
+
width: 100,
|
|
177
|
+
height: 30,
|
|
178
|
+
parent: null,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
for (let index = 0; index < 300; index += 1) {
|
|
182
|
+
node = {
|
|
183
|
+
x: 0,
|
|
184
|
+
y: 0,
|
|
185
|
+
width: 100,
|
|
186
|
+
height: 30,
|
|
187
|
+
parent: node,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
expect(resolveNativeSurfaceVisibleRect(node, 100, 30)).toBeNull();
|
|
192
|
+
});
|
|
146
193
|
});
|
|
147
194
|
|
|
148
195
|
describe("renderNativeChartBase", () => {
|
|
@@ -9,6 +9,8 @@ export interface NativeSurfaceRenderableNode {
|
|
|
9
9
|
parent: NativeSurfaceRenderableNode | null;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const MAX_RENDERABLE_ANCESTOR_DEPTH = 256;
|
|
13
|
+
|
|
12
14
|
export function getRenderableCellRect(
|
|
13
15
|
renderable: Pick<NativeSurfaceRenderableNode, "x" | "y" | "width" | "height">,
|
|
14
16
|
): CellRect {
|
|
@@ -34,8 +36,13 @@ export function resolveNativeSurfaceVisibleRect(
|
|
|
34
36
|
height: terminalHeight,
|
|
35
37
|
};
|
|
36
38
|
let current: NativeSurfaceRenderableNode | null = renderable;
|
|
39
|
+
const seen = new Set<NativeSurfaceRenderableNode>();
|
|
40
|
+
let depth = 0;
|
|
37
41
|
|
|
38
|
-
while (current) {
|
|
42
|
+
while (current && !seen.has(current)) {
|
|
43
|
+
if (depth >= MAX_RENDERABLE_ANCESTOR_DEPTH) return null;
|
|
44
|
+
seen.add(current);
|
|
45
|
+
depth += 1;
|
|
39
46
|
if (current.visible === false) return null;
|
|
40
47
|
|
|
41
48
|
const nextVisible = intersectCellRects(visible, getRenderableCellRect(current));
|
|
@@ -47,6 +47,8 @@ describe("StaticBarChartSurface", () => {
|
|
|
47
47
|
|
|
48
48
|
await act(async () => {
|
|
49
49
|
await testSetup!.mockMouse.moveTo(14, 4);
|
|
50
|
+
});
|
|
51
|
+
await act(async () => {
|
|
50
52
|
await testSetup!.renderOnce();
|
|
51
53
|
await testSetup!.renderOnce();
|
|
52
54
|
});
|
|
@@ -86,6 +88,8 @@ describe("StaticBarChartSurface", () => {
|
|
|
86
88
|
await testSetup!.renderOnce();
|
|
87
89
|
await testSetup!.renderOnce();
|
|
88
90
|
await testSetup!.mockMouse.moveTo(14, 4);
|
|
91
|
+
});
|
|
92
|
+
await act(async () => {
|
|
89
93
|
await testSetup!.renderOnce();
|
|
90
94
|
await testSetup!.renderOnce();
|
|
91
95
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { useNativeRenderer, useUiCapabilities } from "../../../../ui";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveNativeBitmapSize, shouldRenderNativeBitmap } from "../../native/bitmap-support";
|
|
4
4
|
|
|
5
5
|
export interface StaticChartBitmapSize {
|
|
6
6
|
pixelWidth: number;
|
|
@@ -16,20 +16,25 @@ export function useStaticChartBitmapSize(width: number, height: number): StaticC
|
|
|
16
16
|
pixelRatio = 1,
|
|
17
17
|
} = useUiCapabilities();
|
|
18
18
|
const nativeRenderer = useNativeRenderer();
|
|
19
|
+
const rendererCapabilities = nativeRenderer.capabilities;
|
|
19
20
|
const rendererResolution = nativeRenderer.resolution;
|
|
20
21
|
const rendererTerminalWidth = nativeRenderer.terminalWidth;
|
|
21
22
|
const rendererTerminalHeight = nativeRenderer.terminalHeight;
|
|
22
23
|
|
|
23
24
|
return useMemo(() => {
|
|
24
|
-
if (nativeCharts
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
if (shouldRenderNativeBitmap(nativeRenderer, nativeCharts === true)) {
|
|
26
|
+
return resolveNativeBitmapSize({
|
|
27
|
+
width,
|
|
28
|
+
height,
|
|
29
|
+
resolution: rendererResolution,
|
|
30
|
+
terminalWidth: rendererTerminalWidth,
|
|
31
|
+
terminalHeight: rendererTerminalHeight,
|
|
32
|
+
cellWidthPx,
|
|
33
|
+
cellHeightPx,
|
|
34
|
+
pixelRatio,
|
|
35
|
+
});
|
|
31
36
|
}
|
|
32
|
-
if (!canvasCharts
|
|
37
|
+
if (!canvasCharts) return null;
|
|
33
38
|
const scale = Math.max(1, pixelRatio);
|
|
34
39
|
return {
|
|
35
40
|
pixelWidth: Math.max(1, Math.round(width * cellWidthPx * scale)),
|
|
@@ -41,7 +46,9 @@ export function useStaticChartBitmapSize(width: number, height: number): StaticC
|
|
|
41
46
|
cellWidthPx,
|
|
42
47
|
height,
|
|
43
48
|
nativeCharts,
|
|
49
|
+
nativeRenderer,
|
|
44
50
|
pixelRatio,
|
|
51
|
+
rendererCapabilities,
|
|
45
52
|
rendererResolution,
|
|
46
53
|
rendererTerminalHeight,
|
|
47
54
|
rendererTerminalWidth,
|