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.
- package/README.md +23 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app.tsx +10 -7
- package/src/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- package/src/components/chart/comparison-stock-chart/helpers.ts +0 -32
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
- package/src/components/chart/comparison-stock-chart/legend.tsx +45 -44
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +56 -6
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/detached-pane-shell.tsx +5 -2
- package/src/components/layout/header.tsx +13 -5
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/drag/runtime.ts +21 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +395 -2
- package/src/components/layout/shell/index.tsx +128 -2
- package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +26 -14
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +105 -0
- package/src/components/layout/status-bar.tsx +32 -6
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-controls.tsx +19 -10
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- package/src/components/price-performance.tsx +183 -0
- package/src/core/app-services.ts +7 -4
- package/src/data/config/store/index.ts +1 -2
- package/src/market-data/performance.test.ts +54 -0
- package/src/market-data/performance.ts +109 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +218 -15
- package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
- package/src/plugins/builtin/congress-trades/footer.ts +14 -8
- package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
- package/src/plugins/builtin/congress-trades/model.ts +29 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
- package/src/plugins/builtin/congress-trades/table.tsx +10 -1
- package/src/plugins/builtin/news/wire/read-state.ts +9 -32
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- package/src/plugins/builtin/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/queries.ts +8 -2
- package/src/plugins/prediction-markets/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- package/src/plugins/prediction-markets/pane.test.tsx +32 -0
- package/src/plugins/prediction-markets/pane.tsx +21 -1
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +9 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +11 -2
- package/src/renderers/electrobun/bun/desktop/window-style.ts +27 -1
- package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
- package/src/renderers/electrobun/bun/desktop/windows-icons.ts +11 -64
- package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
- package/src/renderers/electrobun/bun/index.ts +22 -2
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/shared/protocol.ts +1 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/main.tsx +2 -1
- package/src/renderers/electrobun/view/styles.css +9 -8
- package/src/renderers/electrobun/view/ui-host.tsx +102 -77
- package/src/renderers/opentui/start.tsx +13 -5
- package/src/state/app/input-capture.ts +12 -0
- package/src/types/plugin.ts +28 -0
- package/src/utils/cli-output.ts +6 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- 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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
}:
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
112
|
-
const wordmarkWidthPx =
|
|
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:
|
|
129
|
-
fontSize:
|
|
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
|
-
{
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 = /
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
+
}
|
package/src/types/plugin.ts
CHANGED
|
@@ -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;
|
package/src/utils/cli-output.ts
CHANGED
|
@@ -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
|
+
export const VERSION = "0.9.3";
|