gloomberb 0.9.1 → 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 (33) hide show
  1. package/package.json +1 -1
  2. package/src/components/chart/comparison-stock-chart/helpers.ts +0 -32
  3. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
  4. package/src/components/chart/comparison-stock-chart/legend.tsx +41 -44
  5. package/src/components/chart/comparison-stock-chart/view.tsx +46 -6
  6. package/src/components/layout/detached-pane-shell.tsx +5 -2
  7. package/src/components/layout/header.tsx +13 -5
  8. package/src/components/layout/window-controls.tsx +19 -10
  9. package/src/components/price-performance.tsx +175 -0
  10. package/src/market-data/performance.test.ts +54 -0
  11. package/src/market-data/performance.ts +109 -0
  12. package/src/plugins/builtin/comparison-chart/index.test.tsx +88 -9
  13. package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
  14. package/src/plugins/builtin/congress-trades/footer.ts +14 -8
  15. package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
  16. package/src/plugins/builtin/congress-trades/model.ts +29 -0
  17. package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
  18. package/src/plugins/builtin/congress-trades/table.tsx +10 -1
  19. package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
  20. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
  21. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +9 -2
  22. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  23. package/src/renderers/electrobun/bun/desktop/window-controls.ts +11 -2
  24. package/src/renderers/electrobun/bun/desktop/window-style.ts +27 -1
  25. package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
  26. package/src/renderers/electrobun/bun/desktop/windows-icons.ts +11 -64
  27. package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
  28. package/src/renderers/electrobun/bun/index.ts +9 -2
  29. package/src/renderers/electrobun/shared/protocol.ts +1 -0
  30. package/src/renderers/electrobun/view/main.tsx +2 -1
  31. package/src/renderers/electrobun/view/styles.css +9 -8
  32. package/src/renderers/electrobun/view/ui-host.tsx +102 -77
  33. package/src/version.ts +1 -1
@@ -1,14 +1,12 @@
1
- import { useCallback, type RefObject } from "react";
1
+ import { useCallback } from "react";
2
2
  import { type DataTableKeyEvent } from "../../../components";
3
3
  import { useShortcut } from "../../../react/input";
4
- import { type ScrollBoxRenderable } from "../../../ui";
5
4
  import { isPlainKey } from "../../../utils/keyboard";
6
5
  import type { CongressTab, DetailMode } from "./model";
7
6
 
