gloomberb 0.9.1 → 0.9.3

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 (136) hide show
  1. package/README.md +23 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app.tsx +10 -7
  5. package/src/capabilities/registry.ts +1 -0
  6. package/src/capabilities/types.ts +23 -0
  7. package/src/cli/commands/api.ts +119 -0
  8. package/src/cli/commands/automation.ts +173 -0
  9. package/src/cli/commands/command-utils.ts +53 -0
  10. package/src/cli/commands/market.ts +527 -0
  11. package/src/cli/commands/overview.ts +222 -0
  12. package/src/cli/commands/search.ts +39 -0
  13. package/src/cli/commands/system.ts +553 -0
  14. package/src/cli/commands/ticker.ts +104 -12
  15. package/src/cli/context.ts +35 -2
  16. package/src/cli/entry.ts +51 -0
  17. package/src/cli/errors.ts +56 -5
  18. package/src/cli/index.ts +73 -11
  19. package/src/cli/options.test.ts +38 -0
  20. package/src/cli/options.ts +95 -0
  21. package/src/cli/pane-functions/index.ts +10 -1
  22. package/src/cli/registry.test.ts +40 -0
  23. package/src/cli/registry.ts +46 -5
  24. package/src/cli/result.test.ts +82 -0
  25. package/src/cli/result.ts +149 -0
  26. package/src/cli/types.ts +11 -0
  27. package/src/cli.test.ts +11 -11
  28. package/src/components/chart/comparison-stock-chart/helpers.ts +0 -32
  29. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  30. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
  31. package/src/components/chart/comparison-stock-chart/legend.tsx +45 -44
  32. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  33. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  34. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  35. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  36. package/src/components/chart/comparison-stock-chart/view.tsx +56 -6
  37. package/src/components/chart/core/pane-settings.test.ts +27 -0
  38. package/src/components/chart/core/pane-settings.ts +60 -1
  39. package/src/components/chart/core/pointer.ts +2 -0
  40. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  41. package/src/components/chart/stock-chart/settings.ts +7 -3
  42. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  43. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  44. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  45. package/src/components/data-table/view.tsx +13 -2
  46. package/src/components/feed-data-table/stack-view.tsx +9 -1
  47. package/src/components/index.ts +1 -1
  48. package/src/components/input-search-bar.test.tsx +103 -0
  49. package/src/components/input-search-bar.tsx +16 -0
  50. package/src/components/layout/detached-pane-shell.tsx +5 -2
  51. package/src/components/layout/header.tsx +13 -5
  52. package/src/components/layout/shell/active-drag.ts +5 -1
  53. package/src/components/layout/shell/drag/runtime.ts +21 -3
  54. package/src/components/layout/shell/fullscreen.ts +10 -0
  55. package/src/components/layout/shell/index.test.tsx +395 -2
  56. package/src/components/layout/shell/index.tsx +128 -2
  57. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  58. package/src/components/layout/shell/pane/layers.tsx +26 -14
  59. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  60. package/src/components/layout/shell/shortcuts.ts +4 -0
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  62. package/src/components/layout/shell/window-mode/index.ts +11 -3
  63. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  64. package/src/components/layout/status-bar.test.tsx +105 -0
  65. package/src/components/layout/status-bar.tsx +32 -6
  66. package/src/components/layout/transient-layout.tsx +57 -0
  67. package/src/components/layout/window-controls.tsx +19 -10
  68. package/src/components/layout/window-edit/mode.test.ts +76 -5
  69. package/src/components/layout/window-edit/mode.ts +47 -2
  70. package/src/components/layout/window-edit/presentation.ts +2 -1
  71. package/src/components/price-performance.tsx +183 -0
  72. package/src/core/app-services.ts +7 -4
  73. package/src/data/config/store/index.ts +1 -2
  74. package/src/market-data/performance.test.ts +54 -0
  75. package/src/market-data/performance.ts +109 -0
  76. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  77. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  78. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  79. package/src/plugins/builtin/comparison-chart/index.test.tsx +218 -15
  80. package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
  81. package/src/plugins/builtin/congress-trades/footer.ts +14 -8
  82. package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
  83. package/src/plugins/builtin/congress-trades/model.ts +29 -0
  84. package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
  85. package/src/plugins/builtin/congress-trades/table.tsx +10 -1
  86. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  87. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  88. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  89. package/src/plugins/builtin/portfolio-list/pane/index.tsx +1 -1
  90. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  91. package/src/plugins/builtin/shared/read-state.ts +43 -0
  92. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  93. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  94. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  95. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  96. package/src/plugins/builtin/substack/pane.tsx +8 -3
  97. package/src/plugins/builtin/substack/read-state.ts +55 -0
  98. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  99. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  100. package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
  101. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
  102. package/src/plugins/pane-manager/docking.ts +9 -2
  103. package/src/plugins/pane-manager/queries.ts +8 -2
  104. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  105. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  106. package/src/plugins/prediction-markets/pane.test.tsx +32 -0
  107. package/src/plugins/prediction-markets/pane.tsx +21 -1
  108. package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
  109. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  110. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  111. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  112. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  113. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +9 -2
  114. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  115. package/src/renderers/electrobun/bun/desktop/window-controls.ts +11 -2
  116. package/src/renderers/electrobun/bun/desktop/window-style.ts +27 -1
  117. package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
  118. package/src/renderers/electrobun/bun/desktop/windows-icons.ts +11 -64
  119. package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
  120. package/src/renderers/electrobun/bun/index.ts +22 -2
  121. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  122. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  123. package/src/renderers/electrobun/shared/protocol.ts +1 -0
  124. package/src/renderers/electrobun/view/app-services.ts +6 -4
  125. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  126. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  127. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  128. package/src/renderers/electrobun/view/main.tsx +2 -1
  129. package/src/renderers/electrobun/view/styles.css +9 -8
  130. package/src/renderers/electrobun/view/ui-host.tsx +102 -77
  131. package/src/renderers/opentui/start.tsx +13 -5
  132. package/src/state/app/input-capture.ts +12 -0
  133. package/src/types/plugin.ts +28 -0
  134. package/src/utils/cli-output.ts +6 -0
  135. package/src/utils/search-focus-navigation.ts +24 -0
  136. package/src/version.ts +1 -1
