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.
Files changed (156) hide show
  1. package/package.json +2 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +9 -6
  4. package/src/cli/registry.test.ts +0 -1
  5. package/src/components/chart/chart-controller.ts +0 -1
  6. package/src/components/chart/chart-data.ts +1 -31
  7. package/src/components/chart/chart-renderer.ts +70 -0
  8. package/src/components/chart/chart-resolution.test.ts +0 -36
  9. package/src/components/chart/chart-resolution.ts +0 -27
  10. package/src/components/chart/chart.test.ts +17 -45
  11. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  12. package/src/components/chart/comparison-chart-data.ts +1 -4
  13. package/src/components/chart/comparison-stock-chart.tsx +62 -77
  14. package/src/components/chart/native/kitty.test.ts +23 -0
  15. package/src/components/chart/native/surface-visibility.ts +48 -0
  16. package/src/components/chart/static-chart-surface.tsx +28 -4
  17. package/src/components/chart/stock-chart.test.ts +0 -16
  18. package/src/components/chart/stock-chart.tsx +52 -71
  19. package/src/components/chart/time-axis-label.tsx +43 -0
  20. package/src/components/command-bar/command-bar.test.tsx +71 -8
  21. package/src/components/command-bar/command-bar.tsx +388 -713
  22. package/src/components/command-bar/confirm-body.tsx +51 -0
  23. package/src/components/command-bar/list-model.ts +77 -0
  24. package/src/components/command-bar/view-model.ts +0 -19
  25. package/src/components/command-bar/workflow-ops.ts +4 -0
  26. package/src/components/data-table-stack-view.test.tsx +39 -4
  27. package/src/components/data-table-stack-view.tsx +11 -104
  28. package/src/components/data-table-view.tsx +43 -79
  29. package/src/components/feed-data-table-stack-view.tsx +10 -17
  30. package/src/components/feed-stack-controller.ts +39 -0
  31. package/src/components/index.ts +6 -2
  32. package/src/components/layout/floating-pane.tsx +30 -2
  33. package/src/components/layout/header.tsx +10 -7
  34. package/src/components/layout/pane-content.tsx +2 -0
  35. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  36. package/src/components/layout/pane-cycle-order.ts +32 -0
  37. package/src/components/layout/pane-footer.tsx +26 -4
  38. package/src/components/layout/pane-header.tsx +5 -5
  39. package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
  40. package/src/components/layout/shell.test.tsx +66 -22
  41. package/src/components/layout/shell.tsx +13 -6
  42. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  43. package/src/components/pane-settings-dialog.tsx +12 -4
  44. package/src/components/pane-template-wizard.test.tsx +3 -4
  45. package/src/components/speedometer-gauge.test.tsx +45 -0
  46. package/src/components/speedometer-gauge.tsx +558 -0
  47. package/src/components/table-view-shared.tsx +158 -0
  48. package/src/components/ticker-list-table-view.tsx +42 -72
  49. package/src/components/ui/confirm-dialog.tsx +66 -0
  50. package/src/components/ui/data-table.tsx +18 -10
  51. package/src/components/ui/index.ts +0 -1
  52. package/src/components/ui/page-stack-view.tsx +39 -10
  53. package/src/components/ui/remote-image.tsx +15 -40
  54. package/src/components/ui/table-layout.test.ts +16 -0
  55. package/src/components/ui/table-layout.ts +10 -3
  56. package/src/components/ui/ui.test.tsx +100 -20
  57. package/src/core/state/app-state.ts +6 -3
  58. package/src/market-data/coordinator.test.ts +1 -1
  59. package/src/market-data/coordinator.ts +0 -1
  60. package/src/news/aggregator.ts +0 -3
  61. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  62. package/src/plugins/builtin/ai/providers.ts +0 -17
  63. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  64. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  65. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  66. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  67. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  68. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  69. package/src/plugins/builtin/chat-controller.ts +75 -6
  70. package/src/plugins/builtin/chat.test.tsx +6 -6
  71. package/src/plugins/builtin/chat.tsx +29 -20
  72. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  73. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  74. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  75. package/src/plugins/builtin/correlation/index.tsx +1 -6
  76. package/src/plugins/builtin/debug.tsx +1 -3
  77. package/src/plugins/builtin/econ/index.tsx +1 -2
  78. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  79. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  80. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  81. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  82. package/src/plugins/builtin/help.test.tsx +2 -2
  83. package/src/plugins/builtin/help.tsx +2 -2
  84. package/src/plugins/builtin/holders/index.tsx +10 -15
  85. package/src/plugins/builtin/insider/index.tsx +22 -89
  86. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  87. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  88. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  89. package/src/plugins/builtin/news.tsx +4 -16
  90. package/src/plugins/builtin/notes.tsx +18 -55
  91. package/src/plugins/builtin/options.test.tsx +58 -2
  92. package/src/plugins/builtin/options.tsx +108 -49
  93. package/src/plugins/builtin/plugin-groups.ts +2 -0
  94. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  95. package/src/plugins/builtin/research/index.tsx +5 -21
  96. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  97. package/src/plugins/builtin/sec.tsx +13 -66
  98. package/src/plugins/builtin/sectors/index.tsx +1 -1
  99. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  100. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  101. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  102. package/src/plugins/builtin/ticker-detail/pane.tsx +36 -28
  103. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
  104. package/src/plugins/builtin/ticker-detail.test.tsx +102 -0
  105. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  106. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  107. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  108. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  109. package/src/plugins/ibkr/gateway-service.ts +1 -1
  110. package/src/plugins/ibkr/index.test.tsx +0 -2
  111. package/src/plugins/ibkr/trade-tab.test.tsx +56 -5
  112. package/src/plugins/ibkr/trade-tab.tsx +13 -11
  113. package/src/plugins/pane-manager.ts +0 -13
  114. package/src/plugins/plugin-runtime.test.tsx +6 -6
  115. package/src/plugins/plugin-runtime.tsx +13 -8
  116. package/src/plugins/prediction-markets/cli.ts +0 -1
  117. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  118. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  119. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  120. package/src/plugins/registry.ts +10 -39
  121. package/src/react/input.ts +11 -2
  122. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  123. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  124. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  125. package/src/renderers/electrobun/view/input-host.tsx +123 -12
  126. package/src/renderers/electrobun/view/key-event.ts +1 -1
  127. package/src/renderers/electrobun/view/styles.css +331 -0
  128. package/src/renderers/electrobun/view/ui-host.tsx +22 -3
  129. package/src/renderers/opentui/chart-surface.tsx +179 -0
  130. package/src/renderers/opentui/host.tsx +6 -0
  131. package/src/renderers/opentui/image-loader.test.ts +25 -0
  132. package/src/renderers/opentui/image-loader.ts +105 -0
  133. package/src/renderers/opentui/image-surface.tsx +266 -0
  134. package/src/renderers/opentui/input-host.tsx +45 -6
  135. package/src/renderers/opentui/start.tsx +48 -30
  136. package/src/renderers/opentui/test-utils.tsx +17 -0
  137. package/src/renderers/opentui/ui-host.tsx +7 -3
  138. package/src/sources/provider-router.ts +0 -4
  139. package/src/state/app-context.test.ts +45 -0
  140. package/src/state/app-context.tsx +4 -0
  141. package/src/state/pane-scroll-registry.tsx +254 -0
  142. package/src/state/use-inline-tickers.test.tsx +73 -0
  143. package/src/state/use-inline-tickers.ts +7 -1
  144. package/src/test-support/plugin-runtime.ts +3 -2
  145. package/src/theme/colors.ts +12 -3
  146. package/src/theme/themes.test.ts +23 -1
  147. package/src/theme/themes.ts +115 -0
  148. package/src/types/broker.ts +1 -1
  149. package/src/types/data-provider.ts +0 -1
  150. package/src/types/plugin.ts +0 -2
  151. package/src/ui/host.tsx +4 -0
  152. package/src/ui/index.tsx +9 -1
  153. package/src/utils/back-navigation.ts +24 -1
  154. package/src/version.ts +1 -1
  155. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  156. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -0,0 +1,331 @@
