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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "http";
|
|
3
|
+
import type { RemoteAppKind, RemoteControlRequest, RemoteControlResponse, RemoteEndpoint } from "./types";
|
|
4
|
+
import { REMOTE_CONTROL_PROTOCOL_VERSION } from "./types";
|
|
5
|
+
import { removeRemoteEndpointFiles, writeRemoteEndpointFiles } from "./endpoint";
|
|
6
|
+
|
|
7
|
+
export interface RemoteControlServerOptions {
|
|
8
|
+
dataDir: string;
|
|
9
|
+
appKind: RemoteAppKind;
|
|
10
|
+
handle: (request: RemoteControlRequest) => Promise<RemoteControlResponse>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RemoteControlServer {
|
|
14
|
+
endpoint: RemoteEndpoint;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
export async function startRemoteControlServer(options: RemoteControlServerOptions): Promise<RemoteControlServer> {
|
|
21
|
+
const token = randomUUID();
|
|
22
|
+
const httpServer = createServer((request, response) => {
|
|
23
|
+
void handleHttpRequest(options.handle, token, request, response);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await new Promise<void>((resolve, reject) => {
|
|
27
|
+
httpServer.once("error", reject);
|
|
28
|
+
httpServer.listen(0, "127.0.0.1", () => {
|
|
29
|
+
httpServer.off("error", reject);
|
|
30
|
+
resolve();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const address = httpServer.address();
|
|
35
|
+
if (!address || typeof address === "string") {
|
|
36
|
+
httpServer.close();
|
|
37
|
+
throw new Error("Remote control server did not bind to a TCP port.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const endpoint: RemoteEndpoint = {
|
|
41
|
+
protocolVersion: REMOTE_CONTROL_PROTOCOL_VERSION,
|
|
42
|
+
appKind: options.appKind,
|
|
43
|
+
pid: process.pid,
|
|
44
|
+
port: address.port,
|
|
45
|
+
token,
|
|
46
|
+
startedAt: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
await writeRemoteEndpointFiles(options.dataDir, endpoint);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
endpoint,
|
|
52
|
+
close: async () => {
|
|
53
|
+
await new Promise<void>((resolve, reject) => {
|
|
54
|
+
httpServer.close((error) => (error ? reject(error) : resolve()));
|
|
55
|
+
}).catch((error) => {
|
|
56
|
+
if ((error as NodeJS.ErrnoException).code !== "ERR_SERVER_NOT_RUNNING") throw error;
|
|
57
|
+
});
|
|
58
|
+
await removeRemoteEndpointFiles(options.dataDir, endpoint);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function handleHttpRequest(
|
|
64
|
+
handle: RemoteControlServerOptions["handle"],
|
|
65
|
+
token: string,
|
|
66
|
+
request: IncomingMessage,
|
|
67
|
+
response: ServerResponse,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
try {
|
|
70
|
+
if (request.method === "GET" && request.url === "/health") {
|
|
71
|
+
sendJson(response, 200, { ok: true });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (request.method !== "POST" || request.url !== "/rpc") {
|
|
75
|
+
sendJson(response, 404, { ok: false, error: { code: "not_found", message: "Unknown remote control endpoint." } });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (request.headers.authorization !== `Bearer ${token}`) {
|
|
79
|
+
sendJson(response, 401, { ok: false, error: { code: "unauthorized", message: "Invalid remote control token." } });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const body = await readBody(request);
|
|
84
|
+
const remoteRequest = JSON.parse(body) as RemoteControlRequest;
|
|
85
|
+
sendJson(response, 200, await handle(remoteRequest));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
sendJson(response, 500, {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: {
|
|
90
|
+
code: "remote_transport_error",
|
|
91
|
+
message: error instanceof Error ? error.message : String(error),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function readBody(request: IncomingMessage): Promise<string> {
|
|
98
|
+
let total = 0;
|
|
99
|
+
const chunks: Uint8Array[] = [];
|
|
100
|
+
for await (const chunk of request) {
|
|
101
|
+
const bytes = chunk instanceof Uint8Array ? new Uint8Array(chunk) : new TextEncoder().encode(String(chunk));
|
|
102
|
+
total += bytes.byteLength;
|
|
103
|
+
if (total > MAX_BODY_BYTES) throw new Error("Remote control request is too large.");
|
|
104
|
+
chunks.push(bytes);
|
|
105
|
+
}
|
|
106
|
+
const body = new Uint8Array(total);
|
|
107
|
+
let offset = 0;
|
|
108
|
+
for (const chunk of chunks) {
|
|
109
|
+
body.set(chunk, offset);
|
|
110
|
+
offset += chunk.byteLength;
|
|
111
|
+
}
|
|
112
|
+
return new TextDecoder().decode(body);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sendJson(response: ServerResponse, status: number, body: unknown): void {
|
|
116
|
+
response.writeHead(status, {
|
|
117
|
+
"content-type": "application/json; charset=utf-8",
|
|
118
|
+
"cache-control": "no-store",
|
|
119
|
+
});
|
|
120
|
+
response.end(JSON.stringify(body));
|
|
121
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export const REMOTE_CONTROL_PROTOCOL_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
export type RemoteAppKind = "tui" | "desktop";
|
|
4
|
+
|
|
5
|
+
export interface RemoteEndpoint {
|
|
6
|
+
protocolVersion: number;
|
|
7
|
+
appKind: RemoteAppKind;
|
|
8
|
+
pid: number;
|
|
9
|
+
port: number;
|
|
10
|
+
token: string;
|
|
11
|
+
startedAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RemoteJsonPatchOperation {
|
|
15
|
+
op: "add" | "replace" | "remove";
|
|
16
|
+
path: string;
|
|
17
|
+
value?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type RemoteStateInclude =
|
|
21
|
+
| "app"
|
|
22
|
+
| "layout"
|
|
23
|
+
| "panes"
|
|
24
|
+
| "commandBar"
|
|
25
|
+
| "commandBar.results"
|
|
26
|
+
| "ui"
|
|
27
|
+
| "schema"
|
|
28
|
+
| "help"
|
|
29
|
+
| "all";
|
|
30
|
+
|
|
31
|
+
export type RemoteControlRequest =
|
|
32
|
+
| { type: "help"; topic?: string }
|
|
33
|
+
| { type: "schema" }
|
|
34
|
+
| { type: "get"; resource: string; include?: RemoteStateInclude[] }
|
|
35
|
+
| { type: "call"; operation: string; input?: unknown; dryRun?: boolean; include?: RemoteStateInclude[] }
|
|
36
|
+
| {
|
|
37
|
+
type: "patch";
|
|
38
|
+
resource: string;
|
|
39
|
+
patch: RemoteJsonPatchOperation[];
|
|
40
|
+
expectRev?: string;
|
|
41
|
+
dryRun?: boolean;
|
|
42
|
+
include?: RemoteStateInclude[];
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
type: "batch";
|
|
46
|
+
requests: RemoteControlRequest[];
|
|
47
|
+
dryRun?: boolean;
|
|
48
|
+
haltOnError?: boolean;
|
|
49
|
+
settle?: "none" | "afterEach" | "afterBatch";
|
|
50
|
+
include?: RemoteStateInclude[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export interface RemoteIncludedState {
|
|
54
|
+
rev: string;
|
|
55
|
+
included: RemoteStateInclude[];
|
|
56
|
+
app?: unknown;
|
|
57
|
+
layout?: unknown;
|
|
58
|
+
panes?: unknown;
|
|
59
|
+
commandBar?: unknown;
|
|
60
|
+
ui?: RemoteUiNodeSnapshot[];
|
|
61
|
+
schema?: RemoteControlSchema;
|
|
62
|
+
help?: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RemoteControlSuccess<T = unknown> {
|
|
66
|
+
ok: true;
|
|
67
|
+
data: T;
|
|
68
|
+
rev?: string;
|
|
69
|
+
state?: RemoteIncludedState;
|
|
70
|
+
warnings?: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RemoteControlFailure {
|
|
74
|
+
ok: false;
|
|
75
|
+
error: {
|
|
76
|
+
code: string;
|
|
77
|
+
message: string;
|
|
78
|
+
details?: unknown;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type RemoteControlResponse<T = unknown> =
|
|
83
|
+
| RemoteControlSuccess<T>
|
|
84
|
+
| RemoteControlFailure;
|
|
85
|
+
|
|
86
|
+
export interface RemoteResourceSchema {
|
|
87
|
+
uri: string;
|
|
88
|
+
description: string;
|
|
89
|
+
patchable?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface RemoteOperationSchema {
|
|
93
|
+
id: string;
|
|
94
|
+
description: string;
|
|
95
|
+
inputShape?: string;
|
|
96
|
+
outputShape?: string;
|
|
97
|
+
returns?: string;
|
|
98
|
+
recommendedNextReads?: string[];
|
|
99
|
+
examples?: unknown[];
|
|
100
|
+
sideEffectLevel: "none" | "local-write" | "network-write" | "external-side-effect" | "external-trade";
|
|
101
|
+
requiresConfirmation?: boolean;
|
|
102
|
+
dryRun?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface RemoteControlSchema {
|
|
106
|
+
protocolVersion: number;
|
|
107
|
+
resources: RemoteResourceSchema[];
|
|
108
|
+
operations: RemoteOperationSchema[];
|
|
109
|
+
help: unknown;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface RemoteUiNodeSnapshot {
|
|
113
|
+
id: string;
|
|
114
|
+
role: string;
|
|
115
|
+
label?: string;
|
|
116
|
+
disabled?: boolean;
|
|
117
|
+
actions: string[];
|
|
118
|
+
metadata?: Record<string, unknown>;
|
|
119
|
+
}
|
|
@@ -55,6 +55,8 @@ import {
|
|
|
55
55
|
desktopWindowStyleMask,
|
|
56
56
|
} from "./desktop/window-style";
|
|
57
57
|
import { applyDesktopWindowControl, type DesktopWindowControlAction } from "./desktop/window-controls";
|
|
58
|
+
import { startRemoteControlServer, type RemoteControlServer } from "../../../remote/server";
|
|
59
|
+
import type { RemoteControlRequest, RemoteControlResponse } from "../../../remote/types";
|
|
58
60
|
|
|
59
61
|
type DesktopRpc = ReturnType<typeof BrowserView.defineRPC<ElectrobunDesktopRpcSchema>>;
|
|
60
62
|
|
|
@@ -70,6 +72,7 @@ let services: AppServices | null = null;
|
|
|
70
72
|
let mainWindow: BrowserWindow | null = null;
|
|
71
73
|
let desktopWorkspace: DesktopWorkspace | null = null;
|
|
72
74
|
let desktopRestartInProgress = false;
|
|
75
|
+
let desktopRemoteControlServer: RemoteControlServer | null = null;
|
|
73
76
|
|
|
74
77
|
const windowRpcRegistry = createDesktopRpcRegistry<DesktopRpc>();
|
|
75
78
|
const contextMenuRequestRpcs = new Map<string, DesktopRpc>();
|
|
@@ -100,6 +103,10 @@ function requireDesktopWorkspace(): DesktopWorkspace {
|
|
|
100
103
|
return desktopWorkspace;
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
function remoteFailure(code: string, message: string): RemoteControlResponse {
|
|
107
|
+
return { ok: false, error: { code, message } };
|
|
108
|
+
}
|
|
109
|
+
|
|
103
110
|
const {
|
|
104
111
|
forEachReadyWindowRpc,
|
|
105
112
|
getRpcWindowKey,
|
|
@@ -130,6 +137,46 @@ const detachedWindowManager = new DesktopDetachedWindowManager<DesktopRpc>({
|
|
|
130
137
|
stateBroadcaster: desktopStateBroadcaster,
|
|
131
138
|
});
|
|
132
139
|
|
|
140
|
+
async function forwardRemoteControlRequest(request: RemoteControlRequest): Promise<RemoteControlResponse> {
|
|
141
|
+
const rpc = getWindowRpc(MAIN_WINDOW_RPC_KEY);
|
|
142
|
+
if (!rpc || !isWindowRpcReady(MAIN_WINDOW_RPC_KEY)) {
|
|
143
|
+
return remoteFailure("remote_unavailable", "The main desktop window is not ready for remote control requests.");
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const response = await rpc.request["remote.request"]({
|
|
147
|
+
request: encodeRpcValue(request) as RemoteControlRequest,
|
|
148
|
+
});
|
|
149
|
+
return decodeRpcValue<RemoteControlResponse>(response);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return remoteFailure(
|
|
152
|
+
"remote_forward_error",
|
|
153
|
+
error instanceof Error ? error.message : String(error),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function ensureDesktopRemoteControlServer(): Promise<void> {
|
|
159
|
+
if (desktopRemoteControlServer) return;
|
|
160
|
+
const config = requireConfig();
|
|
161
|
+
desktopRemoteControlServer = await startRemoteControlServer({
|
|
162
|
+
dataDir: config.dataDir,
|
|
163
|
+
appKind: "desktop",
|
|
164
|
+
handle: forwardRemoteControlRequest,
|
|
165
|
+
});
|
|
166
|
+
console.error("[remote] desktop control endpoint started", {
|
|
167
|
+
port: desktopRemoteControlServer.endpoint.port,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stopDesktopRemoteControlServer(): void {
|
|
172
|
+
const server = desktopRemoteControlServer;
|
|
173
|
+
desktopRemoteControlServer = null;
|
|
174
|
+
if (!server) return;
|
|
175
|
+
void server.close().catch((error) => {
|
|
176
|
+
console.error("[remote] desktop control endpoint cleanup failed", summarizeError(error));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
133
180
|
function openMainWindowDevTools(): void {
|
|
134
181
|
mainWindow?.webview.openDevTools();
|
|
135
182
|
}
|
|
@@ -228,6 +275,7 @@ function disposeWindowScopedResources(windowKey: string): void {
|
|
|
228
275
|
}
|
|
229
276
|
|
|
230
277
|
function teardownServices(): void {
|
|
278
|
+
stopDesktopRemoteControlServer();
|
|
231
279
|
capabilityBridge.disposeAll();
|
|
232
280
|
services?.destroy();
|
|
233
281
|
services = null;
|
|
@@ -306,7 +354,7 @@ async function initialize(
|
|
|
306
354
|
rpc: DesktopRpc,
|
|
307
355
|
payload: Record<string, unknown>,
|
|
308
356
|
) {
|
|
309
|
-
|
|
357
|
+
const init = await initializeDesktopBackend({
|
|
310
358
|
getCurrentConfig: () => currentConfig,
|
|
311
359
|
getCurrentServices: () => services,
|
|
312
360
|
getDesktopSnapshot,
|
|
@@ -328,6 +376,12 @@ async function initialize(
|
|
|
328
376
|
},
|
|
329
377
|
syncConfigAccessors,
|
|
330
378
|
});
|
|
379
|
+
if (init.windowKind === "main") {
|
|
380
|
+
void ensureDesktopRemoteControlServer().catch((error) => {
|
|
381
|
+
console.error("[remote] desktop control endpoint failed", summarizeError(error));
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return init;
|
|
331
385
|
}
|
|
332
386
|
|
|
333
387
|
async function handleBackendRequest(
|
|
@@ -4,6 +4,7 @@ import type { DesktopApplicationMenuCommand } from "../../../types/desktop-menu"
|
|
|
4
4
|
import type { AppConfig } from "../../../types/config";
|
|
5
5
|
import type { UpdateProgress } from "../../../updater";
|
|
6
6
|
import type { CapabilityManifest } from "../../../capabilities";
|
|
7
|
+
import type { RemoteControlRequest, RemoteControlResponse } from "../../../remote/types";
|
|
7
8
|
|
|
8
9
|
export const ELECTROBUN_CONTEXT_MENU_ACTION = "gloom.context-menu.select";
|
|
9
10
|
|
|
@@ -59,6 +60,10 @@ export interface DesktopRestartMessage {
|
|
|
59
60
|
source?: string;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
export interface RemoteControlRequestMessage {
|
|
64
|
+
request: RemoteControlRequest;
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
export interface ElectrobunDesktopRpcSchema {
|
|
63
68
|
bun: {
|
|
64
69
|
requests: {
|
|
@@ -72,7 +77,12 @@ export interface ElectrobunDesktopRpcSchema {
|
|
|
72
77
|
};
|
|
73
78
|
};
|
|
74
79
|
webview: {
|
|
75
|
-
requests: {
|
|
80
|
+
requests: {
|
|
81
|
+
"remote.request": {
|
|
82
|
+
params: RemoteControlRequestMessage;
|
|
83
|
+
response: RemoteControlResponse;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
76
86
|
messages: {
|
|
77
87
|
"context-menu.select": ContextMenuSelectMessage;
|
|
78
88
|
"application-menu.select": ApplicationMenuSelectMessage;
|
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
type DesktopStateMessage,
|
|
10
10
|
type DesktopThemePreviewMessage,
|
|
11
11
|
type ElectrobunBackendInit,
|
|
12
|
+
type RemoteControlRequestMessage,
|
|
12
13
|
type ElectrobunDesktopRpcSchema,
|
|
13
14
|
type UpdateProgressMessage,
|
|
14
15
|
} from "../shared/protocol";
|
|
15
16
|
import { decodeRpcValue, encodeRpcValue } from "./rpc-codec";
|
|
17
|
+
import type { RemoteControlRequest, RemoteControlResponse } from "../../../remote/types";
|
|
16
18
|
|
|
17
19
|
type ContextMenuSelectListener = (message: ContextMenuSelectMessage) => void;
|
|
18
20
|
type ApplicationMenuSelectListener = (message: ApplicationMenuSelectMessage) => void;
|
|
@@ -21,8 +23,10 @@ type DesktopDockPreviewListener = (message: DesktopDockPreviewMessage) => void;
|
|
|
21
23
|
type DesktopThemePreviewListener = (message: DesktopThemePreviewMessage) => void;
|
|
22
24
|
type UpdateProgressListener = (message: UpdateProgressMessage) => void;
|
|
23
25
|
type CapabilityEventListener = (message: CapabilityEventMessage) => void;
|
|
26
|
+
type RemoteControlRequestHandler = (request: RemoteControlRequest) => Promise<RemoteControlResponse>;
|
|
24
27
|
|
|
25
28
|
let initSnapshot: ElectrobunBackendInit | null = null;
|
|
29
|
+
let remoteControlRequestHandler: RemoteControlRequestHandler | null = null;
|
|
26
30
|
const contextMenuSelectListeners = new Map<string, Set<ContextMenuSelectListener>>();
|
|
27
31
|
const applicationMenuSelectListeners = new Set<ApplicationMenuSelectListener>();
|
|
28
32
|
const desktopStateListeners = new Set<DesktopStateListener>();
|
|
@@ -63,7 +67,21 @@ function subscribe<T>(
|
|
|
63
67
|
const rpc = Electroview.defineRPC<ElectrobunDesktopRpcSchema>({
|
|
64
68
|
maxRequestTime: 120_000,
|
|
65
69
|
handlers: {
|
|
66
|
-
requests: {
|
|
70
|
+
requests: {
|
|
71
|
+
"remote.request": async (message: RemoteControlRequestMessage) => {
|
|
72
|
+
if (!remoteControlRequestHandler) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: {
|
|
76
|
+
code: "remote_unavailable",
|
|
77
|
+
message: "The desktop app has not installed a remote control handler yet.",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const decoded = decodeRpcValue<RemoteControlRequest>(message.request);
|
|
82
|
+
return encodeRpcValue(await remoteControlRequestHandler(decoded)) as RemoteControlResponse;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
67
85
|
messages: {
|
|
68
86
|
"context-menu.select": (message) => {
|
|
69
87
|
dispatch(contextMenuSelectListeners, message.requestId, message);
|
|
@@ -140,6 +158,15 @@ export function getElectrobunBackendInitSnapshot(): ElectrobunBackendInit | null
|
|
|
140
158
|
return initSnapshot;
|
|
141
159
|
}
|
|
142
160
|
|
|
161
|
+
export function setElectrobunRemoteRequestHandler(handler: RemoteControlRequestHandler | null): () => void {
|
|
162
|
+
remoteControlRequestHandler = handler;
|
|
163
|
+
return () => {
|
|
164
|
+
if (remoteControlRequestHandler === handler) {
|
|
165
|
+
remoteControlRequestHandler = null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
143
170
|
export function onCapabilityEvent(
|
|
144
171
|
subscriptionId: string,
|
|
145
172
|
listener: (message: CapabilityEventMessage) => void,
|
|
@@ -23,6 +23,7 @@ const COMMON_ALIAS_RULES: AliasRule[] = [
|
|
|
23
23
|
["./kitty/support", "components/chart/native/renderer-selection.ts", "native-stubs/chart/kitty-support.ts"],
|
|
24
24
|
["native/surface/manager", "native-stubs/chart/surface-manager.ts"],
|
|
25
25
|
["native/surface/sync", "native-stubs/chart/surface-sync.ts"],
|
|
26
|
+
["./native-loader", "plugins/ibkr/gateway/service/index.ts", "native-stubs/ibkr-native-loader.ts"],
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
export function electrobunViewPath(...parts: string[]): string {
|
|
@@ -53,8 +53,6 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
53
53
|
scrollRef,
|
|
54
54
|
syncHeaderScroll: _syncHeaderScroll,
|
|
55
55
|
onBodyScrollActivity,
|
|
56
|
-
hoveredIdx,
|
|
57
|
-
setHoveredIdx,
|
|
58
56
|
getItemKey,
|
|
59
57
|
isSelected,
|
|
60
58
|
onSelect,
|
|
@@ -257,9 +255,6 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
257
255
|
focusPane();
|
|
258
256
|
onTableMouseDown?.({});
|
|
259
257
|
}}
|
|
260
|
-
onMouseLeave={() => {
|
|
261
|
-
if (hoveredIdx !== null) setHoveredIdx(null);
|
|
262
|
-
}}
|
|
263
258
|
onScroll={() => {
|
|
264
259
|
markScrollbarActive();
|
|
265
260
|
scheduleBodyScrollActivity();
|
|
@@ -315,7 +310,6 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
315
310
|
const item = items[row.index];
|
|
316
311
|
if (!item) return null;
|
|
317
312
|
const selected = isSelected(item, row.index);
|
|
318
|
-
const hovered = hoveredIdx === row.index && !selected;
|
|
319
313
|
const itemKey = getItemKey(item, row.index);
|
|
320
314
|
return (
|
|
321
315
|
<WebDataTableRow<T, C>
|
|
@@ -335,13 +329,11 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
335
329
|
onRowContextMenu={onRowContextMenu}
|
|
336
330
|
onRowMouseDown={onRowMouseDown}
|
|
337
331
|
onSelectRow={selectRow}
|
|
338
|
-
hovered={hovered}
|
|
339
332
|
getRowBackgroundColor={getRowBackgroundColor}
|
|
340
333
|
renderCell={renderCell}
|
|
341
334
|
renderSectionHeader={renderSectionHeader}
|
|
342
335
|
rowContextMenuSurface={rowContextMenuSurface}
|
|
343
336
|
selected={selected}
|
|
344
|
-
setHoveredIdx={setHoveredIdx}
|
|
345
337
|
/>
|
|
346
338
|
);
|
|
347
339
|
}),
|
|
@@ -12,7 +12,6 @@ import type {
|
|
|
12
12
|
import { WEB_CELL_HEIGHT } from "../input-host";
|
|
13
13
|
import {
|
|
14
14
|
CSS_BG,
|
|
15
|
-
CSS_HOVER_BG,
|
|
16
15
|
CSS_PANEL,
|
|
17
16
|
CSS_SELECTED,
|
|
18
17
|
CSS_SELECTED_TEXT,
|
|
@@ -151,7 +150,6 @@ function WebDataTableRowInner<
|
|
|
151
150
|
onRowContextMenu,
|
|
152
151
|
onRowMouseDown,
|
|
153
152
|
onSelectRow,
|
|
154
|
-
hovered,
|
|
155
153
|
index,
|
|
156
154
|
item,
|
|
157
155
|
itemKey,
|
|
@@ -163,7 +161,6 @@ function WebDataTableRowInner<
|
|
|
163
161
|
rowStart,
|
|
164
162
|
rowContextMenuSurface,
|
|
165
163
|
selected,
|
|
166
|
-
setHoveredIdx,
|
|
167
164
|
}: {
|
|
168
165
|
columns: C[];
|
|
169
166
|
columnGap: number;
|
|
@@ -174,7 +171,6 @@ function WebDataTableRowInner<
|
|
|
174
171
|
onRowContextMenu?: DataTableProps<T, C>["onRowContextMenu"];
|
|
175
172
|
onRowMouseDown?: DataTableProps<T, C>["onRowMouseDown"];
|
|
176
173
|
onSelectRow: (item: T, index: number) => void;
|
|
177
|
-
hovered: boolean;
|
|
178
174
|
index: number;
|
|
179
175
|
item: T;
|
|
180
176
|
itemKey: string;
|
|
@@ -186,7 +182,6 @@ function WebDataTableRowInner<
|
|
|
186
182
|
rowStart: number;
|
|
187
183
|
rowContextMenuSurface: boolean;
|
|
188
184
|
selected: boolean;
|
|
189
|
-
setHoveredIdx: (index: number | null) => void;
|
|
190
185
|
}) {
|
|
191
186
|
useThemeColors();
|
|
192
187
|
const sectionHeader: DataTableSectionHeader | null =
|
|
@@ -242,13 +237,11 @@ function WebDataTableRowInner<
|
|
|
242
237
|
);
|
|
243
238
|
}
|
|
244
239
|
|
|
245
|
-
const rowState = { selected
|
|
240
|
+
const rowState = { selected };
|
|
246
241
|
const rowBackgroundColor = getRowBackgroundColor?.(item, index, rowState);
|
|
247
242
|
const rowBg = selected
|
|
248
243
|
? CSS_SELECTED
|
|
249
|
-
:
|
|
250
|
-
? CSS_HOVER_BG
|
|
251
|
-
: rowBackgroundColor ?? CSS_BG;
|
|
244
|
+
: rowBackgroundColor ?? CSS_BG;
|
|
252
245
|
|
|
253
246
|
return (
|
|
254
247
|
<div
|
|
@@ -260,9 +253,6 @@ function WebDataTableRowInner<
|
|
|
260
253
|
...baseRowStyle,
|
|
261
254
|
backgroundColor: rowBg,
|
|
262
255
|
}}
|
|
263
|
-
onMouseMove={() => {
|
|
264
|
-
if (!hovered) setHoveredIdx(index);
|
|
265
|
-
}}
|
|
266
256
|
onMouseDown={(event) => {
|
|
267
257
|
focusPane();
|
|
268
258
|
onTableMouseDown?.(event);
|
|
@@ -33,6 +33,9 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
|
|
|
33
33
|
const pendingDragRef = useRef<CellMouseEvent | null>(null);
|
|
34
34
|
const propsRef = useRef(props);
|
|
35
35
|
propsRef.current = props;
|
|
36
|
+
const hoverBackgroundColor = typeof props.hoverBackgroundColor === "string"
|
|
37
|
+
? props.hoverBackgroundColor
|
|
38
|
+
: undefined;
|
|
36
39
|
|
|
37
40
|
useImperativeHandle(ref, () => cellBoundsForElement(() => elementRef.current, () => propsRef.current) as unknown as HTMLDivElement, []);
|
|
38
41
|
|
|
@@ -132,6 +135,10 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
|
|
|
132
135
|
callMouseHandler(propsRef.current.onMouseScroll, event, "scroll");
|
|
133
136
|
};
|
|
134
137
|
|
|
138
|
+
const handleMouseOver = (event: MouseEvent) => {
|
|
139
|
+
callMouseHandler(propsRef.current.onMouseOver, event, "over");
|
|
140
|
+
};
|
|
141
|
+
|
|
135
142
|
const handleMouseOut = (event: MouseEvent) => {
|
|
136
143
|
pendingMoveRef.current = null;
|
|
137
144
|
callMouseHandler(propsRef.current.onMouseOut, event, "out");
|
|
@@ -140,18 +147,21 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
|
|
|
140
147
|
return (
|
|
141
148
|
<div
|
|
142
149
|
{...cleanDomProps(props)}
|
|
150
|
+
data-gloom-hover-bg={hoverBackgroundColor ? "true" : undefined}
|
|
143
151
|
data-gloom-interactive={hasDirectMouseHandler(props) ? "true" : undefined}
|
|
144
152
|
ref={elementRef}
|
|
145
153
|
onMouseDown={handleMouseDown}
|
|
154
|
+
onMouseOver={handleMouseOver}
|
|
146
155
|
onMouseMove={(event) => scheduleFrameMouseHandler(event, "move")}
|
|
147
156
|
onMouseUp={(event) => callMouseHandler(propsRef.current.onMouseUp, event, "up")}
|
|
148
157
|
onMouseOut={handleMouseOut}
|
|
149
158
|
onWheel={typeof props.onMouseScroll === "function" ? handleWheel : undefined}
|
|
150
159
|
style={{
|
|
151
160
|
...commonStyle(props),
|
|
161
|
+
"--gloom-box-hover-bg": hoverBackgroundColor,
|
|
152
162
|
...(props.style as CSSProperties | undefined),
|
|
153
163
|
...(props.visible === false ? { display: "none" } : undefined),
|
|
154
|
-
}}
|
|
164
|
+
} as CSSProperties}
|
|
155
165
|
>
|
|
156
166
|
{children as ReactNode}
|
|
157
167
|
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { hasDirectMouseHandler, mouseHandlers } from "./mouse";
|
|
3
|
+
|
|
4
|
+
function fakeMouseEvent() {
|
|
5
|
+
return {
|
|
6
|
+
clientX: 16,
|
|
7
|
+
clientY: 36,
|
|
8
|
+
button: 0,
|
|
9
|
+
detail: 0,
|
|
10
|
+
timeStamp: 1,
|
|
11
|
+
shiftKey: false,
|
|
12
|
+
altKey: false,
|
|
13
|
+
ctrlKey: false,
|
|
14
|
+
metaKey: false,
|
|
15
|
+
preventDefault() {},
|
|
16
|
+
stopPropagation() {},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("desktop web mouse host", () => {
|
|
21
|
+
test("forwards mouse over handlers", () => {
|
|
22
|
+
let cellX = -1;
|
|
23
|
+
let cellY = -1;
|
|
24
|
+
const handlers = mouseHandlers({
|
|
25
|
+
onMouseOver: (event: { x: number; y: number }) => {
|
|
26
|
+
cellX = event.x;
|
|
27
|
+
cellY = event.y;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
handlers.onMouseOver?.(fakeMouseEvent() as never);
|
|
32
|
+
|
|
33
|
+
expect(cellX).toBe(2);
|
|
34
|
+
expect(cellY).toBe(2);
|
|
35
|
+
expect(hasDirectMouseHandler({ onMouseOver: () => {} })).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -73,6 +73,7 @@ export function mouseHandlers(props: Record<string, unknown>) {
|
|
|
73
73
|
);
|
|
74
74
|
return {
|
|
75
75
|
onMouseDown: wrap(props.onMouseDown),
|
|
76
|
+
onMouseOver: wrap(props.onMouseOver),
|
|
76
77
|
onMouseMove: wrap(props.onMouseMove),
|
|
77
78
|
onMouseUp: wrap(props.onMouseUp),
|
|
78
79
|
onMouseOut: wrap(props.onMouseOut),
|
|
@@ -91,6 +92,7 @@ export function callCellMouseHandler(handler: unknown, event: CellMouseEvent): v
|
|
|
91
92
|
|
|
92
93
|
export function hasDirectMouseHandler(props: Record<string, unknown>): boolean {
|
|
93
94
|
return typeof props.onMouseDown === "function"
|
|
95
|
+
|| typeof props.onMouseOver === "function"
|
|
94
96
|
|| typeof props.onMouseUp === "function"
|
|
95
97
|
|| typeof props.onMouseMove === "function"
|
|
96
98
|
|| typeof props.onMouseOut === "function"
|
|
@@ -104,6 +104,7 @@ export function cleanDomProps(props: Record<string, unknown>): Record<string, un
|
|
|
104
104
|
"cursorColor", "selectionBg", "selectionFg", "showCursor", "keyBindings", "wrapText", "wrapMode",
|
|
105
105
|
"initialValue", "value", "onInput", "onChange", "onSubmit", "onCursorChange", "onMouse",
|
|
106
106
|
"scrollX", "scrollY", "focusable", "bitmap", "bitmaps", "crosshair", "text", "font",
|
|
107
|
+
"hoverBackgroundColor",
|
|
107
108
|
]) {
|
|
108
109
|
delete next[key];
|
|
109
110
|
}
|
|
@@ -5,7 +5,7 @@ import { App } from "../../../app";
|
|
|
5
5
|
import { UiHostProvider } from "../../../ui/host";
|
|
6
6
|
import { debugLog } from "../../../utils/debug-log";
|
|
7
7
|
import { measurePerfAsync } from "../../../utils/perf-marks";
|
|
8
|
-
import { backendRequest, initElectrobunBackend } from "./backend-rpc";
|
|
8
|
+
import { backendRequest, initElectrobunBackend, setElectrobunRemoteRequestHandler } from "./backend-rpc";
|
|
9
9
|
import { installElectrobunAiHost } from "./ai-host";
|
|
10
10
|
import { installElectrobunBrokerRemoteClient } from "./broker-remote-client";
|
|
11
11
|
import { installElectrobunConfigStoreHost } from "./config-host";
|
|
@@ -89,6 +89,9 @@ async function boot() {
|
|
|
89
89
|
const desktopWindowBridge = createDesktopWindowBridge(init.windowKind, init.paneId);
|
|
90
90
|
const desktopApplicationMenuBridge = createApplicationMenuBridge();
|
|
91
91
|
const webUiHost = createWebUiHost(init.desktopPlatform);
|
|
92
|
+
const remoteControlAdapter = init.windowKind === "main"
|
|
93
|
+
? { registerHandler: setElectrobunRemoteRequestHandler }
|
|
94
|
+
: undefined;
|
|
92
95
|
measurePerfAsync("startup.electrobun.root-render", async () => {
|
|
93
96
|
root.render(
|
|
94
97
|
<ElectrobunErrorBoundary>
|
|
@@ -102,6 +105,7 @@ async function boot() {
|
|
|
102
105
|
desktopApplicationMenuBridge={desktopApplicationMenuBridge}
|
|
103
106
|
desktopSnapshot={desktopSnapshot}
|
|
104
107
|
desktopThemePreview={init.desktopThemePreview}
|
|
108
|
+
remoteControlAdapter={remoteControlAdapter}
|
|
105
109
|
/>
|
|
106
110
|
</WebDialogHostProvider>
|
|
107
111
|
</WebToastHostProvider>
|