gloomberb 0.9.4 → 0.9.6

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 (138) hide show
  1. package/README.md +49 -8
  2. package/package.json +4 -3
  3. package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
  4. package/patches/react-reconciler@0.33.0.patch +10 -0
  5. package/src/app/global-shortcuts.test.tsx +20 -1
  6. package/src/app/global-shortcuts.ts +2 -0
  7. package/src/app/pane-runtime/index.tsx +11 -4
  8. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
  9. package/src/app.tsx +41 -17
  10. package/src/cli/commands/remote.ts +206 -0
  11. package/src/cli/entry.ts +8 -1
  12. package/src/cli/index.ts +2 -0
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/components/chart/core/keyboard.ts +10 -1
  15. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  16. package/src/components/chart/native/bitmap-support.ts +55 -0
  17. package/src/components/chart/native/kitty/index.test.ts +47 -0
  18. package/src/components/chart/native/surface/visibility.ts +8 -1
  19. package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
  20. package/src/components/chart/static/chart/bitmap.ts +16 -9
  21. package/src/components/chart/static/chart/surface.test.tsx +59 -4
  22. package/src/components/chart/static/chart/surface.tsx +4 -0
  23. package/src/components/chart/stock-chart/index.test.ts +7 -0
  24. package/src/components/command-bar/list/view.tsx +48 -1
  25. package/src/components/command-bar/multi-select-picker.tsx +7 -0
  26. package/src/components/command-bar/routing/navigation-state.ts +10 -1
  27. package/src/components/command-bar/surface/index.test.tsx +14 -0
  28. package/src/components/command-bar/theme-picker.tsx +91 -79
  29. package/src/components/command-bar/workflow/field-row.tsx +30 -5
  30. package/src/components/data-table/view.tsx +0 -12
  31. package/src/components/index.ts +7 -0
  32. package/src/components/layout/floating-pane.tsx +3 -3
  33. package/src/components/layout/pane/footer/index.tsx +23 -0
  34. package/src/components/layout/pane/header.tsx +5 -0
  35. package/src/components/layout/pane/index.tsx +3 -3
  36. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  37. package/src/components/layout/shell/cursor-occlusion.ts +9 -1
  38. package/src/components/layout/shell/index.test.tsx +2 -0
  39. package/src/components/layout/shell/index.tsx +16 -4
  40. package/src/components/layout/shell/pane/actions.ts +21 -7
  41. package/src/components/layout/shell/pane/layers.tsx +2 -2
  42. package/src/components/layout/status-bar.tsx +5 -5
  43. package/src/components/metric-treemap/index.tsx +317 -0
  44. package/src/components/metric-treemap/layout.test.ts +77 -0
  45. package/src/components/metric-treemap/layout.ts +476 -0
  46. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  47. package/src/components/price-performance.tsx +1 -1
  48. package/src/components/price-sparkline/view.tsx +15 -14
  49. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  50. package/src/components/table-view-shared.tsx +0 -10
  51. package/src/components/ticker/list-table-view.tsx +1 -7
  52. package/src/components/ticker/list-table.test.tsx +0 -4
  53. package/src/components/toggle-list.tsx +10 -0
  54. package/src/components/ui/button.tsx +12 -0
  55. package/src/components/ui/data-table/index.tsx +51 -0
  56. package/src/components/ui/data-table/opentui/index.tsx +4 -10
  57. package/src/components/ui/data-table/types.ts +6 -4
  58. package/src/components/ui/fields.tsx +18 -0
  59. package/src/components/ui/list-view.tsx +84 -4
  60. package/src/components/ui/message-composer.tsx +22 -0
  61. package/src/components/ui/tabs.tsx +46 -2
  62. package/src/components/ui/ui.test.tsx +0 -9
  63. package/src/core/state/app/layout-reducer.ts +7 -1
  64. package/src/core/state/app/layout.ts +6 -0
  65. package/src/core/state/app/state.ts +23 -1
  66. package/src/core/state/app/types.ts +4 -2
  67. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  68. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  69. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  70. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  71. package/src/plugins/builtin/chat/channels.ts +5 -18
  72. package/src/plugins/builtin/chat/chat.test.tsx +1 -7
  73. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  74. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  75. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  76. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  77. package/src/plugins/builtin/chat/status-widget.tsx +2 -8
  78. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  79. package/src/plugins/builtin/cloud/plugin.tsx +4 -13
  80. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  81. package/src/plugins/builtin/debug/index.tsx +3 -4
  82. package/src/plugins/builtin/help/components.tsx +1 -1
  83. package/src/plugins/builtin/holders/treemap.tsx +28 -297
  84. package/src/plugins/builtin/holders/types.ts +0 -34
  85. package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
  86. package/src/plugins/builtin/market-heatmap/data.ts +296 -0
  87. package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
  88. package/src/plugins/builtin/options/table.ts +5 -5
  89. package/src/plugins/builtin/plugin-groups.ts +2 -0
  90. package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
  91. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
  92. package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
  93. package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
  94. package/src/plugins/ibkr/gateway/service/index.ts +6 -9
  95. package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
  96. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  97. package/src/remote/app-host.tsx +77 -0
  98. package/src/remote/client.ts +34 -0
  99. package/src/remote/command-bar.ts +98 -0
  100. package/src/remote/controller-utils.ts +111 -0
  101. package/src/remote/controller.test.ts +365 -0
  102. package/src/remote/controller.ts +434 -0
  103. package/src/remote/endpoint.ts +41 -0
  104. package/src/remote/json-patch.test.ts +31 -0
  105. package/src/remote/json-patch.ts +89 -0
  106. package/src/remote/layout-helpers.ts +62 -0
  107. package/src/remote/resources.ts +186 -0
  108. package/src/remote/revision.ts +18 -0
  109. package/src/remote/schema.ts +143 -0
  110. package/src/remote/semantic-helpers.ts +161 -0
  111. package/src/remote/semantic-tree.tsx +112 -0
  112. package/src/remote/server.test.ts +50 -0
  113. package/src/remote/server.ts +121 -0
  114. package/src/remote/types.ts +119 -0
  115. package/src/renderers/electrobun/bun/index.ts +55 -1
  116. package/src/renderers/electrobun/shared/protocol.ts +11 -1
  117. package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
  118. package/src/renderers/electrobun/view/build-assets.ts +1 -0
  119. package/src/renderers/electrobun/view/data-table/index.tsx +0 -8
  120. package/src/renderers/electrobun/view/data-table/row.tsx +2 -12
  121. package/src/renderers/electrobun/view/host/box.tsx +11 -1
  122. package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
  123. package/src/renderers/electrobun/view/host/mouse.ts +2 -0
  124. package/src/renderers/electrobun/view/host/style.ts +1 -0
  125. package/src/renderers/electrobun/view/main.tsx +5 -1
  126. package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
  127. package/src/renderers/electrobun/view/styles.css +7 -0
  128. package/src/renderers/opentui/chart-surface.test.tsx +101 -0
  129. package/src/renderers/opentui/chart-surface.tsx +15 -20
  130. package/src/renderers/opentui/native-smoke.ts +1 -0
  131. package/src/renderers/opentui/start.tsx +32 -0
  132. package/src/sources/yahoo-finance/http.ts +31 -4
  133. package/src/state/app/context/index.test.tsx +47 -0
  134. package/src/state/app/context/index.tsx +48 -18
  135. package/src/ui/host.tsx +1 -0
  136. package/src/ui/index.tsx +183 -7
  137. package/src/version.ts +1 -1
  138. package/src/plugins/builtin/holders/treemap-layout.ts +0 -231