1
+ @keyframes gloom-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
2
+ html, body, #root { width: 100%; height: 100%; min-height: 100%; overflow: hidden; background: #000; }
3
+ body {
4
+ --cell-w: 8px;
5
+ --cell-h: 18px;
6
+ margin: 0;
7
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
8
+ font-size: 12px;
9
+ line-height: var(--cell-h);
10
+ color: #d8dde3;
11
+ cursor: default;
12
+ user-select: none;
13
+ color-scheme: dark;
14
+ -webkit-font-smoothing: antialiased;
15
+ text-rendering: geometricPrecision;
16
+ }
17
+ * { box-sizing: border-box; }
18
+ #root > * { height: 100%; min-height: 100%; }
19
+ input, textarea, button { font: inherit; }
20
+ input, textarea { cursor: text; user-select: text; }
21
+ button, [data-gloom-interactive="true"] { cursor: pointer; }
22
+ [data-gloom-role="pane-window"] {
23
+ background-clip: padding-box;
24
+ isolation: isolate;
25
+ }
26
+ [data-gloom-role="detached-pane-window"] {
27
+ border: 0;
28
+ box-shadow: none;
29
+ background-clip: padding-box;
30
+ }
31
+ [data-gloom-role="app-header"][data-titlebar-overlay="true"],
32
+ .electrobun-webkit-app-region-drag {
33
+ cursor: default;
34
+ -webkit-app-region: drag;
35
+ app-region: drag;
36
+ -webkit-user-select: none;
37
+ user-select: none;
38
+ }
39
+ .electrobun-webkit-app-region-no-drag {
40
+ -webkit-app-region: no-drag;
41
+ app-region: no-drag;
42
+ }
43
+ [data-titlebar-overlay="true"] {
44
+ min-height: __TITLEBAR_OVERLAY_HEIGHT_PX__px;
45
+ max-height: __TITLEBAR_OVERLAY_HEIGHT_PX__px;
46
+ align-items: center;
47
+ }
48
+ [data-titlebar-overlay="true"] * {
49
+ cursor: inherit;
50
+ }
51
+ [data-titlebar-overlay="true"] button,
52
+ [data-titlebar-overlay="true"] input,
53
+ [data-titlebar-overlay="true"] textarea,
54
+ [data-titlebar-overlay="true"] a,
55
+ [data-titlebar-overlay="true"] .electrobun-webkit-app-region-no-drag,
56
+ [data-titlebar-overlay="true"] [data-gloom-interactive="true"] {
57
+ -webkit-app-region: no-drag;
58
+ app-region: no-drag;
59
+ }
60
+ [data-gloom-role="pane-window"][data-floating="true"] {
61
+ border: 0;
62
+ border-radius: 6px;
63
+ overflow: hidden;
64
+ box-shadow: 0 18px 38px rgba(0, 0, 0, .46);
65
+ }
66
+ [data-gloom-role="pane-window"][data-floating="true"]::after {
67
+ content: "";
68
+ position: absolute;
69
+ inset: 0;
70
+ border: 1px solid var(--pane-border-color, #3a4148);
71
+ border-radius: inherit;
72
+ pointer-events: none;
73
+ z-index: 100;
74
+ }
75
+ [data-gloom-role="pane-window"][data-floating="true"][data-focused="true"] {
76
+ box-shadow: 0 18px 40px rgba(0, 0, 0, .5);
77
+ }
78
+ [data-gloom-role="pane-window"][data-floating="false"] {
79
+ border: 0;
80
+ border-radius: 0;
81
+ }
82
+ [data-gloom-role="pane-window"][data-floating="false"][data-focused="true"] {
83
+ box-shadow: inset 0 0 0 1px var(--pane-border-color, #54c99f);
84
+ }
85
+ [data-gloom-role="pane-header"] {
86
+ align-items: center;
87
+ padding-inline: 7px;
88
+ cursor: grab;
89
+ -webkit-user-select: none;
90
+ user-select: none;
91
+ }
92
+ [data-gloom-role="pane-header"] * {
93
+ cursor: inherit;
94
+ }
95
+ [data-gloom-role="pane-action"],
96
+ [data-gloom-role="pane-close"] {
97
+ cursor: pointer;
98
+ }
99
+ body.gloom-dragging [data-gloom-role="pane-header"] {
100
+ cursor: grabbing;
101
+ }
102
+ body.gloom-dragging {
103
+ cursor: grabbing;
104
+ }
105
+ [data-gloom-role="pane-title"] {
106
+ overflow: hidden;
107
+ text-overflow: ellipsis;
108
+ }
109
+ [data-gloom-role="chat-message"]:hover [data-gloom-role="chat-message-row"][data-selected="false"] {
110
+ background-color: var(--chat-hover-bg);
111
+ }
112
+ [data-gloom-role="chat-message-reply-action"] {
113
+ opacity: 0;
114
+ pointer-events: none;
115
+ }
116
+ [data-gloom-role="chat-message"]:hover [data-gloom-role="chat-message-reply-action"],
117
+ [data-gloom-role="chat-message"][data-selected="true"] [data-gloom-role="chat-message-reply-action"] {
118
+ opacity: 1;
119
+ pointer-events: auto;
120
+ }
121
+ [data-gloom-role="status-bar"] {
122
+ min-height: var(--cell-h);
123
+ max-height: var(--cell-h);
124
+ align-items: center;
125
+ border-top: 1px solid color-mix(in srgb, #3a4148 72%, transparent);
126
+ font-size: 11.5px;
127
+ }
128
+ [data-gloom-role="status-bar"] span {
129
+ line-height: var(--cell-h);
130
+ }
131
+ [data-gloom-role="pane-footer"] {
132
+ min-height: var(--cell-h);
133
+ max-height: var(--cell-h);
134
+ align-items: center;
135
+ border-top: 1px solid color-mix(in srgb, var(--pane-footer-border-color, #3a4148) 72%, transparent);
136
+ font-size: 11.5px;
137
+ overflow: hidden;
138
+ }
139
+ [data-gloom-role="pane-footer"][data-empty="true"] {
140
+ border-top-color: transparent;
141
+ }
142
+ [data-gloom-role="pane-footer"] span,
143
+ [data-gloom-role="pane-hint"] span {
144
+ line-height: var(--cell-h);
145
+ }
146
+ [data-gloom-role="pane-hint"] {
147
+ cursor: pointer;
148
+ border-radius: 4px;
149
+ }
150
+ [data-gloom-role="pane-hint"]:hover {
151
+ background: rgba(255,255,255,.08);
152
+ }
153
+ [data-gloom-role="pane-action"],
154
+ [data-gloom-role="pane-close"] {
155
+ border-radius: 4px;
156
+ padding-inline: 2px;
157
+ }
158
+ [data-gloom-role="pane-action"]:hover,
159
+ [data-gloom-role="pane-close"]:hover {
160
+ background: rgba(255,255,255,.08);
161
+ }
162
+ [data-gloom-role="resize-handle"] {
163
+ cursor: nwse-resize;
164
+ }
165
+ [data-gloom-role="chart-surface"] {
166
+ cursor: crosshair;
167
+ }
168
+ [data-gloom-role="tab-list"] {
169
+ scrollbar-width: none;
170
+ -webkit-overflow-scrolling: touch;
171
+ }
172
+ [data-gloom-role="tab-list"]::-webkit-scrollbar {
173
+ width: 0;
174
+ height: 0;
175
+ display: none;
176
+ }
177
+ [data-gloom-role="tab-button"] {
178
+ all: unset;
179
+ box-sizing: border-box;
180
+ }
181
+ [data-gloom-role="tab-button"]:focus-visible {
182
+ outline: 1px solid var(--tab-hover-underline);
183
+ outline-offset: -1px;
184
+ }
185
+ [data-gloom-role="tab-close"] {
186
+ cursor: pointer;
187
+ }
188
+ [data-gloom-role="tab-close"]:hover {
189
+ background: rgba(255,255,255,.1);
190
+ }
191
+ [data-gloom-scrollbar-x],
192
+ [data-gloom-scrollbar-y] {
193
+ scrollbar-color: transparent transparent;
194
+ scrollbar-width: thin;
195
+ }
196
+ [data-gloom-scrollbar-active="true"] {
197
+ scrollbar-color: rgba(216,221,227,.26) transparent;
198
+ }
199
+ [data-gloom-scrollbar-x]::-webkit-scrollbar,
200
+ [data-gloom-scrollbar-y]::-webkit-scrollbar {
201
+ width: 8px;
202
+ height: 8px;
203
+ background: transparent;
204
+ }
205
+ [data-gloom-scrollbar-x]::-webkit-scrollbar-track,
206
+ [data-gloom-scrollbar-y]::-webkit-scrollbar-track {
207
+ background: transparent;
208
+ }
209
+ [data-gloom-scrollbar-x]::-webkit-scrollbar-thumb,
210
+ [data-gloom-scrollbar-y]::-webkit-scrollbar-thumb {
211
+ min-width: 24px;
212
+ min-height: 24px;
213
+ border: 2px solid transparent;
214
+ border-radius: 999px;
215
+ background: transparent;
216
+ background-clip: content-box;
217
+ }
218
+ [data-gloom-scrollbar-x]::-webkit-scrollbar-thumb:hover,
219
+ [data-gloom-scrollbar-y]::-webkit-scrollbar-thumb:hover {
220
+ background: transparent;
221
+ background-clip: content-box;
222
+ }
223
+ [data-gloom-scrollbar-active="true"]::-webkit-scrollbar-thumb,
224
+ [data-gloom-scrollbar-active="true"]::-webkit-scrollbar-thumb:hover {
225
+ background: rgba(216,221,227,.24);
226
+ background-clip: content-box;
227
+ }
228
+ [data-gloom-scrollbar-x]::-webkit-scrollbar-corner,
229
+ [data-gloom-scrollbar-y]::-webkit-scrollbar-corner {
230
+ background: transparent;
231
+ }
232
+ [data-gloom-scrollbar-x="hidden"]:not([data-gloom-scrollbar-y="visible"]),
233
+ [data-gloom-scrollbar-y="hidden"]:not([data-gloom-scrollbar-x="visible"]) {
234
+ scrollbar-width: none;
235
+ }
236
+ [data-gloom-scrollbar-x="hidden"]::-webkit-scrollbar:horizontal {
237
+ height: 0;
238
+ display: none;
239
+ }
240
+ [data-gloom-scrollbar-y="hidden"]::-webkit-scrollbar:vertical {
241
+ width: 0;
242
+ display: none;
243
+ }
244
+ [data-gloom-role="resize-handle"]::after {
245
+ content: "";
246
+ position: absolute;
247
+ right: 4px;
248
+ bottom: 4px;
249
+ width: 9px;
250
+ height: 9px;
251
+ border-right: 1px solid currentColor;
252
+ border-bottom: 1px solid currentColor;
253
+ opacity: .7;
254
+ }
255
+ [data-gloom-role="dock-divider"] {
256
+ background: transparent !important;
257
+ }
258
+ [data-gloom-role="dock-divider"]::before {
259
+ content: "";
260
+ position: absolute;
261
+ background: var(--divider-color);
262
+ opacity: .9;
263
+ }
264
+ [data-gloom-role="dock-divider"][data-axis="horizontal"] {
265
+ cursor: col-resize;
266
+ }
267
+ [data-gloom-role="dock-divider"][data-axis="horizontal"]::before {
268
+ top: 0;
269
+ bottom: 0;
270
+ left: 50%;
271
+ width: 1px;
272
+ }
273
+ [data-gloom-role="dock-divider"][data-axis="vertical"] {
274
+ cursor: row-resize;
275
+ }
276
+ [data-gloom-role="dock-divider"][data-axis="vertical"]::before {
277
+ left: 0;
278
+ right: 0;
279
+ top: 50%;
280
+ height: 1px;
281
+ }
282
+ .gloom-loading, .gloom-fatal { padding: 24px; color: #d8dde3; }
283
+ .gloom-fatal pre { white-space: pre-wrap; color: #ffb4a8; }
284
+ .gloom-toast-viewport {
285
+ position: fixed;
286
+ right: 16px;
287
+ bottom: 16px;
288
+ display: flex;
289
+ flex-direction: column;
290
+ gap: 8px;
291
+ z-index: 10000;
292
+ }
293
+ .gloom-toast {
294
+ min-width: 260px;
295
+ max-width: 420px;
296
+ padding: 10px 12px;
297
+ border: 1px solid #3a4148;
298
+ background: #151b20;
299
+ color: #d8dde3;
300
+ box-shadow: 0 12px 24px rgba(0,0,0,.28);
301
+ }
302
+ .gloom-toast-success { border-color: #54c99f; }
303
+ .gloom-toast-error { border-color: #ff6b5f; }
304
+ .gloom-toast-action {
305
+ margin-top: 8px;
306
+ color: #101417;
307
+ background: #d8dde3;
308
+ border: 0;
309
+ padding: 4px 8px;
310
+ border-radius: 4px;
311
+ }
312
+ .gloom-dialog-backdrop {
313
+ position: fixed;
314
+ inset: 0;
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ background: rgba(0,0,0,.64);
319
+ z-index: 9999;
320
+ }
321
+ .gloom-dialog {
322
+ max-width: min(920px, calc(100vw - 48px));
323
+ max-height: calc(100vh - 48px);
324
+ overflow: auto;
325
+ border: 1px solid #3a4148;
326
+ border-radius: 6px;
327
+ background: #101417;
328
+ color: #d8dde3;
329
+ padding: 0;
330
+ box-shadow: 0 18px 48px rgba(0,0,0,.46), inset 0 1px 0 rgba(255,255,255,.05);
331
+ }
@@ -176,7 +176,7 @@ function cleanDomProps(props: Record<string, unknown>): Record<string, unknown>
176
176
  "margin", "marginX", "marginY", "marginLeft", "marginRight", "marginTop", "marginBottom",
177
177
  "bold", "underline", "inverse", "dim", "italic", "strikethrough", "attributes", "content", "position", "left", "right", "top", "bottom",
178
178
  "zIndex", "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight", "gap",
179
- "border", "borderStyle", "borderColor", "overflow", "selectable", "name", "color",
179
+ "border", "borderStyle", "borderColor", "overflow", "selectable", "visible", "name", "color",
180
180
  "focused", "focusedBackgroundColor", "textColor", "focusedTextColor", "placeholderColor",
181
181
  "cursorColor", "selectionBg", "selectionFg", "showCursor", "keyBindings", "wrapText", "wrapMode",
182
182
  "initialValue", "value", "onInput", "onChange", "onSubmit", "onCursorChange", "onMouse",
@@ -445,7 +445,11 @@ const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { children?:
445
445
  onMouseUp={(event) => callMouseHandler(propsRef.current.onMouseUp, event, "up")}
446
446
  onMouseOut={handleMouseOut}
447
447
  onWheel={typeof props.onMouseScroll === "function" ? handleWheel : undefined}
448
- style={{ ...commonStyle(props), ...(props.style as CSSProperties | undefined) }}
448
+ style={{
449
+ ...commonStyle(props),
450
+ ...(props.style as CSSProperties | undefined),
451
+ ...(props.visible === false ? { display: "none" } : undefined),
452
+ }}
449
453
  >
450
454
  {children as ReactNode}
451
455
  </div>
@@ -956,6 +960,15 @@ const WebScrollBox = forwardRef<ScrollBoxRenderable, Record<string, unknown> & {
956
960
  height: toCellY(element?.clientHeight ?? 0),
957
961
  };
958
962
  },
963
+ getBoundingClientRect() {
964
+ const rect = getElement()?.getBoundingClientRect();
965
+ return {
966
+ x: rect?.x ?? 0,
967
+ y: rect?.y ?? 0,
968
+ width: rect?.width ?? 0,
969
+ height: rect?.height ?? 0,
970
+ };
971
+ },
959
972
  horizontalScrollBar,
960
973
  verticalScrollBar,
961
974
  scrollTo(target: number | { x?: number; y?: number }, y?: number) {
@@ -1008,7 +1021,13 @@ const WebScrollBox = forwardRef<ScrollBoxRenderable, Record<string, unknown> & {
1008
1021
  onMouseOut={(event) => callMouseHandler(props.onMouseOut, event, "out")}
1009
1022
  onScroll={scrollable ? handleScroll : undefined}
1010
1023
  onWheel={scrollable ? handleWheel : undefined}
1011
- style={{ ...commonStyle(props), overflowX, overflowY, ...(props.style as CSSProperties | undefined) }}
1024
+ style={{
1025
+ ...commonStyle(props),
1026
+ overflowX,
1027
+ overflowY,
1028
+ ...(props.style as CSSProperties | undefined),
1029
+ ...(props.visible === false ? { display: "none" } : undefined),
1030
+ }}
1012
1031
  >
1013
1032
  {children as ReactNode}
1014
1033
  </div>
@@ -0,0 +1,179 @@
1
+ import { createElement, forwardRef, useCallback, useEffect, useMemo, useRef, useState, type ForwardedRef, type ReactNode } 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 ChartSurfaceProps } from "../../ui";
17
+
18
+ interface NativeRenderableNode extends BoxRenderable, NativeSurfaceRenderableNode {
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ parent: NativeRenderableNode | null;
24
+ onLifecyclePass: (() => void) | null;
25
+ }
26
+
27
+ interface SurfaceTarget {
28
+ rect: CellRect;
29
+ visibleRect: CellRect | null;
30
+ bitmapKey: string;
31
+ }
32
+
33
+ let nextChartSurfaceId = 1;
34
+
35
+ function assignRef(ref: ForwardedRef<unknown>, value: unknown) {
36
+ if (typeof ref === "function") {
37
+ ref(value);
38
+ return;
39
+ }
40
+ if (ref) {
41
+ (ref as { current: unknown }).current = value;
42
+ }
43
+ }
44
+
45
+ function sameRect(left: CellRect | null, right: CellRect | null): boolean {
46
+ if (left === right) return true;
47
+ if (!left || !right) return false;
48
+ return left.x === right.x
49
+ && left.y === right.y
50
+ && left.width === right.width
51
+ && left.height === right.height;
52
+ }
53
+
54
+ function sameTarget(left: SurfaceTarget | null, right: SurfaceTarget | null): boolean {
55
+ if (left === right) return true;
56
+ if (!left || !right) return false;
57
+ return left.bitmapKey === right.bitmapKey
58
+ && sameRect(left.rect, right.rect)
59
+ && sameRect(left.visibleRect, right.visibleRect);
60
+ }
61
+
62
+ function hashBitmap(bitmap: NativeChartBitmap): string {
63
+ let hash = 0x811c9dc5;
64
+ for (let index = 0; index < bitmap.pixels.length; index += 1) {
65
+ hash ^= bitmap.pixels[index]!;
66
+ hash = Math.imul(hash, 0x01000193);
67
+ }
68
+ return (hash >>> 0).toString(16);
69
+ }
70
+
71
+ function bitmapKey(bitmap: NativeChartBitmap): string {
72
+ return `${bitmap.width}x${bitmap.height}:${hashBitmap(bitmap)}`;
73
+ }
74
+
75
+ export const OpenTuiChartSurface = forwardRef<unknown, ChartSurfaceProps>(function OpenTuiChartSurface(
76
+ { children, bitmap, bitmaps, crosshair: _crosshair, ...props },
77
+ forwardedRef,
78
+ ) {
79
+ const renderer = useNativeRenderer();
80
+ const paneId = useOptionalPaneInstanceId();
81
+ const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
82
+ const surfaceId = useRef(`opentui-chart:${nextChartSurfaceId++}`).current;
83
+ const renderableRef = useRef<NativeRenderableNode | null>(null);
84
+ const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
85
+ const [target, setTarget] = useState<SurfaceTarget | null>(null);
86
+ const nativeBitmap = (bitmaps?.[0] ?? bitmap ?? null) as NativeChartBitmap | null;
87
+ const nativeBitmapKey = useMemo(() => (nativeBitmap ? bitmapKey(nativeBitmap) : null), [nativeBitmap]);
88
+
89
+ const setRenderableRef = useCallback((node: unknown) => {
90
+ renderableRef.current = node as NativeRenderableNode | null;
91
+ assignRef(forwardedRef, node);
92
+ }, [forwardedRef]);
93
+
94
+ useEffect(() => {
95
+ let cancelled = false;
96
+ setKittySupport(getCachedKittySupport(renderer));
97
+ ensureKittySupport(renderer).then((supported) => {
98
+ if (!cancelled) setKittySupport(supported);
99
+ });
100
+ return () => {
101
+ cancelled = true;
102
+ };
103
+ }, [renderer]);
104
+
105
+ useEffect(() => {
106
+ const renderable = renderableRef.current;
107
+ if (!renderable || !nativeBitmap || !nativeBitmapKey || kittySupport !== true) {
108
+ setTarget(null);
109
+ return;
110
+ }
111
+
112
+ let mountTimer: Timer | null = null;
113
+ const previousLifecyclePass = renderable.onLifecyclePass;
114
+ const syncTarget = () => {
115
+ const rect = getRenderableCellRect(renderable);
116
+ if (!renderer.resolution || renderer.terminalWidth <= 0 || renderer.terminalHeight <= 0) {
117
+ setTarget((current) => (current === null ? current : null));
118
+ return;
119
+ }
120
+
121
+ const visibleRect = resolveNativeSurfaceVisibleRect(renderable, renderer.terminalWidth, renderer.terminalHeight);
122
+ const clippedVisibleRect = visibleRect ? intersectCellRects(rect, visibleRect) : null;
123
+ const expectedSize = computeBitmapSize(rect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
124
+ const nextTarget: SurfaceTarget = {
125
+ rect,
126
+ visibleRect: clippedVisibleRect,
127
+ bitmapKey: `${nativeBitmapKey}:${expectedSize.pixelWidth}x${expectedSize.pixelHeight}`,
128
+ };
129
+ setTarget((current) => (sameTarget(current, nextTarget) ? current : nextTarget));
130
+ };
131
+ const lifecyclePass = () => {
132
+ previousLifecyclePass?.();
133
+ syncTarget();
134
+ };
135
+
136
+ renderable.onLifecyclePass = lifecyclePass;
137
+ renderer.registerLifecyclePass(renderable);
138
+ syncTarget();
139
+ mountTimer = setTimeout(() => {
140
+ syncTarget();
141
+ renderer.requestRender();
142
+ }, 0);
143
+
144
+ return () => {
145
+ if (mountTimer) clearTimeout(mountTimer);
146
+ if (renderable.onLifecyclePass === lifecyclePass) {
147
+ renderable.onLifecyclePass = previousLifecyclePass;
148
+ }
149
+ renderer.unregisterLifecyclePass(renderable);
150
+ setTarget(null);
151
+ };
152
+ }, [kittySupport, nativeBitmap, nativeBitmapKey, renderer]);
153
+
154
+ useEffect(() => {
155
+ return () => {
156
+ nativeSurfaceManager.removeSurface(surfaceId);
157
+ };
158
+ }, [nativeSurfaceManager, surfaceId]);
159
+
160
+ useEffect(() => {
161
+ if (kittySupport !== true || !target?.visibleRect || !nativeBitmap) {
162
+ nativeSurfaceManager.removeSurface(surfaceId);
163
+ return;
164
+ }
165
+
166
+ nativeSurfaceManager.upsertSurface({
167
+ id: surfaceId,
168
+ paneId: paneId ?? "__global__",
169
+ rect: target.rect,
170
+ visibleRect: target.visibleRect,
171
+ bitmap: nativeBitmap,
172
+ bitmapKey: target.bitmapKey,
173
+ });
174
+ renderer.requestRender();
175
+ }, [kittySupport, nativeBitmap, nativeSurfaceManager, paneId, renderer, surfaceId, target]);
176
+
177
+ const showFallback = kittySupport !== true || !target || !nativeBitmap;
178
+ return createElement("box" as any, { ...props, ref: setRenderableRef }, showFallback ? children as ReactNode : null);
179
+ });
@@ -38,6 +38,12 @@ export function toKeyEventLike(event: {
38
38
  alt: event.alt ?? false,
39
39
  meta: event.meta ?? event.super ?? false,
40
40
  super: event.super ?? event.meta ?? false,
41
+ get defaultPrevented() {
42
+ return event.defaultPrevented ?? false;
43
+ },
44
+ get propagationStopped() {
45
+ return event.propagationStopped ?? false;
46
+ },
41
47
  preventDefault: () => event.preventDefault?.(),
42
48
  stopPropagation: () => event.stopPropagation?.(),
43
49
  };
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Jimp } from "jimp";
3
+ import { resizeImageToBitmap, type JimpImageLike } from "./image-loader";
4
+
5
+ describe("resizeImageToBitmap", () => {
6
+ test("preserves aspect ratio for contained images", () => {
7
+ const source = new Jimp({ width: 4, height: 2, color: 0xff0000ff }) as unknown as JimpImageLike;
8
+ const bitmap = resizeImageToBitmap(source, { width: 4, height: 4, objectFit: "contain" });
9
+
10
+ expect(bitmap.width).toBe(4);
11
+ expect(bitmap.height).toBe(4);
12
+ expect([...bitmap.pixels.slice(0, 4)]).toEqual([0, 0, 0, 0]);
13
+ expect([...bitmap.pixels.slice(4 * 4, 4 * 4 + 4)]).toEqual([255, 0, 0, 255]);
14
+ });
15
+
16
+ test("fills the target for covered images", () => {
17
+ const source = new Jimp({ width: 4, height: 2, color: 0xff0000ff }) as unknown as JimpImageLike;
18
+ const bitmap = resizeImageToBitmap(source, { width: 4, height: 4, objectFit: "cover" });
19
+
20
+ expect(bitmap.width).toBe(4);
21
+ expect(bitmap.height).toBe(4);
22
+ expect([...bitmap.pixels.slice(0, 4)]).toEqual([255, 0, 0, 255]);
23
+ expect([...bitmap.pixels.slice((bitmap.pixels.length - 4), bitmap.pixels.length)]).toEqual([255, 0, 0, 255]);
24
+ });
25
+ });
@@ -0,0 +1,105 @@
1
+ import { Jimp } from "jimp";
2
+ import type { NativeChartBitmap } from "../../components/chart/native/chart-rasterizer";
3
+
4
+ type ImageObjectFit = "contain" | "cover";
5
+
6
+ export interface JimpImageLike {
7
+ bitmap: {
8
+ width: number;
9
+ height: number;
10
+ data: Uint8Array;
11
+ };
12
+ clone(): JimpImageLike;
13
+ contain(options: { w: number; h: number }): JimpImageLike;
14
+ cover(options: { w: number; h: number }): JimpImageLike;
15
+ }
16
+
17
+ interface ImageBitmapOptions {
18
+ width: number;
19
+ height: number;
20
+ objectFit: ImageObjectFit;
21
+ }
22
+
23
+ const MAX_REMOTE_IMAGE_BYTES = 12 * 1024 * 1024;
24
+ const IMAGE_CACHE_LIMIT = 48;
25
+
26
+ const sourceImageCache = new Map<string, Promise<JimpImageLike>>();
27
+ const bitmapCache = new Map<string, Promise<NativeChartBitmap>>();
28
+
29
+ function remember<K, V>(cache: Map<K, V>, key: K, value: V) {
30
+ cache.set(key, value);
31
+ while (cache.size > IMAGE_CACHE_LIMIT) {
32
+ const firstKey = cache.keys().next().value as K | undefined;
33
+ if (firstKey === undefined) break;
34
+ cache.delete(firstKey);
35
+ }
36
+ }
37
+
38
+ async function fetchImageBytes(src: string): Promise<Buffer> {
39
+ const response = await fetch(src, {
40
+ headers: {
41
+ "User-Agent": "Gloomberb/0.6 image renderer",
42
+ },
43
+ });
44
+ if (!response.ok) {
45
+ throw new Error(`Image request failed with HTTP ${response.status}`);
46
+ }
47
+
48
+ const data = await response.arrayBuffer();
49
+ if (data.byteLength > MAX_REMOTE_IMAGE_BYTES) {
50
+ throw new Error("Image is too large to render inline");
51
+ }
52
+
53
+ return Buffer.from(data);
54
+ }
55
+
56
+ function loadSourceImage(src: string): Promise<JimpImageLike> {
57
+ const cached = sourceImageCache.get(src);
58
+ if (cached) return cached;
59
+
60
+ const promise = fetchImageBytes(src)
61
+ .then((bytes) => Jimp.read(bytes) as Promise<JimpImageLike>)
62
+ .catch((error) => {
63
+ sourceImageCache.delete(src);
64
+ throw error;
65
+ });
66
+
67
+ remember(sourceImageCache, src, promise);
68
+ return promise;
69
+ }
70
+
71
+ export function resizeImageToBitmap(source: JimpImageLike, options: ImageBitmapOptions): NativeChartBitmap {
72
+ const width = Math.max(1, Math.round(options.width));
73
+ const height = Math.max(1, Math.round(options.height));
74
+ const image = source.clone();
75
+
76
+ if (options.objectFit === "cover") {
77
+ image.cover({ w: width, h: height });
78
+ } else {
79
+ image.contain({ w: width, h: height });
80
+ }
81
+
82
+ return {
83
+ width: image.bitmap.width,
84
+ height: image.bitmap.height,
85
+ pixels: new Uint8Array(image.bitmap.data),
86
+ };
87
+ }
88
+
89
+ export function loadOpenTuiImageBitmap(src: string, options: ImageBitmapOptions): Promise<NativeChartBitmap> {
90
+ const width = Math.max(1, Math.round(options.width));
91
+ const height = Math.max(1, Math.round(options.height));
92
+ const key = `${src}\n${options.objectFit}\n${width}x${height}`;
93
+ const cached = bitmapCache.get(key);
94
+ if (cached) return cached;
95
+
96
+ const promise = loadSourceImage(src)
97
+ .then((image) => resizeImageToBitmap(image, { width, height, objectFit: options.objectFit }))
98
+ .catch((error) => {
99
+ bitmapCache.delete(key);
100
+ throw error;
101
+ });
102
+
103
+ remember(bitmapCache, key, promise);
104
+ return promise;
105
+ }