@@ -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
  }
@@ -7,7 +7,7 @@ import type { AppServices } from "../../../core/app-services";
7
7
  import type { AppConfig } from "../../../types/config";
8
8
  import { newsProvider } from "../../../capabilities";
9
9
  import { debugLog } from "../../../utils/debug-log";
10
- import { measurePerf } from "../../../utils/perf-marks";
10
+ import { measurePerf, measurePerfAsync } from "../../../utils/perf-marks";
11
11
  import { getRendererBuiltinPlugins } from "../../../plugins/catalog-ui";
12
12
  import { createRemoteAssetDataClient } from "./remote/asset-data-client";
13
13
  import { RemotePersistence } from "./remote/persistence";
@@ -47,10 +47,11 @@ export function createAppServices({ config }: { config: AppConfig }): AppService
47
47
  }));
48
48
 
49
49
  const plugins = getRendererBuiltinPlugins();
50
+ const pluginReadyPromises: Promise<void>[] = [];
50
51
  for (const plugin of plugins) {
51
- measurePerf("startup.services.register-plugin", () => {
52
- void pluginRegistry.register(plugin);
53
- }, { pluginId: plugin.id });
52
+ pluginReadyPromises.push(measurePerfAsync("startup.services.register-plugin", () => (
53
+ pluginRegistry.register(plugin)
54
+ ), { pluginId: plugin.id }));
54
55
  }