8
7
  export function useCongressTradesKeyboard({
9
8
  activeTab,
10
9
  detailMode,
11
- detailScrollRef,
12
10
  focused,
13
11
  load,
14
12
  openSelectedTicker,
@@ -18,7 +16,6 @@ export function useCongressTradesKeyboard({
18
16
  }: {
19
17
  activeTab: CongressTab;
20
18
  detailMode: DetailMode;
21
- detailScrollRef: RefObject<ScrollBoxRenderable | null>;
22
19
  focused: boolean;
23
20
  load: (refresh?: boolean) => void;
24
21
  openSelectedTicker: () => void;
@@ -26,25 +23,9 @@ export function useCongressTradesKeyboard({
26
23
  openSelectedTradeSource: () => void;
27
24
  selectTab: (tab: string) => void;
28
25
  }) {
29
- const scrollDetailBy = useCallback((delta: number) => {
30
- const scrollBox = detailScrollRef.current;
31
- if (!scrollBox?.viewport) return;
32
- const maxScrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height);
33
- scrollBox.scrollTop = Math.max(0, Math.min(maxScrollTop, scrollBox.scrollTop + delta));
34
- }, [detailScrollRef]);
35
-
36
26
  const handleDetailKeyDown = useCallback((event: DataTableKeyEvent) => {
37
- if (isPlainKey(event, "j", "down")) {
38
- event.preventDefault?.();
39
- event.stopPropagation?.();
40
- scrollDetailBy(1);
41
- return true;
42
- }
43
- if (isPlainKey(event, "k", "up")) {
44
- event.preventDefault?.();
45
- event.stopPropagation?.();
46
- scrollDetailBy(-1);
47
- return true;
27
+ if (detailMode?.kind === "member") {
28
+ return false;
48
29
  }
49
30
  if (isPlainKey(event, "o")) {
50
31
  event.preventDefault?.();
@@ -65,7 +46,7 @@ export function useCongressTradesKeyboard({
65
46
  return true;
66
47
  }
67
48
  return false;
68
- }, [openSelectedTicker, openSelectedTradeMember, openSelectedTradeSource, scrollDetailBy]);
49
+ }, [detailMode?.kind, openSelectedTicker, openSelectedTradeMember, openSelectedTradeSource]);
69
50
 
70
51
  const handleRootKeyDown = useCallback((event: DataTableKeyEvent) => {
71
52
  if (isPlainKey(event, "r")) {
@@ -8,6 +8,8 @@ import type {
8
8
  export const CONGRESS_TRADES_PANE_ID = "congress-trades";
9
9
  export const CONGRESS_TRADE_LIMIT = 200;
10
10
  export const CONGRESS_FILING_LIMIT = 20;
11
+ export const CONGRESS_MEMBER_TRADE_LIMIT = 2000;
12
+ export const CONGRESS_MEMBER_FILING_LIMIT = 500;
11
13
 
12
14
  export type CongressTab = "trades" | "members";
13
15
  export type LoadStatus = "idle" | "loading" | "loaded" | "error";
@@ -25,6 +27,7 @@ export type TradeColumnId =
25
27
  | "side"
26
28
  | "ticker"
27
29
  | "amount"
30
+ | "asset"
28
31
  | "owner";
29
32
  export type TradeColumn = DataTableColumn & { id: TradeColumnId };
30
33
  export type MemberColumnId =
@@ -104,6 +107,8 @@ function compareTrade(
104
107
  return compareText(left.ticker ?? "", right.ticker ?? "");
105
108
  case "amount":
106
109
  return (left.amountHigh ?? left.amountLow ?? 0) - (right.amountHigh ?? right.amountLow ?? 0);
110
+ case "asset":
111
+ return compareText(left.assetName, right.assetName);
107
112
  case "owner":
108
113
  return compareText(left.owner, right.owner);
109
114
  }
@@ -172,6 +177,30 @@ export function buildTradeColumns(width: number): TradeColumn[] {
172
177
  ];
173
178
  }
174
179
 
180
+ export function buildMemberTradeColumns(width: number): TradeColumn[] {
181
+ const filedWidth = 7;
182
+ const txWidth = 7;
183
+ const sideWidth = 8;
184
+ const tickerWidth = 12;
185
+ const amountWidth = 14;
186
+ const ownerWidth = 8;
187
+ const lagWidth = 5;
188
+ const assetWidth = Math.max(
189
+ 18,
190
+ width - filedWidth - txWidth - sideWidth - tickerWidth - amountWidth - ownerWidth - lagWidth - 9,
191
+ );
192
+ return [
193
+ { id: "filed", label: "FILED", width: filedWidth, align: "left" },
194
+ { id: "tx", label: "TX", width: txWidth, align: "left" },
195
+ { id: "side", label: "SIDE", width: sideWidth, align: "left" },
196
+ { id: "ticker", label: "TICKER", width: tickerWidth, align: "left" },
197
+ { id: "amount", label: "AMOUNT", width: amountWidth, align: "right" },
198
+ { id: "asset", label: "ASSET", width: assetWidth, align: "left" },
199
+ { id: "owner", label: "OWNER", width: ownerWidth, align: "left" },
200
+ { id: "lag", label: "LAG", width: lagWidth, align: "right" },
201
+ ];
202
+ }
203
+
175
204
  export function buildMemberColumns(width: number): MemberColumn[] {
176
205
  const districtWidth = 6;
177
206
  const tradesWidth = 7;
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { Box, useRendererHost, type ScrollBoxRenderable } from "../../../ui";
2
+ import { Box, useRendererHost } from "../../../ui";
3
3
  import {
4
4
  DataTableStackView,
5
5
  EmptyState,
@@ -17,6 +17,7 @@ import {
17
17
  import type { PaneProps } from "../../../types/plugin";
18
18
  import {
19
19
  CONGRESS_FILING_LIMIT,
20
+ CONGRESS_MEMBER_FILING_LIMIT,
20
21
  CONGRESS_TRADE_LIMIT,
21
22
  CONGRESS_TRADES_PANE_ID,
22
23
  buildMemberColumns,
@@ -34,7 +35,7 @@ import {
34
35
  type TradeColumn,
35
36
  type TradeColumnId,
36
37
  } from "./model";
37
- import { MemberTradeList, TradeDetail } from "./detail";
38
+ import { MemberTradesDetail, TradeDetail } from "./detail";
38
39
  import { useCongressTradesFooter } from "./footer";
39
40
  import { useCongressTradesKeyboard } from "./keyboard";
40
41
  import {
@@ -61,7 +62,6 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
61
62
  columnId: "trades",
62
63
  direction: "desc",
63
64
  });
64
- const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
65
65
  const fetchGenRef = useRef(0);
66
66
 
67
67
  const load = useCallback((refresh = false) => {
@@ -142,12 +142,6 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
142
142
  if (detailMode?.kind === "member" && !detailMember) setDetailMode(null);
143
143
  }, [detailMember, detailMode, detailTrade]);
144
144
 
145
- useEffect(() => {
146
- if (!detailMode) return;
147
- const scrollBox = detailScrollRef.current;
148
- if (scrollBox) scrollBox.scrollTop = 0;
149
- }, [detailMode]);
150
-
151
145
  const selectTab = useCallback((tab: string) => {
152
146
  setActiveTab(tab === "members" ? "members" : "trades");
153
147
  setDetailMode(null);
@@ -176,7 +170,6 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
176
170
  const { handleDetailKeyDown, handleRootKeyDown } = useCongressTradesKeyboard({
177
171
  activeTab,
178
172
  detailMode,
179
- detailScrollRef,
180
173
  focused,
181
174
  load,
182
175
  openSelectedTicker,
@@ -187,6 +180,7 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
187
180
 
188
181
  useCongressTradesFooter({
189
182
  activeTab,
183
+ detailMode,
190
184
  detailTrade,
191
185
  error,
192
186
  load,
@@ -201,11 +195,12 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
201
195
  const detailContent = detailTrade ? (
202
196
  <TradeDetail trade={detailTrade} width={width} />
203
197
  ) : detailMember ? (
204
- <MemberTradeList
198
+ <MemberTradesDetail
199
+ focused={focused}
205
200
  member={detailMember}
206
- trades={detailMemberTrades}
201
+ initialTrades={detailMemberTrades}
207
202
  width={width}
208
- scrollRef={detailScrollRef}
203
+ filingLimit={payload?.filingCount ?? CONGRESS_MEMBER_FILING_LIMIT}
209
204
  />
210
205
  ) : null;
211
206
  const detailTitle = detailTrade
@@ -12,7 +12,14 @@ import {
12
12
  type MemberColumn,
13
13
  type TradeColumn,
14
14
  } from "./model";
15
- import { sideColor } from "./detail";
15
+
16
+ export function sideColor(side: CloudCongressTradePayload["side"], selected: boolean): string {
17
+ if (selected) return colors.selectedText;
18
+ if (side === "BUY") return colors.positive;
19
+ if (side === "SELL") return colors.negative;
20
+ if (side === "EXCHANGE") return colors.warning;
21
+ return colors.textDim;
22
+ }
16
23
 
17
24
  export function renderCongressTradeCell(
18
25
  trade: CloudCongressTradePayload,
@@ -51,6 +58,8 @@ export function renderCongressTradeCell(
51
58
  text: formatAmountRange(trade.amountLow, trade.amountHigh, trade.amount),
52
59
  color: selectedColor ?? colors.textBright,
53
60
  };
61
+ case "asset":
62
+ return { text: trade.assetName, color: selectedColor ?? colors.text };
54
63
  case "owner":
55
64
  return { text: trade.owner, color: selectedColor ?? colors.textDim };
56
65
  }
@@ -90,20 +90,6 @@ export function buildOverviewStats({
90
90
  valueColor: priceColor(fundamentals.revenueGrowth),
91
91
  });
92
92
  }
93
- if (fundamentals?.return1Y != null) {
94
- stats.push({
95
- label: "1Y Return",
96
- value: formatPercent(fundamentals.return1Y),
97
- valueColor: priceColor(fundamentals.return1Y),
98
- });
99
- }
100
- if (fundamentals?.return3Y != null) {
101
- stats.push({
102
- label: "3Y Return",
103
- value: formatPercent(fundamentals.return3Y),
104
- valueColor: priceColor(fundamentals.return3Y),
105
- });
106
- }
107
93
  if (fundamentals?.enterpriseValue) {
108
94
  stats.push({ label: "EV", value: formatCompact(fundamentals.enterpriseValue) });
109
95
  }
@@ -15,6 +15,11 @@ import {
15
15
  import { selectEffectiveExchangeRates } from "../../../utils/exchange-rate-map";
16
16
  import { EmptyState } from "../../../components";
17
17
  import { ResolvedStockChart } from "../../../components/chart/stock-chart";
18
+ import { PriceReturnStrip } from "../../../components/price-performance";
19
+ import {
20
+ appendQuoteToPriceReturnHistory,
21
+ buildPriceReturnFields,
22
+ } from "../../../market-data/performance";
18
23
  import type { TickerFinancials } from "../../../types/financials";
19
24
  import type { TickerRecord } from "../../../types/ticker";
20
25
  import {
@@ -96,6 +101,19 @@ export function OverviewTab({
96
101
  baseCurrency,
97
102
  toBase,
98
103
  });
104
+ const performanceFields = buildPriceReturnFields(
105
+ appendQuoteToPriceReturnHistory(financials?.priceHistory ?? [], quote),
106
+ ).map((field) => {
107
+ if (field.value != null) return field;
108
+ if (field.id === "1Y" && fundamentals?.return1Y != null) {
109
+ return { ...field, value: fundamentals.return1Y };
110
+ }
111
+ if (field.id === "3Y" && fundamentals?.return3Y != null) {
112
+ return { ...field, value: fundamentals.return3Y };
113
+ }
114
+ return field;
115
+ });
116
+ const hasPerformance = performanceFields.some((field) => field.value != null);
99
117
  const positionRows = buildPositionRows({
100
118
  ticker,
101
119
  quote,
@@ -210,6 +228,13 @@ export function OverviewTab({
210
228
  />
211
229
  )}
212
230
 
231
+ {hasPerformance && (
232
+ <Box flexDirection="column">
233
+ <SectionHeader title="Price Return" />
234
+ <PriceReturnStrip fields={performanceFields} width={contentWidth} />
235
+ </Box>
236
+ )}
237
+
213
238
  {stats.length > 0 && (
214
239
  <Box flexDirection="column">
215
240
  <SectionHeader title="Fundamentals" />
@@ -23,8 +23,13 @@ import {
23
23
  type WindowResizeEvent,
24
24
  } from "./window-events";
25
25
  import type { DesktopStateBroadcaster, DesktopStateRpc } from "./state-broadcaster";
26
+ import { applyWindowsCustomChrome } from "./windows-custom-chrome";
26
27
  import { applyWindowsWindowIcon } from "./windows-icons";
27
- import { desktopTitleBarStyle } from "./window-style";
28
+ import {
29
+ desktopTitleBarStyle,
30
+ desktopWindowRenderer,
31
+ desktopWindowStyleMask,
32
+ } from "./window-style";
28
33
 
29
34
  const INITIAL_DOCK_SUPPRESSION_MS = 800;
30
35
  const WINDOW_CONTROL_DOCK_SUPPRESSION_MS = 5_000;
@@ -188,13 +193,15 @@ export class DesktopDetachedWindowManager<Rpc extends DesktopStateRpc> {
188
193
  title,
189
194
  frame: initialFrame,
190
195
  url: "views://mainview/index.html",
191
- renderer: "native",
196
+ renderer: desktopWindowRenderer(),
192
197
  rpc: rpc as never,
198
+ styleMask: desktopWindowStyleMask(),
193
199
  titleBarStyle: desktopTitleBarStyle(),
194
200
  navigationRules: JSON.stringify(["views://*"]),
195
201
  sandbox: false,
196
202
  });
197
203
  applyWindowsWindowIcon(title);
204
+ applyWindowsCustomChrome(title);
198
205
  updateWindowFrameCache(window, initialFrame, DETACHED_WINDOW_MIN_SIZE);
199
206
  this.windows.set(instanceId, window);
200
207
  this.suppressDockUntil.set(instanceId, Date.now() + INITIAL_DOCK_SUPPRESSION_MS);
@@ -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
  };
@@ -22,7 +22,16 @@ const maximizedWindowRestoreFrames = new WeakMap<ControllableDesktopWindow, Desk
22
22
  function normalizeFrame(frame: Partial<DesktopWindowFrame> | null | undefined): DesktopWindowFrame | null {
23
23
  if (!frame) return null;
24
24
  const { x, y, width, height } = frame;
25
- if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) {
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
+ ) {
26
35
  return null;
27
36
  }
28
37
  return { x, y, width, height };
@@ -56,7 +65,7 @@ export function applyDesktopWindowControl(
56
65
  if (maximizedWindowRestoreFrames.has(window)) {
57
66
  maximizedWindowRestoreFrames.delete(window);
58
67
  window.unmaximize?.();
59
- restoreWindowFrame(window, restoreFrame);
68
+ restoreWindowFrame(window, restoreFrame ?? null);
60
69
  return;
61
70
  }
62
71
  maximizedWindowRestoreFrames.set(window, readWindowFrame(window));
@@ -1,5 +1,31 @@
1
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
+ };
2
10
 
3
11
  export function desktopTitleBarStyle(): DesktopTitleBarStyle {
4
- return process.platform === "darwin" ? "hiddenInset" : "hidden";
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
+ };
5
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
+ }
@@ -1,6 +1,13 @@
1
1
  import { existsSync } from "fs";
2
2
  import { resolve } from "path";
3
- import { dlopen, FFIType, ptr, type Pointer } from "bun:ffi";
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";
4
11
 
5
12
  const WM_SETICON = 0x0080;
6
13
  const ICON_SMALL = 0;
@@ -10,70 +17,10 @@ const LR_LOADFROMFILE = 0x0010;
10
17
  const LR_DEFAULTSIZE = 0x0040;
11
18
  const GCLP_HICON = -14;
12
19
  const GCLP_HICONSM = -34;
13
- const MAX_ATTEMPTS = 20;
14
- const RETRY_DELAY_MS = 100;
15
-
16
- type Win32 = ReturnType<typeof loadWin32>;
17
-
18
- let cachedWin32: Win32 | null | undefined;
20
+ type Win32 = NonNullable<ReturnType<typeof win32OrNull>>;
19
21
  let smallIconHandle: Pointer | null = null;
20
22
  let bigIconHandle: Pointer | null = null;
21
23
 
22
- function loadWin32() {
23
- return dlopen("user32.dll", {
24
- FindWindowW: {
25
- args: [FFIType.ptr, FFIType.ptr],
26
- returns: FFIType.ptr,
27
- },
28
- GetWindowThreadProcessId: {
29
- args: [FFIType.ptr, FFIType.ptr],
30
- returns: FFIType.u32,
31
- },
32
- LoadImageW: {
33
- args: [FFIType.ptr, FFIType.ptr, FFIType.u32, FFIType.int, FFIType.int, FFIType.u32],
34
- returns: FFIType.ptr,
35
- },
36
- SendMessageW: {
37
- args: [FFIType.ptr, FFIType.u32, FFIType.u64, FFIType.ptr],
38
- returns: FFIType.ptr,
39
- },
40
- SetClassLongPtrW: {
41
- args: [FFIType.ptr, FFIType.int, FFIType.ptr],
42
- returns: FFIType.ptr,
43
- },
44
- });
45
- }
46
-
47
- 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
- function wideString(value: string): Buffer {
60
- return 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
- 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
- }
76
-
77
24
  function loadIconHandle(win32: Win32, iconPath: string, size: number): Pointer | null {
78
25
  const iconPathBuffer = wideString(iconPath);
79
26
  const iconHandle = win32.symbols.LoadImageW(
@@ -117,7 +64,7 @@ export function applyWindowsWindowIcon(title: string, attempt = 1): void {
117
64
  const windowHandle = findCurrentProcessWindow(win32, title);
118
65
  if (windowHandle && setWindowIcon(win32, windowHandle)) return;
119
66
 
120
- if (attempt < MAX_ATTEMPTS) {
121
- setTimeout(() => applyWindowsWindowIcon(title, attempt + 1), RETRY_DELAY_MS);
67
+ if (attempt < WINDOWS_HANDLE_MAX_ATTEMPTS) {
68
+ setTimeout(() => applyWindowsWindowIcon(title, attempt + 1), WINDOWS_HANDLE_RETRY_DELAY_MS);
122
69
  }
123
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
+ }
@@ -47,8 +47,13 @@ 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";
50
51
  import { applyWindowsWindowIcon } from "./desktop/windows-icons";
51
- import { desktopTitleBarStyle } from "./desktop/window-style";
52
+ import {
53
+ desktopTitleBarStyle,
54
+ desktopWindowRenderer,
55
+ desktopWindowStyleMask,
56
+ } from "./desktop/window-style";
52
57
  import { applyDesktopWindowControl, type DesktopWindowControlAction } from "./desktop/window-controls";
53
58
 
54
59
  type DesktopRpc = ReturnType<typeof BrowserView.defineRPC<ElectrobunDesktopRpcSchema>>;
@@ -458,13 +463,15 @@ mainWindow = new BrowserWindow({
458
463
  title: "Gloomberb",
459
464
  frame: initialMainWindowFrame,
460
465
  url: "views://mainview/index.html",
461
- renderer: "native",
466
+ renderer: desktopWindowRenderer(),
462
467
  rpc: mainRpc,
468
+ styleMask: desktopWindowStyleMask(),
463
469
  titleBarStyle: desktopTitleBarStyle(),
464
470
  navigationRules: JSON.stringify(["views://*"]),
465
471
  sandbox: false,
466
472
  });
467
473
  applyWindowsWindowIcon("Gloomberb");
474
+ applyWindowsCustomChrome("Gloomberb");
468
475
  updateWindowFrameCache(mainWindow, initialMainWindowFrame, MAIN_WINDOW_MIN_SIZE);
469
476
  detachedWindowManager.focusWindowForRpcKey(MAIN_WINDOW_RPC_KEY);
470
477
  (mainWindow as any).on?.("move", (event: WindowMoveEvent) => {
@@ -14,6 +14,7 @@ export interface ElectrobunBackendInit {
14
14
  desktopThemePreview: DesktopThemePreviewState;
15
15
  pluginState: Record<string, Record<string, unknown>>;
16
16
  capabilityManifests: CapabilityManifest[];
17
+ desktopPlatform: string;
17
18
  windowKind: "main" | "detached";
18
19
  paneId?: string;
19
20
  }
@@ -19,7 +19,7 @@ import { DesktopFatalScreen, ElectrobunErrorBoundary } from "./fatal-screen";
19
19
  import { WebInputHostProvider } from "./input-host";
20
20
  import { webNativeRenderer } from "./native-renderer";
21
21
  import { WebToastHostProvider } from "./toast-host";
22
- import { webRendererHost, webUiHost } from "./ui-host";
22
+ import { createWebUiHost, webRendererHost } from "./ui-host";
23
23
  import { createApplicationMenuBridge } from "./application-menu-bridge";
24
24
  import { createDesktopWindowBridge } from "./desktop/window/bridge";
25
25
  import { prepareDetachedSnapshot } from "./desktop/window/snapshot";
@@ -88,6 +88,7 @@ async function boot() {
88
88
  const config = desktopSnapshot?.config ?? init.config;
89
89
  const desktopWindowBridge = createDesktopWindowBridge(init.windowKind, init.paneId);
90
90
  const desktopApplicationMenuBridge = createApplicationMenuBridge();
91
+ const webUiHost = createWebUiHost(init.desktopPlatform);
91
92
  measurePerfAsync("startup.electrobun.root-render", async () => {
92
93
  root.render(
93
94
  <ElectrobunErrorBoundary>