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.
Files changed (138) hide show
  1. package/README.md +49 -8
  2. package/package.json +4 -3
  3. package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
  4. package/patches/react-reconciler@0.33.0.patch +10 -0
  5. package/src/app/global-shortcuts.test.tsx +20 -1
  6. package/src/app/global-shortcuts.ts +2 -0
  7. package/src/app/pane-runtime/index.tsx +11 -4
  8. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
  9. package/src/app.tsx +41 -17
  10. package/src/cli/commands/remote.ts +206 -0
  11. package/src/cli/entry.ts +8 -1
  12. package/src/cli/index.ts +2 -0
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/components/chart/core/keyboard.ts +10 -1
  15. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  16. package/src/components/chart/native/bitmap-support.ts +55 -0
  17. package/src/components/chart/native/kitty/index.test.ts +47 -0
  18. package/src/components/chart/native/surface/visibility.ts +8 -1
  19. package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
  20. package/src/components/chart/static/chart/bitmap.ts +16 -9
  21. package/src/components/chart/static/chart/surface.test.tsx +59 -4
  22. package/src/components/chart/static/chart/surface.tsx +4 -0
  23. package/src/components/chart/stock-chart/index.test.ts +7 -0
  24. package/src/components/command-bar/list/view.tsx +48 -1
  25. package/src/components/command-bar/multi-select-picker.tsx +7 -0
  26. package/src/components/command-bar/routing/navigation-state.ts +10 -1
  27. package/src/components/command-bar/surface/index.test.tsx +14 -0
  28. package/src/components/command-bar/theme-picker.tsx +91 -79
  29. package/src/components/command-bar/workflow/field-row.tsx +30 -5
  30. package/src/components/data-table/view.tsx +0 -12
  31. package/src/components/index.ts +7 -0
  32. package/src/components/layout/floating-pane.tsx +3 -3
  33. package/src/components/layout/pane/footer/index.tsx +23 -0
  34. package/src/components/layout/pane/header.tsx +5 -0
  35. package/src/components/layout/pane/index.tsx +3 -3
  36. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  37. package/src/components/layout/shell/cursor-occlusion.ts +9 -1
  38. package/src/components/layout/shell/index.test.tsx +2 -0
  39. package/src/components/layout/shell/index.tsx +16 -4
  40. package/src/components/layout/shell/pane/actions.ts +21 -7
  41. package/src/components/layout/shell/pane/layers.tsx +2 -2
  42. package/src/components/layout/status-bar.tsx +5 -5
  43. package/src/components/metric-treemap/index.tsx +317 -0
  44. package/src/components/metric-treemap/layout.test.ts +77 -0
  45. package/src/components/metric-treemap/layout.ts +476 -0
  46. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  47. package/src/components/price-performance.tsx +1 -1
  48. package/src/components/price-sparkline/view.tsx +15 -14
  49. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  50. package/src/components/table-view-shared.tsx +0 -10
  51. package/src/components/ticker/list-table-view.tsx +1 -7
  52. package/src/components/ticker/list-table.test.tsx +0 -4
  53. package/src/components/toggle-list.tsx +10 -0
  54. package/src/components/ui/button.tsx +12 -0
  55. package/src/components/ui/data-table/index.tsx +51 -0
  56. package/src/components/ui/data-table/opentui/index.tsx +4 -10
  57. package/src/components/ui/data-table/types.ts +6 -4
  58. package/src/components/ui/fields.tsx +18 -0
  59. package/src/components/ui/list-view.tsx +84 -4
  60. package/src/components/ui/message-composer.tsx +22 -0
  61. package/src/components/ui/tabs.tsx +46 -2
  62. package/src/components/ui/ui.test.tsx +0 -9
  63. package/src/core/state/app/layout-reducer.ts +7 -1
  64. package/src/core/state/app/layout.ts +6 -0
  65. package/src/core/state/app/state.ts +23 -1
  66. package/src/core/state/app/types.ts +4 -2
  67. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  68. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  69. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  70. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  71. package/src/plugins/builtin/chat/channels.ts +5 -18
  72. package/src/plugins/builtin/chat/chat.test.tsx +1 -7
  73. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  74. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  75. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  76. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  77. package/src/plugins/builtin/chat/status-widget.tsx +2 -8
  78. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  79. package/src/plugins/builtin/cloud/plugin.tsx +4 -13
  80. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  81. package/src/plugins/builtin/debug/index.tsx +3 -4
  82. package/src/plugins/builtin/help/components.tsx +1 -1
  83. package/src/plugins/builtin/holders/treemap.tsx +28 -297
  84. package/src/plugins/builtin/holders/types.ts +0 -34
  85. package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
  86. package/src/plugins/builtin/market-heatmap/data.ts +296 -0
  87. package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
  88. package/src/plugins/builtin/options/table.ts +5 -5
  89. package/src/plugins/builtin/plugin-groups.ts +2 -0
  90. package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
  91. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
  92. package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
  93. package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
  94. package/src/plugins/ibkr/gateway/service/index.ts +6 -9
  95. package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
  96. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  97. package/src/remote/app-host.tsx +77 -0
  98. package/src/remote/client.ts +34 -0
  99. package/src/remote/command-bar.ts +98 -0
  100. package/src/remote/controller-utils.ts +111 -0
  101. package/src/remote/controller.test.ts +365 -0
  102. package/src/remote/controller.ts +434 -0
  103. package/src/remote/endpoint.ts +41 -0
  104. package/src/remote/json-patch.test.ts +31 -0
  105. package/src/remote/json-patch.ts +89 -0
  106. package/src/remote/layout-helpers.ts +62 -0
  107. package/src/remote/resources.ts +186 -0
  108. package/src/remote/revision.ts +18 -0
  109. package/src/remote/schema.ts +143 -0
  110. package/src/remote/semantic-helpers.ts +161 -0
  111. package/src/remote/semantic-tree.tsx +112 -0
  112. package/src/remote/server.test.ts +50 -0
  113. package/src/remote/server.ts +121 -0
  114. package/src/remote/types.ts +119 -0
  115. package/src/renderers/electrobun/bun/index.ts +55 -1
  116. package/src/renderers/electrobun/shared/protocol.ts +11 -1
  117. package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
  118. package/src/renderers/electrobun/view/build-assets.ts +1 -0
  119. package/src/renderers/electrobun/view/data-table/index.tsx +0 -8
  120. package/src/renderers/electrobun/view/data-table/row.tsx +2 -12
  121. package/src/renderers/electrobun/view/host/box.tsx +11 -1
  122. package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
  123. package/src/renderers/electrobun/view/host/mouse.ts +2 -0
  124. package/src/renderers/electrobun/view/host/style.ts +1 -0
  125. package/src/renderers/electrobun/view/main.tsx +5 -1
  126. package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
  127. package/src/renderers/electrobun/view/styles.css +7 -0
  128. package/src/renderers/opentui/chart-surface.test.tsx +101 -0
  129. package/src/renderers/opentui/chart-surface.tsx +15 -20
  130. package/src/renderers/opentui/native-smoke.ts +1 -0
  131. package/src/renderers/opentui/start.tsx +32 -0
  132. package/src/sources/yahoo-finance/http.ts +31 -4
  133. package/src/state/app/context/index.test.tsx +47 -0
  134. package/src/state/app/context/index.tsx +48 -18
  135. package/src/ui/host.tsx +1 -0
  136. package/src/ui/index.tsx +183 -7
  137. package/src/version.ts +1 -1
  138. 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
- <AppProvider
417
- config={config}
418
- sessionStore={desktopWindowBridge?.kind === "detached" ? undefined : services.persistence.sessions}
419
- sessionSnapshot={sessionSnapshot}
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
- desktopWindowBridge={desktopWindowBridge}
431
- desktopApplicationMenuBridge={desktopApplicationMenuBridge}
432
- />
433
- </AppProvider>
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,
@@ -0,0 +1,5 @@
1
+ export const OPEN_TUI_NATIVE_SMOKE_COMMAND = "__gloomberb-smoke-opentui-native";
2
+
3
+ export async function smokeOpenTuiNative(): Promise<void> {
4
+ await import("../renderers/opentui/native-smoke");
5
+ }
@@ -1,4 +1,13 @@
1
- export function resolveChartKeyboardKey(event: { name?: string; sequence?: string }): string {
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 { computeBitmapSize } from "../../native/chart-rasterizer";
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 && rendererResolution && rendererTerminalWidth > 0 && rendererTerminalHeight > 0) {
25
- return computeBitmapSize(
26
- { x: 0, y: 0, width, height },
27
- rendererResolution,
28
- rendererTerminalWidth,
29
- rendererTerminalHeight,
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 && !nativeCharts) return null;
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,