package/README.md CHANGED
@@ -4,11 +4,13 @@
4
4
 
5
5
  # Gloomberb
6
6
 
7
- **Open-source finance terminal. Fast, keyboard-driven and extensible.**
7
+ **Open-source finance terminal. Fast, keyboard-driven, and extensible.**
8
8
 
9
- Available as a desktop app or TUI.
9
+ Desktop app for macOS and Windows. Terminal UI for macOS, Linux, and Windows.
10
10
 
11
- <a href="https://gloomberb.com/download/desktop"><strong>Download desktop for Mac</strong></a>
11
+ <a href="https://gloomberb.com/download/desktop"><strong>Download desktop</strong></a>
12
+ &nbsp;&middot;&nbsp;
13
+ <a href="#install"><strong>Install the TUI</strong></a>
12
14
 
13
15
  <br />
14
16
  <br />
@@ -17,9 +19,22 @@ Available as a desktop app or TUI.
17
19
 
18
20
  </div>
19
21
 
22
+ ## Desktop App or TUI?
23
+
24
+ Gloomberb has two ways in:
25
+
26
+ | Surface | Best for | How it runs |
27
+ |---------|----------|-------------|
28
+ | Desktop app | A polished app window, pop-out panes, OS shortcuts, and built-in updates | Published for macOS and Windows. It also installs a `gloomberb` terminal command for the TUI. |
29
+ | Terminal UI | Fast keyboard workflows inside your terminal, SSH/dev boxes, Linux machines, and script-friendly setups | Runs with `gloomberb` on macOS, Linux, and Windows. |
30
+
31
+ Both share the same command language, plugin system, market data surfaces, portfolios, watchlists, alerts, notes, and AI tools.
32
+
20
33
  ## Install
21
34
 
22
- macOS desktop app + terminal command:
35
+ ### macOS
36
+
37
+ Install the desktop app and the `gloomberb` terminal command:
23
38
 
24
39
  ```bash
25
40
  brew install --cask vincelwt/tap/gloomberb
@@ -27,13 +42,37 @@ brew install --cask vincelwt/tap/gloomberb
27
42
  curl -fsSL gloomberb.com/install | bash
28
43
  ```
29
44
 
