gloomberb 0.9.0 → 0.9.2

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 (51) hide show
  1. package/package.json +6 -2
  2. package/patches/electrobun@1.18.1.patch +85 -0
  3. package/src/app/global-shortcuts.test.tsx +12 -0
  4. package/src/app/global-shortcuts.ts +1 -1
  5. package/src/assets/gloomberb-logo-windows.ico +0 -0
  6. package/src/assets/gloomberb-logo-windows.png +0 -0
  7. package/src/assets/gloomberb-logo.ico +0 -0
  8. package/src/components/chart/comparison-stock-chart/helpers.ts +0 -32
  9. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
  10. package/src/components/chart/comparison-stock-chart/legend.tsx +41 -44
  11. package/src/components/chart/comparison-stock-chart/view.tsx +46 -6
  12. package/src/components/layout/detached-pane-shell.tsx +11 -4
  13. package/src/components/layout/header.tsx +25 -10
  14. package/src/components/layout/titlebar-overlay.ts +16 -0
  15. package/src/components/layout/window-controls.tsx +99 -0
  16. package/src/components/price-performance.tsx +175 -0
  17. package/src/market-data/performance.test.ts +54 -0
  18. package/src/market-data/performance.ts +109 -0
  19. package/src/plugins/builtin/comparison-chart/index.test.tsx +88 -9
  20. package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
  21. package/src/plugins/builtin/congress-trades/footer.ts +14 -8
  22. package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
  23. package/src/plugins/builtin/congress-trades/model.ts +29 -0
  24. package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
  25. package/src/plugins/builtin/congress-trades/table.tsx +10 -1
  26. package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
  27. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
  28. package/src/renderers/electrobun/bun/application-menu/index.test.ts +12 -0
  29. package/src/renderers/electrobun/bun/application-menu/index.ts +4 -0
  30. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +51 -3
  31. package/src/renderers/electrobun/bun/desktop/host-requests.ts +31 -0
  32. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  33. package/src/renderers/electrobun/bun/desktop/window-controls.test.ts +53 -0
  34. package/src/renderers/electrobun/bun/desktop/window-controls.ts +73 -0
  35. package/src/renderers/electrobun/bun/desktop/window-style.ts +31 -0
  36. package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
  37. package/src/renderers/electrobun/bun/desktop/windows-icons.ts +70 -0
  38. package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
  39. package/src/renderers/electrobun/bun/index.ts +36 -7
  40. package/src/renderers/electrobun/bun/window/frame.test.ts +7 -0
  41. package/src/renderers/electrobun/bun/window/frame.ts +5 -0
  42. package/src/renderers/electrobun/shared/protocol.ts +1 -0
  43. package/src/renderers/electrobun/view/build-assets.ts +2 -1
  44. package/src/renderers/electrobun/view/host/text.tsx +29 -9
  45. package/src/renderers/electrobun/view/main.tsx +2 -1
  46. package/src/renderers/electrobun/view/styles.css +29 -8
  47. package/src/renderers/electrobun/view/ui-host.tsx +113 -74
  48. package/src/ui/host.tsx +2 -0
  49. package/src/updater.test.ts +47 -2
  50. package/src/updater.ts +67 -10
  51. package/src/version.ts +1 -1
