gloomberb 0.17.2 → 0.17.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/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +18 -8
- package/src/api-client/types.ts +3 -1
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +18 -1
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +32 -4
- package/src/plugins/builtin/futures/model.ts +65 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/styles.css +6 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +19 -2
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
useUiCapabilities,
|
|
11
11
|
type InputRenderable,
|
|
12
12
|
} from "../../ui";
|
|
13
|
-
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
|
13
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, type ReactNode } from "react";
|
|
14
14
|
import {
|
|
15
15
|
blendHex,
|
|
16
16
|
commandBarBg,
|
|
@@ -24,12 +24,12 @@ import {
|
|
|
24
24
|
useCommandBarPromptBinding,
|
|
25
25
|
type CommandBarPromptBinding,
|
|
26
26
|
} from "../command-bar/panel/prompt-binding";
|
|
27
|
-
import { useAppDispatch, useAppSelector } from "../../state/app/context";
|
|
27
|
+
import { useAppDispatch, useAppSelector, type AppState } from "../../state/app/context";
|
|
28
28
|
import { useViewport } from "../../react/input";
|
|
29
29
|
import { canRetryUpdate } from "../../app/global-shortcuts";
|
|
30
30
|
import { t, tf } from "../../i18n";
|
|
31
31
|
import { displayWidth, truncateToDisplayWidth } from "../../utils/format";
|
|
32
|
-
import { ClippedText } from "../ui/display";
|
|
32
|
+
import { ClippedText, NATIVE_CLIP_STYLE } from "../ui/display";
|
|
33
33
|
import { resolveMarketSummaryFit, useMarketSummary } from "./market-summary";
|
|
34
34
|
import { resolveHeaderPromptGeometry } from "./shell/chrome";
|
|
35
35
|
import { useWindowFullscreen } from "./window-fullscreen";
|
|
@@ -120,6 +120,28 @@ function nativePromptSurfaceStyle(colors: ReturnType<typeof useThemeColors>, ope
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* How fast the closed desktop prompt takes the bar's spare room next to the
|
|
125
|
+
* update slot, which grows by one millionth as much: only empty space, if any.
|
|
126
|
+
*/
|
|
127
|
+
const NATIVE_PROMPT_GROW = 1_000_000;
|
|
128
|
+
/** How much faster the desktop update slot shrinks than an open prompt: it gives all of its room first. */
|
|
129
|
+
const NATIVE_UPDATE_SLOT_SHRINK = 1_000_000;
|
|
130
|
+
/**
|
|
131
|
+
* How the desktop prompt sits in the bar. Closed it draws no box, so it claims
|
|
132
|
+
* only its caret and placeholder, then grows toward `width` (the command
|
|
133
|
+
* sheet's) from the room the update status leaves, showing its key hint once it
|
|
134
|
+
* has room for it; Help and the market cluster never give any. Open it is the
|
|
135
|
+
* top of the sheet and keeps the sheet's width: the update status gives all of
|
|
136
|
+
* its room first (NATIVE_UPDATE_SLOT_SHRINK), and only a window too narrow for
|
|
137
|
+
* Help and the market cluster beside the open prompt takes from it.
|
|
138
|
+
*/
|
|
139
|
+
function nativePromptFit(width: number, open: boolean) {
|
|
140
|
+
return open
|
|
141
|
+
? { width, flexShrink: 1, minWidth: 0, overflow: "hidden" as const }
|
|
142
|
+
: { flexBasis: 0, flexGrow: NATIVE_PROMPT_GROW, flexShrink: 0, minWidth: "min-content", maxWidth: width };
|
|
143
|
+
}
|
|
144
|
+
|
|
123
145
|
/**
|
|
124
146
|
* The command bar's input while the bar is open. Gloomberb's panes are driven
|
|
125
147
|
* by bare single keys, so the input only exists while the bar is open; the idle
|
|
@@ -228,7 +250,7 @@ function HeaderCommandPrompt({
|
|
|
228
250
|
|
|
229
251
|
return (
|
|
230
252
|
<Box
|
|
231
|
-
width
|
|
253
|
+
{...(nativePaneChrome ? nativePromptFit(width, open) : { width })}
|
|
232
254
|
height={1}
|
|
233
255
|
flexDirection="row"
|
|
234
256
|
alignItems="center"
|
|
@@ -254,6 +276,21 @@ function HeaderCommandPrompt({
|
|
|
254
276
|
<Text fg={caretColor} attributes={TextAttributes.BOLD}>{PROMPT_CARET}</Text>
|
|
255
277
|
{open ? (
|
|
256
278
|
binding ? <HeaderPromptInput binding={binding} nativePaneChrome={nativePaneChrome} width={inputWidth} /> : null
|
|
279
|
+
) : nativePaneChrome ? (
|
|
280
|
+
// One line that wraps and hides its second: the key hint drops whole when the
|
|
281
|
+
// prompt is too narrow for it, and the placeholder never does.
|
|
282
|
+
<Box
|
|
283
|
+
flexDirection="row"
|
|
284
|
+
height={1}
|
|
285
|
+
flexGrow={1}
|
|
286
|
+
flexShrink={1}
|
|
287
|
+
minWidth={0}
|
|
288
|
+
overflow="hidden"
|
|
289
|
+
style={{ flexWrap: "wrap", alignContent: "flex-start" }}
|
|
290
|
+
>
|
|
291
|
+
<Text fg={mutedColor}>{placeholder}</Text>
|
|
292
|
+
{shortcut ? <Text fg={shortcutColor}>{` ${shortcut}`}</Text> : null}
|
|
293
|
+
</Box>
|
|
257
294
|
) : (
|
|
258
295
|
<>
|
|
259
296
|
<Text fg={mutedColor}>{placeholder}</Text>
|
|
@@ -266,128 +303,314 @@ function HeaderCommandPrompt({
|
|
|
266
303
|
}
|
|
267
304
|
|
|
268
305
|
/**
|
|
269
|
-
* What the header says about an update
|
|
270
|
-
*
|
|
271
|
-
*
|
|
306
|
+
* What the header says about an update, before either renderer lays it out:
|
|
307
|
+
* a spinner and a line while something runs, a line on its own, the
|
|
308
|
+
* downloaded update with its Restart button, or a failed update with the key
|
|
309
|
+
* that retries it.
|
|
272
310
|
*/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const updateProgress = useAppSelector((state) => state.updateProgress);
|
|
279
|
-
const updateCheckInProgress = useAppSelector((state) => state.updateCheckInProgress);
|
|
280
|
-
const updateNotice = useAppSelector((state) => state.updateNotice);
|
|
281
|
-
// One key: Restart for a downloaded update, retry for a failed one.
|
|
282
|
-
const updateKey = useActionShortcut("install-update");
|
|
311
|
+
type UpdateStatusView =
|
|
312
|
+
| { kind: "busy"; text: string; parts: StatusPart[] }
|
|
313
|
+
| { kind: "text"; text: string }
|
|
314
|
+
| { kind: "restart"; text: string; version: string }
|
|
315
|
+
| { kind: "failed"; message: string; parts: StatusPart[]; retry: boolean };
|
|
283
316
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
317
|
+
/**
|
|
318
|
+
* A line of the status in the pieces the desktop may treat apart: a `cut`
|
|
319
|
+
* piece ends in an ellipsis and gives way, the rest stays whole. The download
|
|
320
|
+
* keeps its percent and the failure the words that say so, while the version
|
|
321
|
+
* and the error give way.
|
|
322
|
+
*/
|
|
323
|
+
interface StatusPart { text: string; cut: boolean }
|
|
324
|
+
|
|
325
|
+
/** Stands in for one value while a line is formatted, to find where that value sits in it. */
|
|
326
|
+
const SPLIT_MARK = "\u0000";
|
|
327
|
+
|
|
328
|
+
/** `marked`, formatted with SPLIT_MARK for `value`, as the pieces around the value: one whole line when it does not show. */
|
|
329
|
+
function partsAround(marked: string, value: string, valueIsCut: boolean): StatusPart[] {
|
|
330
|
+
const [before, after, ...rest] = marked.split(SPLIT_MARK);
|
|
331
|
+
if (after === undefined || rest.length > 0) return [{ text: marked.replaceAll(SPLIT_MARK, value), cut: true }];
|
|
332
|
+
const parts = valueIsCut
|
|
333
|
+
? [{ text: before!, cut: false }, { text: `${value}${after}`, cut: true }]
|
|
334
|
+
: [{ text: before!, cut: true }, { text: `${value}${after}`, cut: false }];
|
|
335
|
+
return parts.filter((part) => part.text.length > 0);
|
|
336
|
+
}
|
|
291
337
|
|
|
338
|
+
function resolveUpdateStatusView({
|
|
339
|
+
canRestart,
|
|
340
|
+
updateAvailable,
|
|
341
|
+
updateCheckInProgress,
|
|
342
|
+
updateNotice,
|
|
343
|
+
updateProgress,
|
|
344
|
+
}: Pick<AppState, "updateAvailable" | "updateProgress" | "updateCheckInProgress" | "updateNotice"> & {
|
|
345
|
+
/** A Restart button can apply a downloaded update here. */
|
|
346
|
+
canRestart: boolean;
|
|
347
|
+
}): UpdateStatusView | null {
|
|
292
348
|
if (updateProgress) {
|
|
293
349
|
if (updateProgress.phase === "downloading") {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
})}
|
|
302
|
-
</Text>
|
|
303
|
-
</Box>
|
|
304
|
-
);
|
|
350
|
+
const version = updateAvailable?.version ?? "";
|
|
351
|
+
const percent = String(updateProgress.percent ?? 0);
|
|
352
|
+
return {
|
|
353
|
+
kind: "busy",
|
|
354
|
+
text: tf("Downloading v{version}: {percent}%", { version, percent }),
|
|
355
|
+
parts: partsAround(tf("Downloading v{version}: {percent}%", { version, percent: SPLIT_MARK }), percent, false),
|
|
356
|
+
};
|
|
305
357
|
}
|
|
306
358
|
if (updateProgress.phase === "replacing") {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
<SpinnerMark name="dots" color={colors.headerText} />
|
|
310
|
-
<Text fg={colors.headerText}>{t("Installing update...")}</Text>
|
|
311
|
-
</Box>
|
|
312
|
-
);
|
|
359
|
+
const text = t("Installing update...");
|
|
360
|
+
return { kind: "busy", text, parts: [{ text, cut: true }] };
|
|
313
361
|
}
|
|
314
362
|
if (updateProgress.phase === "ready") {
|
|
315
363
|
// A desktop update waits for the user: the app never relaunches by itself.
|
|
316
364
|
// The terminal binary is already swapped in and only needs a new launch.
|
|
317
|
-
if (updateProgress.canRestart &&
|
|
318
|
-
return (
|
|
319
|
-
<Box flexDirection="row" gap={1} alignItems="center">
|
|
320
|
-
<Text fg={colors.headerText}>{t("Update ready")}</Text>
|
|
321
|
-
<Button compact label="Restart" shortcut={updateKey || undefined} onPress={onRestart} stopPropagation />
|
|
322
|
-
</Box>
|
|
323
|
-
);
|
|
365
|
+
if (updateProgress.canRestart && canRestart) {
|
|
366
|
+
return { kind: "restart", text: t("Update ready"), version: updateAvailable?.version ?? "" };
|
|
324
367
|
}
|
|
325
|
-
return
|
|
368
|
+
return { kind: "text", text: t("Update ready, restart to apply") };
|
|
326
369
|
}
|
|
327
370
|
if (updateProgress.phase === "done") {
|
|
328
|
-
return
|
|
371
|
+
return { kind: "text", text: t(updateProgress.message ?? "Update ready, restart to apply") };
|
|
329
372
|
}
|
|
330
373
|
if (updateProgress.phase === "error") {
|
|
331
374
|
// One line: an error that carries a stack or a command's output must not grow the header.
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const messageColumns = room - (showHint ? hintColumns : 0);
|
|
340
|
-
if (!nativePaneChrome && messageColumns < UPDATE_ERROR_MIN_COLUMNS) return null;
|
|
341
|
-
return (
|
|
342
|
-
<Box flexDirection="row" overflow="hidden" paddingRight={UPDATE_ERROR_GAP_COLUMNS}>
|
|
343
|
-
<ClippedText fg={colors.headerText} text={message} width={messageColumns} />
|
|
344
|
-
{showHint ? <Text fg={blendHex(colors.headerText, colors.header, 0.62)}>{hint}</Text> : null}
|
|
345
|
-
</Box>
|
|
346
|
-
);
|
|
375
|
+
const error = (updateProgress.error ?? "Unknown error").replace(/\s+/g, " ").trim();
|
|
376
|
+
return {
|
|
377
|
+
kind: "failed",
|
|
378
|
+
message: tf("Update failed: {error}", { error }),
|
|
379
|
+
parts: partsAround(tf("Update failed: {error}", { error: SPLIT_MARK }), error, true),
|
|
380
|
+
retry: canRetryUpdate({ updateAvailable, updateProgress, updateCheckInProgress }),
|
|
381
|
+
};
|
|
347
382
|
}
|
|
348
383
|
}
|
|
349
|
-
|
|
350
384
|
if (updateCheckInProgress) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
<SpinnerMark name="dots" color={colors.headerText} />
|
|
354
|
-
<Text fg={colors.headerText}>{t("Checking for updates...")}</Text>
|
|
355
|
-
</Box>
|
|
356
|
-
);
|
|
385
|
+
const text = t("Checking for updates...");
|
|
386
|
+
return { kind: "busy", text, parts: [{ text, cut: true }] };
|
|
357
387
|
}
|
|
358
|
-
|
|
359
388
|
if (updateAvailable) {
|
|
360
389
|
if (updateAvailable.updateAction.kind === "manual") {
|
|
361
|
-
return
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
);
|
|
390
|
+
return {
|
|
391
|
+
kind: "text",
|
|
392
|
+
text: tf("v{version} available — run {command}", {
|
|
393
|
+
version: updateAvailable.version,
|
|
394
|
+
command: updateAvailable.updateAction.command,
|
|
395
|
+
}),
|
|
396
|
+
};
|
|
369
397
|
}
|
|
370
398
|
if (updateAvailable.updateAction.kind === "managed") {
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
);
|
|
399
|
+
return {
|
|
400
|
+
kind: "text",
|
|
401
|
+
text: tf("v{version} available, update through the package manager that installed Gloomberb", {
|
|
402
|
+
version: updateAvailable.version,
|
|
403
|
+
}),
|
|
404
|
+
};
|
|
378
405
|
}
|
|
406
|
+
return { kind: "text", text: tf("v{version} available — starting download...", { version: updateAvailable.version }) };
|
|
407
|
+
}
|
|
408
|
+
if (updateNotice) return { kind: "text", text: updateNotice };
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* What the header says about an update. The terminal cuts it to `columns`,
|
|
414
|
+
* the room its slot is guaranteed; the desktop and the web let the browser fit
|
|
415
|
+
* it to the room the bar leaves (NativeUpdateStatus). Either way it never runs
|
|
416
|
+
* into the Help pill or the market cluster beside it.
|
|
417
|
+
*/
|
|
418
|
+
function UpdateStatus({ columns, onRestart, trailingGap = false }: {
|
|
419
|
+
columns: number;
|
|
420
|
+
onRestart?: () => void;
|
|
421
|
+
/** The desktop title bar keeps a gap after the status as well as before it. */
|
|
422
|
+
trailingGap?: boolean;
|
|
423
|
+
}) {
|
|
424
|
+
const { nativePaneChrome = false } = useUiCapabilities();
|
|
425
|
+
const dispatch = useAppDispatch();
|
|
426
|
+
const updateAvailable = useAppSelector((state) => state.updateAvailable);
|
|
427
|
+
const updateProgress = useAppSelector((state) => state.updateProgress);
|
|
428
|
+
const updateCheckInProgress = useAppSelector((state) => state.updateCheckInProgress);
|
|
429
|
+
const updateNotice = useAppSelector((state) => state.updateNotice);
|
|
430
|
+
// One key: Restart for a downloaded update, retry for a failed one.
|
|
431
|
+
const updateKey = useActionShortcut("install-update");
|
|
432
|
+
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
if (!updateNotice || updateAvailable || updateProgress || updateCheckInProgress) return;
|
|
435
|
+
const timeout = setTimeout(() => {
|
|
436
|
+
dispatch({ type: "SET_UPDATE_NOTICE", notice: null });
|
|
437
|
+
}, UPDATE_NOTICE_DURATION_MS);
|
|
438
|
+
return () => clearTimeout(timeout);
|
|
439
|
+
}, [dispatch, updateAvailable, updateCheckInProgress, updateNotice, updateProgress]);
|
|
440
|
+
|
|
441
|
+
const view = resolveUpdateStatusView({
|
|
442
|
+
canRestart: !!onRestart,
|
|
443
|
+
updateAvailable,
|
|
444
|
+
updateCheckInProgress,
|
|
445
|
+
updateNotice,
|
|
446
|
+
updateProgress,
|
|
447
|
+
});
|
|
448
|
+
if (!view) return null;
|
|
449
|
+
return nativePaneChrome
|
|
450
|
+
? <NativeUpdateStatus view={view} onRestart={onRestart} trailingGap={trailingGap} updateKey={updateKey} />
|
|
451
|
+
: <TerminalUpdateStatus view={view} columns={columns} onRestart={onRestart} updateKey={updateKey} />;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function TerminalUpdateStatus({ columns, onRestart, updateKey, view }: {
|
|
455
|
+
columns: number;
|
|
456
|
+
onRestart?: () => void;
|
|
457
|
+
updateKey: string;
|
|
458
|
+
view: UpdateStatusView;
|
|
459
|
+
}) {
|
|
460
|
+
const colors = useThemeColors();
|
|
461
|
+
if (view.kind === "busy") {
|
|
379
462
|
return (
|
|
380
|
-
<
|
|
381
|
-
|
|
382
|
-
|
|
463
|
+
<Box flexDirection="row" gap={1}>
|
|
464
|
+
<SpinnerMark name="dots" color={colors.headerText} />
|
|
465
|
+
<Text fg={colors.headerText}>{view.text}</Text>
|
|
466
|
+
</Box>
|
|
383
467
|
);
|
|
384
468
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
469
|
+
if (view.kind === "restart") {
|
|
470
|
+
return (
|
|
471
|
+
<Box flexDirection="row" gap={1} alignItems="center">
|
|
472
|
+
<Text fg={colors.headerText}>{view.text}</Text>
|
|
473
|
+
<Button compact label="Restart" shortcut={updateKey || undefined} onPress={onRestart} stopPropagation />
|
|
474
|
+
</Box>
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
if (view.kind === "failed") {
|
|
478
|
+
const hint = updateKey && view.retry ? ` ${updateKey} ${t("retry")}` : "";
|
|
479
|
+
const hintColumns = displayWidth(hint);
|
|
480
|
+
// The message goes first, and keeps its room before the hint takes any.
|
|
481
|
+
const room = columns - UPDATE_ERROR_GAP_COLUMNS;
|
|
482
|
+
const showHint = hintColumns > 0 && room - hintColumns >= UPDATE_ERROR_MIN_COLUMNS;
|
|
483
|
+
const messageColumns = room - (showHint ? hintColumns : 0);
|
|
484
|
+
if (messageColumns < UPDATE_ERROR_MIN_COLUMNS) return null;
|
|
485
|
+
return (
|
|
486
|
+
<Box flexDirection="row" overflow="hidden" paddingRight={UPDATE_ERROR_GAP_COLUMNS}>
|
|
487
|
+
<ClippedText fg={colors.headerText} text={view.message} width={messageColumns} />
|
|
488
|
+
{showHint ? <Text fg={blendHex(colors.headerText, colors.header, 0.62)}>{hint}</Text> : null}
|
|
489
|
+
</Box>
|
|
490
|
+
);
|
|
388
491
|
}
|
|
492
|
+
return <Text fg={colors.headerText}>{view.text}</Text>;
|
|
493
|
+
}
|
|
389
494
|
|
|
390
|
-
|
|
495
|
+
/**
|
|
496
|
+
* Text in the desktop status that gives way: it ends in an ellipsis as its room
|
|
497
|
+
* shrinks, and goes rather than leave a stub. Alone on its line it goes below
|
|
498
|
+
* about this many characters; beside a piece that stays whole it takes what
|
|
499
|
+
* that piece leaves, down to NATIVE_STATUS_PIECE_MIN_CH.
|
|
500
|
+
*/
|
|
501
|
+
const NATIVE_STATUS_TEXT_MIN_CH = 8;
|
|
502
|
+
const NATIVE_STATUS_PIECE_MIN_CH = 3;
|
|
503
|
+
function nativeCutTextStyle(alone: boolean) {
|
|
504
|
+
return {
|
|
505
|
+
...NATIVE_CLIP_STYLE,
|
|
506
|
+
flexGrow: 1,
|
|
507
|
+
flexShrink: 1,
|
|
508
|
+
flexBasis: `${alone ? NATIVE_STATUS_TEXT_MIN_CH : NATIVE_STATUS_PIECE_MIN_CH}ch`,
|
|
509
|
+
maxWidth: "max-content",
|
|
510
|
+
} as const;
|
|
511
|
+
}
|
|
512
|
+
/** One cell between the parts of the desktop status, as `gap={1}` puts it in the terminal. */
|
|
513
|
+
const NATIVE_STATUS_GAP = "var(--cell-w)";
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* The desktop and web update status, fitted by the browser to whatever room
|
|
517
|
+
* the bar leaves it, never by counting cells. Its parts are given most
|
|
518
|
+
* important first, and the row is one line tall and wraps: a part that does
|
|
519
|
+
* not fit whole moves to a second line the row hides, and so does every part
|
|
520
|
+
* after it, so a button is never cut, a spinner never spins on its own, and
|
|
521
|
+
* nothing runs under Help. Text that may be cut gives way before anything
|
|
522
|
+
* moves, ending in an ellipsis (nativeCutTextStyle). `reverse` lays the parts
|
|
523
|
+
* out right to left, for a status whose most important part is on its right:
|
|
524
|
+
* Restart stays when "Update ready" goes.
|
|
525
|
+
*/
|
|
526
|
+
function NativeStatusRow({ children, reverse = false, trailingGap }: {
|
|
527
|
+
children: ReactNode;
|
|
528
|
+
reverse?: boolean;
|
|
529
|
+
trailingGap: boolean;
|
|
530
|
+
}) {
|
|
531
|
+
return (
|
|
532
|
+
<Box
|
|
533
|
+
height={1}
|
|
534
|
+
flexGrow={1}
|
|
535
|
+
flexShrink={1}
|
|
536
|
+
minWidth={0}
|
|
537
|
+
overflow="hidden"
|
|
538
|
+
paddingLeft={UPDATE_SLOT_PADDING_COLUMNS}
|
|
539
|
+
paddingRight={trailingGap ? UPDATE_SLOT_PADDING_COLUMNS : 0}
|
|
540
|
+
style={{
|
|
541
|
+
flexDirection: reverse ? "row-reverse" : "row",
|
|
542
|
+
flexWrap: "wrap",
|
|
543
|
+
alignContent: "flex-start",
|
|
544
|
+
alignItems: "center",
|
|
545
|
+
justifyContent: reverse ? "flex-end" : "flex-start",
|
|
546
|
+
}}
|
|
547
|
+
>
|
|
548
|
+
{/* Takes no room and always holds the first line, so even the most important part can leave it. */}
|
|
549
|
+
<Box width={0} height={1} />
|
|
550
|
+
{children}
|
|
551
|
+
</Box>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** One piece of a status line, with the whole line as its tooltip in case it is cut. */
|
|
556
|
+
function nativeStatusPiece(part: StatusPart, index: number, { alone, color, title }: { alone: boolean; color: string; title: string }) {
|
|
557
|
+
return part.cut
|
|
558
|
+
? <Text key={index} fg={color} title={title} style={nativeCutTextStyle(alone)}>{part.text}</Text>
|
|
559
|
+
: <Text key={index} fg={color} title={title}>{part.text}</Text>;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function NativeUpdateStatus({ onRestart, trailingGap, updateKey, view }: {
|
|
563
|
+
onRestart?: () => void;
|
|
564
|
+
trailingGap: boolean;
|
|
565
|
+
updateKey: string;
|
|
566
|
+
view: UpdateStatusView;
|
|
567
|
+
}) {
|
|
568
|
+
const colors = useThemeColors();
|
|
569
|
+
const color = colors.headerText;
|
|
570
|
+
if (view.kind === "busy") {
|
|
571
|
+
// Right to left: the percent, then the words, then the spinner, which never shows without them.
|
|
572
|
+
const alone = view.parts.every((part) => part.cut);
|
|
573
|
+
return (
|
|
574
|
+
<NativeStatusRow reverse trailingGap={trailingGap}>
|
|
575
|
+
{view.parts.map((part, index) => nativeStatusPiece(part, index, { alone, color, title: view.text })).reverse()}
|
|
576
|
+
<Box flexShrink={0} style={{ marginRight: NATIVE_STATUS_GAP }}>
|
|
577
|
+
<SpinnerMark name="dots" color={color} />
|
|
578
|
+
</Box>
|
|
579
|
+
</NativeStatusRow>
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (view.kind === "restart") {
|
|
583
|
+
// Right to left: the button, then the words, which go whole before it does.
|
|
584
|
+
return (
|
|
585
|
+
<NativeStatusRow reverse trailingGap={trailingGap}>
|
|
586
|
+
<Button
|
|
587
|
+
compact
|
|
588
|
+
label="Restart"
|
|
589
|
+
shortcut={updateKey || undefined}
|
|
590
|
+
title={view.version ? tf("Restart to apply v{version}", { version: view.version }) : undefined}
|
|
591
|
+
onPress={onRestart}
|
|
592
|
+
stopPropagation
|
|
593
|
+
/>
|
|
594
|
+
<Text fg={color} style={{ marginRight: NATIVE_STATUS_GAP }}>{view.text}</Text>
|
|
595
|
+
</NativeStatusRow>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
if (view.kind === "failed") {
|
|
599
|
+
// The words that say it failed, then as much of the error as reads, then the retry key.
|
|
600
|
+
const hint = updateKey && view.retry ? ` ${updateKey} ${t("retry")}` : "";
|
|
601
|
+
const alone = view.parts.every((part) => part.cut);
|
|
602
|
+
return (
|
|
603
|
+
<NativeStatusRow trailingGap={trailingGap}>
|
|
604
|
+
{view.parts.map((part, index) => nativeStatusPiece(part, index, { alone, color, title: view.message }))}
|
|
605
|
+
{hint ? <Text fg={blendHex(color, colors.header, 0.62)}>{hint}</Text> : null}
|
|
606
|
+
</NativeStatusRow>
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
return (
|
|
610
|
+
<NativeStatusRow trailingGap={trailingGap}>
|
|
611
|
+
{nativeStatusPiece({ text: view.text, cut: true }, 0, { alone: true, color, title: view.text })}
|
|
612
|
+
</NativeStatusRow>
|
|
613
|
+
);
|
|
391
614
|
}
|
|
392
615
|
|
|
393
616
|
/**
|
|
@@ -434,7 +657,8 @@ function HeaderMarketSummary({ nativePaneChrome, width }: { nativePaneChrome: bo
|
|
|
434
657
|
/**
|
|
435
658
|
* Help, one click away on every renderer: the same pane the typed HELP and the
|
|
436
659
|
* `?` key open. It takes its columns from the update status beside it, which
|
|
437
|
-
* is transient, rather than from the
|
|
660
|
+
* is transient, rather than from the market cluster; on the desktop and the
|
|
661
|
+
* web the closed prompt's empty room makes way for it as well.
|
|
438
662
|
*/
|
|
439
663
|
function HeaderHelpButton({ nativePaneChrome, onPress }: { nativePaneChrome: boolean; onPress: () => void }) {
|
|
440
664
|
return (
|
|
@@ -508,6 +732,24 @@ export function Header({
|
|
|
508
732
|
const marketSummary = prompt.marketColumns > 0
|
|
509
733
|
? <HeaderMarketSummary nativePaneChrome={nativePaneChrome} width={prompt.marketColumns} />
|
|
510
734
|
: null;
|
|
735
|
+
// The desktop and the web fit the update status in CSS (NativeUpdateStatus):
|
|
736
|
+
// its slot takes the room the closed prompt leaves, clips at its own edge, and
|
|
737
|
+
// gives all of it up before an open prompt gives any. Help, the market
|
|
738
|
+
// cluster and the caption buttons keep their size.
|
|
739
|
+
const nativeUpdateSlot = nativePaneChrome ? (
|
|
740
|
+
<Box
|
|
741
|
+
flexDirection="row"
|
|
742
|
+
alignItems="center"
|
|
743
|
+
flexGrow={1}
|
|
744
|
+
flexShrink={NATIVE_UPDATE_SLOT_SHRINK}
|
|
745
|
+
minWidth={0}
|
|
746
|
+
overflow="hidden"
|
|
747
|
+
data-gloom-role="header-update-status"
|
|
748
|
+
style={{ alignSelf: "stretch" }}
|
|
749
|
+
>
|
|
750
|
+
<UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} trailingGap={!!titleBarOverlay} />
|
|
751
|
+
</Box>
|
|
752
|
+
) : null;
|
|
511
753
|
|
|
512
754
|
if (titleBarOverlay) {
|
|
513
755
|
return (
|
|
@@ -527,9 +769,11 @@ export function Header({
|
|
|
527
769
|
>
|
|
528
770
|
<Box width={prompt.left} />
|
|
529
771
|
{commandPrompt}
|
|
530
|
-
|
|
531
|
-
<
|
|
532
|
-
|
|
772
|
+
{nativeUpdateSlot ?? (
|
|
773
|
+
<Box flexGrow={1} paddingLeft={UPDATE_SLOT_PADDING_COLUMNS} paddingRight={UPDATE_SLOT_PADDING_COLUMNS} minWidth={0}>
|
|
774
|
+
<UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} />
|
|
775
|
+
</Box>
|
|
776
|
+
)}
|
|
533
777
|
{onOpenHelp ? <HeaderHelpButton nativePaneChrome onPress={onOpenHelp} /> : null}
|
|
534
778
|
{marketSummary}
|
|
535
779
|
{showWindowControls ? <Box flexShrink={0} width={`${WINDOWS_CONTROL_GROUP_WIDTH_PX}px`} /> : null}
|
|
@@ -549,9 +793,11 @@ export function Header({
|
|
|
549
793
|
>
|
|
550
794
|
<Box width={prompt.left} />
|
|
551
795
|
{commandPrompt}
|
|
552
|
-
|
|
553
|
-
<
|
|
554
|
-
|
|
796
|
+
{nativeUpdateSlot ?? (
|
|
797
|
+
<Box flexGrow={1} minWidth={0} paddingLeft={UPDATE_SLOT_PADDING_COLUMNS}>
|
|
798
|
+
<UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} />
|
|
799
|
+
</Box>
|
|
800
|
+
)}
|
|
555
801
|
{onOpenHelp ? <HeaderHelpButton nativePaneChrome={nativePaneChrome} onPress={onOpenHelp} /> : null}
|
|
556
802
|
{marketSummary}
|
|
557
803
|
{showWindowControls ? <WindowControls /> : null}
|