55
56
  measurePerf("startup.services.news-start", () => {
56
57
  newsService.start();
@@ -65,6 +66,7 @@ export function createAppServices({ config }: { config: AppConfig }): AppService
65
66
  marketData,
66
67
  pluginRegistry,
67
68
  newsService,
69
+ ready: Promise.all(pluginReadyPromises).then(() => {}),
68
70
  destroy() {
69
71
  setSharedMarketDataCoordinator(null);
70
72
  setSharedNewsService(null);
@@ -0,0 +1,50 @@
1
+ import { renderAsciiText, type AsciiFontName } from "../../../../ui/ascii-font";
2
+
3
+ const WEB_GLOOMBERB_WORDMARK = [
4
+ " ____ _ _ _ ",
5
+ " / ___| | ___ ___ _ __ ___ | |__ ___ _ __| |__ ",
6
+ "| | _| |/ _ \\ / _ \\| '_ ` _ \\| '_ \\ / _ \\ '__| '_ \\ ",
7
+ "| |_| | | (_) | (_) | | | | | | |_) | __/ | | |_) |",
8
+ " \\____|_|\\___/ \\___/|_| |_| |_|_.__/ \\___|_| |_.__/ ",
9
+ ];
10
+
11
+ type WebWordmarkVariant = "legacy" | "compat" | null;
12
+
13
+ function currentDesktopPlatform(): string {
14
+ const navigatorLike = (globalThis as {
15
+ navigator?: {
16
+ platform?: string;
17
+ userAgent?: string;
18
+ userAgentData?: { platform?: string };
19
+ };
20
+ }).navigator;
21
+ return [
22
+ navigatorLike?.userAgentData?.platform,
23
+ navigatorLike?.platform,
24
+ navigatorLike?.userAgent,
25
+ ].filter(Boolean).join(" ");
26
+ }
27
+
28
+ function isMacDesktopPlatform(desktopPlatform?: string): boolean {
29
+ const raw = desktopPlatform?.trim() || currentDesktopPlatform();
30
+ return /(darwin|mac|iphone|ipad|ipod)/i.test(raw);
31
+ }
32
+
33
+ export function webAsciiTextWordmarkVariant(
34
+ text: string,
35
+ font: AsciiFontName = "tiny",
36
+ desktopPlatform?: string,
37
+ ): WebWordmarkVariant {
38
+ if (font !== "wordmark" || text.trim().toLowerCase() !== "gloomberb") return null;
39
+ return isMacDesktopPlatform(desktopPlatform) ? "legacy" : "compat";
40
+ }
41
+
42
+ export function webAsciiTextLines(
43
+ text: string,
44
+ font: AsciiFontName = "tiny",
45
+ desktopPlatform?: string,
46
+ ): string[] {
47
+ return webAsciiTextWordmarkVariant(text, font, desktopPlatform) === "compat"
48
+ ? WEB_GLOOMBERB_WORDMARK
49
+ : renderAsciiText(text, font);
50
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { renderAsciiText } from "../../../../ui/ascii-font";
3
+ import { webAsciiTextLines, webAsciiTextWordmarkVariant } from "./ascii-text";
4
+
5
+ describe("desktop web ASCII text", () => {
6
+ test("keeps the legacy Gloomberb wordmark on macOS", () => {
7
+ expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "darwin")).toBe("legacy");
8
+ expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "MacIntel")).toBe("legacy");
9
+ expect(webAsciiTextLines("Gloomberb", "wordmark", "darwin")).toEqual(
10
+ renderAsciiText("Gloomberb", "wordmark"),
11
+ );
12
+ });
13
+
14
+ test("uses the web-safe Gloomberb wordmark off macOS", () => {
15
+ const lines = webAsciiTextLines("Gloomberb", "wordmark", "win32");
16
+
17
+ expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "win32")).toBe("compat");
18
+ expect(lines).toHaveLength(5);
19
+ expect(lines[0]).toContain("____");
20
+ expect(lines).not.toEqual(renderAsciiText("Gloomberb", "wordmark"));
21
+ });
22
+ });
@@ -7,21 +7,17 @@ import {
7
7
  type StyledTextChunk,
8
8
  type TextProps,
9
9
  } from "../../../../ui/host";
10
- import { renderAsciiText } from "../../../../ui/ascii-font";
11
10
  import { WEB_CELL_WIDTH } from "../input-host";
11
+ import { webAsciiTextLines, webAsciiTextWordmarkVariant } from "./ascii-text";
12
12
  import { hasDirectMouseHandler, mouseHandlers } from "./mouse";
13
13
  import { cleanDomProps, commonStyle, textStyle } from "./style";
14
14
 
15
- const WEB_GLOOMBERB_WORDMARK = [
16
- " ____ _ _ _ ",
17
- " / ___| | ___ ___ _ __ ___ | |__ ___ _ __| |__ ",
18
- "| | _| |/ _ \\ / _ \\| '_ ` _ \\| '_ \\ / _ \\ '__| '_ \\ ",
19
- "| |_| | | (_) | (_) | | | | | | |_) | __/ | | |_) |",
20
- " \\____|_|\\___/ \\___/|_| |_| |_|_.__/ \\___|_| |_.__/ ",
21
- ];
22
-
23
15
  const WEB_WORDMARK_CHAR_WIDTH_PX = Math.max(10, WEB_CELL_WIDTH);