@@ -12,6 +12,7 @@ import {
12
12
  import {
13
13
  detachedRpcKey,
14
14
  focusWindowForRpcKey,
15
+ paneIdFromDetachedRpcKey,
15
16
  } from "../window/focus";
16
17
  import {
17
18
  applyWindowMoveEvent,
@@ -22,6 +23,16 @@ import {
22
23
  type WindowResizeEvent,
23
24
  } from "./window-events";
24
25
  import type { DesktopStateBroadcaster, DesktopStateRpc } from "./state-broadcaster";
26
+ import { applyWindowsCustomChrome } from "./windows-custom-chrome";
27
+ import { applyWindowsWindowIcon } from "./windows-icons";
28
+ import {
29
+ desktopTitleBarStyle,
30
+ desktopWindowRenderer,
31
+ desktopWindowStyleMask,
32
+ } from "./window-style";
33
+
34
+ const INITIAL_DOCK_SUPPRESSION_MS = 800;
35
+ const WINDOW_CONTROL_DOCK_SUPPRESSION_MS = 5_000;
25
36
 
26
37
  interface DesktopDetachedWindowManagerOptions<Rpc extends DesktopStateRpc> {
27
38
  createRpc: (key: string) => Rpc;
@@ -45,6 +56,8 @@ export class DesktopDetachedWindowManager<Rpc extends DesktopStateRpc> {
45
56
  private readonly dockTimers = new Map<string, ReturnType<typeof setTimeout>>();
46
57
  private readonly closingPanes = new Set<string>();
47
58
  private readonly pendingMoveFlush = new Set<string>();
59
+ private readonly suppressDockUntil = new Map<string, number>();
60
+ private readonly windowControlDockSuppressed = new Set<string>();
48
61
 
49
62
  constructor(private readonly options: DesktopDetachedWindowManagerOptions<Rpc>) {}
50
63
 
@@ -56,6 +69,19 @@ export class DesktopDetachedWindowManager<Rpc extends DesktopStateRpc> {
56
69
  return focusWindowForRpcKey(rpcKey, this.options.getMainWindow(), this.windows);
57
70
  }
58
71
 
72
+ getWindowForRpcKey(rpcKey: string | undefined): BrowserWindow | null {
73
+ const paneId = paneIdFromDetachedRpcKey(rpcKey);
74
+ return paneId ? this.windows.get(paneId) ?? null : null;
75
+ }
76
+
77
+ suppressAutoDockForRpcKey(rpcKey: string | undefined): void {
78
+ const paneId = paneIdFromDetachedRpcKey(rpcKey);
79
+ if (!paneId || !this.windows.has(paneId)) return;
80
+ this.windowControlDockSuppressed.add(paneId);
81
+ this.suppressDockUntil.set(paneId, Date.now() + WINDOW_CONTROL_DOCK_SUPPRESSION_MS);
82
+ this.options.stateBroadcaster.clearDockPreview(paneId);
83
+ }
84
+
59
85
  resolveFrame(instanceId: string): WindowFrame {
60
86
  const detachedEntry = this.options.getConfig().layout.detached.find((entry) => entry.instanceId === instanceId);
61
87
  if (detachedEntry) {
@@ -151,6 +177,8 @@ export class DesktopDetachedWindowManager<Rpc extends DesktopStateRpc> {
151
177
  this.clearTimer(this.frameTimers, instanceId);
152
178
  this.clearTimer(this.dockTimers, instanceId);
153
179
  this.pendingMoveFlush.delete(instanceId);
180
+ this.suppressDockUntil.delete(instanceId);
181
+ this.windowControlDockSuppressed.delete(instanceId);
154
182
  this.options.unregisterWindowRpc(detachedRpcKey(instanceId));
155
183
  }
156
184
 
@@ -160,18 +188,23 @@ export class DesktopDetachedWindowManager<Rpc extends DesktopStateRpc> {
160
188
  ): BrowserWindow {
161
189
  const rpc = this.options.createRpc(detachedRpcKey(instanceId));
162
190
  const initialFrame = normalizeWindowFrameWithMinimum(frame, DEFAULT_WINDOW_FRAME, DETACHED_WINDOW_MIN_SIZE);
191
+ const title = this.resolveTitle(instanceId);
163
192
  const window = new BrowserWindow({
164
- title: this.resolveTitle(instanceId),
193
+ title,
165
194
  frame: initialFrame,
166
195
  url: "views://mainview/index.html",
167
- renderer: "native",
196
+ renderer: desktopWindowRenderer(),
168
197
  rpc: rpc as never,
169
- titleBarStyle: "hiddenInset",
198
+ styleMask: desktopWindowStyleMask(),
199
+ titleBarStyle: desktopTitleBarStyle(),
170
200
  navigationRules: JSON.stringify(["views://*"]),
171
201
  sandbox: false,
172
202
  });
203
+ applyWindowsWindowIcon(title);
204
+ applyWindowsCustomChrome(title);
173
205
  updateWindowFrameCache(window, initialFrame, DETACHED_WINDOW_MIN_SIZE);
174
206
  this.windows.set(instanceId, window);
207
+ this.suppressDockUntil.set(instanceId, Date.now() + INITIAL_DOCK_SUPPRESSION_MS);
175
208
 
176
209
  (window as any).on?.("close", () => {
177
210
  const shouldIgnore = this.closingPanes.delete(instanceId);
@@ -230,6 +263,21 @@ export class DesktopDetachedWindowManager<Rpc extends DesktopStateRpc> {
230
263
  }, 120));
231
264
 
232
265
  this.clearTimer(this.dockTimers, instanceId);
266
+ if (this.windowControlDockSuppressed.has(instanceId)) {
267
+ this.options.stateBroadcaster.clearDockPreview(instanceId);
268
+ if (!edge) {
269
+ this.windowControlDockSuppressed.delete(instanceId);
270
+ }
271
+ return;
272
+ }
273
+
274
+ const suppressDockUntil = this.suppressDockUntil.get(instanceId) ?? 0;
275
+ if (Date.now() < suppressDockUntil) {
276
+ this.options.stateBroadcaster.clearDockPreview(instanceId);
277
+ return;
278
+ }
279
+ this.suppressDockUntil.delete(instanceId);
280
+
233
281
  if (!edge) {
234
282
  this.options.stateBroadcaster.clearDockPreview(instanceId);
235
283
  return;
@@ -3,10 +3,13 @@ import { ContextMenu, Utils, type BrowserWindow } from "electrobun/bun";
3
3
  import { buildSoundCommand } from "../../../../notifications/app-notifier";
4
4
  import type { DesktopRestartMessage } from "../../shared/protocol";
5
5
  import { getContextMenuRequestId, normalizeContextMenuItems } from "../context-menu/normalize";
6
+ import { MAIN_WINDOW_RPC_KEY } from "../window/focus";
7
+ import type { DesktopWindowControlAction } from "./window-controls";
6
8
 
7
9
  interface DesktopHostRequestOptions<TRpc> {
8
10
  clearMainWindow: () => void;
9
11
  closeAllDetachedWindows: () => void;
12
+ controlWindowForRpcKey: (windowKey: string | undefined, action: DesktopWindowControlAction) => boolean;
10
13
  focusWindowForRpcKey: (windowKey: string) => void;
11
14
  getMainWindow: () => BrowserWindow | null;
12
15
  getRpcWindowKey: (rpc: TRpc) => string | undefined;
@@ -37,9 +40,17 @@ function playNotificationSound(sound: string | undefined): void {
37
40
  }
38
41
  }
39
42
 
43
+ function normalizeWindowControlAction(action: unknown): DesktopWindowControlAction {
44
+ if (action === "minimize" || action === "toggle-maximize" || action === "close") {
45
+ return action;
46
+ }
47
+ throw new Error("host.windowControl requires a valid action.");
48
+ }
49
+
40
50
  export function handleDesktopHostRequest<TRpc>({
41
51
  clearMainWindow,
42
52
  closeAllDetachedWindows,
53
+ controlWindowForRpcKey,
43
54
  focusWindowForRpcKey,
44
55
  getMainWindow,
45
56
  getRpcWindowKey,
@@ -69,6 +80,26 @@ export function handleDesktopHostRequest<TRpc>({
69
80
  Utils.quit();
70
81
  return null;
71
82
  }
83
+ case "host.windowControl": {
84
+ const action = normalizeWindowControlAction(payload.action);
85
+ const windowKey = getRpcWindowKey(rpc);
86
+ if (action === "close" && windowKey === MAIN_WINDOW_RPC_KEY) {
87
+ closeAllDetachedWindows();
88
+ teardownServices();
89
+ const mainWindow = getMainWindow();
90
+ if (mainWindow) {
91
+ mainWindow.close();
92
+ clearMainWindow();
93
+ return null;
94
+ }
95
+ Utils.quit();
96
+ return null;
97
+ }
98
+ if (!controlWindowForRpcKey(windowKey, action)) {
99
+ throw new Error("No desktop window is registered for this request.");
100
+ }
101
+ return null;
102
+ }
72
103
  case "host.openExternal":
73
104
  if (typeof payload.url !== "string") {
74
105
  throw new Error("host.openExternal requires a URL.");
@@ -91,6 +91,7 @@ function buildInitializationPayload(
91
91
  desktopThemePreview: options.desktopThemePreview,
92
92
  pluginState: loadDesktopPluginState(services.pluginRegistry),
93
93
  capabilityManifests: services.pluginRegistry.capabilities.manifests({ rendererOnly: true }),
94
+ desktopPlatform: process.platform,
94
95
  windowKind: windowTarget.kind,
95
96
  paneId: windowTarget.paneId,
96
97
  };
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { applyDesktopWindowControl } from "./window-controls";
3
+
4
+ type WindowCall =
5
+ | { type: "close" }
6
+ | { type: "minimize" }
7
+ | { type: "maximize" }
8
+ | { type: "unmaximize" }
9
+ | { type: "setFrame"; frame: { x: number; y: number; width: number; height: number } };
10
+
11
+ function createWindow() {
12
+ const calls: WindowCall[] = [];
13
+ const frame = { x: 120, y: 80, width: 640, height: 420 };
14
+ return {
15
+ calls,
16
+ window: {
17
+ frame,
18
+ close: () => calls.push({ type: "close" }),
19
+ minimize: () => calls.push({ type: "minimize" }),
20
+ maximize: () => calls.push({ type: "maximize" }),
21
+ unmaximize: () => calls.push({ type: "unmaximize" }),
22
+ getFrame: () => frame,
23
+ setFrame: (x: number, y: number, width: number, height: number) => calls.push({
24
+ type: "setFrame",
25
+ frame: { x, y, width, height },
26
+ }),
27
+ },
28
+ };
29
+ }
30
+
31
+ describe("applyDesktopWindowControl", () => {
32
+ test("minimizes and closes windows", () => {
33
+ const target = createWindow();
34
+
35
+ applyDesktopWindowControl(target.window, "minimize");
36
+ applyDesktopWindowControl(target.window, "close");
37
+
38
+ expect(target.calls).toEqual([{ type: "minimize" }, { type: "close" }]);
39
+ });
40
+
41
+ test("toggles maximize from the custom control state", () => {
42
+ const target = createWindow();
43
+
44
+ applyDesktopWindowControl(target.window, "toggle-maximize");
45
+ applyDesktopWindowControl(target.window, "toggle-maximize");
46
+
47
+ expect(target.calls).toEqual([
48
+ { type: "maximize" },
49
+ { type: "unmaximize" },
50
+ { type: "setFrame", frame: { x: 120, y: 80, width: 640, height: 420 } },
51
+ ]);
52
+ });
53
+ });
@@ -0,0 +1,73 @@
1
+ export type DesktopWindowControlAction = "minimize" | "toggle-maximize" | "close";
2
+
3
+ interface DesktopWindowFrame {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+
10
+ export interface ControllableDesktopWindow {
11
+ frame?: Partial<DesktopWindowFrame> | null;
12
+ close?: () => void;
13
+ minimize?: () => void;
14
+ maximize?: () => void;
15
+ unmaximize?: () => void;
16
+ getFrame?: () => Partial<DesktopWindowFrame> | null;
17
+ setFrame?: (x: number, y: number, width: number, height: number) => void;
18
+ }
19
+
20
+ const maximizedWindowRestoreFrames = new WeakMap<ControllableDesktopWindow, DesktopWindowFrame | null>();
21
+
22
+ function normalizeFrame(frame: Partial<DesktopWindowFrame> | null | undefined): DesktopWindowFrame | null {
23
+ if (!frame) return null;
24
+ const { x, y, width, height } = frame;
25
+ if (
26
+ typeof x !== "number"
27
+ || typeof y !== "number"
28
+ || typeof width !== "number"
29
+ || typeof height !== "number"
30
+ || !Number.isFinite(x)
31
+ || !Number.isFinite(y)
32
+ || !Number.isFinite(width)
33
+ || !Number.isFinite(height)
34
+ ) {
35
+ return null;
36
+ }
37
+ return { x, y, width, height };
38
+ }
39
+
40
+ function readWindowFrame(window: ControllableDesktopWindow): DesktopWindowFrame | null {
41
+ return normalizeFrame(window.getFrame?.() ?? window.frame);
42
+ }
43
+
44
+ function restoreWindowFrame(window: ControllableDesktopWindow, frame: DesktopWindowFrame | null): void {
45
+ if (!frame) return;
46
+ window.setFrame?.(frame.x, frame.y, frame.width, frame.height);
47
+ }
48
+
49
+ export function applyDesktopWindowControl(
50
+ window: ControllableDesktopWindow,
51
+ action: DesktopWindowControlAction,
52
+ ): void {
53
+ if (action === "close") {
54
+ maximizedWindowRestoreFrames.delete(window);
55
+ window.close?.();
56
+ return;
57
+ }
58
+
59
+ if (action === "minimize") {
60
+ window.minimize?.();
61
+ return;
62
+ }
63
+
64
+ const restoreFrame = maximizedWindowRestoreFrames.get(window);
65
+ if (maximizedWindowRestoreFrames.has(window)) {
66
+ maximizedWindowRestoreFrames.delete(window);
67
+ window.unmaximize?.();
68
+ restoreWindowFrame(window, restoreFrame ?? null);
69
+ return;
70
+ }
71
+ maximizedWindowRestoreFrames.set(window, readWindowFrame(window));
72
+ window.maximize?.();
73
+ }
@@ -0,0 +1,31 @@
1
+ type DesktopTitleBarStyle = "default" | "hidden" | "hiddenInset";
2
+ type DesktopWindowRenderer = "native" | "cef";
3
+ type DesktopWindowStyleMask = {
4
+ Borderless?: boolean;
5
+ Closable?: boolean;
6
+ Miniaturizable?: boolean;
7
+ Titled?: boolean;
8
+ FullSizeContentView?: boolean;
9
+ };
10
+
11
+ export function desktopTitleBarStyle(): DesktopTitleBarStyle {
12
+ if (process.platform === "darwin") return "hiddenInset";
13
+ if (process.platform === "win32") return "hidden";
14
+ return "hidden";
15
+ }
16
+
17
+ export function desktopWindowRenderer(): DesktopWindowRenderer {
18
+ if (process.platform === "win32") return "cef";
19
+ return "native";
20
+ }
21
+
22
+ export function desktopWindowStyleMask(): DesktopWindowStyleMask {
23
+ if (process.platform !== "win32") return {};
24
+ return {
25
+ Borderless: true,
26
+ Closable: false,
27
+ Miniaturizable: false,
28
+ Titled: false,
29
+ FullSizeContentView: true,
30
+ };
31
+ }
@@ -0,0 +1,58 @@
1
+ import type { Pointer } from "bun:ffi";
2
+ import {
3
+ findCurrentProcessWindow,
4
+ WINDOWS_HANDLE_MAX_ATTEMPTS,
5
+ WINDOWS_HANDLE_RETRY_DELAY_MS,
6
+ win32OrNull,
7
+ } from "./windows-native";
8
+
9
+ const GWL_STYLE = -16;
10
+ const WS_CAPTION = 0x00c00000n;
11
+ const WS_SYSMENU = 0x00080000n;
12
+ const WS_THICKFRAME = 0x00040000n;
13
+ const WS_MINIMIZEBOX = 0x00020000n;
14
+ const WS_MAXIMIZEBOX = 0x00010000n;
15
+ const WINDOWS_NATIVE_CONTROL_STYLE_MASK = WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
16
+ const SWP_NOSIZE = 0x0001;
17
+ const SWP_NOMOVE = 0x0002;
18
+ const SWP_NOZORDER = 0x0004;
19
+ const SWP_NOACTIVATE = 0x0010;
20
+ const SWP_FRAMECHANGED = 0x0020;
21
+ const CUSTOM_CHROME_SET_WINDOW_POS_FLAGS = SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED;
22
+
23
+ type Win32 = NonNullable<ReturnType<typeof win32OrNull>>;
24
+
25
+ function styleValue(value: number | bigint): bigint {
26
+ if (typeof value === "bigint") return value;
27
+ return BigInt(value >>> 0);
28
+ }
29
+
30
+ function applyCustomChromeToWindow(win32: Win32, windowHandle: Pointer): boolean {
31
+ const currentStyle = styleValue(win32.symbols.GetWindowLongPtrW(windowHandle, GWL_STYLE));
32
+ const nextStyle = currentStyle & ~WINDOWS_NATIVE_CONTROL_STYLE_MASK;
33
+ if (nextStyle !== currentStyle) {
34
+ win32.symbols.SetWindowLongPtrW(windowHandle, GWL_STYLE, nextStyle);
35
+ }
36
+
37
+ return win32.symbols.SetWindowPos(
38
+ windowHandle,
39
+ null,
40
+ 0,
41
+ 0,
42
+ 0,
43
+ 0,
44
+ CUSTOM_CHROME_SET_WINDOW_POS_FLAGS,
45
+ );
46
+ }
47
+
48
+ export function applyWindowsCustomChrome(title: string, attempt = 1): void {
49
+ const win32 = win32OrNull();
50
+ if (!win32) return;
51
+
52
+ const windowHandle = findCurrentProcessWindow(win32, title);
53
+ if (windowHandle && applyCustomChromeToWindow(win32, windowHandle)) return;
54
+
55
+ if (attempt < WINDOWS_HANDLE_MAX_ATTEMPTS) {
56
+ setTimeout(() => applyWindowsCustomChrome(title, attempt + 1), WINDOWS_HANDLE_RETRY_DELAY_MS);
57
+ }
58
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync } from "fs";
2
+ import { resolve } from "path";
3
+ import { ptr, type Pointer } from "bun:ffi";
4
+ import {
5
+ findCurrentProcessWindow,
6
+ WINDOWS_HANDLE_MAX_ATTEMPTS,
7
+ WINDOWS_HANDLE_RETRY_DELAY_MS,
8
+ wideString,
9
+ win32OrNull,
10
+ } from "./windows-native";
11
+
12
+ const WM_SETICON = 0x0080;
13
+ const ICON_SMALL = 0;
14
+ const ICON_BIG = 1;
15
+ const IMAGE_ICON = 1;
16
+ const LR_LOADFROMFILE = 0x0010;
17
+ const LR_DEFAULTSIZE = 0x0040;
18
+ const GCLP_HICON = -14;
19
+ const GCLP_HICONSM = -34;
20
+ type Win32 = NonNullable<ReturnType<typeof win32OrNull>>;
21
+ let smallIconHandle: Pointer | null = null;
22
+ let bigIconHandle: Pointer | null = null;
23
+
24
+ function loadIconHandle(win32: Win32, iconPath: string, size: number): Pointer | null {
25
+ const iconPathBuffer = wideString(iconPath);
26
+ const iconHandle = win32.symbols.LoadImageW(
27
+ null,
28
+ ptr(iconPathBuffer),
29
+ IMAGE_ICON,
30
+ size,
31
+ size,
32
+ LR_LOADFROMFILE | LR_DEFAULTSIZE,
33
+ );
34
+
35
+ return iconHandle || null;
36
+ }
37
+
38
+ function ensureIconHandles(win32: Win32): boolean {
39
+ if (smallIconHandle && bigIconHandle) return true;
40
+
41
+ const iconPath = resolve("../Resources/app.ico");
42
+ if (!existsSync(iconPath)) return false;
43
+
44
+ smallIconHandle = smallIconHandle ?? loadIconHandle(win32, iconPath, 16);
45
+ bigIconHandle = bigIconHandle ?? loadIconHandle(win32, iconPath, 32);
46
+
47
+ return Boolean(smallIconHandle && bigIconHandle);
48
+ }
49
+
50
+ function setWindowIcon(win32: Win32, windowHandle: Pointer): boolean {
51
+ if (!ensureIconHandles(win32) || !smallIconHandle || !bigIconHandle) return false;
52
+
53
+ win32.symbols.SendMessageW(windowHandle, WM_SETICON, ICON_SMALL, smallIconHandle);
54
+ win32.symbols.SendMessageW(windowHandle, WM_SETICON, ICON_BIG, bigIconHandle);
55
+ win32.symbols.SetClassLongPtrW(windowHandle, GCLP_HICONSM, smallIconHandle);
56
+ win32.symbols.SetClassLongPtrW(windowHandle, GCLP_HICON, bigIconHandle);
57
+ return true;
58
+ }
59
+
60
+ export function applyWindowsWindowIcon(title: string, attempt = 1): void {
61
+ const win32 = win32OrNull();
62
+ if (!win32) return;
63
+
64
+ const windowHandle = findCurrentProcessWindow(win32, title);
65
+ if (windowHandle && setWindowIcon(win32, windowHandle)) return;
66
+
67
+ if (attempt < WINDOWS_HANDLE_MAX_ATTEMPTS) {
68
+ setTimeout(() => applyWindowsWindowIcon(title, attempt + 1), WINDOWS_HANDLE_RETRY_DELAY_MS);
69
+ }
70
+ }
@@ -0,0 +1,75 @@
1
+ import { dlopen, FFIType, ptr, type Pointer } from "bun:ffi";
2
+
3
+ export const WINDOWS_HANDLE_MAX_ATTEMPTS = 20;
4
+ export const WINDOWS_HANDLE_RETRY_DELAY_MS = 100;
5
+
6
+ type Win32 = ReturnType<typeof loadWin32>;
7
+
8
+ let cachedWin32: Win32 | null | undefined;
9
+
10
+ function loadWin32() {
11
+ return dlopen("user32.dll", {
12
+ FindWindowW: {
13
+ args: [FFIType.ptr, FFIType.ptr],
14
+ returns: FFIType.ptr,
15
+ },
16
+ GetWindowThreadProcessId: {
17
+ args: [FFIType.ptr, FFIType.ptr],
18
+ returns: FFIType.u32,
19
+ },
20
+ LoadImageW: {
21
+ args: [FFIType.ptr, FFIType.ptr, FFIType.u32, FFIType.int, FFIType.int, FFIType.u32],
22
+ returns: FFIType.ptr,
23
+ },
24
+ SendMessageW: {
25
+ args: [FFIType.ptr, FFIType.u32, FFIType.u64, FFIType.ptr],
26
+ returns: FFIType.ptr,
27
+ },
28
+ SetClassLongPtrW: {
29
+ args: [FFIType.ptr, FFIType.int, FFIType.ptr],
30
+ returns: FFIType.ptr,
31
+ },
32
+ GetWindowLongPtrW: {
33
+ args: [FFIType.ptr, FFIType.int],
34
+ returns: FFIType.i64,
35
+ },
36
+ SetWindowLongPtrW: {
37
+ args: [FFIType.ptr, FFIType.int, FFIType.i64],
38
+ returns: FFIType.i64,
39
+ },
40
+ SetWindowPos: {
41
+ args: [FFIType.ptr, FFIType.ptr, FFIType.int, FFIType.int, FFIType.int, FFIType.int, FFIType.u32],
42
+ returns: FFIType.bool,
43
+ },
44
+ });
45
+ }
46
+
47
+ export function win32OrNull(): Win32 | null {
48
+ if (process.platform !== "win32") return null;
49
+ if (cachedWin32 !== undefined) return cachedWin32;
50
+
51
+ try {
52
+ cachedWin32 = loadWin32();
53
+ } catch {
54
+ cachedWin32 = null;
55
+ }
56
+ return cachedWin32;
57
+ }
58
+
59
+ export function wideString(value: string): Uint8Array {
60
+ return Uint8Array.from(Buffer.from(`${value}\0`, "utf16le"));
61
+ }
62
+
63
+ function readWindowProcessId(win32: Win32, windowHandle: Pointer): number {
64
+ const processIdBuffer = new Uint32Array(1);
65
+ win32.symbols.GetWindowThreadProcessId(windowHandle, ptr(processIdBuffer));
66
+ return processIdBuffer[0] ?? 0;
67
+ }
68
+
69
+ export function findCurrentProcessWindow(win32: Win32, title: string): Pointer | null {
70
+ const titleBuffer = wideString(title);
71
+ const windowHandle = win32.symbols.FindWindowW(null, ptr(titleBuffer));
72
+ if (!windowHandle) return null;
73
+ if (readWindowProcessId(win32, windowHandle) !== process.pid) return null;
74
+ return windowHandle;
75
+ }
@@ -16,7 +16,7 @@ import { ELECTROBUN_CONTEXT_MENU_ACTION, type DesktopRestartMessage, type Electr
16
16
  import { decodeRpcValue, encodeRpcValue } from "../view/rpc-codec";
17
17
  import { contextMenuSelectionMessage } from "./context-menu/click";
18
18
  import type { DesktopWorkspace } from "./desktop/workspace";
19
- import { buildApplicationMenu } from "./application-menu";
19
+ import { buildDesktopApplicationMenu } from "./application-menu";
20
20
  import { applicationMenuCommand } from "./application-menu/click";
21
21
  import { registerElectrobunCoreCapabilities } from "./core-capabilities";
22
22
  import { setNativeIbkrGatewayModuleLoader } from "../../../plugins/ibkr/gateway/service";
@@ -25,8 +25,8 @@ import {
25
25
  } from "./desktop/update";
26
26
  import { DesktopCapabilityBridge } from "./desktop/capability-bridge";
27
27
  import {
28
- DEFAULT_WINDOW_FRAME,
29
28
  MAIN_WINDOW_MIN_SIZE,
29
+ defaultMainWindowFrame,
30
30
  normalizeWindowFrameWithMinimum,
31
31
  } from "./window/frame";
32
32
  import { MAIN_WINDOW_RPC_KEY } from "./window/focus";
@@ -47,6 +47,14 @@ import { handleDesktopHostRequest } from "./desktop/host-requests";
47
47
  import { handleDesktopWorkspaceRequest } from "./desktop/workspace/requests";
48
48
  import { handleDesktopBackendRequest } from "./desktop/backend-requests";
49
49
  import { initializeDesktopBackend } from "./desktop/initialization";
50
+ import { applyWindowsCustomChrome } from "./desktop/windows-custom-chrome";
51
+ import { applyWindowsWindowIcon } from "./desktop/windows-icons";
52
+ import {
53
+ desktopTitleBarStyle,
54
+ desktopWindowRenderer,
55
+ desktopWindowStyleMask,
56
+ } from "./desktop/window-style";
57
+ import { applyDesktopWindowControl, type DesktopWindowControlAction } from "./desktop/window-controls";
50
58
 
51
59
  type DesktopRpc = ReturnType<typeof BrowserView.defineRPC<ElectrobunDesktopRpcSchema>>;
52
60
 
@@ -273,6 +281,18 @@ function closeAllDetachedWindows(): void {
273
281
  detachedWindowManager.closeAll();
274
282
  }
275
283
 
284
+ function controlWindowForRpcKey(windowKey: string | undefined, action: DesktopWindowControlAction): boolean {
285
+ const targetWindow = windowKey === MAIN_WINDOW_RPC_KEY
286
+ ? mainWindow
287
+ : detachedWindowManager.getWindowForRpcKey(windowKey);
288
+ if (!targetWindow) return false;
289
+ if (action !== "close") {
290
+ detachedWindowManager.suppressAutoDockForRpcKey(windowKey);
291
+ }
292
+ applyDesktopWindowControl(targetWindow, action);
293
+ return true;
294
+ }
295
+
276
296
  async function initialize(
277
297
  rpc: DesktopRpc,
278
298
  payload: Record<string, unknown>,
@@ -343,6 +363,7 @@ async function handleBackendRequest(
343
363
  restartDesktopApp,
344
364
  rpc,
345
365
  teardownServices,
366
+ controlWindowForRpcKey,
346
367
  trackContextMenuRequest: (requestId, targetRpc) => {
347
368
  contextMenuRequestRpcs.clear();
348
369
  contextMenuRequestRpcs.set(requestId, targetRpc);
@@ -383,7 +404,7 @@ async function handleBackendRequest(
383
404
  }
384
405
 
385
406
  function installApplicationMenu() {
386
- ApplicationMenu.setApplicationMenu(buildApplicationMenu());
407
+ ApplicationMenu.setApplicationMenu(buildDesktopApplicationMenu());
387
408
  }
388
409
 
389
410
  function createWindowRpc(key: string): DesktopRpc {
@@ -432,18 +453,26 @@ ApplicationMenu.on("application-menu-clicked", (event: unknown) => {
432
453
  installApplicationMenu();
433
454
 
434
455
  const mainRpc = createWindowRpc(MAIN_WINDOW_RPC_KEY);
456
+ const initialMainWindowFrame = normalizeWindowFrameWithMinimum(
457
+ defaultMainWindowFrame(),
458
+ defaultMainWindowFrame(),
459
+ MAIN_WINDOW_MIN_SIZE,
460
+ );
435
461
 
436
462
  mainWindow = new BrowserWindow({
437
463
  title: "Gloomberb",
438
- frame: normalizeWindowFrameWithMinimum(DEFAULT_WINDOW_FRAME, DEFAULT_WINDOW_FRAME, MAIN_WINDOW_MIN_SIZE),
464
+ frame: initialMainWindowFrame,
439
465
  url: "views://mainview/index.html",
440
- renderer: "native",
466
+ renderer: desktopWindowRenderer(),
441
467
  rpc: mainRpc,
442
- titleBarStyle: "hiddenInset",
468
+ styleMask: desktopWindowStyleMask(),
469
+ titleBarStyle: desktopTitleBarStyle(),
443
470
  navigationRules: JSON.stringify(["views://*"]),
444
471
  sandbox: false,
445
472
  });
446
- updateWindowFrameCache(mainWindow, DEFAULT_WINDOW_FRAME, MAIN_WINDOW_MIN_SIZE);
473
+ applyWindowsWindowIcon("Gloomberb");
474
+ applyWindowsCustomChrome("Gloomberb");
475
+ updateWindowFrameCache(mainWindow, initialMainWindowFrame, MAIN_WINDOW_MIN_SIZE);
447
476
  detachedWindowManager.focusWindowForRpcKey(MAIN_WINDOW_RPC_KEY);
448
477
  (mainWindow as any).on?.("move", (event: WindowMoveEvent) => {
449
478
  applyWindowMoveEvent(mainWindow, event);
@@ -1,8 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  DEFAULT_WINDOW_FRAME,
4
+ DEFAULT_WINDOWS_WINDOW_FRAME,
4
5
  MAIN_WINDOW_MIN_SIZE,
5
6
  constrainWindowFrame,
7
+ defaultMainWindowFrame,
6
8
  normalizeWindowFrameWithMinimum,
7
9
  } from "./frame";
8
10
 
@@ -37,4 +39,9 @@ describe("electrobun window frames", () => {
37
39
  test("uses the default frame when no frame data is available", () => {
38
40
  expect(normalizeWindowFrameWithMinimum(null)).toEqual(DEFAULT_WINDOW_FRAME);
39
41
  });
42
+
43
+ test("uses a compact default main window frame on Windows", () => {
44
+ expect(defaultMainWindowFrame("win32")).toEqual(DEFAULT_WINDOWS_WINDOW_FRAME);
45
+ expect(defaultMainWindowFrame("darwin")).toEqual(DEFAULT_WINDOW_FRAME);
46
+ });
40
47
  });
@@ -11,11 +11,16 @@ export interface WindowMinimumSize {
11
11
  }
12
12
 
13
13
  export const DEFAULT_WINDOW_FRAME: WindowFrame = { x: 64, y: 48, width: 1440, height: 920 };
14
+ export const DEFAULT_WINDOWS_WINDOW_FRAME: WindowFrame = { x: 32, y: 32, width: 960, height: 680 };
14
15
  // Emergency fallback until Electrobun exposes stable native minimum-size handling:
15
16
  // https://github.com/blackboardsh/electrobun/issues/188
16
17
  export const MAIN_WINDOW_MIN_SIZE: WindowMinimumSize = { width: 640, height: 400 };
17
18
  export const DETACHED_WINDOW_MIN_SIZE: WindowMinimumSize = { width: 360, height: 240 };
18
19
 
20
+ export function defaultMainWindowFrame(platform = process.platform): WindowFrame {
21
+ return platform === "win32" ? DEFAULT_WINDOWS_WINDOW_FRAME : DEFAULT_WINDOW_FRAME;
22
+ }
23
+
19
24
  export function normalizeWindowFrame(
20
25
  frame: Partial<WindowFrame> | null | undefined,
21
26
  fallback: WindowFrame = DEFAULT_WINDOW_FRAME,