gloomberb 0.4.0 → 0.5.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 (218) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/app.tsx +184 -69
  4. package/src/cli/commands/ticker.ts +45 -15
  5. package/src/cli/helpers.ts +4 -2
  6. package/src/cli/registry.test.ts +42 -35
  7. package/src/cli.test.ts +4 -2
  8. package/src/components/chart/chart-control-hint.tsx +39 -0
  9. package/src/components/chart/chart-controller.test.ts +110 -0
  10. package/src/components/chart/chart-controller.ts +364 -2
  11. package/src/components/chart/chart-data.ts +177 -29
  12. package/src/components/chart/chart-indicator-selector.tsx +53 -0
  13. package/src/components/chart/chart-pane-settings.ts +20 -0
  14. package/src/components/chart/chart-renderer.ts +220 -30
  15. package/src/components/chart/chart-resolution.test.ts +67 -0
  16. package/src/components/chart/chart-resolution.ts +74 -4
  17. package/src/components/chart/chart-scroll.test.ts +69 -0
  18. package/src/components/chart/chart-scroll.ts +104 -0
  19. package/src/components/chart/chart-types.ts +12 -0
  20. package/src/components/chart/chart-viewport.test.ts +5 -1
  21. package/src/components/chart/chart-viewport.ts +1 -1
  22. package/src/components/chart/chart.test.ts +249 -10
  23. package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
  24. package/src/components/chart/comparison-chart-renderer.ts +40 -3
  25. package/src/components/chart/comparison-stock-chart.tsx +218 -123
  26. package/src/components/chart/indicators/bands.test.ts +50 -0
  27. package/src/components/chart/indicators/bands.ts +29 -0
  28. package/src/components/chart/indicators/moving-averages.test.ts +54 -0
  29. package/src/components/chart/indicators/moving-averages.ts +42 -0
  30. package/src/components/chart/indicators/options.test.ts +32 -0
  31. package/src/components/chart/indicators/options.ts +107 -0
  32. package/src/components/chart/indicators/oscillators.test.ts +79 -0
  33. package/src/components/chart/indicators/oscillators.ts +92 -0
  34. package/src/components/chart/indicators/types.ts +36 -0
  35. package/src/components/chart/native/chart-rasterizer.ts +51 -0
  36. package/src/components/chart/native/kitty.test.ts +68 -0
  37. package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
  38. package/src/components/chart/stock-chart.test.ts +239 -0
  39. package/src/components/chart/stock-chart.tsx +2257 -372
  40. package/src/components/command-bar/command-bar.test.tsx +337 -5
  41. package/src/components/command-bar/command-bar.tsx +296 -120
  42. package/src/components/command-bar/helpers.ts +1 -1
  43. package/src/components/command-bar/root-shortcuts.ts +33 -4
  44. package/src/components/command-bar/view-model.test.ts +3 -3
  45. package/src/components/command-bar/view-model.ts +4 -4
  46. package/src/components/command-bar/workflow-ops.ts +10 -0
  47. package/src/components/layout/floating-pane.tsx +3 -1
  48. package/src/components/layout/header.tsx +5 -4
  49. package/src/components/layout/pane-sizing.ts +6 -0
  50. package/src/components/layout/pane.tsx +5 -1
  51. package/src/components/layout/shell.test.tsx +263 -32
  52. package/src/components/layout/shell.tsx +10 -6
  53. package/src/components/layout/status-bar.test.tsx +16 -7
  54. package/src/components/layout/status-bar.tsx +4 -1
  55. package/src/components/markdown-editor.tsx +1 -0
  56. package/src/components/pane-settings-dialog.tsx +13 -152
  57. package/src/components/pane-template-wizard.tsx +1 -0
  58. package/src/components/price-selector-dialog.tsx +12 -7
  59. package/src/components/ticker-badge-text.test.tsx +50 -0
  60. package/src/components/ticker-badge-text.tsx +17 -2
  61. package/src/components/ticker-badge.tsx +2 -2
  62. package/src/components/ticker-list-table.tsx +62 -2
  63. package/src/components/toggle-list.tsx +15 -2
  64. package/src/components/ui/data-table.tsx +82 -5
  65. package/src/components/ui/external-link.tsx +48 -11
  66. package/src/components/ui/fields.tsx +2 -1
  67. package/src/components/ui/index.ts +8 -1
  68. package/src/components/ui/multi-select-chips.tsx +69 -0
  69. package/src/components/ui/multi-select-dialog.tsx +267 -0
  70. package/src/components/ui/multi-select.ts +78 -0
  71. package/src/components/ui/ui.test.tsx +343 -1
  72. package/src/data/plugin-state-store.ts +30 -19
  73. package/src/data/resource-store.ts +133 -103
  74. package/src/data/session-store.ts +19 -12
  75. package/src/data/sqlite-cache.test.ts +37 -2
  76. package/src/data/sqlite-database.ts +8 -2
  77. package/src/data/sqlite-retry.test.ts +63 -0
  78. package/src/data/sqlite-retry.ts +66 -0
  79. package/src/data/ticker-store.ts +29 -18
  80. package/src/index.tsx +6 -1
  81. package/src/market-data/coordinator.test.ts +114 -5
  82. package/src/market-data/coordinator.ts +70 -1
  83. package/src/market-data/hooks.test.tsx +73 -2
  84. package/src/market-data/hooks.tsx +16 -4
  85. package/src/notifications/app-notifier.test.ts +177 -0
  86. package/src/notifications/app-notifier.ts +241 -0
  87. package/src/pane-settings.test.ts +2 -0
  88. package/src/plugins/builtin/ai/index.test.tsx +1 -1
  89. package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
  90. package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
  91. package/src/plugins/builtin/ai/ticker-context.ts +3 -2
  92. package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
  93. package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
  94. package/src/plugins/builtin/alerts/index.test.tsx +357 -0
  95. package/src/plugins/builtin/alerts/index.tsx +519 -0
  96. package/src/plugins/builtin/alerts/types.ts +14 -0
  97. package/src/plugins/builtin/analytics/index.test.tsx +300 -0
  98. package/src/plugins/builtin/analytics/index.tsx +775 -0
  99. package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
  100. package/src/plugins/builtin/analytics/metrics.ts +155 -0
  101. package/src/plugins/builtin/ask-ai.test.tsx +2 -0
  102. package/src/plugins/builtin/chat-controller.test.ts +92 -18
  103. package/src/plugins/builtin/chat-controller.ts +40 -11
  104. package/src/plugins/builtin/chat.test.tsx +715 -4
  105. package/src/plugins/builtin/chat.tsx +420 -119
  106. package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
  107. package/src/plugins/builtin/comparison-chart.tsx +1 -2
  108. package/src/plugins/builtin/correlation/compute.test.ts +147 -0
  109. package/src/plugins/builtin/correlation/compute.ts +115 -0
  110. package/src/plugins/builtin/correlation/index.tsx +385 -0
  111. package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
  112. package/src/plugins/builtin/correlation/settings.test.ts +34 -0
  113. package/src/plugins/builtin/correlation/settings.ts +95 -0
  114. package/src/plugins/builtin/debug.tsx +36 -50
  115. package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
  116. package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
  117. package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
  118. package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
  119. package/src/plugins/builtin/earnings/index.tsx +337 -0
  120. package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
  121. package/src/plugins/builtin/econ/calendar-source.ts +121 -0
  122. package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
  123. package/src/plugins/builtin/econ/fred-client.ts +79 -0
  124. package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
  125. package/src/plugins/builtin/econ/index.tsx +999 -0
  126. package/src/plugins/builtin/econ/types.ts +13 -0
  127. package/src/plugins/builtin/fred-settings.ts +10 -0
  128. package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
  129. package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
  130. package/src/plugins/builtin/help.test.tsx +1 -5
  131. package/src/plugins/builtin/help.tsx +1 -16
  132. package/src/plugins/builtin/insider/index.tsx +417 -0
  133. package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
  134. package/src/plugins/builtin/insider/insider-data.ts +49 -0
  135. package/src/plugins/builtin/layout-manager.tsx +33 -16
  136. package/src/plugins/builtin/market-movers/index.tsx +573 -0
  137. package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
  138. package/src/plugins/builtin/market-movers/screener.ts +105 -0
  139. package/src/plugins/builtin/options.tsx +4 -3
  140. package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
  141. package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
  142. package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
  143. package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
  144. package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
  145. package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
  146. package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
  147. package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
  148. package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
  149. package/src/plugins/builtin/sec.tsx +114 -7
  150. package/src/plugins/builtin/sectors/index.tsx +359 -0
  151. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  152. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
  153. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
  154. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
  155. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
  156. package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
  157. package/src/plugins/builtin/world-indices/index.tsx +410 -0
  158. package/src/plugins/builtin/world-indices/indices.ts +52 -0
  159. package/src/plugins/builtin/yield-curve/index.tsx +242 -0
  160. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
  161. package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
  162. package/src/plugins/catalog.ts +22 -0
  163. package/src/plugins/gridlock-notification.ts +15 -0
  164. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  165. package/src/plugins/ibkr/gateway-service.test.ts +22 -0
  166. package/src/plugins/ibkr/gateway-service.ts +6 -0
  167. package/src/plugins/ibkr/index.test.tsx +1 -1
  168. package/src/plugins/ibkr/index.tsx +8 -2
  169. package/src/plugins/ibkr/trade-tab.tsx +23 -5
  170. package/src/plugins/ibkr/trade-utils.test.ts +1 -1
  171. package/src/plugins/ibkr/trade-utils.ts +7 -6
  172. package/src/plugins/ibkr/trading-pane.tsx +8 -7
  173. package/src/plugins/plugin-runtime.test.tsx +2 -0
  174. package/src/plugins/plugin-runtime.tsx +10 -0
  175. package/src/plugins/prediction-markets/controller-data.ts +129 -14
  176. package/src/plugins/prediction-markets/controller.ts +1 -1
  177. package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
  178. package/src/plugins/prediction-markets/index.tsx +5 -1
  179. package/src/plugins/prediction-markets/pane.test.tsx +78 -0
  180. package/src/plugins/prediction-markets/pane.tsx +7 -3
  181. package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
  182. package/src/plugins/prediction-markets/services/fetch.ts +17 -7
  183. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  184. package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
  185. package/src/plugins/prediction-markets/test-helpers.tsx +22 -5
  186. package/src/plugins/registry.ts +45 -13
  187. package/src/sources/provider-router.test.ts +3 -2
  188. package/src/sources/sec-edgar.ts +13 -1
  189. package/src/sources/yahoo-finance.ts +75 -1
  190. package/src/state/app-context-selector.test.tsx +17 -1
  191. package/src/state/app-context.test.tsx +24 -0
  192. package/src/state/app-context.tsx +29 -6
  193. package/src/theme/colors.ts +5 -0
  194. package/src/theme/themes.ts +25 -0
  195. package/src/types/config.test.ts +107 -0
  196. package/src/types/config.ts +30 -1
  197. package/src/types/data-provider.ts +13 -0
  198. package/src/types/plugin.ts +35 -1
  199. package/src/updater.test.ts +2 -0
  200. package/src/utils/api-client.ts +12 -0
  201. package/src/utils/format.test.ts +11 -1
  202. package/src/utils/format.ts +86 -8
  203. package/src/utils/inline-content-tokenizer.test.ts +22 -0
  204. package/src/utils/inline-content-tokenizer.ts +76 -0
  205. package/src/utils/link-tokenizer.test.ts +27 -0
  206. package/src/utils/link-tokenizer.ts +93 -0
  207. package/src/utils/market-format.test.ts +109 -0
  208. package/src/utils/market-format.ts +299 -0
  209. package/src/utils/market-status.ts +3 -2
  210. package/src/utils/selection-clipboard.test.ts +43 -0
  211. package/src/utils/selection-clipboard.ts +88 -0
  212. package/src/utils/terminal-input-reset.test.ts +50 -0
  213. package/src/utils/terminal-input-reset.ts +24 -0
  214. package/src/utils/throttled-fetch.test.ts +145 -0
  215. package/src/utils/throttled-fetch.ts +212 -0
  216. package/src/utils/ticker-tokenizer.test.ts +31 -0
  217. package/src/utils/ticker-tokenizer.ts +7 -42
  218. package/src/version.ts +1 -1