24
16
 
17
+ interface WebAsciiTextProps extends AsciiTextProps {
18
+ desktopPlatform?: string;
19
+ }
20
+
25
21
  function isStyledTextContent(value: unknown): value is { chunks: StyledTextChunk[] } {
26
22
  return value instanceof StyledText
27
23
  || (!!value && typeof value === "object" && Array.isArray((value as { chunks?: unknown }).chunks));
@@ -100,16 +96,17 @@ export function WebAsciiText({
100
96
  bg,
101
97
  backgroundColor,
102
98
  selectable = false,
99
+ desktopPlatform,
103
100
  ...props
104
- }: AsciiTextProps) {
105
- const isWordmark = font === "wordmark";
106
- const lines = isWordmark && text.trim().toLowerCase() === "gloomberb"
107
- ? WEB_GLOOMBERB_WORDMARK
108
- : renderAsciiText(text, font);
101
+ }: WebAsciiTextProps) {
102
+ const wordmarkVariant = webAsciiTextWordmarkVariant(text, font, desktopPlatform);
103
+ const isCompatWordmark = wordmarkVariant === "compat";
104
+ const isLegacyWordmark = wordmarkVariant === "legacy";
105
+ const lines = webAsciiTextLines(text, font, desktopPlatform);
109
106
  const resolvedColor = color ?? fg;
110
107
  const resolvedBackground = bg ?? backgroundColor;
111
- const lineHeightPx = isWordmark ? 16 : 12;
112
- const wordmarkWidthPx = isWordmark
108
+ const lineHeightPx = isCompatWordmark ? 16 : 12;
109
+ const wordmarkWidthPx = isCompatWordmark
113
110
  ? Math.max(...lines.map((line) => line.length)) * WEB_WORDMARK_CHAR_WIDTH_PX
114
111
  : undefined;
115
112
  return (
@@ -118,15 +115,16 @@ export function WebAsciiText({
118
115
  data-gloom-role={(props["data-gloom-role"] as string | undefined) ?? "ascii-text"}
119
116
  style={{
120
117
  ...commonStyle({ ...props, fg: resolvedColor, bg: resolvedBackground }),
121
- display: "block",
118
+ display: isLegacyWordmark ? "flex" : "block",
119
+ flexDirection: isLegacyWordmark ? "column" : undefined,
122
120
  flexShrink: 0,
123
121
  ...(wordmarkWidthPx != null && props.width == null
124
122
  ? { width: `${wordmarkWidthPx}px`, minWidth: `${wordmarkWidthPx}px` }
125
123
  : {}),
126
124
  color: resolvedColor,
127
125
  backgroundColor: resolvedBackground,
128
- fontFamily: isWordmark ? "\"Cascadia Mono\", Consolas, \"Courier New\", monospace" : undefined,
129
- fontSize: isWordmark ? "16px" : undefined,
126
+ fontFamily: isCompatWordmark ? "\"Cascadia Mono\", Consolas, \"Courier New\", monospace" : undefined,
127
+ fontSize: isCompatWordmark ? "16px" : undefined,
130
128
  fontVariantLigatures: "none",
131
129
  lineHeight: `${lineHeightPx}px`,
132
130
  whiteSpace: "pre",
@@ -135,7 +133,13 @@ export function WebAsciiText({
135
133
  ...(props.style as CSSProperties | undefined),
136
134
  }}
137
135
  >
138
- {lines.join("\n")}
136
+ {isLegacyWordmark
137
+ ? lines.map((line, index) => (
138
+ <span key={index} style={{ display: "block", height: "12px", lineHeight: "12px" }}>
139
+ {line}
140
+ </span>
141
+ ))
142
+ : lines.join("\n")}
139
143
  </div>
140
144
  );
141
145
  }
@@ -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>
@@ -32,15 +32,9 @@ button, [data-gloom-interactive="true"] { cursor: pointer; }
32
32
  [data-gloom-role="app-header"][data-titlebar-overlay="true"],
33
33
  .electrobun-webkit-app-region-drag {
34
34
  cursor: default;
35
- -webkit-app-region: drag;
36
- app-region: drag;
37
35
  -webkit-user-select: none;
38
36
  user-select: none;
39
37
  }
40
- .electrobun-webkit-app-region-no-drag {
41
- -webkit-app-region: no-drag;
42
- app-region: no-drag;
43
- }
44
38
  [data-titlebar-overlay="true"] {
45
39
  min-height: __TITLEBAR_OVERLAY_HEIGHT_PX__px;
46
40
  max-height: __TITLEBAR_OVERLAY_HEIGHT_PX__px;
@@ -55,15 +49,22 @@ button, [data-gloom-interactive="true"] { cursor: pointer; }
55
49
  [data-titlebar-overlay="true"] a,
56
50
  [data-titlebar-overlay="true"] .electrobun-webkit-app-region-no-drag,
57
51
  [data-titlebar-overlay="true"] [data-gloom-interactive="true"] {
58
- -webkit-app-region: no-drag;
59
- app-region: no-drag;
52
+ cursor: pointer;
60
53
  }
61
54
  [data-gloom-role="window-controls"] {
62
55
  height: 100%;
63
56
  color: var(--gloom-header-text, #f2f2f2);
64
57
  }
65
58
  [data-gloom-role="window-control"] {
59
+ appearance: none;
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ border: 0;
64
+ padding: 0;
65
+ background: transparent;
66
66
  color: color-mix(in srgb, var(--gloom-header-text, #f2f2f2) 86%, transparent);
67
+ cursor: pointer;
67
68
  transition: background-color 90ms linear, color 90ms linear;
68
69
  }
69
70
  [data-gloom-role="window-control"] svg {
@@ -31,92 +31,117 @@ function currentDesktopPlatform(): string {
31
31
  const navigatorWithUserAgentData = navigator as Navigator & {
32
32
  userAgentData?: { platform?: string };
33
33
  };
34
- return navigatorWithUserAgentData.userAgentData?.platform ?? navigator.platform ?? "";
34
+ return [
35
+ navigatorWithUserAgentData.userAgentData?.platform,
36
+ navigator.platform,
37
+ navigator.userAgent,
38
+ ].filter((value): value is string => Boolean(value)).join(" ");
35
39
  }
36
40
 
37
41
  const DESKTOP_PLATFORM = currentDesktopPlatform();
38
- const USES_WINDOWS_CONTROLS = /win/i.test(DESKTOP_PLATFORM);
42
+ const USES_WINDOWS_CONTROLS = !/(darwin|ipad|iphone|linux|mac)/i.test(DESKTOP_PLATFORM);
43
+ const NON_WINDOWS_DESKTOP_PLATFORMS = /^(darwin|linux|freebsd|openbsd|aix|sunos)$/i;
39
44
 
40
- export const webUiHost: UiHost = {
41
- kind: "desktop-web",
42
- capabilities: {
43
- nativePaneChrome: true,
44
- titleBarOverlay: true,
45
- precisePointer: true,
46
- fractionalViewport: true,
47
- cellWidthPx: WEB_CELL_WIDTH,
48
- cellHeightPx: WEB_CELL_HEIGHT,
49
- pixelRatio: Math.min(window.devicePixelRatio || 1, 2),
50
- canvasCharts: true,
51
- nativeContextMenu: NATIVE_CONTEXT_MENU_SUPPORTED,
52
- windowControls: USES_WINDOWS_CONTROLS ? "windows" : undefined,
53
- },
54
- Box: WebBox,
55
- Text: WebText,
56
- Span: WebSpan,
57
- Strong: WebStrong,
58
- Underline: WebUnderline,
59
- ScrollBox: WebScrollBox,
60
- Input: WebInput,
61
- Textarea: WebTextarea,
62
- Button: WebButton,
63
- TextField: WebTextField,
64
- MessageComposer: WebMessageComposer,
65
- ListView: WebListView,
66
- SegmentedControl: WebSegmentedControl,
67
- DialogFrame: WebDialogFrame,
68
- PageStackView: WebPageStackView,
69
- DataTable: WebDataTable,
70
- Tabs: WebTabs,
71
- ChartSurface: WebChartSurface,
72
- ImageSurface: ({ children, src, alt = "", objectFit = "contain", ...props }) => {
73
- const imageSrc = typeof src === "string" ? src.trim() : "";
74
- const [failed, setFailed] = useState(false);
75
- useEffect(() => setFailed(false), [imageSrc]);
76
- const baseStyle = commonStyle(props);
77
- return (
78
- <div
45
+ function usesWindowsWindowControls(desktopPlatform?: string): boolean {
46
+ const platform = desktopPlatform?.trim();
47
+ if (!platform) {
48
+ return USES_WINDOWS_CONTROLS;
49
+ }
50
+ if (/^win/i.test(platform)) {
51
+ return true;
52
+ }
53
+ if (NON_WINDOWS_DESKTOP_PLATFORMS.test(platform)) {
54
+ return false;
55
+ }
56
+ return USES_WINDOWS_CONTROLS;
57
+ }
58
+
59
+ export function createWebUiHost(desktopPlatform?: string): UiHost {
60
+ const usesWindowsControls = usesWindowsWindowControls(desktopPlatform);
61
+
62
+ return {
63
+ kind: "desktop-web",
64
+ capabilities: {
65
+ nativePaneChrome: true,
66
+ titleBarOverlay: true,
67
+ precisePointer: true,
68
+ fractionalViewport: true,
69
+ cellWidthPx: WEB_CELL_WIDTH,
70
+ cellHeightPx: WEB_CELL_HEIGHT,
71
+ pixelRatio: Math.min(window.devicePixelRatio || 1, 2),
72
+ canvasCharts: true,
73
+ nativeContextMenu: NATIVE_CONTEXT_MENU_SUPPORTED,
74
+ windowControls: usesWindowsControls ? "windows" : undefined,
75
+ },
76
+ Box: WebBox,
77
+ Text: WebText,
78
+ Span: WebSpan,
79
+ Strong: WebStrong,
80
+ Underline: WebUnderline,
81
+ ScrollBox: WebScrollBox,
82
+ Input: WebInput,
83
+ Textarea: WebTextarea,
84
+ Button: WebButton,
85
+ TextField: WebTextField,
86
+ MessageComposer: WebMessageComposer,
87
+ ListView: WebListView,
88
+ SegmentedControl: WebSegmentedControl,
89
+ DialogFrame: WebDialogFrame,
90
+ PageStackView: WebPageStackView,
91
+ DataTable: WebDataTable,
92
+ Tabs: WebTabs,
93
+ ChartSurface: WebChartSurface,
94
+ ImageSurface: ({ children, src, alt = "", objectFit = "contain", ...props }) => {
95
+ const imageSrc = typeof src === "string" ? src.trim() : "";
96
+ const [failed, setFailed] = useState(false);
97
+ useEffect(() => setFailed(false), [imageSrc]);
98
+ const baseStyle = commonStyle(props);
99
+ return (
100
+ <div
101
+ {...cleanDomProps(props)}
102
+ style={{
103
+ ...baseStyle,
104
+ overflow: baseStyle.overflow ?? "hidden",
105
+ ...(props.style as CSSProperties | undefined),
106
+ }}
107
+ >
108
+ {imageSrc && !failed ? (
109
+ <img
110
+ src={imageSrc}
111
+ alt={alt}
112
+ draggable={false}
113
+ onError={() => setFailed(true)}
114
+ style={{
115
+ width: "100%",
116
+ height: "100%",
117
+ display: "block",
118
+ objectFit,
119
+ }}
120
+ />
121
+ ) : children as ReactNode}
122
+ </div>
123
+ );
124
+ },
125
+ SpinnerMark: ({ color, ...props }) => (
126
+ <span
79
127
  {...cleanDomProps(props)}
128
+ aria-hidden="true"
80
129
  style={{
81
- ...baseStyle,
82
- overflow: baseStyle.overflow ?? "hidden",
130
+ color,
131
+ display: "inline-block",
132
+ width: "1ch",
133
+ animation: "gloom-spin 0.9s steps(8) infinite",
83
134
  ...(props.style as CSSProperties | undefined),
84
135
  }}
85
136
  >
86
- {imageSrc && !failed ? (
87
- <img
88
- src={imageSrc}
89
- alt={alt}
90
- draggable={false}
91
- onError={() => setFailed(true)}
92
- style={{
93
- width: "100%",
94
- height: "100%",
95
- display: "block",
96
- objectFit,
97
- }}
98
- />
99
- ) : children as ReactNode}
100
- </div>
101
- );
102
- },
103
- SpinnerMark: ({ color, ...props }) => (
104
- <span
105
- {...cleanDomProps(props)}
106
- aria-hidden="true"
107
- style={{
108
- color,
109
- display: "inline-block",
110
- width: "1ch",
111
- animation: "gloom-spin 0.9s steps(8) infinite",
112
- ...(props.style as CSSProperties | undefined),
113
- }}
114
- >
115
- *
116
- </span>
117
- ),
118
- AsciiText: WebAsciiText,
119
- };
137
+ *
138
+ </span>
139
+ ),
140
+ AsciiText: (props) => <WebAsciiText {...props} desktopPlatform={desktopPlatform} />,
141
+ };
142
+ }
143
+
144
+ export const webUiHost: UiHost = createWebUiHost();
120
145
 
121
146
  export const webRendererHost: RendererHost = {
122
147
  supportsNativeDesktopNotifications: true,
@@ -16,18 +16,26 @@ import { ToastHostProvider } from "../../ui/toast";
16
16
  import { colors } from "../../theme/colors";
17
17
  import { startMainThreadMonitor } from "../../utils/main-thread-monitor";
18
18
  import { measurePerfAsync } from "../../utils/perf-marks";
19
+ import type { CliLaunchRequest } from "../../types/plugin";
19
20
 
20
- export async function startOpenTuiApp(): Promise<void> {
21
+ export interface StartOpenTuiAppOptions {
22
+ externalPlugins?: Awaited<ReturnType<typeof loadExternalPlugins>>;
23
+ cliArgs?: string[];
24
+ skipCliDispatch?: boolean;
25
+ cliLaunchRequest?: CliLaunchRequest | null;
26
+ }
27
+
28
+ export async function startOpenTuiApp(options: StartOpenTuiAppOptions = {}): Promise<void> {
21
29
  setConfigStoreHost(nodeConfigStoreHost);
22
30
  debugLog.interceptConsole();
23
31
 
24
32
  const appLog = debugLog.createLogger("app");
25
33
  appLog.info("Gloomberb starting");
26
34
 
27
- const cliArgs = process.argv.slice(2);
28
- const externalPlugins = await measurePerfAsync("startup.opentui.load-external-plugins", () => loadExternalPlugins());
29
- let cliLaunchRequest = null;
30
- if (cliArgs.length > 0) {
35
+ const cliArgs = options.cliArgs ?? process.argv.slice(2);
36
+ const externalPlugins = options.externalPlugins ?? await measurePerfAsync("startup.opentui.load-external-plugins", () => loadExternalPlugins());
37
+ let cliLaunchRequest = options.cliLaunchRequest ?? null;
38
+ if (!options.skipCliDispatch && cliArgs.length > 0) {
31
39
  const dispatchResult = await dispatchCli(cliArgs, { externalPlugins });
32
40
  if (dispatchResult.kind === "handled") return;
33
41
  if (dispatchResult.kind === "launch-ui") {
@@ -0,0 +1,12 @@
1
+ import { useEffect } from "react";
2
+ import { useAppDispatch } from "./context";
3
+
4
+ export function useAppInputCapture(captured: boolean) {
5
+ const dispatch = useAppDispatch();
6
+
7
+ useEffect(() => {
8
+ if (!captured) return;
9
+ dispatch({ type: "SET_INPUT_CAPTURED", captured: true });
10
+ return () => dispatch({ type: "SET_INPUT_CAPTURED", captured: false });
11
+ }, [captured, dispatch]);
12
+ }
@@ -4,6 +4,8 @@ import type { PluginEvents } from "../plugins/event-bus";
4
4
  import type { PluginLogger } from "../utils/debug-log";
5
5
  import type { BrokerAdapter } from "./broker";
6
6
  import type { PluginCapability } from "../capabilities";
7
+ import type { CliGlobalOptions } from "../cli/options";
8
+ import type { CliResult, CliResultRenderOptions } from "../cli/result";
7
9
  import type { ContextMenuContext, ContextMenuItem } from "./context-menu";
8
10
  import type {
9
11
  AppConfig,
@@ -245,6 +247,8 @@ export type CliDispatchResult =
245
247
  export interface CliCommandContext {
246
248
  initConfigData(): Promise<import("../cli/types").ConfigContext>;
247
249
  initMarketData(): Promise<import("../cli/types").MarketContext>;
250
+ initServices(): Promise<import("../cli/types").CliServicesContext>;
251
+ cliOptions: CliGlobalOptions;
248
252
  plugins: GloomPlugin[];
249
253
  fail(message: string, details?: string): never;
250
254
  closeAndFail(
@@ -259,6 +263,10 @@ export interface CliCommandContext {
259
263
  renderStat: typeof import("../utils/cli-output").renderStat;
260
264
  renderTable: typeof import("../utils/cli-output").renderTable;
261
265
  };
266
+ printResult<T, Row extends Record<string, unknown> = Record<string, unknown>>(
267
+ result: CliResult<T>,
268
+ options?: CliResultRenderOptions<T, Row>,
269
+ ): void;
262
270
  log: PluginLogger;
263
271
  }
264
272
 
@@ -270,6 +278,25 @@ export interface CliCommandDef {
270
278
  execute(args: string[], ctx: CliCommandContext): void | CliDispatchResult | Promise<void | CliDispatchResult>;
271
279
  }
272
280
 
281
+ export interface PluginCliCommandDescriptor {
282
+ name: string;
283
+ aliases?: string[];
284
+ summary: string;
285
+ inputShape?: string;
286
+ outputShape?: string;
287
+ examples?: string[];
288
+ sideEffectLevel?: "none" | "local-write" | "network-write" | "external-trade" | "external-side-effect";
289
+ requirements?: string[];
290
+ batch?: boolean;
291
+ formats?: Array<"text" | "json" | "csv" | "ndjson">;
292
+ safety?: string[];
293
+ execute?: CliCommandDef["execute"];
294
+ }
295
+
296
+ export interface PluginCliDescriptor {
297
+ commands?: PluginCliCommandDescriptor[];
298
+ }
299
+
273
300
  export interface CommandDef {
274
301
  id: string;
275
302
  label: string;
@@ -489,6 +516,7 @@ export interface GloomPlugin {
489
516
  toggleable?: boolean;
490
517
  order?: number;
491
518
  cliCommands?: CliCommandDef[];
519
+ cli?: PluginCliDescriptor;
492
520
 
493
521
  setup?(ctx: GloomPluginContext): void | Promise<void>;
494
522
  dispose?(): void;
@@ -8,11 +8,17 @@ export interface CliTableColumn {
8
8
  }
9
9
 
10
10
  const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
11
+ let colorEnabledOverride: boolean | null = null;
11
12
 
12
13
  function colorEnabled(): boolean {
14
+ if (colorEnabledOverride != null) return colorEnabledOverride;
13
15
  return !!process.stdout.isTTY && process.env.NO_COLOR !== "1" && process.env.TERM !== "dumb";
14
16
  }
15
17
 
18
+ export function setCliColorEnabledOverride(value: boolean | null): void {
19
+ colorEnabledOverride = value;
20
+ }
21
+
16
22
  function applyAnsi(text: string, code: string): string {
17
23
  if (!colorEnabled()) return text;
18
24
  return `\x1b[${code}m${text}\x1b[0m`;
@@ -0,0 +1,24 @@
1
+ import { isPlainKeyboardEvent, type KeyboardModifierEventLike } from "./keyboard";
2
+
3
+ export interface SearchFocusNavigationEvent extends KeyboardModifierEventLike {
4
+ sequence?: string;
5
+ preventDefault?: () => void;
6
+ stopPropagation?: () => void;
7
+ }
8
+
9
+ export function isPlainArrowDown(event: SearchFocusNavigationEvent): boolean {
10
+ if (!isPlainKeyboardEvent(event)) return false;
11
+ const name = event.name?.toLowerCase();
12
+ return name === "down" || event.sequence === "\u001b[B" || event.sequence === "\u001bOB";
13
+ }
14
+
15
+ export function isPlainArrowUp(event: SearchFocusNavigationEvent): boolean {
16
+ if (!isPlainKeyboardEvent(event)) return false;
17
+ const name = event.name?.toLowerCase();
18
+ return name === "up" || event.sequence === "\u001b[A" || event.sequence === "\u001bOA";
19
+ }
20
+
21
+ export function stopSearchFocusNavigation(event: SearchFocusNavigationEvent): void {
22
+ event.preventDefault?.();
23
+ event.stopPropagation?.();
24
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.9.1";
1
+ export const VERSION = "0.9.3";