gloomberb 0.18.2 → 0.18.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/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +106 -5
- package/src/api-client/data.ts +15 -0
- package/src/api-client/index.ts +2 -0
- package/src/api-client/request.ts +5 -3
- package/src/api-client/telemetry.ts +17 -0
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/runtime/update.ts +28 -8
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- package/src/cli/commands/market.ts +73 -33
- package/src/cli/commands/ticker.ts +82 -25
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +8 -0
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/listing-arg.ts +15 -2
- package/src/cli/name-entries.ts +51 -12
- package/src/cli/pane-functions/capabilities.ts +2 -0
- package/src/cli/pane-functions/catalog.ts +16 -0
- package/src/cli/pane-functions/dom.ts +2 -1
- package/src/cli/pane-functions/headless.ts +25 -6
- package/src/cli/pane-functions/index.ts +64 -5
- package/src/cli/pane-functions/resolver.ts +19 -5
- package/src/cli/report-tables.ts +136 -18
- package/src/cli/result.ts +12 -4
- package/src/cli/symbol-list.ts +47 -11
- package/src/components/chart/composite/composite-chart.tsx +14 -5
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/interactions.ts +2 -1
- package/src/components/chart/composite/panel-surface.tsx +11 -0
- package/src/components/chart/composite/types.ts +6 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
- package/src/components/command-bar/routes/root/command-items.ts +32 -6
- package/src/components/data-table/view.tsx +23 -3
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +1 -0
- package/src/components/layout/floating-pane.tsx +1 -0
- package/src/components/layout/header.tsx +228 -55
- package/src/components/layout/pane/header.tsx +10 -3
- package/src/components/layout/pane/index.tsx +1 -0
- package/src/components/layout/pane/pane-tabs.tsx +9 -1
- package/src/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +3 -1
- package/src/core/app-services.ts +4 -0
- package/src/i18n/es.ts +22 -0
- package/src/i18n/ja.ts +22 -0
- package/src/i18n/ko.ts +22 -0
- package/src/i18n/zh-cn.ts +22 -0
- package/src/i18n/zh-tw.ts +22 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
- package/src/plugins/builtin/alerts/events.ts +1 -0
- package/src/plugins/builtin/alerts/research-rules.ts +1 -0
- package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
- package/src/plugins/builtin/attention/pane.tsx +16 -14
- package/src/plugins/builtin/awards/pane.tsx +6 -6
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/broker-trading/dock.tsx +4 -2
- package/src/plugins/builtin/broker-trading/pane.tsx +6 -4
- package/src/plugins/builtin/broker-trading/ticket.tsx +10 -5
- package/src/plugins/builtin/buildout/pane/index.tsx +9 -6
- package/src/plugins/builtin/catalysts/client.ts +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +3 -4
- package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
- package/src/plugins/builtin/company-attention/pane.tsx +13 -11
- package/src/plugins/builtin/company-kpis/pane.tsx +4 -3
- package/src/plugins/builtin/correlation/headless.ts +2 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
- package/src/plugins/builtin/correlation/settings.ts +1 -0
- package/src/plugins/builtin/cot/pane.tsx +4 -3
- package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
- package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
- package/src/plugins/builtin/doe/pane.tsx +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +10 -0
- package/src/plugins/builtin/equity-screener/model.ts +28 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +105 -98
- package/src/plugins/builtin/exposure/pane.tsx +10 -9
- package/src/plugins/builtin/gpu/board.tsx +4 -1
- package/src/plugins/builtin/gpu/changes.tsx +10 -10
- package/src/plugins/builtin/gpu/equities.tsx +4 -1
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/holders/client.ts +2 -1
- package/src/plugins/builtin/holders/headless.ts +2 -1
- package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/insider/base-values.ts +11 -7
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +19 -6
- package/src/plugins/builtin/insider/index.tsx +17 -8
- package/src/plugins/builtin/insider/issuer.ts +34 -4
- package/src/plugins/builtin/insider/signals-client.ts +2 -2
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- package/src/plugins/builtin/iv-history/events.ts +145 -0
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +91 -7
- package/src/plugins/builtin/jodi/pane.tsx +4 -4
- package/src/plugins/builtin/market-movers/client.ts +4 -3
- package/src/plugins/builtin/market-movers/screener.ts +4 -3
- package/src/plugins/builtin/notes/cli.ts +1 -1
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +41 -13
- package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
- package/src/plugins/builtin/options-positioning/headless.ts +11 -3
- package/src/plugins/builtin/options-positioning/model.ts +24 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
- package/src/plugins/builtin/options-scenario/client.ts +19 -1
- package/src/plugins/builtin/options-scenario/headless.ts +13 -6
- package/src/plugins/builtin/options-scenario/pane.tsx +13 -5
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/power/client.ts +2 -2
- package/src/plugins/builtin/power/pane.tsx +36 -31
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
- package/src/plugins/builtin/seasonality/client.ts +2 -1
- package/src/plugins/builtin/sectors/client.ts +4 -3
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- package/src/plugins/builtin/shared/research-trading.tsx +2 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -1
- package/src/plugins/builtin/social-mentions/client.ts +2 -1
- package/src/plugins/builtin/vol-surface/client.ts +23 -3
- package/src/plugins/builtin/vol-surface/headless.ts +33 -14
- package/src/plugins/builtin/vol-surface/model.ts +30 -10
- package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/renderers/dom/desktop/controls.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- package/src/renderers/dom/host/tabs.tsx +16 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +39 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
- package/src/renderers/electrobun/view/news-source.ts +4 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
- package/src/renderers/opentui/start.tsx +7 -0
- package/src/sources/gloomberb-cloud/index.ts +7 -0
- package/src/sources/provider-router/index.ts +16 -0
- package/src/telemetry/crash-reports.ts +4 -0
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/tickers/company-name-listing.ts +9 -7
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/listing-fate.ts +30 -0
- package/src/types/data-provider.ts +6 -0
- package/src/types/headless.ts +7 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +87 -23
- package/src/utils/currency-pair.ts +26 -0
- package/src/utils/sec.ts +68 -0
|
@@ -37,8 +37,8 @@ import { isNotATickerMessage } from "../not-a-ticker";
|
|
|
37
37
|
import { isCliFailure } from "../errors";
|
|
38
38
|
import { isTickerLinkPeer } from "../../layout/ticker-navigation";
|
|
39
39
|
import { knownGapMessage, commandPointer, commandPointerLeads, topicPointer } from "../known-gaps";
|
|
40
|
-
import { selectReportTables } from "../report-tables";
|
|
41
|
-
import { cliOutputWritten } from "../result";
|
|
40
|
+
import { selectReportTables, type CliReportTables } from "../report-tables";
|
|
41
|
+
import { cliOutputWritten, type CliErrorObject } from "../result";
|
|
42
42
|
import type { CatalogCommandHit } from "../unknown-command";
|
|
43
43
|
import { CLI_EXIT_CODES } from "../exit-codes";
|
|
44
44
|
import { shotOptionRefusal } from "./shot-options";
|
|
@@ -204,6 +204,63 @@ function foundNothing(data: { rowCount: number; unavailableSymbols: readonly str
|
|
|
204
204
|
return data.rowCount === 0 || data.unavailableSymbols.length >= data.rowCount;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/** A listing key as asked or reported (`BHP:XASX`, `fred:DGS10`, `SPY`), in one case. */
|
|
208
|
+
const listingKey = (symbol: string) => symbol.trim().toUpperCase();
|
|
209
|
+
const bareKey = (key: string) => key.split(":")[0]!;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The listings asked that a report has no data for. A report names one by its
|
|
213
|
+
* full key; one that names it by the bare symbol (`SAN` for `SAN:EPA`), or by
|
|
214
|
+
* the listing a bare symbol resolved to (`SXR8:XETRA` for `SXR8`), stands for
|
|
215
|
+
* the one listing asked that way, never for a listing asked by its own key:
|
|
216
|
+
* with `BHP:XASX` and `BHP` asked, `BHP:XASX` unavailable leaves `BHP`.
|
|
217
|
+
*/
|
|
218
|
+
function missingListings(asked: readonly string[], unavailable: readonly string[]): Set<string> {
|
|
219
|
+
const keys = asked.map(listingKey);
|
|
220
|
+
const exact = new Set(keys);
|
|
221
|
+
const missing = new Set<string>();
|
|
222
|
+
for (const entry of unavailable.map(listingKey)) {
|
|
223
|
+
if (exact.has(entry)) {
|
|
224
|
+
missing.add(entry);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const loose = keys.filter((key) => bareKey(key) === bareKey(entry) && key.includes(":") !== entry.includes(":"));
|
|
228
|
+
if (loose.length === 1) missing.add(loose[0]!);
|
|
229
|
+
}
|
|
230
|
+
return missing;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Whether a table holds a value past its first column (a row's label or date) and its reasons: a figure, a time, any text. */
|
|
234
|
+
function tablesHoldValues(tables: CliReportTables): boolean {
|
|
235
|
+
return tables.tables.some((table) => {
|
|
236
|
+
const skipped = new Set(table.columns.flatMap((header, index) => index === 0 || header === "status" || header === "value_status" ? [index] : []));
|
|
237
|
+
return table.rows.some((row) => row.some((cell, index) => !skipped.has(index) && cell !== "" && cell != null));
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Why `fn` fails a report it printed, or null when the report has data. It
|
|
243
|
+
* fails when it is empty (a pane behind a wall reads nothing), when it has no
|
|
244
|
+
* data for any listing asked and its tables hold no value either, or when it
|
|
245
|
+
* is a list (`failsWithoutData`) with no name that has any. A report with data
|
|
246
|
+
* for some of what was asked, warnings or errors beside it, is a result: a
|
|
247
|
+
* loss-maker's P/E band without a P/E, or IV history with only realized
|
|
248
|
+
* volatility, exits 0.
|
|
249
|
+
*/
|
|
250
|
+
export function reportFailure(
|
|
251
|
+
data: { empty: boolean; rowCount: number; symbols: readonly string[]; unavailableSymbols: readonly string[]; errors?: readonly string[]; reason?: unknown },
|
|
252
|
+
asked: string,
|
|
253
|
+
{ failsWithoutData = false, tables }: { failsWithoutData?: boolean; tables?: CliReportTables } = {},
|
|
254
|
+
): CliErrorObject | null {
|
|
255
|
+
const missing = missingListings(data.symbols, data.unavailableSymbols);
|
|
256
|
+
const noListing = data.symbols.length > 0 && data.symbols.every((symbol) => missing.has(listingKey(symbol)));
|
|
257
|
+
const noValues = tables ? !tablesHoldValues(tables) : false;
|
|
258
|
+
if (!data.empty && !(noListing && noValues) && !(failsWithoutData && foundNothing(data))) return null;
|
|
259
|
+
const reasons = [...data.errors ?? [], ...typeof data.reason === "string" ? [data.reason] : []];
|
|
260
|
+
const unavailable = data.unavailableSymbols.length > 0 ? `no data for ${data.unavailableSymbols.join(", ")}` : "no data";
|
|
261
|
+
return { code: "no_data", message: `${asked}: ${reasons.length > 0 ? reasons.join("; ") : unavailable}.`.replace(/\.\.$/, ".") };
|
|
262
|
+
}
|
|
263
|
+
|
|
207
264
|
export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
|
|
208
265
|
await runPaneCliCommand(ctx, async () => {
|
|
209
266
|
await withPaneRuntime(ctx, args, async ({ parsed, context, registry, resolved }) => {
|
|
@@ -277,13 +334,15 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
|
|
|
277
334
|
? { ...report.data, glossary: glossary.map(({ term, short, definition }) => ({ term, ...(short ? { short } : {}), definition })) }
|
|
278
335
|
: report.data;
|
|
279
336
|
const warnings = [...parsed.listingNotes ?? [], ...await choice ?? [], ...report.data.warnings ?? []];
|
|
337
|
+
// A report with no data is printed as it is, and fails: exit 1, and `ok: false` in JSON with the reason.
|
|
338
|
+
const failure = reportFailure(report.data, ["fn", resolved.token, parsed.arg.trim()].filter(Boolean).join(" "), {
|
|
339
|
+
failsWithoutData: resolved.headless?.failsWithoutData, tables: report.tables });
|
|
280
340
|
// The report states its own freshness in its data and text; JSON cites it as `source` too.
|
|
281
|
-
ctx.printResult({ data, exportFreshness: report.data.freshness, ...(warnings.length > 0 ? { warnings } : {}) }, {
|
|
341
|
+
ctx.printResult({ data, exportFreshness: report.data.freshness, ...(warnings.length > 0 ? { warnings } : {}), ...(failure ? { failure } : {}) }, {
|
|
282
342
|
text: () => glossary ? `${report.text}\n\n${renderReportGlossary(resolved.token, glossary)}` : report.text,
|
|
283
343
|
...(tabular ? { tables: selectReportTables(report.tables, resolved.tableSection) } : {}),
|
|
284
344
|
});
|
|
285
|
-
|
|
286
|
-
if (resolved.headless?.failsWithoutData && foundNothing(report.data)) process.exitCode = CLI_EXIT_CODES.error;
|
|
345
|
+
if (failure) process.exitCode = CLI_EXIT_CODES.error;
|
|
287
346
|
}, { strictHeadlessOptions: true, tableSection: true });
|
|
288
347
|
});
|
|
289
348
|
}
|
|
@@ -23,7 +23,9 @@ import type { MarketContext } from "../types";
|
|
|
23
23
|
import {
|
|
24
24
|
buildPaneFunctionLookup,
|
|
25
25
|
buildTemplateContext,
|
|
26
|
+
findFunctionCode,
|
|
26
27
|
nearFunctionCodes,
|
|
28
|
+
suggestedFunctionCodes,
|
|
27
29
|
suggestPaneFunctions,
|
|
28
30
|
type NearFunctionCode,
|
|
29
31
|
type PaneFunctionCatalog,
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
optionSettings,
|
|
37
39
|
type ParsedPaneFunctionArgs,
|
|
38
40
|
} from "./options";
|
|
41
|
+
import { argumentSymbols, validateSymbolCount } from "./headless";
|
|
39
42
|
import {
|
|
40
43
|
capabilityHasOption,
|
|
41
44
|
capabilityPaneSettings,
|
|
@@ -65,9 +68,10 @@ export interface ResolvedPaneFunction {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
async function buildPaneInstance(
|
|
68
|
-
resolved: Pick<ResolvedPaneFunction, "pane" | "template" | "headless" | "createOptions" | "optionSettings" | "capability" | "options">,
|
|
71
|
+
resolved: Pick<ResolvedPaneFunction, "token" | "pane" | "template" | "headless" | "createOptions" | "optionSettings" | "capability" | "options">,
|
|
69
72
|
context: MarketContext,
|
|
70
73
|
target: string,
|
|
74
|
+
command: "fn" | "shot",
|
|
71
75
|
): Promise<PaneInstanceConfig> {
|
|
72
76
|
// Free text is a phrase, not a symbol. Treating it as a ticker makes the
|
|
73
77
|
// pane look for a provider for values such as "HIGH GROWTH SOFTWARE".
|
|
@@ -88,7 +92,11 @@ async function buildPaneInstance(
|
|
|
88
92
|
return { ...spec.settings, ...resolved.optionSettings };
|
|
89
93
|
}
|
|
90
94
|
const chartSpec = parseChartSpec(spec.settings?.chartSpec);
|
|
91
|
-
if (!chartSpec)
|
|
95
|
+
if (!chartSpec) {
|
|
96
|
+
// A chart of too few tickers (CMP of one) has none to draw: say how many it takes.
|
|
97
|
+
if (resolved.headless) validateSymbolCount(resolved.headless, resolved.token, argumentSymbols(target), command);
|
|
98
|
+
throw new Error("The chart template did not produce a valid chart specification.");
|
|
99
|
+
}
|
|
92
100
|
return {
|
|
93
101
|
...spec.settings,
|
|
94
102
|
chartSpec: applyChartComposerCapabilityOptions(
|
|
@@ -243,15 +251,21 @@ async function unknownPaneFunctionError(
|
|
|
243
251
|
// NIM is one slip from NI, but a bank figure is in `fundamentals`, not in a function's statement lines.
|
|
244
252
|
const bank = bankFigurePointer(name, arg);
|
|
245
253
|
if (bank) return new UnknownPaneFunctionError(name, bank.details, null, bank.message);
|
|
246
|
-
|
|
254
|
+
// A word that asks for a function's data (IVOL for HIVG and OVDV) names them as a slip from a code names it.
|
|
255
|
+
const suggested = suggestedFunctionCodes(registry, name);
|
|
256
|
+
const near = [...suggested, ...nearFunctionCodes(registry, name).filter(({ code }) => !suggested.some((entry) => entry.code === code))];
|
|
247
257
|
// A catalog that cannot be read still leaves the pointer to it.
|
|
248
258
|
const closest = (await suggestPaneFunctions(registry, context, name).catch(logFailure("cli.pane-functions-resolver", "Find pane function suggestions failed", [])))
|
|
249
259
|
.filter((entry) => !near.some(({ code }) => code === entry.token));
|
|
250
260
|
const tokenWidth = Math.max(0, ...closest.map((entry) => entry.token.length));
|
|
251
261
|
const word = /^[\w.:-]+$/.test(name) ? name : JSON.stringify(name);
|
|
252
262
|
const argument = arg.trim() ? ` ${/^[\w.:,=%+@/$^-]+$/.test(arg.trim()) ? arg.trim() : JSON.stringify(arg.trim())}` : "";
|
|
263
|
+
// One code is run on the argument; the functions a word asks for are each.
|
|
264
|
+
const runs = suggested.length > 0 ? suggested : near.length === 1 ? near : [];
|
|
253
265
|
return new UnknownPaneFunctionError(name, [
|
|
254
|
-
|
|
266
|
+
// A function a word asks for, typed with no argument, shows the one it takes (`<ticker>`).
|
|
267
|
+
...runs.map(({ code }, index) => [index === 0 ? "Run:" : " ", "gloomberb", command, code,
|
|
268
|
+
argument.trim() || (suggested.length > 0 ? findFunctionCode(registry, code)?.argument : "")].filter(Boolean).join(" ")),
|
|
255
269
|
...closest.length > 0
|
|
256
270
|
? ["Closest matches:", ...closest.map((entry) => ` ${entry.token.padEnd(tokenWidth)} ${entry.label}`), `Search the catalog: gloomberb catalog ${word}`]
|
|
257
271
|
: near.length > 0 ? [] : [`No function code is close to ${word}. List every function: gloomberb catalog`],
|
|
@@ -318,7 +332,7 @@ export async function resolvePaneFunction(
|
|
|
318
332
|
options: normalizedOptions,
|
|
319
333
|
...(tableSection !== undefined ? { tableSection } : {}),
|
|
320
334
|
};
|
|
321
|
-
resolved.instance = await buildPaneInstance(resolved, context, args.arg);
|
|
335
|
+
resolved.instance = await buildPaneInstance(resolved, context, args.arg, resolutionSettings.command ?? "fn");
|
|
322
336
|
return resolved;
|
|
323
337
|
}
|
|
324
338
|
|
package/src/cli/report-tables.ts
CHANGED
|
@@ -69,7 +69,8 @@ export interface CliReportEntry {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
type Cell =
|
|
72
|
-
|
|
72
|
+
/** `unit`: what the column draws a number of this row in, so a missing figure still names its unit. */
|
|
73
|
+
| { kind: "blank"; unit?: string }
|
|
73
74
|
| { kind: "text"; text: string; time?: boolean }
|
|
74
75
|
| { kind: "number"; value: number; unit: string }
|
|
75
76
|
/** Text that may read as a number, once the whole column is known. */
|
|
@@ -80,6 +81,8 @@ type ResolvedCell = Exclude<Cell, { kind: "textual" }>;
|
|
|
80
81
|
interface ResolvedColumn {
|
|
81
82
|
header: string;
|
|
82
83
|
cells: ResolvedCell[];
|
|
84
|
+
/** The unit the column draws its numbers in, for a table with no rows to say it. */
|
|
85
|
+
unit?: string;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
const BLANK = { kind: "blank" } as const;
|
|
@@ -265,8 +268,9 @@ function resolveColumn(header: string, cells: Cell[], parseText: boolean): Resol
|
|
|
265
268
|
};
|
|
266
269
|
}
|
|
267
270
|
|
|
271
|
+
/** The units a column's numbers are in, a missing figure's own among them, so the header does not change with which figures are missing. */
|
|
268
272
|
function numberUnits(cells: readonly ResolvedCell[]): Set<string> {
|
|
269
|
-
return new Set(cells.flatMap((cell) => cell.kind === "number" ? [cell.unit] : []));
|
|
273
|
+
return new Set(cells.flatMap((cell) => cell.kind === "number" ? [cell.unit] : cell.kind === "blank" && cell.unit != null ? [cell.unit] : []));
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
function cellValue(cell: ResolvedCell): CliReportValue {
|
|
@@ -312,8 +316,8 @@ function rowUnits(columns: readonly ResolvedColumn[], mixed: readonly boolean[],
|
|
|
312
316
|
* metrics whose rows each have one, and otherwise a `<header> unit` column
|
|
313
317
|
* beside a column that mixes them (a rate in % and a spread in bp).
|
|
314
318
|
*/
|
|
315
|
-
function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCount: number): CliReportTable {
|
|
316
|
-
const units = columns.map((column) => numberUnits(column.cells));
|
|
319
|
+
function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCount: number, status?: StatusColumn): CliReportTable {
|
|
320
|
+
const units = columns.map((column) => rowCount === 0 && column.unit != null ? new Set([column.unit]) : numberUnits(column.cells));
|
|
317
321
|
const mixed = units.map((columnUnits) => columnUnits.size > 1);
|
|
318
322
|
const byRow = mixed.some(Boolean) ? rowUnits(columns, mixed, rowCount) : null;
|
|
319
323
|
const layout = columns.flatMap((column, index) => {
|
|
@@ -338,6 +342,7 @@ function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCou
|
|
|
338
342
|
},
|
|
339
343
|
];
|
|
340
344
|
});
|
|
345
|
+
if (status) layout.push({ header: status.header, value: (row: number) => status.values[row] ?? "" });
|
|
341
346
|
return {
|
|
342
347
|
title,
|
|
343
348
|
columns: layout.map((column) => column.header),
|
|
@@ -345,28 +350,141 @@ function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCou
|
|
|
345
350
|
};
|
|
346
351
|
}
|
|
347
352
|
|
|
353
|
+
/** The `status` column a table of rows ends its own columns with: why a figure is missing, by row. */
|
|
354
|
+
interface StatusColumn {
|
|
355
|
+
header: string;
|
|
356
|
+
values: string[];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* A number a probe draws through a column's format, to read the unit the
|
|
361
|
+
* column draws numbers in. A figure format rounds it; a format that prints
|
|
362
|
+
* it whole passes values through as text and draws no figures of its own.
|
|
363
|
+
*/
|
|
364
|
+
const UNIT_PROBE = 0.1234567891;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* A row with nothing in it that a format can still read through: every
|
|
368
|
+
* field, call and conversion of it is empty. For a format that reads fields of
|
|
369
|
+
* the row beyond its value and would throw on an empty one.
|
|
370
|
+
*/
|
|
371
|
+
const EMPTY_ROW: unknown = (() => {
|
|
372
|
+
const empty: unknown = new Proxy(function empty() {}, {
|
|
373
|
+
get: (_target, key) => key === Symbol.toPrimitive ? (hint: string) => (hint === "number" ? Number.NaN : "") : empty,
|
|
374
|
+
apply: () => empty,
|
|
375
|
+
});
|
|
376
|
+
return empty;
|
|
377
|
+
})();
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The unit a column draws a number of this row in (`%` for an IV, `""` for a
|
|
381
|
+
* plain figure), read from what its format draws for one; null when it draws
|
|
382
|
+
* no figure for one, passes the value through as it is, or throws.
|
|
383
|
+
*/
|
|
384
|
+
function drawnUnit<Row>(column: CliReportColumn<Row>, row: Row): string | null {
|
|
385
|
+
const unit = column.unitKey ? (row as Record<string, unknown> | undefined)?.[column.unitKey] : undefined;
|
|
386
|
+
if (typeof unit === "string" && unit) return unit;
|
|
387
|
+
if (!column.format) return null;
|
|
388
|
+
try {
|
|
389
|
+
const drawn = column.format(UNIT_PROBE, row);
|
|
390
|
+
if (typeof drawn !== "string" || drawn.trim() === String(UNIT_PROBE)) return null;
|
|
391
|
+
const read = readDisplay(drawn);
|
|
392
|
+
return read && read !== BLANK_DISPLAY ? read.unit : null;
|
|
393
|
+
} catch {
|
|
394
|
+
// Intentional ignore: a format that cannot draw a number for this row draws no unit for it.
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The unit a column draws its figures in whatever the rows, read without one:
|
|
401
|
+
* on an empty row, or through a row of nothing when the format throws on an
|
|
402
|
+
* empty one. The same with rows and without, so a header and the table's
|
|
403
|
+
* status column do not depend on what was loaded. Null for a column of text.
|
|
404
|
+
*/
|
|
405
|
+
function columnUnit<Row>(column: CliReportColumn<Row>): string | null {
|
|
406
|
+
if (column.unitKey) return "";
|
|
407
|
+
if (!column.format) return null;
|
|
408
|
+
try {
|
|
409
|
+
column.format(UNIT_PROBE, {} as Row);
|
|
410
|
+
return drawnUnit(column, {} as Row);
|
|
411
|
+
} catch {
|
|
412
|
+
// Intentional ignore: the format reads a field an empty row lacks; read it through a row of nothing.
|
|
413
|
+
return drawnUnit(column, EMPTY_ROW as Row);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** What a status cell says: each reason once, after the columns it is the reason for. */
|
|
418
|
+
function statusText(reasons: ReadonlyArray<{ header: string; text: string }>): string {
|
|
419
|
+
const byText = new Map<string, string[]>();
|
|
420
|
+
for (const { header, text } of reasons) byText.set(text, [...byText.get(text) ?? [], header]);
|
|
421
|
+
return [...byText].map(([text, headers]) => `${headers.join(", ")}: ${text}`).join("; ");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const isMissing = (value: unknown) => value == null || (typeof value === "number" && !Number.isFinite(value));
|
|
425
|
+
|
|
348
426
|
// Codes a spreadsheet must not turn into numbers, even when they are digits.
|
|
349
427
|
const IDENTIFIER_KEY = /^(?:id|cik|code|cusip|figi|isin|symbol|ticker)$|(?:Id|Cik|Code|Cusip|Figi|Isin|Symbol|Ticker)$/;
|
|
350
428
|
|
|
351
|
-
/**
|
|
429
|
+
/**
|
|
430
|
+
* A table of rows under the columns the text view draws. A figure that is
|
|
431
|
+
* missing still names the unit its column draws it in, so a header is the same
|
|
432
|
+
* whichever figures a symbol has. With `status`, a reason the text draws in
|
|
433
|
+
* place of a missing figure (`not quoted`, `under 7d`) leaves the cell empty
|
|
434
|
+
* and goes to the `status` column the table ends its own columns with, so a
|
|
435
|
+
* column of figures holds only numbers. Only a row whose own value is missing,
|
|
436
|
+
* in a column that draws that row's figures, has a reason: text a format draws
|
|
437
|
+
* from the rest of the row (a range, N/M, ATM) stays. A table has the column
|
|
438
|
+
* when one of its columns draws figures through a format, read from the
|
|
439
|
+
* columns alone so it is there with or without rows, and a table of text has
|
|
440
|
+
* none; beside a column of its own named Status it is `value_status`.
|
|
441
|
+
*/
|
|
352
442
|
export function exportRowsTable<Row extends Record<string, unknown>>(
|
|
353
443
|
title: string,
|
|
354
444
|
columns: readonly CliReportColumn<Row>[],
|
|
355
445
|
rows: readonly Row[],
|
|
446
|
+
{ status = false }: { status?: boolean } = {},
|
|
356
447
|
): CliReportTable {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
448
|
+
const reasons = rows.map((): Array<{ header: string; text: string }> => []);
|
|
449
|
+
const units = columns.map(columnUnit);
|
|
450
|
+
// Decided by the columns alone, so a table has the column, or not, whatever rows it has.
|
|
451
|
+
const withStatus = status && units.some((unit) => unit != null);
|
|
452
|
+
const resolved = columns.map((column, columnIndex): ResolvedColumn => {
|
|
453
|
+
const rowUnits = rows.map((row) => drawnUnit(column, row));
|
|
454
|
+
const values = rows.map((row) => (row as Record<string, unknown> | undefined)?.[column.key]);
|
|
455
|
+
const { header, cells } = resolveColumn(
|
|
456
|
+
column.header,
|
|
457
|
+
rows.map((row, index): Cell => {
|
|
458
|
+
const value = values[index];
|
|
459
|
+
const unit = column.unitKey ? (row as Record<string, unknown> | undefined)?.[column.unitKey] : undefined;
|
|
460
|
+
if (typeof unit === "string" && unit && typeof value === "number" && Number.isFinite(value)) {
|
|
461
|
+
return { kind: "number", value: exportNumber(value), unit };
|
|
462
|
+
}
|
|
463
|
+
return exportCell(value, column.format ? column.format(value, row) : null, column.key);
|
|
464
|
+
}),
|
|
465
|
+
!IDENTIFIER_KEY.test(column.key),
|
|
466
|
+
);
|
|
467
|
+
const table: ResolvedColumn = {
|
|
468
|
+
header,
|
|
469
|
+
cells: cells.map((cell, index): ResolvedCell => {
|
|
470
|
+
const unit = rowUnits[index];
|
|
471
|
+
const blank: ResolvedCell = unit != null ? { kind: "blank", unit } : BLANK;
|
|
472
|
+
if (cell.kind === "blank") return cell.unit == null ? blank : cell;
|
|
473
|
+
// Text drawn in place of a figure this row does not have, where the column draws this row's figures: a reason.
|
|
474
|
+
// Text drawn from the rest of the row (a range, N/M, ATM) is no figure's absence and stays.
|
|
475
|
+
if (!withStatus || cell.kind !== "text" || cell.time || unit == null || !isMissing(values[index])) return cell;
|
|
476
|
+
reasons[index]!.push({ header: column.header, text: cell.text });
|
|
477
|
+
return blank;
|
|
478
|
+
}),
|
|
479
|
+
};
|
|
480
|
+
const unit = units[columnIndex];
|
|
481
|
+
return unit == null ? table : { ...table, unit };
|
|
482
|
+
});
|
|
483
|
+
const statusColumn = withStatus ? {
|
|
484
|
+
header: columns.some((column) => column.header.trim().toLowerCase() === "status") ? "value_status" : "status",
|
|
485
|
+
values: reasons.map(statusText),
|
|
486
|
+
} : undefined;
|
|
487
|
+
return assembleTable(title, resolved, rows.length, statusColumn);
|
|
370
488
|
}
|
|
371
489
|
|
|
372
490
|
/** Label/value lines as `Metric,Value`; a number's unit moves into its label. */
|
package/src/cli/result.ts
CHANGED
|
@@ -46,6 +46,12 @@ export interface CliResult<T = unknown> {
|
|
|
46
46
|
* `fear-greed` prints its row alone.
|
|
47
47
|
*/
|
|
48
48
|
exportFreshness?: ReportFreshness;
|
|
49
|
+
/**
|
|
50
|
+
* Why a result that still prints counts as a failure (a report with no
|
|
51
|
+
* data): the command exits 1, and JSON says `ok: false` with this as
|
|
52
|
+
* `error` beside the data, so the envelope and the exit status agree.
|
|
53
|
+
*/
|
|
54
|
+
failure?: CliErrorObject;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export interface CliErrorObject {
|
|
@@ -110,8 +116,9 @@ export interface CliResultRenderOptions<T = unknown, Row = Record<string, unknow
|
|
|
110
116
|
exportNotes?: readonly string[];
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
interface CliResultJsonEnvelope<T> extends Omit<CliResult<T>, "freshness" | "exportFreshness"> {
|
|
114
|
-
ok:
|
|
119
|
+
interface CliResultJsonEnvelope<T> extends Omit<CliResult<T>, "freshness" | "exportFreshness" | "failure"> {
|
|
120
|
+
ok: boolean;
|
|
121
|
+
error?: CliErrorObject;
|
|
115
122
|
/** Where the data comes from, as CSV's closing `#` lines say it. */
|
|
116
123
|
source?: ExportSource;
|
|
117
124
|
columns?: Array<Pick<CliResultColumn, "key" | "header" | "align" | "width">>;
|
|
@@ -398,14 +405,15 @@ export function serializeCliResult<T, Row extends Record<string, unknown> = Reco
|
|
|
398
405
|
options: CliGlobalOptions,
|
|
399
406
|
renderOptions: CliResultRenderOptions<T, Row> = {},
|
|
400
407
|
): string {
|
|
401
|
-
const { freshness, exportFreshness, ...rest } = result;
|
|
408
|
+
const { freshness, exportFreshness, failure, ...rest } = result;
|
|
402
409
|
const cited = freshness ?? exportFreshness;
|
|
403
410
|
if (options.format === "json") {
|
|
404
411
|
const columns = serializeColumns(renderOptions.columns);
|
|
405
412
|
const window = Array.isArray(result.data) ? windowed(result.data as unknown[], options, renderOptions) : null;
|
|
406
413
|
const cut = window ? cutMetadata(window) : null;
|
|
407
414
|
const envelope: CliResultJsonEnvelope<T> = {
|
|
408
|
-
ok:
|
|
415
|
+
ok: !failure,
|
|
416
|
+
...(failure ? { error: failure } : {}),
|
|
409
417
|
...rest,
|
|
410
418
|
...(freshness || cut ? { metadata: { ...rest.metadata, ...(freshness ? { freshness } : {}), ...(cut ? { rows: cut } : {}) } } : {}),
|
|
411
419
|
data: window ? window.rows as T : result.data,
|
package/src/cli/symbol-list.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { splitSymbolArgs, splitSymbolWords, stripStrayEdges, uniqueSymbols } fro
|
|
|
4
4
|
|
|
5
5
|
/** Header cells that name a spreadsheet's symbol column, compared without case, spaces or punctuation. */
|
|
6
6
|
const SYMBOL_HEADERS = new Set(["symbol", "symbols", "ticker", "tickers", "code", "stockcode", "securitycode", "instrument"]);
|
|
7
|
+
/** A header naming a column of company names, read when no column names the symbols. */
|
|
8
|
+
const NAME_HEADERS = new Set(["name", "names", "company", "companies", "companyname", "security", "securityname", "issuer", "issuername"]);
|
|
7
9
|
|
|
8
10
|
/** One symbol as it can be typed: AAPL, $nvda, TTF.AS, SAN:EPA, BRK-B, 7203.T, ^GSPC, ES=F. */
|
|
9
11
|
const SYMBOL_TOKEN = /^\$?[\p{L}\p{N}^][\p{L}\p{N}.:\-^=/&]{0,23}$/u;
|
|
@@ -82,17 +84,32 @@ interface SymbolText {
|
|
|
82
84
|
symbols: string[];
|
|
83
85
|
/** Cells beside the symbol column of a table with no header, in reading order; empty for a list or a CSV with a header. */
|
|
84
86
|
ignored: string[];
|
|
87
|
+
/**
|
|
88
|
+
* The symbols are a list's entries, each a line or a cell of a list split
|
|
89
|
+
* by semicolons or commas, and are read as typed entries are
|
|
90
|
+
* (`readNameEntries`): one with spaces is a company's name (`Altria
|
|
91
|
+
* Group`), or symbols written on one line (`AAPL MSFT`).
|
|
92
|
+
*/
|
|
93
|
+
entries?: true;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
/**
|
|
88
97
|
* The symbols in a file or piped text, in order. A spreadsheet export reads
|
|
89
|
-
* from the column its header names (Symbol, Ticker, Code)
|
|
98
|
+
* from the column its header names (Symbol, Ticker, Code), else from its
|
|
99
|
+
* Name or Company column, as `entries`. Without a header,
|
|
90
100
|
* every cell is a symbol unless some column after the first holds a name, a
|
|
91
101
|
* price or a weight: that table is read by its first column, and the cells
|
|
92
102
|
* beside it are returned as `ignored` so the command can name them. A list
|
|
93
103
|
* split by commas, spaces or newlines gives every entry, even when a line of
|
|
94
|
-
* it is ragged or carries a stray separator (`"BIMAS ,"`). A
|
|
95
|
-
*
|
|
104
|
+
* it is ragged or carries a stray separator (`"BIMAS ,"`). A list of lines
|
|
105
|
+
* gives each line whole, and a list of names split by semicolons or commas
|
|
106
|
+
* (one line, or one name a line) each cell, as `entries`: a line with spaces
|
|
107
|
+
* is a company's name (`Altria Group`) or symbols on one line, which reading
|
|
108
|
+
* the entries tells apart. Names beside other text (`Apple Inc,Technology`)
|
|
109
|
+
* are a table read by its first column, its names as `entries`. A list split
|
|
110
|
+
* by spaces with figures beside the symbols (`AAPL 150.00`) is a table. A
|
|
111
|
+
* byte order mark, Windows line ends, blank lines and lines starting with #
|
|
112
|
+
* or // are ignored.
|
|
96
113
|
*/
|
|
97
114
|
export function parseSymbolText(text: string): SymbolText {
|
|
98
115
|
const normalized = text.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
|
@@ -105,13 +122,27 @@ export function parseSymbolText(text: string): SymbolText {
|
|
|
105
122
|
const exact = header.findIndex((cell) => SYMBOL_HEADERS.has(cell));
|
|
106
123
|
const column = exact >= 0 ? exact : header.findIndex((cell) => cell.includes("symbol") || cell.includes("ticker"));
|
|
107
124
|
if (column >= 0) return { symbols: records.slice(1).map((cells) => cells[column] ?? "").filter(Boolean), ignored: [] };
|
|
125
|
+
// Without one, a delimited export's Name or Company column holds the companies, read as typed names are.
|
|
126
|
+
const nameColumn = delimiter ? header.findIndex((cell) => NAME_HEADERS.has(cell)) : -1;
|
|
127
|
+
if (nameColumn >= 0) return { symbols: records.slice(1).map((cells) => cells[nameColumn] ?? "").filter(Boolean), ignored: [], entries: true };
|
|
108
128
|
// A separator or quote mark stuck to a cell is dropped (and named) when it is read, so it does not make the list a table.
|
|
109
129
|
const cleaned = records.map((cells) => cells.map((cell) => stripStrayEdges(cell).text));
|
|
130
|
+
const figures = cleaned.some((cells) => cells.some((cell, index) => index > 0 && NUMBER_CELL.test(cell) && /\d/.test(cell)));
|
|
131
|
+
// Lines split by spaces alone are entries, unless figures sit beside the symbols.
|
|
132
|
+
if (!delimiter && !figures) return { symbols: records.map((cells) => cells.join(" ")), ignored: [], entries: true };
|
|
110
133
|
const grid = cleaned.every((cells) => cells.every((cell, index) => !cell || (looksLikeSymbol(cell) && (index === 0 || !NUMBER_CELL.test(cell)))));
|
|
111
134
|
if (grid) return { symbols: records.flat().filter(Boolean), ignored: [] };
|
|
135
|
+
// Names split by semicolons or commas, with no figures: one pasted line, or one name a line with a separator left
|
|
136
|
+
// on it, is a list. A name beside a sector or any other text is a table, read by its first column below.
|
|
137
|
+
const names = cleaned.some((cells) => /\s/.test(cells[0] ?? ""));
|
|
138
|
+
if (!figures && (records.length === 1 || (names && cleaned.every((cells) => cells.filter(Boolean).length <= 1)))) {
|
|
139
|
+
return { symbols: records.flat().filter(Boolean), ignored: [], entries: true };
|
|
140
|
+
}
|
|
112
141
|
return {
|
|
113
142
|
symbols: records.map((cells) => cells[0] ?? "").filter(Boolean),
|
|
114
143
|
ignored: records.flatMap((cells) => cells.slice(1)).filter(Boolean),
|
|
144
|
+
// A first column of names (Apple Inc) is read as typed names are.
|
|
145
|
+
...names ? { entries: true as const } : {},
|
|
115
146
|
};
|
|
116
147
|
}
|
|
117
148
|
|
|
@@ -151,8 +182,10 @@ export interface SymbolListSource {
|
|
|
151
182
|
stdinPiped?: boolean;
|
|
152
183
|
/**
|
|
153
184
|
* Reads the typed entries (`splitSymbolArgs`) as symbols, such as
|
|
154
|
-
* `readNameEntries`, which keeps a company's name whole
|
|
155
|
-
*
|
|
185
|
+
* `readNameEntries`, which keeps a company's name whole, and then the
|
|
186
|
+
* entries of each list a file or standard input holds. Called first for
|
|
187
|
+
* what was typed, even when that is nothing. Without it every entry is its
|
|
188
|
+
* words, each a symbol.
|
|
156
189
|
*/
|
|
157
190
|
readEntries?: (entries: string[]) => Promise<string[]>;
|
|
158
191
|
}
|
|
@@ -176,17 +209,20 @@ export async function readSymbolList(source: SymbolListSource): Promise<{ symbol
|
|
|
176
209
|
const readStdin = source.readStdin ?? (() => Bun.stdin.text());
|
|
177
210
|
const wantsStdin = source.args.includes("-") || source.file === "-";
|
|
178
211
|
const typedArgs = source.args.filter((arg) => arg !== "-");
|
|
212
|
+
const readEntries = (entries: string[]) => source.readEntries ? source.readEntries(entries) : Promise.resolve(splitSymbolWords(entries));
|
|
179
213
|
const typed = source.readEntries ? await source.readEntries(splitSymbolArgs(typedArgs)) : splitSymbolWords(typedArgs);
|
|
180
214
|
const collected = [...typed];
|
|
181
215
|
// A separator left on the end of what was typed: `UPST;`, or a list that ends in a comma.
|
|
182
216
|
const strays = new Set<string>(stripStrayEdges(typedArgs.join(" ")).notes);
|
|
183
217
|
const ignored: string[] = [];
|
|
184
|
-
const addCells = ({ symbols, ignored: beside }: SymbolText) => {
|
|
185
|
-
|
|
218
|
+
const addCells = async ({ symbols, ignored: beside, entries }: SymbolText) => {
|
|
219
|
+
const cells = symbols.map((cell) => {
|
|
186
220
|
const cleaned = stripStrayEdges(cell);
|
|
187
221
|
cleaned.notes.forEach((note) => strays.add(note));
|
|
188
|
-
|
|
189
|
-
}
|
|
222
|
+
return cleaned.text;
|
|
223
|
+
}).filter(Boolean);
|
|
224
|
+
// A list's lines and names are read as typed entries are: `Altria Group` stays one company.
|
|
225
|
+
collected.push(...entries ? await readEntries(cells) : cells);
|
|
190
226
|
ignored.push(...beside);
|
|
191
227
|
};
|
|
192
228
|
if (source.file != null && source.file !== "-") {
|
|
@@ -198,10 +234,10 @@ export async function readSymbolList(source: SymbolListSource): Promise<{ symbol
|
|
|
198
234
|
const code = (error as NodeJS.ErrnoException).code;
|
|
199
235
|
throw new SymbolListError(code === "ENOENT" ? `No file at ${source.file}.` : `Could not read ${source.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
200
236
|
}
|
|
201
|
-
addCells(parseSymbolText(text));
|
|
237
|
+
await addCells(parseSymbolText(text));
|
|
202
238
|
}
|
|
203
239
|
if (wantsStdin || (typed.length === 0 && source.file == null && (source.stdinPiped ?? stdinIsPiped()))) {
|
|
204
|
-
addCells(parseSymbolText(await readStdin()));
|
|
240
|
+
await addCells(parseSymbolText(await readStdin()));
|
|
205
241
|
}
|
|
206
242
|
const ignoredNote = ignoredCellsNote(ignored);
|
|
207
243
|
return { ...uniqueSymbols(collected), notes: [...strays, ...ignoredNote ? [ignoredNote] : []] };
|
|
@@ -145,6 +145,7 @@ function ThemedCompositeChart({
|
|
|
145
145
|
onCursorDateChange,
|
|
146
146
|
onViewportChange,
|
|
147
147
|
onActivate,
|
|
148
|
+
onPress,
|
|
148
149
|
onToggleSeries,
|
|
149
150
|
isSeriesToggleable,
|
|
150
151
|
timePick,
|
|
@@ -527,6 +528,14 @@ function ThemedCompositeChart({
|
|
|
527
528
|
removeDrawing,
|
|
528
529
|
pickDrawColor,
|
|
529
530
|
});
|
|
531
|
+
const pressFromPointer = useCallback(() => {
|
|
532
|
+
onPress?.();
|
|
533
|
+
activateFromPointer();
|
|
534
|
+
}, [activateFromPointer, onPress]);
|
|
535
|
+
const pressPlot = useCallback(() => {
|
|
536
|
+
cancelKeyboardPlacement();
|
|
537
|
+
onPress?.();
|
|
538
|
+
}, [cancelKeyboardPlacement, onPress]);
|
|
530
539
|
const handleEmptyMouseScroll = useCallback((event: ChartMouseEvent) => {
|
|
531
540
|
const scroll = event.scroll;
|
|
532
541
|
if (!interactive || !navigationFrame || !scroll) return;
|
|
@@ -597,7 +606,7 @@ function ThemedCompositeChart({
|
|
|
597
606
|
accessoryWidth={legendAccessoryWidth}
|
|
598
607
|
formatValue={formatValue}
|
|
599
608
|
showLatestChangePercent={showLatestChangePercent}
|
|
600
|
-
onActivate={
|
|
609
|
+
onActivate={pressFromPointer}
|
|
601
610
|
onToggleSeries={onToggleSeries}
|
|
602
611
|
isSeriesToggleable={isSeriesToggleable}
|
|
603
612
|
keyboardIndex={legendKeyboardIndex}
|
|
@@ -716,7 +725,7 @@ function ThemedCompositeChart({
|
|
|
716
725
|
accessoryWidth={legendAccessoryWidth}
|
|
717
726
|
formatValue={formatValue}
|
|
718
727
|
showLatestChangePercent={showLatestChangePercent}
|
|
719
|
-
onActivate={
|
|
728
|
+
onActivate={pressFromPointer}
|
|
720
729
|
onToggleSeries={onToggleSeries}
|
|
721
730
|
isSeriesToggleable={isSeriesToggleable}
|
|
722
731
|
keyboardIndex={legendKeyboardIndex}
|
|
@@ -734,11 +743,11 @@ function ThemedCompositeChart({
|
|
|
734
743
|
showColors={isDrawingTool(armedTool) || !!selectedDrawingId}
|
|
735
744
|
levelTool={levelsEditable}
|
|
736
745
|
onArmTool={(tool) => {
|
|
737
|
-
|
|
746
|
+
pressFromPointer();
|
|
738
747
|
armTool(tool);
|
|
739
748
|
}}
|
|
740
749
|
onPickColor={(color) => {
|
|
741
|
-
|
|
750
|
+
pressFromPointer();
|
|
742
751
|
pickDrawColor(color);
|
|
743
752
|
}}
|
|
744
753
|
/>
|
|
@@ -782,7 +791,7 @@ function ThemedCompositeChart({
|
|
|
782
791
|
onToolSpanChange={setToolSpan}
|
|
783
792
|
onDragActiveChange={setPointerDragging}
|
|
784
793
|
keyboardToolDrag={keyboardToolDrag?.panelId === panel.id ? keyboardToolDrag : null}
|
|
785
|
-
onPointerPress={
|
|
794
|
+
onPointerPress={pressPlot}
|
|
786
795
|
showTextFallback={showTextFallback}
|
|
787
796
|
onPickTime={timePick?.onPick}
|
|
788
797
|
levels={levelHost?.panel.id === panel.id ? levels ?? null : null}
|