gloomberb 0.6.0 → 0.7.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/package.json +2 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +9 -6
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +70 -0
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +17 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-stock-chart.tsx +62 -77
- package/src/components/chart/native/kitty.test.ts +23 -0
- package/src/components/chart/native/surface-visibility.ts +48 -0
- package/src/components/chart/static-chart-surface.tsx +28 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +52 -71
- package/src/components/chart/time-axis-label.tsx +43 -0
- package/src/components/command-bar/command-bar.test.tsx +71 -8
- package/src/components/command-bar/command-bar.tsx +388 -713
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/list-model.ts +77 -0
- package/src/components/command-bar/view-model.ts +0 -19
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +6 -2
- package/src/components/layout/floating-pane.tsx +30 -2
- package/src/components/layout/header.tsx +10 -7
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +26 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
- package/src/components/layout/shell.test.tsx +66 -22
- package/src/components/layout/shell.tsx +13 -6
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +558 -0
- package/src/components/table-view-shared.tsx +158 -0
- package/src/components/ticker-list-table-view.tsx +42 -72
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +18 -10
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/remote-image.tsx +15 -40
- package/src/components/ui/table-layout.test.ts +16 -0
- package/src/components/ui/table-layout.ts +10 -3
- package/src/components/ui/ui.test.tsx +100 -20
- package/src/core/state/app-state.ts +6 -3
- package/src/market-data/coordinator.test.ts +1 -1
- package/src/market-data/coordinator.ts +0 -1
- package/src/news/aggregator.ts +0 -3
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +105 -0
- package/src/plugins/builtin/chat-controller.ts +75 -6
- package/src/plugins/builtin/chat.test.tsx +6 -6
- package/src/plugins/builtin/chat.tsx +29 -20
- package/src/plugins/builtin/cloud-tweets.tsx +0 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +478 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +10 -15
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +18 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +108 -49
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +5 -21
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +36 -28
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +102 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +56 -5
- package/src/plugins/ibkr/trade-tab.tsx +13 -11
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +6 -6
- package/src/plugins/plugin-runtime.tsx +13 -8
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +10 -39
- package/src/react/input.ts +11 -2
- package/src/renderers/electrobun/view/data-table.tsx +28 -6
- package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
- package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
- package/src/renderers/electrobun/view/input-host.tsx +123 -12
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/styles.css +331 -0
- package/src/renderers/electrobun/view/ui-host.tsx +22 -3
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/host.tsx +6 -0
- package/src/renderers/opentui/image-loader.test.ts +25 -0
- package/src/renderers/opentui/image-loader.ts +105 -0
- package/src/renderers/opentui/image-surface.tsx +266 -0
- package/src/renderers/opentui/input-host.tsx +45 -6
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +7 -3
- package/src/sources/provider-router.ts +0 -4
- package/src/state/app-context.test.ts +45 -0
- package/src/state/app-context.tsx +4 -0
- package/src/state/pane-scroll-registry.tsx +254 -0
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +7 -1
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.ts +12 -3
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/data-provider.ts +0 -1
- package/src/types/plugin.ts +0 -2
- package/src/ui/host.tsx +4 -0
- package/src/ui/index.tsx +9 -1
- package/src/utils/back-navigation.ts +24 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { createElement, forwardRef, useCallback, useEffect, useMemo, useRef, useState, type ForwardedRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
computeBitmapSize,
|
|
4
|
+
intersectCellRects,
|
|
5
|
+
type CellRect,
|
|
6
|
+
type NativeChartBitmap,
|
|
7
|
+
} from "../../components/chart/native/chart-rasterizer";
|
|
8
|
+
import { getCachedKittySupport, ensureKittySupport } from "../../components/chart/native/kitty-support";
|
|
9
|
+
import { getNativeSurfaceManager } from "../../components/chart/native/surface-manager";
|
|
10
|
+
import {
|
|
11
|
+
getRenderableCellRect,
|
|
12
|
+
resolveNativeSurfaceVisibleRect,
|
|
13
|
+
type NativeSurfaceRenderableNode,
|
|
14
|
+
} from "../../components/chart/native/surface-visibility";
|
|
15
|
+
import { useOptionalPaneInstanceId } from "../../state/app-context";
|
|
16
|
+
import { useNativeRenderer, type BoxRenderable, type ImageSurfaceProps } from "../../ui";
|
|
17
|
+
import { loadOpenTuiImageBitmap } from "./image-loader";
|
|
18
|
+
|
|
19
|
+
interface NativeRenderableNode extends BoxRenderable, NativeSurfaceRenderableNode {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
parent: NativeRenderableNode | null;
|
|
25
|
+
onLifecyclePass: (() => void) | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SurfaceTarget {
|
|
29
|
+
rect: CellRect;
|
|
30
|
+
visibleRect: CellRect | null;
|
|
31
|
+
pixelWidth: number;
|
|
32
|
+
pixelHeight: number;
|
|
33
|
+
bitmapKey: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CellInsets {
|
|
37
|
+
top: number;
|
|
38
|
+
right: number;
|
|
39
|
+
bottom: number;
|
|
40
|
+
left: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let nextImageSurfaceId = 1;
|
|
44
|
+
|
|
45
|
+
function assignRef(ref: ForwardedRef<unknown>, value: unknown) {
|
|
46
|
+
if (typeof ref === "function") {
|
|
47
|
+
ref(value);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (ref) {
|
|
51
|
+
(ref as { current: unknown }).current = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readInset(value: unknown): number {
|
|
56
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveInsets(props: Record<string, unknown>): CellInsets {
|
|
60
|
+
const border = props.border ? 1 : 0;
|
|
61
|
+
const padding = readInset(props.padding);
|
|
62
|
+
const paddingX = readInset(props.paddingX ?? padding);
|
|
63
|
+
const paddingY = readInset(props.paddingY ?? padding);
|
|
64
|
+
return {
|
|
65
|
+
top: border + readInset(props.paddingTop ?? paddingY),
|
|
66
|
+
right: border + readInset(props.paddingRight ?? paddingX),
|
|
67
|
+
bottom: border + readInset(props.paddingBottom ?? paddingY),
|
|
68
|
+
left: border + readInset(props.paddingLeft ?? paddingX),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sameRect(left: CellRect | null, right: CellRect | null): boolean {
|
|
73
|
+
if (left === right) return true;
|
|
74
|
+
if (!left || !right) return false;
|
|
75
|
+
return left.x === right.x
|
|
76
|
+
&& left.y === right.y
|
|
77
|
+
&& left.width === right.width
|
|
78
|
+
&& left.height === right.height;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sameTarget(left: SurfaceTarget | null, right: SurfaceTarget | null): boolean {
|
|
82
|
+
if (left === right) return true;
|
|
83
|
+
if (!left || !right) return false;
|
|
84
|
+
return left.bitmapKey === right.bitmapKey
|
|
85
|
+
&& left.pixelWidth === right.pixelWidth
|
|
86
|
+
&& left.pixelHeight === right.pixelHeight
|
|
87
|
+
&& sameRect(left.rect, right.rect)
|
|
88
|
+
&& sameRect(left.visibleRect, right.visibleRect);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function insetRect(rect: CellRect, insets: CellInsets): CellRect | null {
|
|
92
|
+
const width = rect.width - insets.left - insets.right;
|
|
93
|
+
const height = rect.height - insets.top - insets.bottom;
|
|
94
|
+
if (width <= 0 || height <= 0) return null;
|
|
95
|
+
return {
|
|
96
|
+
x: rect.x + insets.left,
|
|
97
|
+
y: rect.y + insets.top,
|
|
98
|
+
width,
|
|
99
|
+
height,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const OpenTuiImageSurface = forwardRef<unknown, ImageSurfaceProps>(function OpenTuiImageSurface(
|
|
104
|
+
{ children, src, alt: _alt, objectFit = "contain", ...props },
|
|
105
|
+
forwardedRef,
|
|
106
|
+
) {
|
|
107
|
+
const renderer = useNativeRenderer();
|
|
108
|
+
const paneId = useOptionalPaneInstanceId();
|
|
109
|
+
const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
|
|
110
|
+
const surfaceId = useRef(`opentui-image:${nextImageSurfaceId++}`).current;
|
|
111
|
+
const renderableRef = useRef<NativeRenderableNode | null>(null);
|
|
112
|
+
const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
|
|
113
|
+
const [target, setTarget] = useState<SurfaceTarget | null>(null);
|
|
114
|
+
const [bitmapState, setBitmapState] = useState<{ key: string; bitmap: NativeChartBitmap } | null>(null);
|
|
115
|
+
const [loadFailed, setLoadFailed] = useState(false);
|
|
116
|
+
const imageSrc = typeof src === "string" ? src.trim() : "";
|
|
117
|
+
const resolvedObjectFit = objectFit === "cover" ? "cover" : "contain";
|
|
118
|
+
const insets = useMemo(() => resolveInsets(props), [
|
|
119
|
+
props.border,
|
|
120
|
+
props.padding,
|
|
121
|
+
props.paddingX,
|
|
122
|
+
props.paddingY,
|
|
123
|
+
props.paddingTop,
|
|
124
|
+
props.paddingRight,
|
|
125
|
+
props.paddingBottom,
|
|
126
|
+
props.paddingLeft,
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const setRenderableRef = useCallback((node: unknown) => {
|
|
130
|
+
renderableRef.current = node as NativeRenderableNode | null;
|
|
131
|
+
assignRef(forwardedRef, node);
|
|
132
|
+
}, [forwardedRef]);
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
let cancelled = false;
|
|
136
|
+
setKittySupport(getCachedKittySupport(renderer));
|
|
137
|
+
ensureKittySupport(renderer).then((supported) => {
|
|
138
|
+
if (!cancelled) setKittySupport(supported);
|
|
139
|
+
});
|
|
140
|
+
return () => {
|
|
141
|
+
cancelled = true;
|
|
142
|
+
};
|
|
143
|
+
}, [renderer]);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
setLoadFailed(false);
|
|
147
|
+
setBitmapState(null);
|
|
148
|
+
}, [imageSrc]);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const renderable = renderableRef.current;
|
|
152
|
+
if (!renderable || !imageSrc || kittySupport !== true) {
|
|
153
|
+
setTarget(null);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let mountTimer: Timer | null = null;
|
|
158
|
+
const previousLifecyclePass = renderable.onLifecyclePass;
|
|
159
|
+
const syncTarget = () => {
|
|
160
|
+
const outerRect = getRenderableCellRect(renderable);
|
|
161
|
+
const rect = insetRect(outerRect, insets);
|
|
162
|
+
if (!rect || !renderer.resolution || renderer.terminalWidth <= 0 || renderer.terminalHeight <= 0) {
|
|
163
|
+
setTarget((current) => (current === null ? current : null));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const outerVisibleRect = resolveNativeSurfaceVisibleRect(renderable, renderer.terminalWidth, renderer.terminalHeight);
|
|
168
|
+
const visibleRect = outerVisibleRect ? intersectCellRects(rect, outerVisibleRect) : null;
|
|
169
|
+
const bitmapSize = computeBitmapSize(rect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
|
|
170
|
+
const bitmapKey = `${imageSrc}\n${resolvedObjectFit}\n${bitmapSize.pixelWidth}x${bitmapSize.pixelHeight}`;
|
|
171
|
+
const nextTarget: SurfaceTarget = {
|
|
172
|
+
rect,
|
|
173
|
+
visibleRect,
|
|
174
|
+
pixelWidth: bitmapSize.pixelWidth,
|
|
175
|
+
pixelHeight: bitmapSize.pixelHeight,
|
|
176
|
+
bitmapKey,
|
|
177
|
+
};
|
|
178
|
+
setTarget((current) => (sameTarget(current, nextTarget) ? current : nextTarget));
|
|
179
|
+
};
|
|
180
|
+
const lifecyclePass = () => {
|
|
181
|
+
previousLifecyclePass?.();
|
|
182
|
+
syncTarget();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
renderable.onLifecyclePass = lifecyclePass;
|
|
186
|
+
renderer.registerLifecyclePass(renderable);
|
|
187
|
+
syncTarget();
|
|
188
|
+
mountTimer = setTimeout(() => {
|
|
189
|
+
syncTarget();
|
|
190
|
+
renderer.requestRender();
|
|
191
|
+
}, 0);
|
|
192
|
+
|
|
193
|
+
return () => {
|
|
194
|
+
if (mountTimer) clearTimeout(mountTimer);
|
|
195
|
+
if (renderable.onLifecyclePass === lifecyclePass) {
|
|
196
|
+
renderable.onLifecyclePass = previousLifecyclePass;
|
|
197
|
+
}
|
|
198
|
+
renderer.unregisterLifecyclePass(renderable);
|
|
199
|
+
setTarget(null);
|
|
200
|
+
};
|
|
201
|
+
}, [imageSrc, insets, kittySupport, renderer, resolvedObjectFit]);
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (!target || !imageSrc || kittySupport !== true) {
|
|
205
|
+
setBitmapState(null);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let cancelled = false;
|
|
210
|
+
setBitmapState((current) => (current?.key === target.bitmapKey ? current : null));
|
|
211
|
+
loadOpenTuiImageBitmap(imageSrc, {
|
|
212
|
+
width: target.pixelWidth,
|
|
213
|
+
height: target.pixelHeight,
|
|
214
|
+
objectFit: resolvedObjectFit,
|
|
215
|
+
}).then((bitmap) => {
|
|
216
|
+
if (!cancelled) {
|
|
217
|
+
setLoadFailed(false);
|
|
218
|
+
setBitmapState({ key: target.bitmapKey, bitmap });
|
|
219
|
+
}
|
|
220
|
+
}).catch(() => {
|
|
221
|
+
if (!cancelled) {
|
|
222
|
+
setLoadFailed(true);
|
|
223
|
+
setBitmapState(null);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return () => {
|
|
228
|
+
cancelled = true;
|
|
229
|
+
};
|
|
230
|
+
}, [imageSrc, kittySupport, resolvedObjectFit, target]);
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
return () => {
|
|
234
|
+
nativeSurfaceManager.removeSurface(surfaceId);
|
|
235
|
+
};
|
|
236
|
+
}, [nativeSurfaceManager, surfaceId]);
|
|
237
|
+
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (kittySupport !== true
|
|
240
|
+
|| loadFailed
|
|
241
|
+
|| !target?.visibleRect
|
|
242
|
+
|| !bitmapState
|
|
243
|
+
|| bitmapState.key !== target.bitmapKey) {
|
|
244
|
+
nativeSurfaceManager.removeSurface(surfaceId);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
nativeSurfaceManager.upsertSurface({
|
|
249
|
+
id: surfaceId,
|
|
250
|
+
paneId: paneId ?? "__global__",
|
|
251
|
+
rect: target.rect,
|
|
252
|
+
visibleRect: target.visibleRect,
|
|
253
|
+
bitmap: bitmapState.bitmap,
|
|
254
|
+
bitmapKey: bitmapState.key,
|
|
255
|
+
});
|
|
256
|
+
renderer.requestRender();
|
|
257
|
+
}, [bitmapState, kittySupport, loadFailed, nativeSurfaceManager, paneId, renderer, surfaceId, target]);
|
|
258
|
+
|
|
259
|
+
const showFallback = kittySupport !== true
|
|
260
|
+
|| loadFailed
|
|
261
|
+
|| !target
|
|
262
|
+
|| !bitmapState
|
|
263
|
+
|| bitmapState.key !== target.bitmapKey;
|
|
264
|
+
|
|
265
|
+
return (createElement as any)("box", { ...props, ref: setRenderableRef }, showFallback ? children : null);
|
|
266
|
+
});
|
|
@@ -1,14 +1,53 @@
|
|
|
1
|
-
import { useMemo, type ReactNode } from "react";
|
|
2
|
-
import { InputHostProvider, type InputHost } from "../../react/input";
|
|
1
|
+
import { useLayoutEffect, useMemo, useRef, type ReactNode } from "react";
|
|
2
|
+
import { InputHostProvider, type InputHost, type KeyEventLike, type ShortcutOptions } from "../../react/input";
|
|
3
3
|
import { toKeyEventLike, useKeyboard, useTerminalDimensions } from "./host";
|
|
4
4
|
|
|
5
|
+
interface ShortcutEntry {
|
|
6
|
+
handlerRef: { current: (event: KeyEventLike) => void };
|
|
7
|
+
enabledRef: { current: boolean };
|
|
8
|
+
phase: NonNullable<ShortcutOptions["phase"]>;
|
|
9
|
+
order: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let nextShortcutOrder = 1;
|
|
13
|
+
|
|
5
14
|
export function OpenTuiInputHostProvider({ children }: { children: ReactNode }) {
|
|
15
|
+
const shortcutsRef = useRef<ShortcutEntry[]>([]);
|
|
16
|
+
const dispatchShortcut = (event: KeyEventLike) => {
|
|
17
|
+
const shortcuts = shortcutsRef.current;
|
|
18
|
+
for (const phase of ["normal", "after"] as const) {
|
|
19
|
+
if (phase === "after" && (event.defaultPrevented || event.propagationStopped)) return;
|
|
20
|
+
for (const entry of shortcuts) {
|
|
21
|
+
if (entry.phase !== phase || !entry.enabledRef.current) continue;
|
|
22
|
+
entry.handlerRef.current(event);
|
|
23
|
+
if (event.propagationStopped) return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
useKeyboard((event) => {
|
|
29
|
+
dispatchShortcut(toKeyEventLike(event));
|
|
30
|
+
});
|
|
31
|
+
|
|
6
32
|
const host = useMemo<InputHost>(() => ({
|
|
7
33
|
useShortcut(handler, options) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
34
|
+
const handlerRef = useRef(handler);
|
|
35
|
+
const enabledRef = useRef(options?.enabled !== false);
|
|
36
|
+
handlerRef.current = handler;
|
|
37
|
+
enabledRef.current = options?.enabled !== false;
|
|
38
|
+
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const entry: ShortcutEntry = {
|
|
41
|
+
handlerRef,
|
|
42
|
+
enabledRef,
|
|
43
|
+
phase: options?.phase ?? "normal",
|
|
44
|
+
order: nextShortcutOrder++,
|
|
45
|
+
};
|
|
46
|
+
shortcutsRef.current = [...shortcutsRef.current, entry].sort((a, b) => a.order - b.order);
|
|
47
|
+
return () => {
|
|
48
|
+
shortcutsRef.current = shortcutsRef.current.filter((current) => current !== entry);
|
|
49
|
+
};
|
|
50
|
+
}, [options?.phase, options?.scope]);
|
|
12
51
|
},
|
|
13
52
|
useViewport() {
|
|
14
53
|
const dimensions = useTerminalDimensions();
|
|
@@ -34,38 +34,56 @@ export async function startOpenTuiApp(): Promise<void> {
|
|
|
34
34
|
cliLaunchRequest = dispatchResult.request;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
startMainThreadMonitor("opentui");
|
|
37
|
+
const stopMainThreadMonitor = startMainThreadMonitor("opentui");
|
|
38
38
|
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
let host: Awaited<ReturnType<typeof createOpenTuiHost>> | null = null;
|
|
40
|
+
let exitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
41
|
+
const finishProcessExit = () => {
|
|
42
|
+
stopMainThreadMonitor();
|
|
43
|
+
if (exitTimer) return;
|
|
44
|
+
exitTimer = setTimeout(() => {
|
|
45
|
+
process.exit(process.exitCode ?? 0);
|
|
46
|
+
}, 0);
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
let dataDir = await getDataDir();
|
|
50
|
+
if (!dataDir) {
|
|
51
|
+
dataDir = join(process.env.HOME || "~", ".gloomberb");
|
|
52
|
+
}
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
if (!existsSync(dataDir)) {
|
|
55
|
+
mkdirSync(dataDir, { recursive: true });
|
|
56
|
+
}
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
const config = await measurePerfAsync("startup.opentui.init-data-dir", () => initDataDir(dataDir));
|
|
59
|
+
host = await measurePerfAsync("startup.opentui.create-host", () => createOpenTuiHost());
|
|
60
|
+
host.renderer.once("destroy", finishProcessExit);
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
host.render(
|
|
63
|
+
<UiHostProvider ui={openTuiUiHost} renderer={host.rendererHost} nativeRenderer={host.nativeRenderer}>
|
|
64
|
+
<OpenTuiInputHostProvider>
|
|
65
|
+
<ToastHostProvider host={openTuiToastHost}>
|
|
66
|
+
<OpenTuiDialogHostProvider
|
|
67
|
+
size="medium"
|
|
68
|
+
dialogOptions={{ style: { backgroundColor: colors.bg, borderColor: colors.borderFocused, borderStyle: "single", paddingX: 2, paddingY: 1 } }}
|
|
69
|
+
backdropColor="#000000"
|
|
70
|
+
backdropOpacity={0.8}
|
|
71
|
+
>
|
|
72
|
+
<App
|
|
73
|
+
config={config}
|
|
74
|
+
externalPlugins={externalPlugins}
|
|
75
|
+
cliLaunchRequest={cliLaunchRequest}
|
|
76
|
+
/>
|
|
77
|
+
</OpenTuiDialogHostProvider>
|
|
78
|
+
</ToastHostProvider>
|
|
79
|
+
</OpenTuiInputHostProvider>
|
|
80
|
+
</UiHostProvider>,
|
|
81
|
+
);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (exitTimer) clearTimeout(exitTimer);
|
|
84
|
+
stopMainThreadMonitor();
|
|
85
|
+
host?.renderer.off("destroy", finishProcessExit);
|
|
86
|
+
host?.destroy();
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
71
89
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMemo, type ReactNode } from "react";
|
|
2
|
+
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
2
3
|
import { testRender as openTuiTestRender } from "@opentui/react/test-utils";
|
|
3
4
|
import { createRoot as openTuiCreateRoot, useRenderer } from "@opentui/react";
|
|
4
5
|
import { UiHostProvider, type NativeRendererHost, type RendererHost } from "../../ui";
|
|
@@ -8,6 +9,22 @@ import { openTuiUiHost } from "./ui-host";
|
|
|
8
9
|
import { OpenTuiDialogHostProvider } from "./dialog-host";
|
|
9
10
|
import { openTuiToastHost } from "./toast-host";
|
|
10
11
|
|
|
12
|
+
export function TestDialogProvider({ children }: { children: ReactNode }) {
|
|
13
|
+
return (
|
|
14
|
+
<DialogProvider
|
|
15
|
+
dialogOptions={{
|
|
16
|
+
style: {
|
|
17
|
+
backgroundColor: "#000000",
|
|
18
|
+
borderColor: "#ffffff",
|
|
19
|
+
borderStyle: "single",
|
|
20
|
+
},
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</DialogProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
function createTestNativeRendererHost(renderer: any): NativeRendererHost {
|
|
12
29
|
if (typeof renderer.write !== "function") {
|
|
13
30
|
renderer.write = (data: string | Uint8Array) => {
|
|
@@ -3,6 +3,8 @@ import { createElement } from "react";
|
|
|
3
3
|
import "opentui-spinner/react";
|
|
4
4
|
import { TextAttributes, type UiHost, type TextProps } from "../../ui/host";
|
|
5
5
|
import { renderAsciiText } from "../../ui/ascii-font";
|
|
6
|
+
import { OpenTuiImageSurface } from "./image-surface";
|
|
7
|
+
import { OpenTuiChartSurface } from "./chart-surface";
|
|
6
8
|
|
|
7
9
|
function mapTextAttributes(appAttributes: number | undefined, props?: TextProps): number | undefined {
|
|
8
10
|
const flags = typeof appAttributes === "number" ? appAttributes : 0;
|
|
@@ -42,7 +44,9 @@ function mapTextContent(content: unknown): unknown {
|
|
|
42
44
|
|
|
43
45
|
export const openTuiUiHost: UiHost = {
|
|
44
46
|
kind: "opentui",
|
|
45
|
-
capabilities: {
|
|
47
|
+
capabilities: {
|
|
48
|
+
nativeCharts: true,
|
|
49
|
+
},
|
|
46
50
|
Box: ({ children, ...props }) => <box {...props}>{children}</box>,
|
|
47
51
|
Text: ({ children, ...props }) => {
|
|
48
52
|
const textProps = stripTextProps(props);
|
|
@@ -65,8 +69,8 @@ export const openTuiUiHost: UiHost = {
|
|
|
65
69
|
ScrollBox: ({ children, ...props }) => <scrollbox {...props}>{children}</scrollbox>,
|
|
66
70
|
Input: ({ children, ...props }) => createElement("input" as any, props, children),
|
|
67
71
|
Textarea: ({ children, ...props }) => createElement("textarea" as any, props, children),
|
|
68
|
-
ChartSurface:
|
|
69
|
-
ImageSurface:
|
|
72
|
+
ChartSurface: OpenTuiChartSurface,
|
|
73
|
+
ImageSurface: OpenTuiImageSurface,
|
|
70
74
|
SpinnerMark: ({ children, ...props }) => createElement("spinner" as any, props, children),
|
|
71
75
|
AsciiText: ({ text, font = "tiny", color, fg, bg, backgroundColor, selectable = false, ...props }) => {
|
|
72
76
|
const resolvedColor = color ?? fg;
|
|
@@ -134,10 +134,6 @@ function isIntradayRange(range: TimeRange): boolean {
|
|
|
134
134
|
return range === "1D" || range === "1W" || range === "1M" || range === "3M";
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
function hasMeaningfulFundamentals(data: TickerFinancials | null | undefined): boolean {
|
|
138
|
-
return !!data && Object.keys(data.fundamentals ?? {}).length > 0;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
137
|
function hasMeaningfulProfile(data: TickerFinancials | null | undefined): boolean {
|
|
142
138
|
return !!data && !!(
|
|
143
139
|
data.profile?.description
|
|
@@ -111,6 +111,38 @@ describe("resolveTickerForPane", () => {
|
|
|
111
111
|
)?.zIndex).toBe(21);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
test("raises the session-focused floating pane above tied saved z-order", () => {
|
|
115
|
+
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
116
|
+
config.layout = {
|
|
117
|
+
...config.layout,
|
|
118
|
+
dockRoot: null,
|
|
119
|
+
floating: [
|
|
120
|
+
{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 60, height: 22, zIndex: 50 },
|
|
121
|
+
{ instanceId: "chat:main", x: 8, y: 4, width: 48, height: 18, zIndex: 50 },
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
config.layouts = config.layouts.map((entry, index) => (
|
|
125
|
+
index === config.activeLayoutIndex ? { ...entry, layout: cloneLayout(config.layout) } : entry
|
|
126
|
+
));
|
|
127
|
+
const sessionSnapshot: AppSessionSnapshot = {
|
|
128
|
+
paneState: {},
|
|
129
|
+
focusedPaneId: "ticker-detail:main",
|
|
130
|
+
activePanel: "right",
|
|
131
|
+
statusBarVisible: true,
|
|
132
|
+
openPaneIds: ["ticker-detail:main", "chat:main"],
|
|
133
|
+
hydrationTargets: [],
|
|
134
|
+
exchangeCurrencies: ["USD"],
|
|
135
|
+
savedAt: Date.now(),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const state = createInitialState(config, sessionSnapshot);
|
|
139
|
+
const tickerEntry = state.config.layout.floating.find((entry) => entry.instanceId === "ticker-detail:main");
|
|
140
|
+
const chatEntry = state.config.layout.floating.find((entry) => entry.instanceId === "chat:main");
|
|
141
|
+
|
|
142
|
+
expect(tickerEntry?.zIndex).toBe(51);
|
|
143
|
+
expect((tickerEntry?.zIndex ?? 0) > (chatEntry?.zIndex ?? 0)).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
114
146
|
test("raises the desktop-snapshot focused floating pane during hydration", () => {
|
|
115
147
|
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
116
148
|
config.layout = {
|
|
@@ -455,6 +487,19 @@ describe("quote merging", () => {
|
|
|
455
487
|
});
|
|
456
488
|
|
|
457
489
|
describe("layout focus fallback", () => {
|
|
490
|
+
test("cycles backward from a stale focused pane to the last visible pane", () => {
|
|
491
|
+
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
492
|
+
const state = createInitialState(config);
|
|
493
|
+
state.focusedPaneId = "hidden:stale";
|
|
494
|
+
|
|
495
|
+
const next = appReducer(state, {
|
|
496
|
+
type: "FOCUS_PREV",
|
|
497
|
+
paneOrder: ["portfolio-list:main", "ticker-detail:main", "ticker-detail:nvda"],
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
expect(next.focusedPaneId).toBe("ticker-detail:nvda");
|
|
501
|
+
});
|
|
502
|
+
|
|
458
503
|
test("focuses the highest remaining floating pane when the focused floating pane closes", () => {
|
|
459
504
|
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
460
505
|
const backgroundPane = createPaneInstance("chat", {
|
|
@@ -183,6 +183,10 @@ export function usePaneInstanceId(): string {
|
|
|
183
183
|
return paneId;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
export function useOptionalPaneInstanceId(): string | null {
|
|
187
|
+
return useContext(PaneContext);
|
|
188
|
+
}
|
|
189
|
+
|
|
186
190
|
export function usePaneInstance(): PaneInstanceConfig | null {
|
|
187
191
|
const paneId = useContext(PaneContext);
|
|
188
192
|
return useAppSelector((state) => (paneId ? findPaneInstance(state.config.layout, paneId) ?? null : null));
|