@@ -3,9 +3,11 @@ import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
3
3
  import { EmptyState } from "./ui";
4
4
  import { colors, hoverBg } from "../theme/colors";
5
5
  import type { ColumnConfig } from "../types/config";
6
- import type { TickerFinancials } from "../types/financials";
6
+ import type { TickerFinancials, PricePoint } from "../types/financials";
7
7
  import type { TickerRecord } from "../types/ticker";
8
8
  import { padTo } from "../utils/format";
9
+ import { renderChart, resolveChartPalette, type StyledContent } from "./chart/chart-renderer";
10
+ import type { ProjectedChartPoint } from "./chart/chart-data";
9
11
 
10
12
  export interface TickerTableCell {
11
13
  text: string;
@@ -64,6 +66,7 @@ interface TickerListTableProps {
64
66
  onRowActivate?: (ticker: TickerRecord) => void;
65
67
  emptyTitle?: string;
66
68
  emptyHint?: string;
69
+ showSparklines?: boolean;
67
70
  }
68
71
 
69
72
  const TickerListHeader = memo(function TickerListHeader({
@@ -86,7 +89,13 @@ const TickerListHeader = memo(function TickerListHeader({
86
89
  scrollX
87
90
  focusable={false}
88
91
  >
89
- <box flexDirection="row" height={1} paddingX={1}>
92
+ <box
93
+ flexDirection="row"
94
+ height={1}
95
+ width="100%"
96
+ paddingX={1}
97
+ backgroundColor={colors.panel}
98
+ >
90
99
  {columns.map((column) => {
91
100
  const isSorted = sortColumnId === column.id;
92
101
  const indicator = isSorted ? (sortDirection === "asc" ? " \u25B2" : " \u25BC") : "";
@@ -95,6 +104,7 @@ const TickerListHeader = memo(function TickerListHeader({
95
104
  <box
96
105
  key={column.id}
97
106
  width={column.width + 1}
107
+ backgroundColor={colors.panel}
98
108
  onMouseDown={onHeaderClick ? (event) => {
99
109
  event.preventDefault();
100
110
  onHeaderClick(column.id);
@@ -111,6 +121,38 @@ const TickerListHeader = memo(function TickerListHeader({
111
121
  );
112
122
  });
113
123
 
124
+ function renderSparkline(priceHistory: PricePoint[]): StyledContent | null {
125
+ if (priceHistory.length < 5) return null;
126
+
127
+ const last20 = priceHistory.slice(-20);
128
+ const points: ProjectedChartPoint[] = last20.map((pt) => ({
129
+ date: pt.date,
130
+ open: pt.open ?? pt.close,
131
+ high: pt.high ?? pt.close,
132
+ low: pt.low ?? pt.close,
133
+ close: pt.close,
134
+ volume: pt.volume ?? 0,
135
+ }));
136
+
137
+ const first = last20[0]?.close ?? 0;
138
+ const last = last20[last20.length - 1]?.close ?? 0;
139
+ const trend = last >= first ? "positive" : "negative";
140
+ const palette = resolveChartPalette(colors, trend);
141
+
142
+ const result = renderChart(points, {
143
+ width: 10,
144
+ height: 1,
145
+ showVolume: false,
146
+ volumeHeight: 0,
147
+ cursorX: null,
148
+ cursorY: null,
149
+ mode: "line",
150
+ colors: palette,
151
+ });
152
+
153
+ return result.lines[0] ?? null;
154
+ }
155
+
114
156
  const TickerListRow = memo(function TickerListRow({
115
157
  columns,
116
158
  ticker,
@@ -123,6 +165,8 @@ const TickerListRow = memo(function TickerListRow({
123
165
  setCursorSymbol,
124
166
  resolveCell,
125
167
  onRowActivate,
168
+ showSparklines,
169
+ priceHistory,
126
170
  }: {
127
171
  columns: ColumnConfig[];
128
172
  ticker: TickerRecord;
@@ -135,10 +179,16 @@ const TickerListRow = memo(function TickerListRow({
135
179
  setCursorSymbol: (symbol: string) => void;
136
180
  resolveCell: ResolveTickerTableCell;
137
181
  onRowActivate?: (ticker: TickerRecord) => void;
182
+ showSparklines?: boolean;
183
+ priceHistory?: PricePoint[];
138
184
  }) {
139
185
  const lastActivatedAtRef = useRef<number | null>(null);
140
186
  const rowBg = isSelected ? colors.selected : isHovered ? hoverBg() : colors.bg;
141
187
 
188
+ const sparkline = showSparklines && priceHistory && priceHistory.length >= 5
189
+ ? renderSparkline(priceHistory)
190
+ : null;
191
+
142
192
  return (
143
193
  <box
144
194
  flexDirection="row"
@@ -175,6 +225,11 @@ const TickerListRow = memo(function TickerListRow({
175
225
  </box>
176
226
  );
177
227
  })}
228
+ {sparkline && (
229
+ <box width={12}>
230
+ <text content={sparkline} />
231
+ </box>
232
+ )}
178
233
  </box>
179
234
  );
180
235
  }, (previous, next) => (
@@ -189,6 +244,8 @@ const TickerListRow = memo(function TickerListRow({
189
244
  && previous.setCursorSymbol === next.setCursorSymbol
190
245
  && previous.resolveCell === next.resolveCell
191
246
  && previous.onRowActivate === next.onRowActivate
247
+ && previous.showSparklines === next.showSparklines
248
+ && previous.priceHistory === next.priceHistory
192
249
  ));
193
250
 
194
251
  export function TickerListTable({
@@ -211,6 +268,7 @@ export function TickerListTable({
211
268
  onRowActivate,
212
269
  emptyTitle = "No tickers.",
213
270
  emptyHint = "Press Ctrl+P to add one.",
271
+ showSparklines,
214
272
  }: TickerListTableProps) {
215
273
  const safeFlashSymbols = flashSymbols ?? new Map<string, QuoteFlashDirection>();
216
274
 
@@ -255,6 +313,8 @@ export function TickerListTable({
255
313
  setCursorSymbol={setCursorSymbol}
256
314
  resolveCell={resolveCell}
257
315
  onRowActivate={onRowActivate}
316
+ showSparklines={showSparklines}
317
+ priceHistory={financialsMap.get(ticker.metadata.ticker)?.priceHistory}
258
318
  />
259
319
  );
260
320
  })
@@ -20,6 +20,7 @@ export interface ToggleListProps {
20
20
  flexGrow?: number;
21
21
  scrollable?: boolean;
22
22
  showSelectedDescription?: boolean;
23
+ rowIdPrefix?: string;
23
24
  }
24
25
 
25
26
  export function ToggleList({
@@ -32,6 +33,7 @@ export function ToggleList({
32
33
  flexGrow,
33
34
  scrollable,
34
35
  showSelectedDescription = true,
36
+ rowIdPrefix,
35
37
  }: ToggleListProps) {
36
38
  const listItems: ListViewItem[] = items.map((item) => ({
37
39
  id: item.id,
@@ -52,17 +54,28 @@ export function ToggleList({
52
54
  onActivate={(item) => {
53
55
  onToggle?.(item.id);
54
56
  }}
55
- renderRow={(item, state) => {
57
+ renderRow={(item, state, index) => {
56
58
  const toggleItem = items.find((entry) => entry.id === item.id);
57
59
  const checked = toggleItem?.enabled ? "\u2713" : " ";
60
+ const activate = (event: any) => {
61
+ event.stopPropagation?.();
62
+ if (state.disabled) return;
63
+ onSelect?.(index);
64
+ onToggle?.(item.id);
65
+ };
58
66
  return (
59
- <box flexDirection="row">
67
+ <box
68
+ id={rowIdPrefix ? `${rowIdPrefix}:${item.id}` : undefined}
69
+ flexDirection="row"
70
+ onMouseDown={activate}
71
+ >
60
72
  <text fg={state.selected ? colors.selectedText : colors.textDim}>
61
73
  {state.selected ? "\u25b8 " : " "}
62
74
  </text>
63
75
  <text
64
76
  fg={state.selected ? colors.text : colors.textDim}
65
77
  attributes={state.selected ? TextAttributes.BOLD : 0}
78
+ onMouseDown={activate}
66
79
  >
67
80
  {`[${checked}] ${item.label}`}
68
81
  </text>
@@ -1,7 +1,8 @@
1
- import { useMemo, useState, type RefObject } from "react";
1
+ import { useCallback, useEffect, useMemo, useState, type RefObject } from "react";
2
2
  import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
3
3
  import { colors, hoverBg } from "../../theme/colors";
4
4
  import type { ColumnConfig } from "../../types/config";
5
+ import { useAppDispatch, usePaneInstance } from "../../state/app-context";
5
6
  import { padTo } from "../../utils/format";
6
7
  import { useDoubleClickActivation } from "../use-double-click-activation";
7
8
  import { EmptyState } from "./status";
@@ -14,9 +15,17 @@ export type DataTableColumn = Pick<
14
15
  export interface DataTableCell {
15
16
  text: string;
16
17
  color?: string;
18
+ attributes?: number;
17
19
  onMouseDown?: (event: any) => void;
18
20
  }
19
21
 
22
+ export interface DataTableSectionHeader {
23
+ text: string;
24
+ color?: string;
25
+ backgroundColor?: string;
26
+ attributes?: number;
27
+ }
28
+
20
29
  export interface DataTableProps<
21
30
  T,
22
31
  C extends DataTableColumn = DataTableColumn,
@@ -42,10 +51,15 @@ export interface DataTableProps<
42
51
  index: number,
43
52
  rowState: { selected: boolean; hovered: boolean },
44
53
  ) => DataTableCell;
54
+ renderSectionHeader?: (
55
+ item: T,
56
+ index: number,
57
+ ) => DataTableSectionHeader | null;
45
58
  emptyStateTitle: string;
46
59
  emptyStateHint?: string;
47
60
  virtualize?: boolean;
48
61
  overscan?: number;
62
+ showHorizontalScrollbar?: boolean;
49
63
  }
50
64
 
51
65
  interface DataTableRowPointerTarget<T> {
@@ -70,11 +84,15 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
70
84
  onSelect,
71
85
  onActivate,
72
86
  renderCell,
87
+ renderSectionHeader,
73
88
  emptyStateTitle,
74
89
  emptyStateHint,
75
90
  virtualize = false,
76
91
  overscan = 3,
92
+ showHorizontalScrollbar = true,
77
93
  }: DataTableProps<T, C>) {
94
+ const dispatch = useAppDispatch();
95
+ const paneInstanceId = usePaneInstance()?.instanceId ?? null;
78
96
  const [scrollVersion, setScrollVersion] = useState(0);
79
97
  const scrollTop = virtualize ? (scrollRef.current?.scrollTop ?? 0) : 0;
80
98
  const viewportHeight = virtualize
@@ -106,11 +124,36 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
106
124
  }
107
125
  onBodyScrollActivity();
108
126
  };
127
+ const focusPane = useCallback(() => {
128
+ if (!paneInstanceId) return;
129
+ dispatch({ type: "FOCUS_PANE", paneId: paneInstanceId });
130
+ }, [dispatch, paneInstanceId]);
131
+
132
+ useEffect(() => {
133
+ if (headerScrollRef.current) {
134
+ headerScrollRef.current.horizontalScrollBar.visible = false;
135
+ if (!showHorizontalScrollbar) {
136
+ headerScrollRef.current.scrollLeft = 0;
137
+ }
138
+ }
139
+ if (scrollRef.current) {
140
+ scrollRef.current.horizontalScrollBar.visible = showHorizontalScrollbar;
141
+ if (!showHorizontalScrollbar) {
142
+ scrollRef.current.scrollLeft = 0;
143
+ }
144
+ }
145
+ }, [columns.length, headerScrollRef, items.length, scrollRef, showHorizontalScrollbar]);
109
146
 
110
147
  return (
111
148
  <>
112
- <scrollbox ref={headerScrollRef} height={1} scrollX focusable={false}>
113
- <box flexDirection="row" height={1} paddingX={1}>
149
+ <scrollbox ref={headerScrollRef} height={1} scrollX={showHorizontalScrollbar} focusable={false}>
150
+ <box
151
+ flexDirection="row"
152
+ height={1}
153
+ width="100%"
154
+ paddingX={1}
155
+ backgroundColor={colors.panel}
156
+ >
114
157
  {columns.map((column) => {
115
158
  const isSorted = sortColumnId === column.id;
116
159
  const indicator = isSorted
@@ -127,7 +170,9 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
127
170
  <box
128
171
  key={column.id}
129
172
  width={column.width + 1}
173
+ backgroundColor={colors.panel}
130
174
  onMouseDown={(event) => {
175
+ focusPane();
131
176
  event.preventDefault();
132
177
  onHeaderClick(column.id);
133
178
  }}
@@ -147,10 +192,13 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
147
192
  <scrollbox
148
193
  ref={scrollRef}
149
194
  flexGrow={1}
150
- scrollX
195
+ scrollX={showHorizontalScrollbar}
151
196
  scrollY
152
197
  focusable={false}
153
- onMouseDown={() => queueMicrotask(syncHeaderScroll)}
198
+ onMouseDown={() => {
199
+ focusPane();
200
+ queueMicrotask(syncHeaderScroll);
201
+ }}
154
202
  onMouseUp={() => queueMicrotask(handleBodyScrollActivity)}
155
203
  onMouseDrag={() => queueMicrotask(handleBodyScrollActivity)}
156
204
  onMouseScroll={() => queueMicrotask(handleBodyScrollActivity)}
@@ -164,6 +212,32 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
164
212
  {virtualize && startIndex > 0 && <box height={startIndex} />}
165
213
  {visibleItems.map((item, visibleIndex) => {
166
214
  const index = startIndex + visibleIndex;
215
+ const sectionHeader = renderSectionHeader?.(item, index) ?? null;
216
+
217
+ if (sectionHeader) {
218
+ return (
219
+ <box
220
+ key={getItemKey(item, index)}
221
+ flexDirection="row"
222
+ height={1}
223
+ width="100%"
224
+ paddingX={1}
225
+ backgroundColor={sectionHeader.backgroundColor ?? colors.bg}
226
+ onMouseDown={(event) => {
227
+ focusPane();
228
+ event.preventDefault();
229
+ }}
230
+ >
231
+ <text
232
+ attributes={sectionHeader.attributes ?? TextAttributes.BOLD}
233
+ fg={sectionHeader.color ?? colors.textBright}
234
+ >
235
+ {sectionHeader.text}
236
+ </text>
237
+ </box>
238
+ );
239
+ }
240
+
167
241
  const selected = isSelected(item, index);
168
242
  const hovered = hoveredIdx === index && !selected;
169
243
  const rowBg = selected
@@ -181,6 +255,7 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
181
255
  backgroundColor={rowBg}
182
256
  onMouseMove={() => setHoveredIdx(index)}
183
257
  onMouseDown={(event) => {
258
+ focusPane();
184
259
  event.preventDefault();
185
260
  handleRowMouseDown(getItemKey(item, index), {
186
261
  item,
@@ -198,6 +273,7 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
198
273
  key={column.id}
199
274
  width={column.width + 1}
200
275
  onMouseDown={(event) => {
276
+ focusPane();
201
277
  if (cell.onMouseDown) {
202
278
  cell.onMouseDown(event);
203
279
  return;
@@ -211,6 +287,7 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
211
287
  }}
212
288
  >
213
289
  <text
290
+ attributes={cell.attributes ?? TextAttributes.NONE}
214
291
  fg={
215
292
  cell.color ??
216
293
  (selected ? colors.selectedText : colors.text)
@@ -1,22 +1,59 @@
1
1
  import { TextAttributes } from "@opentui/core";
2
- import { exec } from "child_process";
2
+ import { spawn } from "child_process";
3
3
  import { colors } from "../../theme/colors";
4
4
 
5
5
  export function openUrl(url: string) {
6
- const cmd = process.platform === "darwin"
7
- ? `open "${url}"`
6
+ if (!url.trim()) return;
7
+
8
+ const child = process.platform === "darwin"
9
+ ? spawn("open", [url], { detached: true, stdio: "ignore" })
8
10
  : process.platform === "win32"
9
- ? `start "" "${url}"`
10
- : `xdg-open "${url}"`;
11
- exec(cmd, { stdio: "ignore" } as any);
11
+ ? spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" })
12
+ : spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
13
+
14
+ child.unref();
15
+ }
16
+
17
+ function handleOpen(
18
+ url: string,
19
+ onOpen: (url: string) => void,
20
+ event?: { preventDefault?: () => void; stopPropagation?: () => void },
21
+ ) {
22
+ event?.preventDefault?.();
23
+ event?.stopPropagation?.();
24
+ onOpen(url);
25
+ }
26
+
27
+ export function ExternalLinkText(
28
+ { url, label, color = colors.textBright, onOpen = openUrl }: {
29
+ url: string;
30
+ label?: string;
31
+ color?: string;
32
+ onOpen?: (url: string) => void;
33
+ },
34
+ ) {
35
+ return (
36
+ <text
37
+ fg={color}
38
+ attributes={TextAttributes.UNDERLINE}
39
+ onMouseDown={(event: any) => handleOpen(url, onOpen, event)}
40
+ >
41
+ {label ?? url}
42
+ </text>
43
+ );
12
44
  }
13
45
 
14
- export function ExternalLink({ url, label }: { url: string; label?: string }) {
46
+ export function ExternalLink(
47
+ { url, label, color, onOpen }: {
48
+ url: string;
49
+ label?: string;
50
+ color?: string;
51
+ onOpen?: (url: string) => void;
52
+ },
53
+ ) {
15
54
  return (
16
- <box height={1} onMouseDown={() => openUrl(url)}>
17
- <text fg={colors.textBright} attributes={TextAttributes.UNDERLINE}>
18
- {label ?? url}
19
- </text>
55
+ <box height={1}>
56
+ <ExternalLinkText url={url} label={label} color={color} onOpen={onOpen} />
20
57
  </box>
21
58
  );
22
59
  }
@@ -82,6 +82,7 @@ export function TextField({
82
82
  ref={resolvedInputRef}
83
83
  width={width}
84
84
  value={value}
85
+ selectable={!isPassword}
85
86
  placeholder={isPassword ? "" : placeholder}
86
87
  focused={focused}
87
88
  textColor={isPassword ? backgroundColor : textColor}
@@ -115,7 +116,7 @@ export function TextField({
115
116
  resolvedInputRef.current?.focus?.();
116
117
  }}
117
118
  >
118
- <text fg={maskedTextColor}>
119
+ <text fg={maskedTextColor} selectable={false}>
119
120
  {maskedBefore}
120
121
  {focused && (
121
122
  <span bg={maskedTextColor} fg={backgroundColor}>{maskedCursorChar}</span>
@@ -6,6 +6,7 @@ export type {
6
6
  DataTableCell,
7
7
  DataTableColumn,
8
8
  DataTableProps,
9
+ DataTableSectionHeader,
9
10
  } from "./data-table";
10
11
 
11
12
  export { Tabs } from "./tabs";
@@ -14,6 +15,12 @@ export type { TabItem, TabsProps } from "./tabs";
14
15
  export { Button, IconButton } from "./button";
15
16
  export type { ButtonProps, ButtonVariant, IconButtonProps } from "./button";
16
17
 
18
+ export { MultiSelectChips } from "./multi-select-chips";
19
+ export type { MultiSelectChipsProps, MultiSelectChipOption } from "./multi-select-chips";
20
+ export { MultiSelectDialogButton, MultiSelectDialogContent } from "./multi-select-dialog";
21
+ export type { MultiSelectDialogButtonProps, MultiSelectDialogContentProps } from "./multi-select-dialog";
22
+ export type { MultiSelectOption } from "./multi-select";
23
+
17
24
  export { Checkbox, Switch, RadioGroup, SegmentedControl } from "./toggle";
18
25
  export type {
19
26
  CheckboxProps,
@@ -42,7 +49,7 @@ export type {
42
49
  export { Section, FieldRow, DialogFrame } from "./frame";
43
50
  export type { SectionProps, FieldRowProps, DialogFrameProps } from "./frame";
44
51
 
45
- export { ExternalLink, openUrl } from "./external-link";
52
+ export { ExternalLink, ExternalLinkText, openUrl } from "./external-link";
46
53
  export { PageStackView } from "./page-stack-view";
47
54
 
48
55
  export { Spinner, ProgressBar, SkeletonRow, LoadingBlock } from "./loading";
@@ -0,0 +1,69 @@
1
+ import { TextAttributes } from "@opentui/core";
2
+ import { colors } from "../../theme/colors";
3
+ import { toggleMultiSelectValue, type MultiSelectOption } from "./multi-select";
4
+
5
+ export type MultiSelectChipOption = MultiSelectOption;
6
+
7
+ export interface MultiSelectChipsProps {
8
+ label?: string;
9
+ options: MultiSelectChipOption[];
10
+ selectedValues: string[];
11
+ onChange?: (values: string[]) => void;
12
+ disabled?: boolean;
13
+ emptyLabel?: string;
14
+ idPrefix?: string;
15
+ }
16
+
17
+ export function MultiSelectChips({
18
+ label,
19
+ options,
20
+ selectedValues,
21
+ onChange,
22
+ disabled = false,
23
+ emptyLabel = "No options",
24
+ idPrefix,
25
+ }: MultiSelectChipsProps) {
26
+ const selectedSet = new Set(selectedValues);
27
+
28
+ return (
29
+ <box flexDirection="row" height={1} gap={1}>
30
+ {label && <text fg={colors.textDim}>{`${label}:`}</text>}
31
+ {options.length === 0 && <text fg={colors.textMuted}>{emptyLabel}</text>}
32
+ {options.map((option) => {
33
+ const selected = selectedSet.has(option.value);
34
+ const optionDisabled = disabled || option.disabled;
35
+ const bg = selected ? colors.selected : colors.panel;
36
+ const fg = optionDisabled
37
+ ? colors.textMuted
38
+ : selected
39
+ ? colors.selectedText
40
+ : colors.textDim;
41
+ const marker = selected ? "[x]" : "[ ]";
42
+ const text = `${marker} ${option.label}`;
43
+ const toggle = () => {
44
+ if (optionDisabled) return;
45
+ onChange?.(toggleMultiSelectValue(options, selectedValues, option.value));
46
+ };
47
+
48
+ return (
49
+ <box
50
+ key={option.value}
51
+ id={idPrefix ? `${idPrefix}:${option.value}` : undefined}
52
+ height={1}
53
+ width={text.length}
54
+ backgroundColor={bg}
55
+ onMouseDown={toggle}
56
+ >
57
+ <text
58
+ fg={fg}
59
+ attributes={selected ? TextAttributes.BOLD : 0}
60
+ onMouseDown={toggle}
61
+ >
62
+ {text}
63
+ </text>
64
+ </box>
65
+ );
66
+ })}
67
+ </box>
68
+ );
69
+ }