gloomberb 0.4.1 → 0.5.0
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 +2 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/shell.test.tsx +186 -27
- package/src/components/layout/shell.tsx +5 -4
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +62 -18
- package/src/plugins/builtin/chat-controller.ts +23 -12
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +415 -119
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export interface InlineLinkTextToken {
|
|
2
|
+
kind: "text";
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface InlineLinkUrlToken {
|
|
7
|
+
kind: "link";
|
|
8
|
+
value: string;
|
|
9
|
+
url: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type InlineLinkToken = InlineLinkTextToken | InlineLinkUrlToken;
|
|
13
|
+
|
|
14
|
+
const LINK_TOKEN_RE = /\b(?:https?:\/\/|www\.)[^\s<>"']+/gi;
|
|
15
|
+
const ALWAYS_TRIM_RE = /[.,!?;:]/;
|
|
16
|
+
const BRACKET_PAIRS: Record<string, string> = {
|
|
17
|
+
")": "(",
|
|
18
|
+
"]": "[",
|
|
19
|
+
"}": "{",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function countChar(value: string, char: string): number {
|
|
23
|
+
let total = 0;
|
|
24
|
+
for (const current of value) {
|
|
25
|
+
if (current === char) total += 1;
|
|
26
|
+
}
|
|
27
|
+
return total;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function trimTrailingLinkPunctuation(rawValue: string): { value: string; trailing: string } {
|
|
31
|
+
let value = rawValue;
|
|
32
|
+
|
|
33
|
+
while (value.length > 0) {
|
|
34
|
+
const lastChar = value[value.length - 1]!;
|
|
35
|
+
if (ALWAYS_TRIM_RE.test(lastChar)) {
|
|
36
|
+
value = value.slice(0, -1);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const openingChar = BRACKET_PAIRS[lastChar];
|
|
41
|
+
if (!openingChar) break;
|
|
42
|
+
|
|
43
|
+
if (countChar(value, lastChar) > countChar(value, openingChar)) {
|
|
44
|
+
value = value.slice(0, -1);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { value, trailing: rawValue.slice(value.length) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeLinkUrl(value: string): string {
|
|
55
|
+
return /^https?:\/\//i.test(value) ? value : `https://${value}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function tokenizeInlineLinks(text: string): InlineLinkToken[] {
|
|
59
|
+
const tokens: InlineLinkToken[] = [];
|
|
60
|
+
let cursor = 0;
|
|
61
|
+
let match: RegExpExecArray | null;
|
|
62
|
+
|
|
63
|
+
LINK_TOKEN_RE.lastIndex = 0;
|
|
64
|
+
while ((match = LINK_TOKEN_RE.exec(text)) !== null) {
|
|
65
|
+
const rawValue = match[0];
|
|
66
|
+
const start = match.index;
|
|
67
|
+
const end = start + rawValue.length;
|
|
68
|
+
|
|
69
|
+
if (start > cursor) {
|
|
70
|
+
tokens.push({ kind: "text", value: text.slice(cursor, start) });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const trimmed = trimTrailingLinkPunctuation(rawValue);
|
|
74
|
+
if (trimmed.value) {
|
|
75
|
+
tokens.push({
|
|
76
|
+
kind: "link",
|
|
77
|
+
value: trimmed.value,
|
|
78
|
+
url: normalizeLinkUrl(trimmed.value),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (trimmed.trailing) {
|
|
82
|
+
tokens.push({ kind: "text", value: trimmed.trailing });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cursor = end;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (cursor < text.length) {
|
|
89
|
+
tokens.push({ kind: "text", value: text.slice(cursor) });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return tokens;
|
|
93
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
formatMarketCost,
|
|
4
|
+
formatMarketCostWithCurrency,
|
|
5
|
+
formatCompactMarketPriceWithCurrency,
|
|
6
|
+
formatMarketPrice,
|
|
7
|
+
formatMarketPriceWithCurrency,
|
|
8
|
+
formatMarketQuantity,
|
|
9
|
+
formatSignedMarketPrice,
|
|
10
|
+
resolveAssetDisplayKind,
|
|
11
|
+
} from "./market-format";
|
|
12
|
+
|
|
13
|
+
describe("resolveAssetDisplayKind", () => {
|
|
14
|
+
test("prefers explicit cash balances", () => {
|
|
15
|
+
expect(resolveAssetDisplayKind({ isCashBalance: true, assetCategory: "STK" })).toBe("cash");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("maps common broker security types", () => {
|
|
19
|
+
expect(resolveAssetDisplayKind({ assetCategory: "CRYPTO" })).toBe("crypto");
|
|
20
|
+
expect(resolveAssetDisplayKind({ assetCategory: "STK" })).toBe("equity");
|
|
21
|
+
expect(resolveAssetDisplayKind({ contractSecType: "OPT" })).toBe("contract");
|
|
22
|
+
expect(resolveAssetDisplayKind({ assetCategory: "FOREX" })).toBe("cash");
|
|
23
|
+
expect(resolveAssetDisplayKind({ assetCategory: "CURRENCY" })).toBe("cash");
|
|
24
|
+
expect(resolveAssetDisplayKind({ assetCategory: "CCY" })).toBe("cash");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("falls back to contract formatting when only a multiplier is present", () => {
|
|
28
|
+
expect(resolveAssetDisplayKind({ multiplier: 100 })).toBe("contract");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("formatMarketQuantity", () => {
|
|
33
|
+
test("preserves additional FX precision while trimming trailing zeroes", () => {
|
|
34
|
+
expect(formatMarketQuantity(-351957.025, { isCashBalance: true })).toBe("-351,957.025");
|
|
35
|
+
expect(formatMarketQuantity(-303029.144938754, { isCashBalance: true })).toBe("-303,029.144939");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("shows whole equities without decimals and fractional equities with up to four decimals", () => {
|
|
39
|
+
expect(formatMarketQuantity(10, { assetCategory: "STK" })).toBe("10");
|
|
40
|
+
expect(formatMarketQuantity(10.125, { assetCategory: "STK" })).toBe("10.125");
|
|
41
|
+
expect(formatMarketQuantity(10.123456, { assetCategory: "STK" })).toBe("10.1235");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("uses higher precision for crypto quantities", () => {
|
|
45
|
+
expect(formatMarketQuantity(0.123456789, { assetCategory: "CRYPTO" })).toBe("0.12345679");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("formatMarketPrice", () => {
|
|
50
|
+
test("uses semantic precision for equities, cash, and crypto", () => {
|
|
51
|
+
expect(formatMarketPrice(190.2, { assetCategory: "STK" })).toBe("190.2");
|
|
52
|
+
expect(formatMarketPrice(259.7499, { assetCategory: "STK" })).toBe("259.75");
|
|
53
|
+
expect(formatMarketPrice(1.084567, { isCashBalance: true })).toBe("1.084567");
|
|
54
|
+
expect(formatMarketPrice(1.17364, { assetCategory: "CURRENCY" })).toBe("1.17364");
|
|
55
|
+
expect(formatMarketPrice(0.000123456789, { assetCategory: "CRYPTO" })).toBe("0.00012346");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("can adapt chart precision to the visible price range", () => {
|
|
59
|
+
expect(formatMarketPrice(1.167815, { assetCategory: "CURRENCY", priceRange: 0.12 })).toBe("1.17");
|
|
60
|
+
expect(formatMarketPrice(1.167815, { assetCategory: "CURRENCY", priceRange: 0.0024 })).toBe("1.1678");
|
|
61
|
+
expect(formatMarketPrice(259.7499, {
|
|
62
|
+
assetCategory: "STK",
|
|
63
|
+
minimumFractionDigits: 2,
|
|
64
|
+
precisionOffset: 1,
|
|
65
|
+
priceRange: 80,
|
|
66
|
+
})).toBe("259.75");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("can lock price displays to a specific number of decimals", () => {
|
|
70
|
+
expect(formatMarketPrice(18, { assetCategory: "STK", fixedFractionDigits: 1 })).toBe("18.0");
|
|
71
|
+
expect(formatMarketPriceWithCurrency(18, "USD", { assetCategory: "STK", fixedFractionDigits: 2 })).toBe("$18.00");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("fits within the supplied width before falling back to truncation elsewhere", () => {
|
|
75
|
+
expect(formatMarketPrice(1.084567, { isCashBalance: true, maxWidth: 6 })).toBe("1.0846");
|
|
76
|
+
expect(formatMarketQuantity(123456.789, { assetCategory: "CRYPTO", maxWidth: 7 })).toBe("123,457");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("formats signed price changes without double signs", () => {
|
|
80
|
+
expect(formatSignedMarketPrice(0.123456, { isCashBalance: true })).toBe("+0.123456");
|
|
81
|
+
expect(formatSignedMarketPrice(-0.123456, { isCashBalance: true })).toBe("-0.123456");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("formatMarketCost", () => {
|
|
86
|
+
test("keeps equity cost displays conservative while preserving FX and crypto precision", () => {
|
|
87
|
+
expect(formatMarketCost(119.3687, { assetCategory: "STK" })).toBe("119.37");
|
|
88
|
+
expect(formatMarketCost(1.084567, { isCashBalance: true })).toBe("1.084567");
|
|
89
|
+
expect(formatMarketCost(113905.720075, { assetCategory: "CRYPTO" })).toBe("113,905.720075");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("formatMarketPriceWithCurrency", () => {
|
|
94
|
+
test("renders price-like values with a symbol and variable decimals", () => {
|
|
95
|
+
expect(formatMarketPriceWithCurrency(190.25, "USD", { assetCategory: "STK" })).toBe("$190.25");
|
|
96
|
+
expect(formatMarketPriceWithCurrency(1.084567, "EUR", { isCashBalance: true })).toBe("€1.084567");
|
|
97
|
+
expect(formatMarketPriceWithCurrency(1.17364, "USD", { assetCategory: "CURRENCY" })).toBe("$1.17364");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("preserves chart-style compact notation for large values", () => {
|
|
101
|
+
expect(formatCompactMarketPriceWithCurrency(21_970, "JPY")).toBe("¥22.0K");
|
|
102
|
+
expect(formatCompactMarketPriceWithCurrency(12_340, "HKD")).toBe("HK$12.3K");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("formats position cost values with tighter equity precision", () => {
|
|
106
|
+
expect(formatMarketCostWithCurrency(119.3687, "HKD", { assetCategory: "STK" })).toBe("HK$119.37");
|
|
107
|
+
expect(formatMarketCostWithCurrency(50.9507, "USD", { assetCategory: "OPT", multiplier: 100 })).toBe("$50.9507");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { formatCurrency } from "./format";
|
|
2
|
+
|
|
3
|
+
export type AssetDisplayKind = "cash" | "crypto" | "equity" | "contract" | "other";
|
|
4
|
+
|
|
5
|
+
export interface AssetDisplayContext {
|
|
6
|
+
isCashBalance?: boolean;
|
|
7
|
+
assetCategory?: string;
|
|
8
|
+
contractSecType?: string;
|
|
9
|
+
multiplier?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MarketFormatOptions extends AssetDisplayContext {
|
|
13
|
+
maxWidth?: number;
|
|
14
|
+
minimumFractionDigits?: number;
|
|
15
|
+
precisionOffset?: number;
|
|
16
|
+
priceRange?: number;
|
|
17
|
+
fixedFractionDigits?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CASH_TYPES = new Set(["CASH", "FX", "FOREX", "CCY", "CURRENCY", "CURRENCYPAIR"]);
|
|
21
|
+
const CRYPTO_TYPES = new Set(["CRYPTO", "CRYPTOCURRENCY", "COIN", "TOKEN"]);
|
|
22
|
+
const EQUITY_TYPES = new Set(["STK", "STOCK", "EQUITY", "ETF", "ETN", "ETP", "FUND", "MUTUALFUND", "CEF", "ADR"]);
|
|
23
|
+
const CONTRACT_TYPES = new Set(["OPT", "OPTION", "OPTIONS", "FUT", "FUTURE", "FUTURES", "FOP"]);
|
|
24
|
+
|
|
25
|
+
const currencySymbols = new Map<string, string>();
|
|
26
|
+
const numberFormatters = new Map<string, Intl.NumberFormat>();
|
|
27
|
+
|
|
28
|
+
function normalizeType(value?: string): string {
|
|
29
|
+
return (value ?? "").trim().toUpperCase().replace(/[\s_-]+/g, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getNumberFormatter(
|
|
33
|
+
minimumFractionDigits: number,
|
|
34
|
+
maximumFractionDigits: number,
|
|
35
|
+
useGrouping: boolean,
|
|
36
|
+
): Intl.NumberFormat {
|
|
37
|
+
const key = `${minimumFractionDigits}:${maximumFractionDigits}:${useGrouping ? 1 : 0}`;
|
|
38
|
+
let formatter = numberFormatters.get(key);
|
|
39
|
+
if (!formatter) {
|
|
40
|
+
formatter = new Intl.NumberFormat("en-US", {
|
|
41
|
+
minimumFractionDigits,
|
|
42
|
+
maximumFractionDigits,
|
|
43
|
+
useGrouping,
|
|
44
|
+
});
|
|
45
|
+
numberFormatters.set(key, formatter);
|
|
46
|
+
}
|
|
47
|
+
return formatter;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getCurrencySymbol(currency: string): string {
|
|
51
|
+
const normalizedCurrency = currency.trim().toUpperCase() || "USD";
|
|
52
|
+
const cached = currencySymbols.get(normalizedCurrency);
|
|
53
|
+
if (cached) return cached;
|
|
54
|
+
|
|
55
|
+
const formatter = new Intl.NumberFormat("en-US", {
|
|
56
|
+
style: "currency",
|
|
57
|
+
currency: normalizedCurrency,
|
|
58
|
+
currencyDisplay: "symbol",
|
|
59
|
+
minimumFractionDigits: 0,
|
|
60
|
+
maximumFractionDigits: 0,
|
|
61
|
+
});
|
|
62
|
+
const symbol = formatter.formatToParts(0).find((part) => part.type === "currency")?.value ?? normalizedCurrency;
|
|
63
|
+
currencySymbols.set(normalizedCurrency, symbol);
|
|
64
|
+
return symbol;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fitsWidth(text: string, maxWidth: number | undefined): boolean {
|
|
68
|
+
return maxWidth == null || text.length <= maxWidth;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isEffectivelyInteger(value: number): boolean {
|
|
72
|
+
return Math.abs(value - Math.round(value)) < 1e-9;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatVariableNumber(
|
|
76
|
+
value: number,
|
|
77
|
+
maxFractionDigits: number,
|
|
78
|
+
maxWidth: number | undefined,
|
|
79
|
+
minimumFractionDigits = 0,
|
|
80
|
+
): string {
|
|
81
|
+
const groupedModes = [true, false];
|
|
82
|
+
|
|
83
|
+
for (const useGrouping of groupedModes) {
|
|
84
|
+
for (let decimals = maxFractionDigits; decimals >= minimumFractionDigits; decimals -= 1) {
|
|
85
|
+
const formatter = getNumberFormatter(Math.min(minimumFractionDigits, decimals), decimals, useGrouping);
|
|
86
|
+
const formatted = formatter.format(value);
|
|
87
|
+
if (fitsWidth(formatted, maxWidth)) return formatted;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return getNumberFormatter(0, 0, false).format(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getQuantityMaxFractionDigits(kind: AssetDisplayKind, value: number): number {
|
|
95
|
+
if (isEffectivelyInteger(value)) return 0;
|
|
96
|
+
|
|
97
|
+
switch (kind) {
|
|
98
|
+
case "cash":
|
|
99
|
+
return 6;
|
|
100
|
+
case "crypto":
|
|
101
|
+
return 8;
|
|
102
|
+
case "equity":
|
|
103
|
+
return 4;
|
|
104
|
+
case "contract":
|
|
105
|
+
return 4;
|
|
106
|
+
case "other":
|
|
107
|
+
default:
|
|
108
|
+
return 4;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getBasePriceMaxFractionDigits(kind: AssetDisplayKind, value: number): number {
|
|
113
|
+
switch (kind) {
|
|
114
|
+
case "cash":
|
|
115
|
+
return 6;
|
|
116
|
+
case "crypto":
|
|
117
|
+
return 8;
|
|
118
|
+
case "equity":
|
|
119
|
+
return Math.abs(value) >= 1 ? 2 : 4;
|
|
120
|
+
case "contract":
|
|
121
|
+
return 4;
|
|
122
|
+
case "other":
|
|
123
|
+
default:
|
|
124
|
+
return Math.abs(value) >= 1 ? 2 : 4;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getAdaptivePriceFractionDigits(priceRange: number | undefined, precisionOffset = 0): number | null {
|
|
129
|
+
if (priceRange === undefined || !Number.isFinite(priceRange) || priceRange <= 0) return null;
|
|
130
|
+
|
|
131
|
+
// The chart price axis renders four labels by default, so the visible range
|
|
132
|
+
// divided across three intervals is a good proxy for the current zoom step.
|
|
133
|
+
const visibleStep = priceRange / 3;
|
|
134
|
+
if (!Number.isFinite(visibleStep) || visibleStep <= 0) return null;
|
|
135
|
+
|
|
136
|
+
return Math.max(0, Math.ceil(-Math.log10(visibleStep)) + precisionOffset);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getPriceMaxFractionDigits(
|
|
140
|
+
kind: AssetDisplayKind,
|
|
141
|
+
value: number,
|
|
142
|
+
priceRange: number | undefined,
|
|
143
|
+
precisionOffset: number,
|
|
144
|
+
): number {
|
|
145
|
+
const baseMaxFractionDigits = kind === "other" && priceRange !== undefined
|
|
146
|
+
? 6
|
|
147
|
+
: getBasePriceMaxFractionDigits(kind, value);
|
|
148
|
+
const adaptiveFractionDigits = getAdaptivePriceFractionDigits(priceRange, precisionOffset);
|
|
149
|
+
return adaptiveFractionDigits === null
|
|
150
|
+
? baseMaxFractionDigits
|
|
151
|
+
: Math.min(baseMaxFractionDigits, adaptiveFractionDigits);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getCostMaxFractionDigits(kind: AssetDisplayKind): number {
|
|
155
|
+
switch (kind) {
|
|
156
|
+
case "cash":
|
|
157
|
+
return 6;
|
|
158
|
+
case "crypto":
|
|
159
|
+
return 8;
|
|
160
|
+
case "contract":
|
|
161
|
+
return 4;
|
|
162
|
+
case "equity":
|
|
163
|
+
return 2;
|
|
164
|
+
case "other":
|
|
165
|
+
default:
|
|
166
|
+
return 2;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function compactScaledPrice(
|
|
171
|
+
value: number,
|
|
172
|
+
divisor: number,
|
|
173
|
+
suffix: string,
|
|
174
|
+
currency: string,
|
|
175
|
+
maxWidth: number | undefined,
|
|
176
|
+
): string {
|
|
177
|
+
const sign = value < 0 ? "-" : "";
|
|
178
|
+
const symbol = getCurrencySymbol(currency);
|
|
179
|
+
const numericWidth = maxWidth == null
|
|
180
|
+
? undefined
|
|
181
|
+
: Math.max(1, maxWidth - sign.length - symbol.length - suffix.length);
|
|
182
|
+
const formatted = formatVariableNumber(Math.abs(value) / divisor, 1, numericWidth, 1);
|
|
183
|
+
return `${sign}${symbol}${formatted}${suffix}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function resolveAssetDisplayKind({
|
|
187
|
+
isCashBalance,
|
|
188
|
+
assetCategory,
|
|
189
|
+
contractSecType,
|
|
190
|
+
multiplier,
|
|
191
|
+
}: AssetDisplayContext): AssetDisplayKind {
|
|
192
|
+
if (isCashBalance) return "cash";
|
|
193
|
+
|
|
194
|
+
const normalizedType = normalizeType(contractSecType || assetCategory);
|
|
195
|
+
if (CASH_TYPES.has(normalizedType) || normalizedType.includes("FOREX") || normalizedType.includes("CURRENCY")) return "cash";
|
|
196
|
+
if (CRYPTO_TYPES.has(normalizedType) || normalizedType.includes("CRYPTO")) return "crypto";
|
|
197
|
+
if (EQUITY_TYPES.has(normalizedType)) return "equity";
|
|
198
|
+
if (CONTRACT_TYPES.has(normalizedType)) return "contract";
|
|
199
|
+
if ((multiplier ?? 1) > 1) return "contract";
|
|
200
|
+
return "other";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function formatMarketQuantity(value: number | undefined, options: MarketFormatOptions = {}): string {
|
|
204
|
+
if (value === undefined || value === null || Number.isNaN(value)) return "—";
|
|
205
|
+
const kind = resolveAssetDisplayKind(options);
|
|
206
|
+
const maxFractionDigits = getQuantityMaxFractionDigits(kind, value);
|
|
207
|
+
return formatVariableNumber(value, maxFractionDigits, options.maxWidth);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function formatMarketPrice(value: number | undefined, options: MarketFormatOptions = {}): string {
|
|
211
|
+
if (value === undefined || value === null || Number.isNaN(value)) return "—";
|
|
212
|
+
const kind = resolveAssetDisplayKind(options);
|
|
213
|
+
const fixedFractionDigits = options.fixedFractionDigits;
|
|
214
|
+
if (fixedFractionDigits !== undefined) {
|
|
215
|
+
const maxFractionDigits = kind === "other" && options.priceRange !== undefined
|
|
216
|
+
? 6
|
|
217
|
+
: getBasePriceMaxFractionDigits(kind, value);
|
|
218
|
+
const clampedFixedFractionDigits = Math.max(0, Math.min(fixedFractionDigits, maxFractionDigits));
|
|
219
|
+
return formatVariableNumber(value, clampedFixedFractionDigits, options.maxWidth, clampedFixedFractionDigits);
|
|
220
|
+
}
|
|
221
|
+
const minimumFractionDigits = Math.max(
|
|
222
|
+
0,
|
|
223
|
+
Math.min(options.minimumFractionDigits ?? 0, getBasePriceMaxFractionDigits(kind, value)),
|
|
224
|
+
);
|
|
225
|
+
const maxFractionDigits = Math.max(
|
|
226
|
+
getPriceMaxFractionDigits(kind, value, options.priceRange, options.precisionOffset ?? 0),
|
|
227
|
+
minimumFractionDigits,
|
|
228
|
+
);
|
|
229
|
+
return formatVariableNumber(value, maxFractionDigits, options.maxWidth, minimumFractionDigits);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function formatMarketCost(value: number | undefined, options: MarketFormatOptions = {}): string {
|
|
233
|
+
if (value === undefined || value === null || Number.isNaN(value)) return "—";
|
|
234
|
+
const kind = resolveAssetDisplayKind(options);
|
|
235
|
+
return formatVariableNumber(value, getCostMaxFractionDigits(kind), options.maxWidth);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function formatSignedMarketPrice(value: number | undefined, options: MarketFormatOptions = {}): string {
|
|
239
|
+
if (value === undefined || value === null || Number.isNaN(value)) return "—";
|
|
240
|
+
if (value > 0) {
|
|
241
|
+
const maxWidth = options.maxWidth == null ? undefined : Math.max(1, options.maxWidth - 1);
|
|
242
|
+
return `+${formatMarketPrice(value, { ...options, maxWidth })}`;
|
|
243
|
+
}
|
|
244
|
+
return formatMarketPrice(value, options);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function formatMarketPriceWithCurrency(
|
|
248
|
+
value: number | undefined,
|
|
249
|
+
currency = "USD",
|
|
250
|
+
options: MarketFormatOptions = {},
|
|
251
|
+
): string {
|
|
252
|
+
if (value === undefined || value === null || Number.isNaN(value)) return "—";
|
|
253
|
+
const normalizedCurrency = currency.trim().toUpperCase() || "USD";
|
|
254
|
+
const sign = value < 0 ? "-" : "";
|
|
255
|
+
const symbol = getCurrencySymbol(normalizedCurrency);
|
|
256
|
+
const numericWidth = options.maxWidth == null
|
|
257
|
+
? undefined
|
|
258
|
+
: Math.max(1, options.maxWidth - sign.length - symbol.length);
|
|
259
|
+
const body = formatMarketPrice(Math.abs(value), { ...options, maxWidth: numericWidth });
|
|
260
|
+
return `${sign}${symbol}${body}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function formatMarketCostWithCurrency(
|
|
264
|
+
value: number | undefined,
|
|
265
|
+
currency = "USD",
|
|
266
|
+
options: MarketFormatOptions = {},
|
|
267
|
+
): string {
|
|
268
|
+
if (value === undefined || value === null || Number.isNaN(value)) return "—";
|
|
269
|
+
const normalizedCurrency = currency.trim().toUpperCase() || "USD";
|
|
270
|
+
const sign = value < 0 ? "-" : "";
|
|
271
|
+
const symbol = getCurrencySymbol(normalizedCurrency);
|
|
272
|
+
const numericWidth = options.maxWidth == null
|
|
273
|
+
? undefined
|
|
274
|
+
: Math.max(1, options.maxWidth - sign.length - symbol.length);
|
|
275
|
+
const body = formatMarketCost(Math.abs(value), { ...options, maxWidth: numericWidth });
|
|
276
|
+
return `${sign}${symbol}${body}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function formatCompactMarketPriceWithCurrency(
|
|
280
|
+
value: number | undefined,
|
|
281
|
+
currency = "USD",
|
|
282
|
+
options: MarketFormatOptions = {},
|
|
283
|
+
): string {
|
|
284
|
+
if (value === undefined || value === null || Number.isNaN(value)) return "—";
|
|
285
|
+
const abs = Math.abs(value);
|
|
286
|
+
|
|
287
|
+
if (abs >= 1e6) {
|
|
288
|
+
return compactScaledPrice(value, 1e6, "M", currency, options.maxWidth);
|
|
289
|
+
}
|
|
290
|
+
if (abs >= 1e3 && abs < 1e5) {
|
|
291
|
+
return compactScaledPrice(value, 1e3, "K", currency, options.maxWidth);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return formatMarketPriceWithCurrency(value, currency, options);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function formatMarketMoney(value: number | undefined, currency = "USD"): string {
|
|
298
|
+
return formatCurrency(value, currency);
|
|
299
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MarketState, Quote, QuoteFieldProvenance } from "../types/financials";
|
|
2
2
|
import { colors, priceColor } from "../theme/colors";
|
|
3
3
|
import { formatPercentRaw } from "./format";
|
|
4
|
+
import { formatMarketPrice } from "./market-format";
|
|
4
5
|
|
|
5
6
|
export interface ActiveQuoteDisplay {
|
|
6
7
|
price: number;
|
|
@@ -107,11 +108,11 @@ export function getExtendedHoursInfo(quote: Quote | null | undefined): { text: s
|
|
|
107
108
|
if (!quote) return null;
|
|
108
109
|
if ((quote.marketState === "PRE" || quote.marketState === "PREPRE") && quote.preMarketPrice != null) {
|
|
109
110
|
const chg = quote.preMarketChangePercent ?? 0;
|
|
110
|
-
return { text: `Pre ${quote.preMarketPrice
|
|
111
|
+
return { text: `Pre ${formatMarketPrice(quote.preMarketPrice)} ${formatPercentRaw(chg)}`, color: priceColor(chg) };
|
|
111
112
|
}
|
|
112
113
|
if ((quote.marketState === "POST" || quote.marketState === "POSTPOST") && quote.postMarketPrice != null) {
|
|
113
114
|
const chg = quote.postMarketChangePercent ?? 0;
|
|
114
|
-
return { text: `AH ${quote.postMarketPrice
|
|
115
|
+
return { text: `AH ${formatMarketPrice(quote.postMarketPrice)} ${formatPercentRaw(chg)}`, color: priceColor(chg) };
|
|
115
116
|
}
|
|
116
117
|
return null;
|
|
117
118
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { copyActiveSelection, isCopyShortcut, isPasteShortcut } from "./selection-clipboard";
|
|
3
|
+
|
|
4
|
+
describe("selection clipboard sync", () => {
|
|
5
|
+
test("copies the active selection when present", () => {
|
|
6
|
+
const renderer = {
|
|
7
|
+
copyToClipboardOSC52: (text: string) => text === "AAPL",
|
|
8
|
+
getSelection: () => ({
|
|
9
|
+
getSelectedText: () => "AAPL",
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(copyActiveSelection(renderer as any)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("ignores empty or missing selections", () => {
|
|
17
|
+
expect(copyActiveSelection({
|
|
18
|
+
copyToClipboardOSC52: () => true,
|
|
19
|
+
getSelection: () => null,
|
|
20
|
+
} as any)).toBe(false);
|
|
21
|
+
|
|
22
|
+
expect(copyActiveSelection({
|
|
23
|
+
copyToClipboardOSC52: () => true,
|
|
24
|
+
getSelection: () => ({
|
|
25
|
+
getSelectedText: () => "",
|
|
26
|
+
}),
|
|
27
|
+
} as any)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("matches explicit copy shortcuts only", () => {
|
|
31
|
+
expect(isCopyShortcut({ name: "c", super: true, meta: false, ctrl: false, shift: false } as any)).toBe(true);
|
|
32
|
+
expect(isCopyShortcut({ name: "c", super: false, meta: true, ctrl: false, shift: false } as any)).toBe(true);
|
|
33
|
+
expect(isCopyShortcut({ name: "c", super: false, meta: false, ctrl: true, shift: true } as any)).toBe(true);
|
|
34
|
+
expect(isCopyShortcut({ name: "c", super: false, ctrl: true, shift: false } as any)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("matches explicit paste shortcuts only", () => {
|
|
38
|
+
expect(isPasteShortcut({ name: "v", super: true, meta: false, ctrl: false, shift: false } as any)).toBe(true);
|
|
39
|
+
expect(isPasteShortcut({ name: "v", super: false, meta: true, ctrl: false, shift: false } as any)).toBe(true);
|
|
40
|
+
expect(isPasteShortcut({ name: "v", super: false, meta: false, ctrl: true, shift: true } as any)).toBe(true);
|
|
41
|
+
expect(isPasteShortcut({ name: "v", super: false, meta: false, ctrl: true, shift: false } as any)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { CliRenderer, KeyEvent } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
function readTextFromSystemClipboard(): string | null {
|
|
4
|
+
if (process.platform !== "darwin") {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const result = Bun.spawnSync(["pbpaste"], {
|
|
10
|
+
stdout: "pipe",
|
|
11
|
+
stderr: "ignore",
|
|
12
|
+
});
|
|
13
|
+
if (!result.success) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return result.stdout.toString().replace(/\r\n/g, "\n");
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function copyTextToSystemClipboard(text: string): boolean {
|
|
23
|
+
if (text.length === 0 || process.platform !== "darwin") {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = Bun.spawnSync(["pbcopy"], {
|
|
29
|
+
stdin: new Blob([text]),
|
|
30
|
+
stdout: "ignore",
|
|
31
|
+
stderr: "ignore",
|
|
32
|
+
});
|
|
33
|
+
return result.success;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function copySelectionText(
|
|
40
|
+
renderer: Pick<CliRenderer, "copyToClipboardOSC52">,
|
|
41
|
+
text: string,
|
|
42
|
+
): boolean {
|
|
43
|
+
if (text.length === 0) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return renderer.copyToClipboardOSC52(text) || copyTextToSystemClipboard(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function copyActiveSelection(
|
|
51
|
+
renderer: Pick<CliRenderer, "getSelection" | "copyToClipboardOSC52">,
|
|
52
|
+
): boolean {
|
|
53
|
+
const text = renderer.getSelection()?.getSelectedText() ?? "";
|
|
54
|
+
return copySelectionText(renderer, text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isCopyShortcut(
|
|
58
|
+
event: Pick<KeyEvent, "name" | "super" | "meta" | "ctrl" | "shift">,
|
|
59
|
+
): boolean {
|
|
60
|
+
const keyName = event.name.toLowerCase();
|
|
61
|
+
return keyName === "c" && (
|
|
62
|
+
event.super === true
|
|
63
|
+
|| event.meta === true
|
|
64
|
+
|| (event.ctrl === true && event.shift === true)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isPasteShortcut(
|
|
69
|
+
event: Pick<KeyEvent, "name" | "super" | "meta" | "ctrl" | "shift">,
|
|
70
|
+
): boolean {
|
|
71
|
+
const keyName = event.name.toLowerCase();
|
|
72
|
+
return keyName === "v" && (
|
|
73
|
+
event.super === true
|
|
74
|
+
|| event.meta === true
|
|
75
|
+
|| (event.ctrl === true && event.shift === true)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function pasteSystemClipboard(
|
|
80
|
+
renderer: Pick<CliRenderer, "keyInput">,
|
|
81
|
+
): boolean {
|
|
82
|
+
const text = readTextFromSystemClipboard();
|
|
83
|
+
if (!text) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
renderer.keyInput.processPaste(new TextEncoder().encode(text));
|
|
87
|
+
return true;
|
|
88
|
+
}
|