30
- Both install `Gloomberb.app` and a `gloomberb` terminal command that runs the TUI through the app bundle, so the Bun runtime is stored once.
45
+ Both install `Gloomberb.app` and a `gloomberb` command that runs the TUI through the app bundle, so the bundled runtime is stored once.
31
46
 
32
- Desktop-only download:
47
+ Prefer a direct download?
33
48
 
34
49
  - [Download Gloomberb for Mac](https://gloomberb.com/download/desktop)
35
50
 
36
- Terminal-only install:
51
+ ### Linux
52
+
53
+ Install the standalone TUI binary:
54
+
55
+ ```bash
56
+ curl -fsSL gloomberb.com/install | bash
57
+ ```
58
+
59
+ This installs `gloomberb` to `~/.local/bin` by default. A Linux desktop package is not published yet.
60
+
61
+ ### Windows
62
+
63
+ Install the desktop app:
64
+
65
+ - [Download GloomberbSetup.exe for Windows x64](https://github.com/vincelwt/gloomberb/releases/latest/download/stable-win-x64-GloomberbSetup.exe)
66
+
67
+ The installer adds the app and a `gloomberb` terminal command. For a terminal-only setup, install Bun and use the package:
68
+
69
+ ```powershell
70
+ bun install -g gloomberb
71
+ ```
72
+
73
+ ### Terminal Package
74
+
75
+ Already have Bun installed on any supported OS?
37
76
 
38
77
  ```bash
39
78
  bun install -g gloomberb
@@ -45,7 +84,7 @@ Then run:
45
84
  gloomberb
46
85
  ```
47
86
 
48
- On macOS, app updates replace the app bundle in place and keep the terminal command pointing at the updated bundle. Homebrew users can also update through `brew upgrade --cask gloomberb`.
87
+ On macOS and Windows, desktop updates replace the installed app in place and keep the terminal command pointing at the updated runtime. Homebrew users can also update through `brew upgrade --cask gloomberb`.
49
88
 
50
89
  For the best terminal experience, use a [Kitty](https://sw.kovidgoyal.net/kitty/)-compatible terminal such as Ghostty, Kitty, or WezTerm.
51
90
 
@@ -58,6 +97,7 @@ Open command mode with `Ctrl+P`, then type a command. Press `` ` `` to open tick
58
97
  | `DES AAPL` | Security details |
59
98
  | `GP NVDA` | Price chart |
60
99
  | `TOP` | Ranked market stories |
100
+ | `HM` | Market heatmap |
61
101
  | `MOST` | Market movers |
62
102
  | `PF` | Portfolios and watchlists |
63
103
  | `KELLY AAPL` | Position sizing |
@@ -177,6 +217,7 @@ Use `HELP` inside Gloomberb for the live shortcut list. The common command-bar p
177
217
  | Shortcut | Function |
178
218
  |----------|----------|
179
219
  | `TOP` | Ranked market stories |
220
+ | `HM` | Market heatmap for large US stocks and ETFs |
180
221
  | `MOST` | Top gainers, losers, most active, and trending tickers |
181
222
  | `PM <query>` | Polymarket and Kalshi prediction data |
182
223
  | `N` | News feed |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Market research and portfolio tracker for the terminal",
5
5
  "module": "src/index.tsx",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "build": "bun run scripts/build.ts",
19
19
  "build:all": "bun run scripts/build.ts --all",
20
20
  "desktop:view:build": "bun run scripts/build-electrobun-view.ts",
21
- "desktop:dev": "electrobun dev --watch",
21
+ "desktop:dev": "bun run scripts/electrobun-dev.ts",
22
22
  "desktop:build": "electrobun build",
23
23
  "desktop:release": "electrobun build --env=stable",
24
24
  "desktop:release:windows": "electrobun build --env=stable --targets=win-x64",
@@ -63,6 +63,7 @@
63
63
  },
64
64
  "patchedDependencies": {
65
65
  "@opentui/core@0.3.2": "patches/@opentui%2Fcore@0.3.2.patch",
66
- "electrobun@1.18.1": "patches/electrobun@1.18.1.patch"
66
+ "electrobun@1.18.1": "patches/electrobun@1.18.1.patch",
67
+ "react-reconciler@0.33.0": "patches/react-reconciler@0.33.0.patch"
67
68
  }
68
69
  }
@@ -1,5 +1,5 @@
1
1
  diff --git a/index-218h9p3f.js b/index-218h9p3f.js
2
- index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346939bb068 100644
2
+ index b9445d2cca9477dfe06026da451b6d19268fcbdd..da790c31da185325405227329dad2ac12f5329e7 100644
3
3
  --- a/index-218h9p3f.js
4
4
  +++ b/index-218h9p3f.js
5
5
  @@ -6487,17 +6487,27 @@ class MouseParser {
@@ -210,7 +210,101 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
210
210
  }
211
211
  throw new Error(`opentui is not supported on the current platform: ${process.platform}-${process.arch}`);
212
212
  }
213
- @@ -22288,6 +22323,8 @@ class MouseEvent {
213
+ @@ -17996,5 +18031,7 @@ function isGapType(value) {
214
+ class BoxRenderable extends Renderable {
215
+ _backgroundColor;
216
+ + _hoverBackgroundColor;
217
+ + _isHovered = false;
218
+ _border;
219
+ _borderStyle;
220
+ _borderColor;
221
+ @@ -18023,6 +18060,7 @@ class BoxRenderable extends Renderable {
222
+ this._focusable = true;
223
+ }
224
+ this._backgroundColor = parseColor(options.backgroundColor || this._defaultOptions.backgroundColor);
225
+ + this._hoverBackgroundColor = options.hoverBackgroundColor === undefined ? undefined : parseColor(options.hoverBackgroundColor);
226
+ this._border = options.border ?? this._defaultOptions.border;
227
+ if (!options.border && (options.borderStyle || options.borderColor || options.focusedBorderColor || options.customBorderChars)) {
228
+ this._border = true;
229
+ @@ -18064,11 +18102,26 @@ class BoxRenderable extends Renderable {
230
+ }
231
+ set backgroundColor(value) {
232
+ const newColor = parseColor(value ?? this._defaultOptions.backgroundColor);
233
+ - if (this._backgroundColor !== newColor) {
234
+ + if (!this._backgroundColor.equals(newColor)) {
235
+ this._backgroundColor = newColor;
236
+ this.requestRender();
237
+ }
238
+ }
239
+ + get hoverBackgroundColor() {
240
+ + return this._hoverBackgroundColor;
241
+ + }
242
+ + set hoverBackgroundColor(value) {
243
+ + const newColor = value === undefined || value === null ? undefined : parseColor(value);
244
+ + const changed = newColor === undefined
245
+ + ? this._hoverBackgroundColor !== undefined
246
+ + : !this._hoverBackgroundColor?.equals(newColor);
247
+ + if (changed) {
248
+ + this._hoverBackgroundColor = newColor;
249
+ + if (this._isHovered) {
250
+ + this.requestRender();
251
+ + }
252
+ + }
253
+ + }
254
+ get border() {
255
+ return this._border;
256
+ }
257
+ @@ -18152,9 +18205,39 @@ class BoxRenderable extends Renderable {
258
+ this.requestRender();
259
+ }
260
+ }
261
+ + onMouseEvent(event) {
262
+ + if (!this._hoverBackgroundColor) {
263
+ + if (this._isHovered) {
264
+ + this._isHovered = false;
265
+ + this.requestRender();
266
+ + }
267
+ + return;
268
+ + }
269
+ + if (event.type === "over" && !this._isHovered) {
270
+ + this._isHovered = true;
271
+ + this.requestRender();
272
+ + } else if (event.type === "out" && this._isHovered) {
273
+ + this._isHovered = false;
274
+ + this.requestRender();
275
+ + }
276
+ + }
277
+ + getActiveHoverBackgroundColor() {
278
+ + if (this._isHovered && this._hoverBackgroundColor) {
279
+ + return this._hoverBackgroundColor;
280
+ + }
281
+ + let parent = this.parent;
282
+ + while (parent) {
283
+ + if (parent instanceof BoxRenderable && parent._isHovered && parent._hoverBackgroundColor) {
284
+ + return parent._hoverBackgroundColor;
285
+ + }
286
+ + parent = parent.parent;
287
+ + }
288
+ + return undefined;
289
+ + }
290
+ renderSelf(buffer) {
291
+ const hasBorder = this.borderSides.top || this.borderSides.right || this.borderSides.bottom || this.borderSides.left;
292
+ - const hasVisibleFill = this.shouldFill && this._backgroundColor.a > 0;
293
+ + const backgroundColor = this.getActiveHoverBackgroundColor() ?? this._backgroundColor;
294
+ + const hasVisibleFill = this.shouldFill && backgroundColor.a > 0;
295
+ if (!hasBorder && !hasVisibleFill) {
296
+ return;
297
+ }
298
+ @@ -18171,7 +18254,7 @@ class BoxRenderable extends Renderable {
299
+ customBorderChars: this._customBorderChars,
300
+ border: this._border,
301
+ borderColor: currentBorderColor,
302
+ - backgroundColor: this._backgroundColor,
303
+ + backgroundColor,
304
+ shouldFill: this.shouldFill,
305
+ title: this._title,
306
+ titleAlignment: this._titleAlignment,
307
+ @@ -22288,6 +22371,8 @@ class MouseEvent {
214
308
  button;
215
309
  x;
216
310
  y;
@@ -219,7 +313,7 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
219
313
  source;
220
314
  modifiers;
221
315
  scroll;
222
- @@ -22307,6 +22344,8 @@ class MouseEvent {
316
+ @@ -22307,6 +22392,8 @@ class MouseEvent {
223
317
  this.button = attributes.button;
224
318
  this.x = attributes.x;
225
319
  this.y = attributes.y;
@@ -228,16 +322,18 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
228
322
  this.modifiers = attributes.modifiers;
229
323
  this.scroll = attributes.scroll;
230
324
  this.source = attributes.source;
231
- @@ -22457,6 +22496,8 @@ class CliRenderer extends EventEmitter9 {
325
+ @@ -22457,6 +22544,10 @@ class CliRenderer extends EventEmitter9 {
232
326
  resizeDebounceDelay = 100;
233
327
  enableMouseMovement = false;
234
328
  _useMouse = true;
235
329
  + _mouseProtocolSignature = "disabled";
236
330
  + _pixelMouseEnabled = false;
331
+ + _pendingMouseMoveEvent = null;
332
+ + _pendingMouseMoveTimer = null;
237
333
  autoFocus = true;
238
334
  _screenMode = "alternate-screen";
239
335
  _footerHeight = DEFAULT_FOOTER_HEIGHT;
240
- @@ -22491,7 +22532,7 @@ class CliRenderer extends EventEmitter9 {
336
+ @@ -22491,7 +22582,7 @@ class CliRenderer extends EventEmitter9 {
241
337
  this.handleResize(width, height);
242
338
  }).bind(this);
243
339
  _capabilities = null;
@@ -246,7 +342,7 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
246
342
  _hasPointer = false;
247
343
  _lastPointerModifiers = {
248
344
  shift: false,
249
- @@ -22738,7 +22779,13 @@ Captured external output:
345
+ @@ -22738,7 +22829,13 @@ Captured external output:
250
346
  privateCapabilityRepliesActive: false,
251
347
  pixelResolutionQueryActive: false,
252
348
  explicitWidthCprActive: false,
@@ -261,7 +357,7 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
261
357
  },
262
358
  clock: this.clock
263
359
  });
264
- @@ -24021,12 +24068,14 @@ Captured external output:
360
+ @@ -24021,12 +24118,14 @@ Captured external output:
265
361
  enableMouse() {
266
362
  this._useMouse = true;
267
363
  this.lib.enableMouse(this.rendererPtr, this.enableMouseMovement);
@@ -276,7 +372,7 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
276
372
  }
277
373
  enableKittyKeyboard(flags = 3) {
278
374
  this.lib.enableKittyKeyboard(this.rendererPtr, flags);
279
- @@ -24036,6 +24085,52 @@ Captured external output:
375
+ @@ -24036,6 +24135,52 @@ Captured external output:
280
376
  this.lib.disableKittyKeyboard(this.rendererPtr);
281
377
  this.updateStdinParserProtocolContext({ kittyKeyboardEnabled: false }, true);
282
378
  }
@@ -329,7 +425,7 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
329
425
  set useThread(useThread) {
330
426
  this._useThread = useThread;
331
427
  this.lib.setUseThread(this.rendererPtr, useThread);
332
- @@ -24141,6 +24236,7 @@ Captured external output:
428
+ @@ -24141,6 +24286,7 @@ Captured external output:
333
429
  }
334
430
  this.lib.processCapabilityResponse(this.rendererPtr, sequence);
335
431
  this._capabilities = this.lib.getTerminalCapabilities(this.rendererPtr);
@@ -337,7 +433,7 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
337
433
  if (this._capabilities?.terminal?.from_xtversion) {
338
434
  this.resolveXtVersionWaiters();
339
435
  }
340
- @@ -24170,6 +24266,7 @@ Captured external output:
436
+ @@ -24170,6 +24316,7 @@ Captured external output:
341
437
  if (sequence === "\x1B[I") {
342
438
  if (this.shouldRestoreModesOnNextFocus) {
343
439
  this.lib.restoreTerminalModes(this.rendererPtr);
@@ -345,7 +441,16 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
345
441
  this.shouldRestoreModesOnNextFocus = false;
346
442
  }
347
443
  if (this._terminalFocusState !== true) {
348
- @@ -24264,6 +24361,7 @@ Captured external output:
444
+ @@ -24218,7 +24365,7 @@ Captured external output:
445
+ this._keyHandler.processParsedKey(event.key);
446
+ return;
447
+ case "mouse":
448
+ - if (this._useMouse && this.processSingleMouseEvent(event.event)) {
449
+ + if (this._useMouse && this.processMouseEvent(event.event)) {
450
+ return;
451
+ }
452
+ this.dispatchSequenceHandlers(event.raw);
453
+ @@ -24264,6 +24411,7 @@ Captured external output:
349
454
  }
350
455
  this.waitingForPixelResolution = false;
351
456
  this.updateStdinParserProtocolContext({ pixelResolutionQueryActive: false }, true);
@@ -353,7 +458,54 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
353
458
  return true;
354
459
  }
355
460
  return false;
356
- @@ -24298,9 +24396,15 @@ Captured external output:
461
+ @@ -24292,15 +24440,62 @@ Captured external output:
462
+ }
463
+ return event;
464
+ }
465
+ + cloneMouseEventAttributes(mouseEvent) {
466
+ + return {
467
+ + ...mouseEvent,
468
+ + modifiers: { ...mouseEvent.modifiers }
469
+ + };
470
+ + }
471
+ + processMouseEvent(mouseEvent) {
472
+ + if (mouseEvent.type !== "move") {
473
+ + this.flushPendingMouseMove();
474
+ + return this.processSingleMouseEvent(mouseEvent);
475
+ + }
476
+ + if (this._pendingMouseMoveTimer === null) {
477
+ + const handled = this.processSingleMouseEvent(mouseEvent);
478
+ + this.schedulePendingMouseMove();
479
+ + return handled;
480
+ + }
481
+ + this._pendingMouseMoveEvent = this.cloneMouseEventAttributes(mouseEvent);
482
+ + return true;
483
+ + }
484
+ + schedulePendingMouseMove() {
485
+ + if (this._pendingMouseMoveTimer !== null || this._isDestroyed) {
486
+ + return;
487
+ + }
488
+ + const delay = Math.max(100, Math.floor(this.targetFrameTime));
489
+ + this._pendingMouseMoveTimer = this.clock.setTimeout(() => {
490
+ + this._pendingMouseMoveTimer = null;
491
+ + this.flushPendingMouseMove();
492
+ + }, delay);
493
+ + }
494
+ + flushPendingMouseMove() {
495
+ + if (this._pendingMouseMoveTimer !== null) {
496
+ + this.clock.clearTimeout(this._pendingMouseMoveTimer);
497
+ + this._pendingMouseMoveTimer = null;
498
+ + }
499
+ + const pending = this._pendingMouseMoveEvent;
500
+ + this._pendingMouseMoveEvent = null;
501
+ + if (!pending || this._isDestroyed) {
502
+ + return false;
503
+ + }
504
+ + return this.processSingleMouseEvent(pending);
505
+ + }
506
+ processSingleMouseEvent(mouseEvent) {
507
+ if (this._splitHeight > 0) {
508
+ if (mouseEvent.y < this.renderOffset) {
357
509
  return false;
358
510
  }
359
511
  mouseEvent.y -= this.renderOffset;
@@ -369,31 +521,43 @@ index b9445d2cca9477dfe06026da451b6d19268fcbdd..3efd05fa41eb713ac6e539764b15c346
369
521
  this._hasPointer = true;
370
522
  this._lastPointerModifiers = mouseEvent.modifiers;
371
523
  if (this._console.visible) {
372
- @@ -24441,6 +24545,8 @@ Captured external output:
373
- button: 0,
374
- x: this._latestPointer.x,
375
- y: this._latestPointer.y,
524
+ @@ -24437,6 +24632,8 @@ Captured external output:
525
+ return;
526
+ }
527
+ const baseEvent = {
376
528
  + pixelX: this._latestPointer.pixelX,
377
529
  + pixelY: this._latestPointer.pixelY,
378
- modifiers: this._lastPointerModifiers
379
- };
380
- if (lastOver && !lastOver.isDestroyed) {
381
- @@ -24531,6 +24637,7 @@ Captured external output:
530
+ type: "move",
531
+ button: 0,
532
+ x: this._latestPointer.x,
533
+ @@ -24527,6 +24724,7 @@ Captured external output:
534
+ const pendingSplitFooterTransition = this.pendingSplitFooterTransition;
535
+ const previousGeometry = calculateRenderGeometry(this._screenMode, this._terminalWidth, this._terminalHeight, this._footerHeight);
536
+ const prevWidth = this._terminalWidth;
537
+ + this.syncPixelMouseMode();
538
+ const previousTerminalHeight = this._terminalHeight;
382
539
  const visiblePreviousSplitHeight = pendingSplitFooterTransition?.sourceHeight ?? previousGeometry.effectiveFooterHeight;
383
540
  this._terminalWidth = width;
384
- this._terminalHeight = height;
385
- + this.syncPixelMouseMode();
386
- this.queryPixelResolution();
387
- this.setCapturedRenderable(undefined);
388
- this.stdinParser?.resetMouseState();
389
- @@ -24848,6 +24955,7 @@ Captured external output:
541
+ @@ -24832,6 +25030,11 @@ Captured external output:
542
+ this.clock.clearTimeout(this.resizeTimeoutId);
543
+ this.resizeTimeoutId = null;
544
+ }
545
+ + if (this._pendingMouseMoveTimer !== null) {
546
+ + this.clock.clearTimeout(this._pendingMouseMoveTimer);
547
+ + this._pendingMouseMoveTimer = null;
548
+ + }
549
+ + this._pendingMouseMoveEvent = null;
550
+ if (this.capabilityTimeoutId !== null) {
551
+ this.clock.clearTimeout(this.capabilityTimeoutId);
552
+ this.capabilityTimeoutId = null;
553
+ @@ -24847,6 +25050,7 @@ Captured external output:
554
+ this.renderTimeout = null;
555
+ }
390
556
  this.themeModeState.cancelRefresh();
557
+ + this.disablePixelMouseMode();
391
558
  this._isRunning = false;
392
559
  this.waitingForPixelResolution = false;
393
- + this.disablePixelMouseMode();
394
560
  this.updateStdinParserProtocolContext({
395
- privateCapabilityRepliesActive: false,
396
- pixelResolutionQueryActive: false,
397
561
  diff --git a/lib/parse.mouse.d.ts b/lib/parse.mouse.d.ts
398
562
  index c29f953fde0b78f09d2ec7a830e5a72452c1fe7e..04613b921bd9b4a9a7a1a4fbd0234d4012df67a1 100644
399
563
  --- a/lib/parse.mouse.d.ts
@@ -450,6 +614,44 @@ index 6b733d534d8e9b77c5c2773f93c7a22063c18d84..a5c318d591e9f3775e276e555c15052c
450
614
  }
451
615
  export interface StdinParserOptions {
452
616
  timeoutMs?: number;
617
+ diff --git a/renderables/Box.d.ts b/renderables/Box.d.ts
618
+ index 7ecb70d38ca41d1afdc078780985827e8f10e3a7..0845141e20ebd71a163dba67880d494a05b27d9a 100644
619
+ --- a/renderables/Box.d.ts
620
+ +++ b/renderables/Box.d.ts
621
+ @@ -5,6 +5,7 @@ import { type ColorInput, RGBA } from "../lib/RGBA.js";
622
+ import type { RenderContext } from "../types.js";
623
+ export interface BoxOptions<TRenderable extends Renderable = BoxRenderable> extends RenderableOptions<TRenderable> {
624
+ backgroundColor?: string | RGBA;
625
+ + hoverBackgroundColor?: string | RGBA;
626
+ borderStyle?: BorderStyle;
627
+ border?: boolean | BorderSides[];
628
+ borderColor?: string | RGBA;
629
+ @@ -22,6 +23,8 @@ export interface BoxOptions<TRenderable extends Renderable = BoxRenderable> exte
630
+ }
631
+ export declare class BoxRenderable extends Renderable {
632
+ protected _backgroundColor: RGBA;
633
+ + protected _hoverBackgroundColor?: RGBA;
634
+ + protected _isHovered: boolean;
635
+ protected _border: boolean | BorderSides[];
636
+ protected _borderStyle: BorderStyle;
637
+ protected _borderColor: RGBA;
638
+ @@ -50,6 +53,8 @@ export declare class BoxRenderable extends Renderable {
639
+ set customBorderChars(value: BorderCharacters | undefined);
640
+ get backgroundColor(): RGBA;
641
+ set backgroundColor(value: RGBA | string | undefined);
642
+ + get hoverBackgroundColor(): RGBA | undefined;
643
+ + set hoverBackgroundColor(value: RGBA | string | undefined);
644
+ get border(): boolean | BorderSides[];
645
+ set border(value: boolean | BorderSides[]);
646
+ get borderStyle(): BorderStyle;
647
+ @@ -66,6 +71,7 @@ export declare class BoxRenderable extends Renderable {
648
+ set bottomTitle(value: string | undefined);
649
+ get bottomTitleAlignment(): "left" | "center" | "right";
650
+ set bottomTitleAlignment(value: "left" | "center" | "right");
651
+ + protected getActiveHoverBackgroundColor(): RGBA | undefined;
652
+ protected renderSelf(buffer: OptimizedBuffer): void;
653
+ protected getScissorRect(): {
654
+ x: number;
453
655
  diff --git a/renderer.d.ts b/renderer.d.ts
454
656
  index 96189246f423809fc52d2ad182d57ed2e80ac098..7dd0baf1697041cf8ad556b115cd13ad2e18bc65 100644
455
657
  --- a/renderer.d.ts
@@ -0,0 +1,10 @@
1
+ diff --git a/cjs/react-reconciler.development.js b/cjs/react-reconciler.development.js
2
+ --- a/cjs/react-reconciler.development.js
3
+ +++ b/cjs/react-reconciler.development.js
4
+ @@ -17213,5 +17213,6 @@
5
+ hasOwnProperty = Object.prototype.hasOwnProperty,
6
+ supportsUserTiming =
7
+ + "undefined" !== typeof window &&
8
+ "undefined" !== typeof console &&
9
+ "function" === typeof console.timeStamp &&
10
+ "undefined" !== typeof performance &&
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { createTestRenderer } from "@opentui/core/testing";
3
3
  import { act } from "react";
4
4
  import { createOpenTuiTestRoot as createRoot, TestDialogProvider } from "../renderers/opentui/test-utils";
5
- import { createDefaultConfig } from "../types/config";
5
+ import { cloneLayout, createDefaultConfig } from "../types/config";
6
6
  import { createInitialState, type AppAction, type AppState } from "../state/app/context";
7
7
  import type { PluginRegistry } from "../plugins/registry";
8
8
  import { useAppGlobalShortcuts } from "./global-shortcuts";
@@ -164,6 +164,25 @@ describe("useAppGlobalShortcuts", () => {
164
164
  expect(event.propagationStopped).toBe(true);
165
165
  });
166
166
 
167
+ test("switches saved layouts with Ctrl-number and consumes the shortcut", async () => {
168
+ const actions: AppAction[] = [];
169
+ const config = createDefaultConfig("/tmp/gloomberb-global-shortcuts-layouts");
170
+ config.layouts = [
171
+ { name: "One", layout: cloneLayout(config.layout) },
172
+ { name: "Two", layout: cloneLayout(config.layout) },
173
+ { name: "Three", layout: cloneLayout(config.layout) },
174
+ ];
175
+ config.activeLayoutIndex = 0;
176
+ const state = createInitialState(config);
177
+ await renderHarness(state, createRegistry(), (action) => actions.push(action));
178
+
179
+ const event = await emitKeypress({ name: "2", ctrl: true });
180
+
181
+ expect(actions).toEqual([{ type: "SWITCH_LAYOUT", index: 1 }]);
182
+ expect(event.defaultPrevented).toBe(true);
183
+ expect(event.propagationStopped).toBe(true);
184
+ });
185
+
167
186
  test("does not run plain plugin shortcuts while input is captured", async () => {
168
187
  let executed = 0;
169
188
  const actions: AppAction[] = [];
@@ -76,6 +76,8 @@ export function useAppGlobalShortcuts({
76
76
  if (idx < layouts.length && idx !== state.config.activeLayoutIndex) {
77
77
  dispatch({ type: "SWITCH_LAYOUT", index: idx });
78
78
  }
79
+ event.preventDefault();
80
+ event.stopPropagation();
79
81
  return;
80
82
  }
81
83
 
@@ -73,23 +73,30 @@ export function useAppPaneRuntime({
73
73
  return resolvePaneTargetInLayout(layout, paneId);
74
74
  }, [state.config.layout]);
75
75
 
76
- const persistLayout = useCallback((layout: LayoutConfig, options?: { pushHistory?: boolean }) => {
76
+ const persistLayout = useCallback((layout: LayoutConfig, options?: { pushHistory?: boolean; focusedPaneId?: string | null }) => {
77
77
  const currentState = stateRef.current;
78
78
  const normalizedLayout = normalizePaneLayout(layout);
79
79
  if (options?.pushHistory !== false) {
80
80
  dispatch({ type: "PUSH_LAYOUT_HISTORY" });
81
81
  }
82
- dispatch({ type: "UPDATE_LAYOUT", layout: normalizedLayout });
82
+ const hasFocusTarget = !!options && Object.prototype.hasOwnProperty.call(options, "focusedPaneId");
83
+ dispatch(hasFocusTarget
84
+ ? { type: "UPDATE_LAYOUT", layout: normalizedLayout, focusedPaneId: options.focusedPaneId ?? null }
85
+ : { type: "UPDATE_LAYOUT", layout: normalizedLayout });
83
86
  scheduleConfigSave(syncConfigActiveLayoutState(
84
87
  { ...currentState.config, layout: normalizedLayout },
85
88
  currentState.paneState,
86
- currentState.focusedPaneId,
89
+ hasFocusTarget ? (options.focusedPaneId ?? null) : currentState.focusedPaneId,
87
90
  currentState.activePanel,
88
91
  ));
89
92
  }, [dispatch, stateRef]);
90
93
 
91
94
  const activatePane = useCallback((paneId: string, layout: LayoutConfig = state.config.layout) => {
92
- dispatch({ type: "SET_ACTIVE_PANEL", panel: resolvePanelForPane({ layout, paneId, pluginRegistry }) });
95
+ dispatch({
96
+ type: "SET_ACTIVE_PANEL",
97
+ panel: resolvePanelForPane({ layout, paneId, pluginRegistry }),
98
+ preserveFocus: true,
99
+ });
93
100
  dispatch({ type: "FOCUS_PANE", paneId });
94
101
  }, [dispatch, pluginRegistry, state.config.layout]);
95
102
 
@@ -15,7 +15,7 @@ interface UseAppPaneSettingsRuntimeOptions {
15
15
  dataProvider: DataProvider;
16
16
  dialog: DialogApi;
17
17
  dispatch: Dispatch<AppAction>;
18
- persistLayout: (layout: LayoutConfig, options?: { pushHistory?: boolean }) => void;
18
+ persistLayout: (layout: LayoutConfig, options?: { pushHistory?: boolean; focusedPaneId?: string | null }) => void;
19
19
  pluginRegistry: PluginRegistry;
20
20
  resolvePaneTarget: (paneId: string, layout?: LayoutConfig) => string | null;
21
21
  stateRef: { current: AppState };