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
|
@@ -3,15 +3,12 @@ import { useDialog, useDialogKeyboard, type AlertContext } from "@opentui-ui/dia
|
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import type {
|
|
5
5
|
PaneSettingField,
|
|
6
|
-
PaneSettingOption,
|
|
7
|
-
PaneSettingOrderedMultiSelectField,
|
|
8
6
|
PaneSettingTextField,
|
|
9
7
|
} from "../types/plugin";
|
|
10
8
|
import type { PluginRegistry } from "../plugins/registry";
|
|
11
9
|
import { useAppState } from "../state/app-context";
|
|
12
10
|
import { colors } from "../theme/colors";
|
|
13
|
-
import {
|
|
14
|
-
import { Button, DialogFrame, ListView, TextField } from "./ui";
|
|
11
|
+
import { Button, DialogFrame, ListView, MultiSelectDialogContent, TextField } from "./ui";
|
|
15
12
|
|
|
16
13
|
interface PaneSettingsDialogContentProps extends AlertContext {
|
|
17
14
|
paneId: string;
|
|
@@ -56,37 +53,6 @@ function coerceSelectedValues(value: unknown): string[] {
|
|
|
56
53
|
: [];
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
function toggleSelectedValue(currentValues: string[], value: string): string[] {
|
|
60
|
-
return currentValues.includes(value)
|
|
61
|
-
? currentValues.filter((entry) => entry !== value)
|
|
62
|
-
: [...currentValues, value];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function moveSelectedValue(
|
|
66
|
-
field: PaneSettingOrderedMultiSelectField,
|
|
67
|
-
currentValues: string[],
|
|
68
|
-
selectedOption: string,
|
|
69
|
-
direction: "up" | "down",
|
|
70
|
-
): string[] {
|
|
71
|
-
if (!currentValues.includes(selectedOption)) return currentValues;
|
|
72
|
-
|
|
73
|
-
const optionValueSet = new Set(field.options.map((option) => option.value));
|
|
74
|
-
const ordered = currentValues.filter((value) => optionValueSet.has(value));
|
|
75
|
-
const index = ordered.indexOf(selectedOption);
|
|
76
|
-
if (index < 0) return currentValues;
|
|
77
|
-
|
|
78
|
-
const targetIndex = direction === "up"
|
|
79
|
-
? Math.max(0, index - 1)
|
|
80
|
-
: Math.min(ordered.length - 1, index + 1);
|
|
81
|
-
if (targetIndex === index) return currentValues;
|
|
82
|
-
|
|
83
|
-
const next = [...ordered];
|
|
84
|
-
const [entry] = next.splice(index, 1);
|
|
85
|
-
next.splice(targetIndex, 0, entry!);
|
|
86
|
-
const unknownValues = currentValues.filter((value) => !optionValueSet.has(value));
|
|
87
|
-
return [...next, ...unknownValues];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
56
|
function SelectFieldDialog({
|
|
91
57
|
dismiss,
|
|
92
58
|
dialogId,
|
|
@@ -116,7 +82,7 @@ function SelectFieldDialog({
|
|
|
116
82
|
return (
|
|
117
83
|
<DialogFrame
|
|
118
84
|
title={field.label}
|
|
119
|
-
footer="Use ↑↓ to choose · enter to apply · esc
|
|
85
|
+
footer="Use ↑↓ to choose · enter to apply · esc cancel"
|
|
120
86
|
>
|
|
121
87
|
<ListView
|
|
122
88
|
items={field.options.map((option) => ({
|
|
@@ -175,7 +141,7 @@ function TextFieldDialog({
|
|
|
175
141
|
return (
|
|
176
142
|
<DialogFrame
|
|
177
143
|
title={field.label}
|
|
178
|
-
footer="Enter to apply · esc
|
|
144
|
+
footer="Enter to apply · esc cancel"
|
|
179
145
|
>
|
|
180
146
|
<box flexDirection="column" gap={1}>
|
|
181
147
|
<TextField
|
|
@@ -216,121 +182,16 @@ function MultiSelectFieldDialog({
|
|
|
216
182
|
currentValue: unknown;
|
|
217
183
|
onApply: (value: string[]) => Promise<void>;
|
|
218
184
|
}) {
|
|
219
|
-
const optionByValue = new Map(field.options.map((option) => [option.value, option]));
|
|
220
|
-
const [selectedValues, setSelectedValues] = useState(() => coerceSelectedValues(currentValue));
|
|
221
|
-
const knownSelectedValues = selectedValues.filter((value) => optionByValue.has(value));
|
|
222
|
-
const orderedOptions = field.options;
|
|
223
|
-
const [selectedOptionId, setSelectedOptionId] = useState(orderedOptions[0]?.value ?? "");
|
|
224
|
-
const selectedIndex = Math.max(0, orderedOptions.findIndex((option) => option.value === selectedOptionId));
|
|
225
|
-
const selectedOptionValue = orderedOptions[selectedIndex]?.value ?? "";
|
|
226
|
-
const selectedValueOrder = knownSelectedValues.indexOf(selectedOptionValue);
|
|
227
|
-
const canMoveUp = field.type === "ordered-multi-select" && selectedValueOrder > 0;
|
|
228
|
-
const canMoveDown = field.type === "ordered-multi-select"
|
|
229
|
-
&& selectedValueOrder >= 0
|
|
230
|
-
&& selectedValueOrder < knownSelectedValues.length - 1;
|
|
231
|
-
|
|
232
|
-
useEffect(() => {
|
|
233
|
-
setSelectedValues(coerceSelectedValues(currentValue));
|
|
234
|
-
}, [currentValue]);
|
|
235
|
-
|
|
236
|
-
useEffect(() => {
|
|
237
|
-
if (orderedOptions.some((option) => option.value === selectedOptionId)) return;
|
|
238
|
-
setSelectedOptionId(orderedOptions[0]?.value ?? "");
|
|
239
|
-
}, [orderedOptions, selectedOptionId]);
|
|
240
|
-
|
|
241
|
-
const toggleItems = orderedOptions.map((option) => {
|
|
242
|
-
const order = knownSelectedValues.indexOf(option.value);
|
|
243
|
-
const orderDescription = field.type === "ordered-multi-select" && order >= 0
|
|
244
|
-
? `Order ${order + 1} of ${knownSelectedValues.length}.`
|
|
245
|
-
: null;
|
|
246
|
-
|
|
247
|
-
return {
|
|
248
|
-
id: option.value,
|
|
249
|
-
label: option.label,
|
|
250
|
-
enabled: selectedValues.includes(option.value),
|
|
251
|
-
description: [option.description, orderDescription].filter((entry): entry is string => !!entry).join(" "),
|
|
252
|
-
};
|
|
253
|
-
});
|
|
254
|
-
const listHeight = Math.min(12, Math.max(6, toggleItems.length));
|
|
255
|
-
|
|
256
|
-
const applySelectedValues = async (nextValues: string[]) => {
|
|
257
|
-
const previousValues = selectedValues;
|
|
258
|
-
setSelectedValues(nextValues);
|
|
259
|
-
try {
|
|
260
|
-
await onApply(nextValues);
|
|
261
|
-
} catch (error) {
|
|
262
|
-
setSelectedValues(previousValues);
|
|
263
|
-
throw error;
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const toggleOption = async (option: PaneSettingOption | undefined) => {
|
|
268
|
-
if (!option) return;
|
|
269
|
-
await applySelectedValues(toggleSelectedValue(selectedValues, option.value));
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const moveOption = async (direction: "up" | "down") => {
|
|
273
|
-
if (field.type !== "ordered-multi-select") return;
|
|
274
|
-
const option = orderedOptions[selectedIndex];
|
|
275
|
-
if (!option) return;
|
|
276
|
-
await applySelectedValues(moveSelectedValue(field, selectedValues, option.value, direction));
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
useDialogKeyboard((event) => {
|
|
280
|
-
event.stopPropagation();
|
|
281
|
-
if (event.name === "up" || event.name === "k") {
|
|
282
|
-
const nextIndex = Math.max(0, selectedIndex - 1);
|
|
283
|
-
setSelectedOptionId(orderedOptions[nextIndex]?.value ?? selectedOptionId);
|
|
284
|
-
} else if (event.name === "down" || event.name === "j") {
|
|
285
|
-
const nextIndex = Math.min(orderedOptions.length - 1, selectedIndex + 1);
|
|
286
|
-
setSelectedOptionId(orderedOptions[nextIndex]?.value ?? selectedOptionId);
|
|
287
|
-
}
|
|
288
|
-
else if (isSpaceKey(event)) {
|
|
289
|
-
void toggleOption(orderedOptions[selectedIndex]).catch(() => {});
|
|
290
|
-
} else if (event.name === "[" && field.type === "ordered-multi-select") {
|
|
291
|
-
void moveOption("up").catch(() => {});
|
|
292
|
-
} else if (event.name === "]" && field.type === "ordered-multi-select") {
|
|
293
|
-
void moveOption("down").catch(() => {});
|
|
294
|
-
} else if (event.name === "enter" || event.name === "return") {
|
|
295
|
-
dismiss();
|
|
296
|
-
} else if (event.name === "escape") {
|
|
297
|
-
dismiss();
|
|
298
|
-
}
|
|
299
|
-
}, dialogId);
|
|
300
|
-
|
|
301
185
|
return (
|
|
302
|
-
<
|
|
186
|
+
<MultiSelectDialogContent
|
|
187
|
+
dismiss={dismiss}
|
|
188
|
+
dialogId={dialogId}
|
|
303
189
|
title={field.label}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
<ToggleList
|
|
310
|
-
items={toggleItems}
|
|
311
|
-
selectedIdx={selectedIndex}
|
|
312
|
-
bgColor={colors.commandBg}
|
|
313
|
-
height={listHeight}
|
|
314
|
-
scrollable
|
|
315
|
-
showSelectedDescription={false}
|
|
316
|
-
onSelect={(index) => setSelectedOptionId(orderedOptions[index]?.value ?? selectedOptionId)}
|
|
317
|
-
onToggle={(id) => {
|
|
318
|
-
setSelectedOptionId(id);
|
|
319
|
-
void toggleOption(optionByValue.get(id)).catch(() => {});
|
|
320
|
-
}}
|
|
321
|
-
/>
|
|
322
|
-
<box flexDirection="row" gap={1}>
|
|
323
|
-
<Button label="Toggle" variant="secondary" onPress={() => { void toggleOption(orderedOptions[selectedIndex]).catch(() => {}); }} />
|
|
324
|
-
{field.type === "ordered-multi-select" && (
|
|
325
|
-
<>
|
|
326
|
-
<Button label="Move Up" variant="ghost" disabled={!canMoveUp} onPress={() => { void moveOption("up").catch(() => {}); }} />
|
|
327
|
-
<Button label="Move Down" variant="ghost" disabled={!canMoveDown} onPress={() => { void moveOption("down").catch(() => {}); }} />
|
|
328
|
-
</>
|
|
329
|
-
)}
|
|
330
|
-
<Button label="Done" variant="primary" onPress={dismiss} />
|
|
331
|
-
</box>
|
|
332
|
-
</box>
|
|
333
|
-
</DialogFrame>
|
|
190
|
+
options={field.options}
|
|
191
|
+
selectedValues={coerceSelectedValues(currentValue)}
|
|
192
|
+
onChange={onApply}
|
|
193
|
+
ordered={field.type === "ordered-multi-select"}
|
|
194
|
+
/>
|
|
334
195
|
);
|
|
335
196
|
}
|
|
336
197
|
|
|
@@ -415,7 +276,7 @@ export function PaneSettingsDialogContent({
|
|
|
415
276
|
|
|
416
277
|
if (!descriptor) {
|
|
417
278
|
return (
|
|
418
|
-
<DialogFrame title="Pane Settings" footer="Press esc to
|
|
279
|
+
<DialogFrame title="Pane Settings" footer="Press esc to cancel">
|
|
419
280
|
<text fg={colors.textDim}>This pane is no longer configurable.</text>
|
|
420
281
|
</DialogFrame>
|
|
421
282
|
);
|
|
@@ -424,7 +285,7 @@ export function PaneSettingsDialogContent({
|
|
|
424
285
|
const title = descriptor.settingsDef.title ?? `${descriptor.paneDef.name} Settings`;
|
|
425
286
|
|
|
426
287
|
return (
|
|
427
|
-
<DialogFrame title={title} footer="Use ↑↓ to choose · enter to edit · esc
|
|
288
|
+
<DialogFrame title={title} footer="Use ↑↓ to choose · enter to edit · esc cancel">
|
|
428
289
|
<ListView
|
|
429
290
|
items={fields.map((field) => ({
|
|
430
291
|
id: field.key,
|
|
@@ -4,6 +4,7 @@ import type { InputRenderable } from "@opentui/core";
|
|
|
4
4
|
import { useDialogKeyboard, type PromptContext } from "@opentui-ui/dialog/react";
|
|
5
5
|
import type { Quote } from "../types/financials";
|
|
6
6
|
import { colors } from "../theme/colors";
|
|
7
|
+
import { formatMarketPrice } from "../utils/market-format";
|
|
7
8
|
import { DialogFrame, ListView, NumberField } from "./ui";
|
|
8
9
|
import { padTo } from "../utils/format";
|
|
9
10
|
|
|
@@ -17,6 +18,7 @@ export interface PriceSelectorDialogProps {
|
|
|
17
18
|
label: string;
|
|
18
19
|
currentValue?: number;
|
|
19
20
|
quote?: Quote;
|
|
21
|
+
assetCategory?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function buildPresets(quote: Quote | undefined): PricePreset[] {
|
|
@@ -46,20 +48,22 @@ function buildPresets(quote: Quote | undefined): PricePreset[] {
|
|
|
46
48
|
return presets;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function formatMarketContext(quote: Quote | undefined): string {
|
|
51
|
+
function formatMarketContext(quote: Quote | undefined, assetCategory?: string): string {
|
|
50
52
|
if (!quote) return "No quote data available";
|
|
51
53
|
const parts: string[] = [];
|
|
52
|
-
parts.push(`Last ${quote.price
|
|
54
|
+
parts.push(`Last ${formatMarketPrice(quote.price, { assetCategory })}`);
|
|
53
55
|
if (quote.bid != null) {
|
|
54
|
-
const
|
|
56
|
+
const bidValue = formatMarketPrice(quote.bid, { assetCategory });
|
|
57
|
+
const bidStr = quote.bidSize != null ? `Bid ${bidValue} × ${quote.bidSize}` : `Bid ${bidValue}`;
|
|
55
58
|
parts.push(bidStr);
|
|
56
59
|
}
|
|
57
60
|
if (quote.ask != null) {
|
|
58
|
-
const
|
|
61
|
+
const askValue = formatMarketPrice(quote.ask, { assetCategory });
|
|
62
|
+
const askStr = quote.askSize != null ? `Ask ${askValue} × ${quote.askSize}` : `Ask ${askValue}`;
|
|
59
63
|
parts.push(askStr);
|
|
60
64
|
}
|
|
61
65
|
if (quote.bid != null && quote.ask != null) {
|
|
62
|
-
parts.push(`Spd ${(quote.ask - quote.bid)
|
|
66
|
+
parts.push(`Spd ${formatMarketPrice(quote.ask - quote.bid, { assetCategory })}`);
|
|
63
67
|
}
|
|
64
68
|
return parts.join(" · ");
|
|
65
69
|
}
|
|
@@ -72,6 +76,7 @@ export function PriceSelectorDialog({
|
|
|
72
76
|
label,
|
|
73
77
|
currentValue,
|
|
74
78
|
quote,
|
|
79
|
+
assetCategory,
|
|
75
80
|
}: PromptContext<string> & PriceSelectorDialogProps) {
|
|
76
81
|
const presets = buildPresets(quote);
|
|
77
82
|
const [index, setIndex] = useState(0);
|
|
@@ -135,7 +140,7 @@ export function PriceSelectorDialog({
|
|
|
135
140
|
}
|
|
136
141
|
>
|
|
137
142
|
<box flexDirection="column">
|
|
138
|
-
<text fg={colors.textDim}>{formatMarketContext(quote)}</text>
|
|
143
|
+
<text fg={colors.textDim}>{formatMarketContext(quote, assetCategory)}</text>
|
|
139
144
|
<box height={1} />
|
|
140
145
|
|
|
141
146
|
{presets.length > 0 && (
|
|
@@ -163,7 +168,7 @@ export function PriceSelectorDialog({
|
|
|
163
168
|
{padTo(item.label, presetWidth)}
|
|
164
169
|
</text>
|
|
165
170
|
<text fg={state.selected ? colors.text : colors.textMuted}>
|
|
166
|
-
{preset
|
|
171
|
+
{preset ? formatMarketPrice(preset.value, { assetCategory }) : ""}
|
|
167
172
|
</text>
|
|
168
173
|
{preset?.detail && (
|
|
169
174
|
<text fg={colors.textDim}>{` ${preset.detail}`}</text>
|
|
@@ -96,6 +96,56 @@ describe("TickerBadgeText", () => {
|
|
|
96
96
|
expect(opened).toEqual(["TSLA"]);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
test("renders inline links alongside ticker badges", async () => {
|
|
100
|
+
testSetup = await testRender(
|
|
101
|
+
<TickerBadgeText
|
|
102
|
+
text="Read https://example.com while watching $TSLA"
|
|
103
|
+
lineWidth={60}
|
|
104
|
+
catalog={{ TSLA: makeCatalogEntry() }}
|
|
105
|
+
textColor="#ffffff"
|
|
106
|
+
openTicker={() => {}}
|
|
107
|
+
/>,
|
|
108
|
+
{ width: 60, height: 4 },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await testSetup.renderOnce();
|
|
112
|
+
|
|
113
|
+
const frame = testSetup.captureCharFrame();
|
|
114
|
+
expect(frame).toContain("https://example.com");
|
|
115
|
+
expect(frame).toContain("TSLA -5%");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("opens detected links without trailing punctuation when clicked", async () => {
|
|
119
|
+
const opened: string[] = [];
|
|
120
|
+
testSetup = await testRender(
|
|
121
|
+
<TickerBadgeText
|
|
122
|
+
text="Read https://example.com/story."
|
|
123
|
+
lineWidth={60}
|
|
124
|
+
catalog={{}}
|
|
125
|
+
textColor="#ffffff"
|
|
126
|
+
openTicker={() => {}}
|
|
127
|
+
openLink={(url) => opened.push(url)}
|
|
128
|
+
/>,
|
|
129
|
+
{ width: 60, height: 4 },
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await testSetup.renderOnce();
|
|
133
|
+
|
|
134
|
+
const lines = testSetup.captureCharFrame().split("\n");
|
|
135
|
+
const row = lines.findIndex((line) => line.includes("https://example.com/story."));
|
|
136
|
+
const col = lines[row]?.indexOf("https://example.com/story") ?? -1;
|
|
137
|
+
|
|
138
|
+
expect(row).toBeGreaterThanOrEqual(0);
|
|
139
|
+
expect(col).toBeGreaterThanOrEqual(0);
|
|
140
|
+
|
|
141
|
+
await act(async () => {
|
|
142
|
+
await testSetup!.mockMouse.click(col + 1, row);
|
|
143
|
+
await testSetup!.renderOnce();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(opened).toEqual(["https://example.com/story"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
99
149
|
test("shows the current price when the badge is hovered", async () => {
|
|
100
150
|
testSetup = await testRender(
|
|
101
151
|
<TickerBadgeText
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { TickerBadge } from "./ticker-badge";
|
|
3
|
-
import {
|
|
3
|
+
import { ExternalLinkText, openUrl } from "./ui";
|
|
4
|
+
import { tokenizeInlineContent } from "../utils/inline-content-tokenizer";
|
|
4
5
|
import type { InlineTickerCatalogEntry } from "../state/use-inline-tickers";
|
|
5
6
|
|
|
6
7
|
export interface TickerBadgeTextProps {
|
|
@@ -9,6 +10,7 @@ export interface TickerBadgeTextProps {
|
|
|
9
10
|
catalog: Record<string, InlineTickerCatalogEntry>;
|
|
10
11
|
textColor: string;
|
|
11
12
|
openTicker: (symbol: string) => void;
|
|
13
|
+
openLink?: (url: string) => void;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export function TickerBadgeText({
|
|
@@ -17,9 +19,10 @@ export function TickerBadgeText({
|
|
|
17
19
|
catalog,
|
|
18
20
|
textColor,
|
|
19
21
|
openTicker,
|
|
22
|
+
openLink = openUrl,
|
|
20
23
|
}: TickerBadgeTextProps) {
|
|
21
24
|
const [hoveredSymbol, setHoveredSymbol] = useState<string | null>(null);
|
|
22
|
-
const tokens =
|
|
25
|
+
const tokens = tokenizeInlineContent(text);
|
|
23
26
|
|
|
24
27
|
return (
|
|
25
28
|
<box
|
|
@@ -33,6 +36,18 @@ export function TickerBadgeText({
|
|
|
33
36
|
return <text key={`text:${index}`} fg={textColor}>{token.value}</text>;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
if (token.kind === "link") {
|
|
40
|
+
return (
|
|
41
|
+
<ExternalLinkText
|
|
42
|
+
key={`link:${index}`}
|
|
43
|
+
url={token.url}
|
|
44
|
+
label={token.value}
|
|
45
|
+
color={textColor}
|
|
46
|
+
onOpen={openLink}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
const entry = catalog[token.symbol];
|
|
37
52
|
if (!entry || entry.status === "missing") {
|
|
38
53
|
return <text key={`raw:${index}`} fg={textColor}>{token.value}</text>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TextAttributes } from "@opentui/core";
|
|
2
2
|
import { colors, priceColor } from "../theme/colors";
|
|
3
|
-
import {
|
|
3
|
+
import { formatMarketPriceWithCurrency } from "../utils/market-format";
|
|
4
4
|
import type { Quote } from "../types/financials";
|
|
5
5
|
|
|
6
6
|
export interface TickerBadgeProps {
|
|
@@ -50,7 +50,7 @@ export function TickerBadge({
|
|
|
50
50
|
}: TickerBadgeProps) {
|
|
51
51
|
const tone = status === "ready" && quote ? priceColor(quote.changePercent) : colors.borderFocused;
|
|
52
52
|
const label = hovered && quote
|
|
53
|
-
? `${symbol} ${
|
|
53
|
+
? `${symbol} ${formatMarketPriceWithCurrency(quote.price, quote.currency, { minimumFractionDigits: 2 })}`
|
|
54
54
|
: status === "ready" && quote
|
|
55
55
|
? formatBadgeChange(quote.changePercent)
|
|
56
56
|
: "…";
|
|
@@ -3,9 +3,11 @@ import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
|
3
3
|
import { EmptyState } from "./ui";
|
|
4
4
|
import { colors, hoverBg } from "../theme/colors";
|
|
5
5
|
import type { ColumnConfig } from "../types/config";
|
|
6
|
-
import type { TickerFinancials } from "../types/financials";
|
|
6
|
+
import type { TickerFinancials, PricePoint } from "../types/financials";
|
|
7
7
|
import type { TickerRecord } from "../types/ticker";
|
|
8
8
|
import { padTo } from "../utils/format";
|
|
9
|
+
import { renderChart, resolveChartPalette, type StyledContent } from "./chart/chart-renderer";
|
|
10
|
+
import type { ProjectedChartPoint } from "./chart/chart-data";
|
|
9
11
|
|
|
10
12
|
export interface TickerTableCell {
|
|
11
13
|
text: string;
|
|
@@ -64,6 +66,7 @@ interface TickerListTableProps {
|
|
|
64
66
|
onRowActivate?: (ticker: TickerRecord) => void;
|
|
65
67
|
emptyTitle?: string;
|
|
66
68
|
emptyHint?: string;
|
|
69
|
+
showSparklines?: boolean;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
const TickerListHeader = memo(function TickerListHeader({
|
|
@@ -118,6 +121,38 @@ const TickerListHeader = memo(function TickerListHeader({
|
|
|
118
121
|
);
|
|
119
122
|
});
|
|
120
123
|
|
|
124
|
+
function renderSparkline(priceHistory: PricePoint[]): StyledContent | null {
|
|
125
|
+
if (priceHistory.length < 5) return null;
|
|
126
|
+
|
|
127
|
+
const last20 = priceHistory.slice(-20);
|
|
128
|
+
const points: ProjectedChartPoint[] = last20.map((pt) => ({
|
|
129
|
+
date: pt.date,
|
|
130
|
+
open: pt.open ?? pt.close,
|
|
131
|
+
high: pt.high ?? pt.close,
|
|
132
|
+
low: pt.low ?? pt.close,
|
|
133
|
+
close: pt.close,
|
|
134
|
+
volume: pt.volume ?? 0,
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
const first = last20[0]?.close ?? 0;
|
|
138
|
+
const last = last20[last20.length - 1]?.close ?? 0;
|
|
139
|
+
const trend = last >= first ? "positive" : "negative";
|
|
140
|
+
const palette = resolveChartPalette(colors, trend);
|
|
141
|
+
|
|
142
|
+
const result = renderChart(points, {
|
|
143
|
+
width: 10,
|
|
144
|
+
height: 1,
|
|
145
|
+
showVolume: false,
|
|
146
|
+
volumeHeight: 0,
|
|
147
|
+
cursorX: null,
|
|
148
|
+
cursorY: null,
|
|
149
|
+
mode: "line",
|
|
150
|
+
colors: palette,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return result.lines[0] ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
121
156
|
const TickerListRow = memo(function TickerListRow({
|
|
122
157
|
columns,
|
|
123
158
|
ticker,
|
|
@@ -130,6 +165,8 @@ const TickerListRow = memo(function TickerListRow({
|
|
|
130
165
|
setCursorSymbol,
|
|
131
166
|
resolveCell,
|
|
132
167
|
onRowActivate,
|
|
168
|
+
showSparklines,
|
|
169
|
+
priceHistory,
|
|
133
170
|
}: {
|
|
134
171
|
columns: ColumnConfig[];
|
|
135
172
|
ticker: TickerRecord;
|
|
@@ -142,10 +179,16 @@ const TickerListRow = memo(function TickerListRow({
|
|
|
142
179
|
setCursorSymbol: (symbol: string) => void;
|
|
143
180
|
resolveCell: ResolveTickerTableCell;
|
|
144
181
|
onRowActivate?: (ticker: TickerRecord) => void;
|
|
182
|
+
showSparklines?: boolean;
|
|
183
|
+
priceHistory?: PricePoint[];
|
|
145
184
|
}) {
|
|
146
185
|
const lastActivatedAtRef = useRef<number | null>(null);
|
|
147
186
|
const rowBg = isSelected ? colors.selected : isHovered ? hoverBg() : colors.bg;
|
|
148
187
|
|
|
188
|
+
const sparkline = showSparklines && priceHistory && priceHistory.length >= 5
|
|
189
|
+
? renderSparkline(priceHistory)
|
|
190
|
+
: null;
|
|
191
|
+
|
|
149
192
|
return (
|
|
150
193
|
<box
|
|
151
194
|
flexDirection="row"
|
|
@@ -182,6 +225,11 @@ const TickerListRow = memo(function TickerListRow({
|
|
|
182
225
|
</box>
|
|
183
226
|
);
|
|
184
227
|
})}
|
|
228
|
+
{sparkline && (
|
|
229
|
+
<box width={12}>
|
|
230
|
+
<text content={sparkline} />
|
|
231
|
+
</box>
|
|
232
|
+
)}
|
|
185
233
|
</box>
|
|
186
234
|
);
|
|
187
235
|
}, (previous, next) => (
|
|
@@ -196,6 +244,8 @@ const TickerListRow = memo(function TickerListRow({
|
|
|
196
244
|
&& previous.setCursorSymbol === next.setCursorSymbol
|
|
197
245
|
&& previous.resolveCell === next.resolveCell
|
|
198
246
|
&& previous.onRowActivate === next.onRowActivate
|
|
247
|
+
&& previous.showSparklines === next.showSparklines
|
|
248
|
+
&& previous.priceHistory === next.priceHistory
|
|
199
249
|
));
|
|
200
250
|
|
|
201
251
|
export function TickerListTable({
|
|
@@ -218,6 +268,7 @@ export function TickerListTable({
|
|
|
218
268
|
onRowActivate,
|
|
219
269
|
emptyTitle = "No tickers.",
|
|
220
270
|
emptyHint = "Press Ctrl+P to add one.",
|
|
271
|
+
showSparklines,
|
|
221
272
|
}: TickerListTableProps) {
|
|
222
273
|
const safeFlashSymbols = flashSymbols ?? new Map<string, QuoteFlashDirection>();
|
|
223
274
|
|
|
@@ -262,6 +313,8 @@ export function TickerListTable({
|
|
|
262
313
|
setCursorSymbol={setCursorSymbol}
|
|
263
314
|
resolveCell={resolveCell}
|
|
264
315
|
onRowActivate={onRowActivate}
|
|
316
|
+
showSparklines={showSparklines}
|
|
317
|
+
priceHistory={financialsMap.get(ticker.metadata.ticker)?.priceHistory}
|
|
265
318
|
/>
|
|
266
319
|
);
|
|
267
320
|
})
|
|
@@ -20,6 +20,7 @@ export interface ToggleListProps {
|
|
|
20
20
|
flexGrow?: number;
|
|
21
21
|
scrollable?: boolean;
|
|
22
22
|
showSelectedDescription?: boolean;
|
|
23
|
+
rowIdPrefix?: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function ToggleList({
|
|
@@ -32,6 +33,7 @@ export function ToggleList({
|
|
|
32
33
|
flexGrow,
|
|
33
34
|
scrollable,
|
|
34
35
|
showSelectedDescription = true,
|
|
36
|
+
rowIdPrefix,
|
|
35
37
|
}: ToggleListProps) {
|
|
36
38
|
const listItems: ListViewItem[] = items.map((item) => ({
|
|
37
39
|
id: item.id,
|
|
@@ -52,17 +54,28 @@ export function ToggleList({
|
|
|
52
54
|
onActivate={(item) => {
|
|
53
55
|
onToggle?.(item.id);
|
|
54
56
|
}}
|
|
55
|
-
renderRow={(item, state) => {
|
|
57
|
+
renderRow={(item, state, index) => {
|
|
56
58
|
const toggleItem = items.find((entry) => entry.id === item.id);
|
|
57
59
|
const checked = toggleItem?.enabled ? "\u2713" : " ";
|
|
60
|
+
const activate = (event: any) => {
|
|
61
|
+
event.stopPropagation?.();
|
|
62
|
+
if (state.disabled) return;
|
|
63
|
+
onSelect?.(index);
|
|
64
|
+
onToggle?.(item.id);
|
|
65
|
+
};
|
|
58
66
|
return (
|
|
59
|
-
<box
|
|
67
|
+
<box
|
|
68
|
+
id={rowIdPrefix ? `${rowIdPrefix}:${item.id}` : undefined}
|
|
69
|
+
flexDirection="row"
|
|
70
|
+
onMouseDown={activate}
|
|
71
|
+
>
|
|
60
72
|
<text fg={state.selected ? colors.selectedText : colors.textDim}>
|
|
61
73
|
{state.selected ? "\u25b8 " : " "}
|
|
62
74
|
</text>
|
|
63
75
|
<text
|
|
64
76
|
fg={state.selected ? colors.text : colors.textDim}
|
|
65
77
|
attributes={state.selected ? TextAttributes.BOLD : 0}
|
|
78
|
+
onMouseDown={activate}
|
|
66
79
|
>
|
|
67
80
|
{`[${checked}] ${item.label}`}
|
|
68
81
|
</text>
|