termcast 1.3.52 → 1.3.54

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 (50) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +209 -7
  3. package/dist/app.js.map +1 -1
  4. package/dist/cli.js +4 -4
  5. package/dist/cli.js.map +1 -1
  6. package/dist/components/candle-chart.d.ts +110 -0
  7. package/dist/components/candle-chart.d.ts.map +1 -0
  8. package/dist/components/candle-chart.js +295 -0
  9. package/dist/components/candle-chart.js.map +1 -0
  10. package/dist/components/list.d.ts.map +1 -1
  11. package/dist/components/list.js +3 -0
  12. package/dist/components/list.js.map +1 -1
  13. package/dist/components/table.d.ts +2 -0
  14. package/dist/components/table.d.ts.map +1 -1
  15. package/dist/components/table.js +41 -4
  16. package/dist/components/table.js.map +1 -1
  17. package/dist/examples/simple-candle-chart-data.d.ts +9064 -0
  18. package/dist/examples/simple-candle-chart-data.d.ts.map +1 -0
  19. package/dist/examples/simple-candle-chart-data.js +12683 -0
  20. package/dist/examples/simple-candle-chart-data.js.map +1 -0
  21. package/dist/examples/simple-candle-chart.d.ts +2 -0
  22. package/dist/examples/simple-candle-chart.d.ts.map +1 -0
  23. package/dist/examples/simple-candle-chart.js +125 -0
  24. package/dist/examples/simple-candle-chart.js.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/internal/dialog.d.ts +1 -0
  30. package/dist/internal/dialog.d.ts.map +1 -1
  31. package/dist/internal/dialog.js +4 -0
  32. package/dist/internal/dialog.js.map +1 -1
  33. package/dist/state.d.ts +1 -0
  34. package/dist/state.d.ts.map +1 -1
  35. package/dist/state.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/app.tsx +269 -8
  38. package/src/cli.tsx +5 -5
  39. package/src/components/candle-chart.tsx +410 -0
  40. package/src/components/list.tsx +3 -0
  41. package/src/components/table.tsx +46 -4
  42. package/src/examples/simple-candle-chart-data.ts +12683 -0
  43. package/src/examples/simple-candle-chart.tsx +363 -0
  44. package/src/examples/simple-candle-chart.vitest.tsx +269 -0
  45. package/src/examples/simple-detail-table.vitest.tsx +2 -2
  46. package/src/examples/simple-table-wrap.vitest.tsx +19 -19
  47. package/src/examples/table-flex-grow.vitest.tsx +8 -8
  48. package/src/index.tsx +7 -0
  49. package/src/internal/dialog.tsx +5 -0
  50. package/src/state.tsx +1 -0
@@ -0,0 +1,410 @@
1
+ /**
2
+ * CandleChart component for rendering trading-style OHLC candlestick charts.
3
+ *
4
+ * Uses a custom opentui Renderable (CandleChartRenderable) for direct drawing
5
+ * to OptimizedBuffer via setCell(). Each data point occupies exactly one
6
+ * terminal column, rendering:
7
+ * - Body (open-to-close): left-half block characters (▌/▘/▖) with 2x
8
+ * vertical sub-row resolution
9
+ * - Wick (high-to-low): thin vertical line (│) extending above/below body
10
+ *
11
+ * Color encodes direction:
12
+ * - Green (default): close >= open (bullish)
13
+ * - Red (default): close < open (bearish)
14
+ *
15
+ * Layout reuses the same Y-axis + X-axis label pattern as Graph component.
16
+ */
17
+
18
+ import { useMemo } from 'react'
19
+ import { Renderable, RGBA } from '@opentui/core'
20
+ import type { RenderableOptions, RenderContext } from '@opentui/core'
21
+ import type { OptimizedBuffer } from '@opentui/core'
22
+ import { extend } from '@opentui/react'
23
+ import { useTheme } from 'termcast/src/theme'
24
+ import { Color, resolveColor } from 'termcast/src/colors'
25
+
26
+ // ── Data types ───────────────────────────────────────────────────────
27
+
28
+ export interface CandleData {
29
+ /** Price at the start of the period */
30
+ open: number
31
+ /** Price at the end of the period */
32
+ close: number
33
+ /** Highest price during the period */
34
+ high: number
35
+ /** Lowest price during the period */
36
+ low: number
37
+ }
38
+
39
+ // ── Block characters (same as Graph filled variant) ──────────────────
40
+ // Left-half and quadrant characters give 2x vertical sub-row resolution
41
+ // while keeping each bar 50% cell width for visible gaps between columns.
42
+ const LEFT_HALF = '▌' // U+258C — full height, left 50%
43
+ const QUAD_UL = '▘' // U+2598 — top half, left 50%
44
+ const QUAD_LL = '▖' // U+2596 — bottom half, left 50%
45
+
46
+ // ── CandleChartRenderable ────────────────────────────────────────────
47
+
48
+ export interface CandleChartPlotOptions extends RenderableOptions {
49
+ candles?: CandleData[]
50
+ xLabels?: string[]
51
+ yMin?: number
52
+ yMax?: number
53
+ yTicks?: number
54
+ yFormat?: (v: number) => string
55
+ axisColor?: string
56
+ upColor?: string // hex color for bullish candles (close >= open)
57
+ downColor?: string // hex color for bearish candles (close < open)
58
+ wickColor?: string // hex color for wick lines
59
+ }
60
+
61
+ export class CandleChartRenderable extends Renderable {
62
+ private _candles: CandleData[] = []
63
+ private _xLabels: string[] = []
64
+ private _yMin = 0
65
+ private _yMax = 100
66
+ private _yTicks = 5
67
+ private _yFormat: (v: number) => string = (v) => {
68
+ return v >= 1000 ? v.toFixed(0) : v.toFixed(1)
69
+ }
70
+ private _axisColor: string = '#666666'
71
+ private _upColor: string = '#34EE7F' // Color.Green
72
+ private _downColor: string = '#FF7B7B' // Color.Red
73
+ private _wickColor: string = '#666666'
74
+
75
+ constructor(ctx: RenderContext, options: CandleChartPlotOptions) {
76
+ super(ctx, options)
77
+ if (options.candles) this._candles = options.candles
78
+ if (options.xLabels) this._xLabels = options.xLabels
79
+ if (options.yMin !== undefined) this._yMin = options.yMin
80
+ if (options.yMax !== undefined) this._yMax = options.yMax
81
+ if (options.yTicks !== undefined) this._yTicks = options.yTicks
82
+ if (options.yFormat) this._yFormat = options.yFormat
83
+ if (options.axisColor) this._axisColor = options.axisColor
84
+ if (options.upColor) this._upColor = options.upColor
85
+ if (options.downColor) this._downColor = options.downColor
86
+ if (options.wickColor) this._wickColor = options.wickColor
87
+ }
88
+
89
+ set candles(value: CandleData[]) { this._candles = value; this.requestRender() }
90
+ set xLabels(value: string[]) { this._xLabels = value; this.requestRender() }
91
+ set yMin(value: number) { this._yMin = value; this.requestRender() }
92
+ set yMax(value: number) { this._yMax = value; this.requestRender() }
93
+ set yTicks(value: number) { this._yTicks = value; this.requestRender() }
94
+ set yFormat(value: (v: number) => string) { this._yFormat = value; this.requestRender() }
95
+ set axisColor(value: string) { this._axisColor = value; this.requestRender() }
96
+ set upColor(value: string) { this._upColor = value; this.requestRender() }
97
+ set downColor(value: string) { this._downColor = value; this.requestRender() }
98
+ set wickColor(value: string) { this._wickColor = value; this.requestRender() }
99
+
100
+ // ── Layout: compute plot area and Y labels ─────────────────────
101
+ private computeLayout(): {
102
+ plotX: number; plotY: number; plotW: number; plotH: number
103
+ yAxisWidth: number; yLabels: string[]
104
+ } | null {
105
+ const totalW = this.width
106
+ const totalH = this.height
107
+ if (totalW <= 0 || totalH <= 0) return null
108
+
109
+ const safeTicks = Math.max(2, this._yTicks)
110
+ const yLabels: string[] = []
111
+ for (let i = 0; i < safeTicks; i++) {
112
+ const value = this._yMin + (this._yMax - this._yMin) * (1 - i / (safeTicks - 1))
113
+ yLabels.push(this._yFormat(value))
114
+ }
115
+ const yAxisWidth = Math.max(...yLabels.map((l) => l.length))
116
+
117
+ const plotX = this.x + yAxisWidth + 1
118
+ const plotY = this.y
119
+ const plotH = totalH - 1
120
+ const plotW = totalW - yAxisWidth - 1
121
+ if (plotW <= 0 || plotH <= 0) return null
122
+
123
+ return { plotX, plotY, plotW, plotH, yAxisWidth, yLabels }
124
+ }
125
+
126
+ // ── Axes: Y labels + separator, X labels ──────────────────────
127
+ private drawAxes(buffer: OptimizedBuffer, layout: {
128
+ plotX: number; plotY: number; plotW: number; plotH: number
129
+ yAxisWidth: number; yLabels: string[]
130
+ }): void {
131
+ const { plotX, plotY, plotW, plotH, yAxisWidth, yLabels } = layout
132
+ const axisRgba = RGBA.fromHex(this._axisColor)
133
+
134
+ // Y-axis labels + separator
135
+ const safeTicks = Math.max(2, this._yTicks)
136
+ const labelRows = new Set<number>()
137
+ for (let i = 0; i < safeTicks; i++) {
138
+ const row = Math.round(plotY + (i / (safeTicks - 1)) * (plotH - 1))
139
+ labelRows.add(row)
140
+ const label = yLabels[i]!
141
+ buffer.drawText(label, this.x + yAxisWidth - label.length, row, axisRgba)
142
+ buffer.drawText('│', this.x + yAxisWidth, row, axisRgba)
143
+ }
144
+ for (let row = plotY; row < plotY + plotH; row++) {
145
+ if (!labelRows.has(row)) {
146
+ buffer.drawText('│', this.x + yAxisWidth, row, axisRgba)
147
+ }
148
+ }
149
+
150
+ // X-axis labels
151
+ if (this._xLabels.length > 0) {
152
+ const xAxisRow = plotY + plotH
153
+ const labelCount = this._xLabels.length
154
+ for (let i = 0; i < labelCount; i++) {
155
+ const label = this._xLabels[i]!
156
+ const labelX = plotX + Math.round((i / Math.max(1, labelCount - 1)) * (plotW - 1))
157
+ const centeredX = Math.max(plotX, Math.min(labelX - Math.floor(label.length / 2), plotX + plotW - label.length))
158
+ buffer.drawText(label, centeredX, xAxisRow, axisRgba)
159
+ }
160
+ }
161
+ }
162
+
163
+ // ── Convert a price value to a virtual sub-row position ────────
164
+ // Returns a float in [0, virtualH-1] where 0 = top (yMax), virtualH-1 = bottom (yMin)
165
+ private priceToVRow({ price, virtualH }: { price: number; virtualH: number }): number {
166
+ const yRange = this._yMax - this._yMin
167
+ if (yRange === 0) return 0
168
+ const normalized = (price - this._yMin) / yRange
169
+ return (1 - normalized) * (virtualH - 1)
170
+ }
171
+
172
+ // ── Aggregate candles into column buckets ───────────────────────
173
+ // When data.length > plotW, multiple candles share a column. We bucket
174
+ // them with standard OHLC aggregation: open=first.open, close=last.close,
175
+ // high=max(highs), low=min(lows). When data.length <= plotW, candles map
176
+ // 1:1 to contiguous columns (right-aligned so latest data is at the right edge).
177
+ private aggregateToColumns({ plotW }: { plotW: number }): CandleData[] {
178
+ const candles = this._candles
179
+ if (candles.length === 0) return []
180
+
181
+ if (candles.length <= plotW) {
182
+ // 1:1 mapping — return as-is, caller will right-align
183
+ return candles
184
+ }
185
+
186
+ // Bucket candles into plotW columns
187
+ const result: CandleData[] = []
188
+ for (let col = 0; col < plotW; col++) {
189
+ const start = Math.floor((col * candles.length) / plotW)
190
+ const end = Math.floor(((col + 1) * candles.length) / plotW)
191
+ const slice = candles.slice(start, Math.max(start + 1, end))
192
+ const first = slice[0]!
193
+ const last = slice[slice.length - 1]!
194
+ result.push({
195
+ open: first.open,
196
+ close: last.close,
197
+ high: Math.max(...slice.map((c) => c.high)),
198
+ low: Math.min(...slice.map((c) => c.low)),
199
+ })
200
+ }
201
+ return result
202
+ }
203
+
204
+ // ── Render candles ─────────────────────────────────────────────
205
+ // Each candle occupies exactly one terminal column. Uses 2x vertical
206
+ // sub-row resolution (same as Graph filled variant).
207
+ //
208
+ // For each column:
209
+ // 1. Map OHLC to virtual Y positions
210
+ // 2. Draw wick (│) from high to low, excluding body range
211
+ // 3. Draw body (▌/▘/▖) from min(open,close) to max(open,close)
212
+ // 4. Color: green if close >= open, red otherwise
213
+ private renderCandles(buffer: OptimizedBuffer, plotX: number, plotY: number, plotW: number, plotH: number): void {
214
+ const transparent = RGBA.fromValues(0, 0, 0, 0)
215
+ const virtualH = plotH * 2 // 2 sub-rows per terminal row
216
+ const yRange = this._yMax - this._yMin
217
+ if (yRange === 0) return
218
+
219
+ const upRgba = RGBA.fromHex(this._upColor)
220
+ const downRgba = RGBA.fromHex(this._downColor)
221
+ const wickRgba = RGBA.fromHex(this._wickColor)
222
+
223
+ const columns = this.aggregateToColumns({ plotW })
224
+ if (columns.length === 0) return
225
+
226
+ // Right-align: latest candles at the right edge of the plot.
227
+ // When fewer candles than columns, offset pushes them rightward.
228
+ const offset = plotW - columns.length
229
+
230
+ for (let i = 0; i < columns.length; i++) {
231
+ const candle = columns[i]!
232
+ const col = offset + i
233
+
234
+ if (col < 0 || col >= plotW) continue
235
+
236
+ const isBullish = candle.close >= candle.open
237
+ const bodyColor = isBullish ? upRgba : downRgba
238
+
239
+ // Convert prices to virtual sub-row positions (0 = top, virtualH-1 = bottom)
240
+ const highVRow = Math.round(this.priceToVRow({ price: candle.high, virtualH }))
241
+ const lowVRow = Math.round(this.priceToVRow({ price: candle.low, virtualH }))
242
+ const openVRow = Math.round(this.priceToVRow({ price: candle.open, virtualH }))
243
+ const closeVRow = Math.round(this.priceToVRow({ price: candle.close, virtualH }))
244
+
245
+ const bodyTopVRow = Math.min(openVRow, closeVRow)
246
+ const bodyBotVRow = Math.max(openVRow, closeVRow)
247
+
248
+ // Clamp all values to valid range
249
+ const wickTop = Math.max(0, Math.min(highVRow, virtualH - 1))
250
+ const wickBot = Math.max(0, Math.min(lowVRow, virtualH - 1))
251
+ const bodyTop = Math.max(0, Math.min(bodyTopVRow, virtualH - 1))
252
+ const bodyBot = Math.max(0, Math.min(bodyBotVRow, virtualH - 1))
253
+
254
+ // Draw each terminal row for this column
255
+ for (let row = 0; row < plotH; row++) {
256
+ const vTop = row * 2 // virtual sub-row for top half of this terminal row
257
+ const vBot = row * 2 + 1 // virtual sub-row for bottom half
258
+
259
+ // Check what each sub-row contains: body, wick, or nothing
260
+ const topIsBody = vTop >= bodyTop && vTop <= bodyBot
261
+ const botIsBody = vBot >= bodyTop && vBot <= bodyBot
262
+ const topIsWick = !topIsBody && vTop >= wickTop && vTop <= wickBot
263
+ const botIsWick = !botIsBody && vBot >= wickTop && vBot <= wickBot
264
+
265
+ if (topIsBody || botIsBody) {
266
+ // Body takes priority — draw block character
267
+ if (topIsBody && botIsBody) {
268
+ buffer.setCell(plotX + col, plotY + row, LEFT_HALF, bodyColor, transparent)
269
+ } else if (topIsBody) {
270
+ // Top is body, bottom might be wick
271
+ buffer.setCell(plotX + col, plotY + row, QUAD_UL, bodyColor, transparent)
272
+ } else {
273
+ // Bottom is body, top might be wick
274
+ buffer.setCell(plotX + col, plotY + row, QUAD_LL, bodyColor, transparent)
275
+ }
276
+ } else if (topIsWick || botIsWick) {
277
+ // Only wick in this terminal row — draw thin line
278
+ buffer.setCell(plotX + col, plotY + row, '│', wickRgba, transparent)
279
+ }
280
+ }
281
+
282
+ // Handle doji case: when open == close, body is a single sub-row line.
283
+ // If bodyTop == bodyBot, ensure at least one cell is drawn with body color.
284
+ if (bodyTop === bodyBot) {
285
+ const row = Math.floor(bodyTop / 2)
286
+ const subRow = bodyTop % 2
287
+ if (row >= 0 && row < plotH) {
288
+ const char = subRow === 0 ? QUAD_UL : QUAD_LL
289
+ buffer.setCell(plotX + col, plotY + row, char, bodyColor, transparent)
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ // ── Main render ────────────────────────────────────────────────
296
+ protected renderSelf(buffer: OptimizedBuffer): void {
297
+ const layout = this.computeLayout()
298
+ if (!layout) return
299
+
300
+ this.drawAxes(buffer, layout)
301
+
302
+ const yRange = this._yMax - this._yMin
303
+ if (yRange === 0 || this._candles.length === 0) return
304
+
305
+ const { plotX, plotY, plotW, plotH } = layout
306
+ this.renderCandles(buffer, plotX, plotY, plotW, plotH)
307
+ }
308
+ }
309
+
310
+ // ── Register the custom renderable ───────────────────────────────────
311
+
312
+ extend({ 'candle-chart-plot': CandleChartRenderable })
313
+
314
+ declare module '@opentui/react' {
315
+ interface OpenTUIComponents {
316
+ 'candle-chart-plot': typeof CandleChartRenderable
317
+ }
318
+ }
319
+
320
+ // ── CandleChart React component ──────────────────────────────────────
321
+
322
+ export interface CandleChartProps {
323
+ /** OHLC candle data. Each entry maps to one terminal column. When there
324
+ * are more candles than available columns, adjacent candles are aggregated
325
+ * into OHLC buckets (open=first, close=last, high=max, low=min). */
326
+ data: CandleData[]
327
+ /** Height of the plot area in terminal rows. The total rendered height is
328
+ * this value plus one extra row for X-axis labels (if provided). Default: 15. */
329
+ height?: number
330
+ /** Labels displayed along the X-axis below the chart, evenly spaced
331
+ * from left to right (e.g. `['12d', '8d', '4d', 'Now']`). */
332
+ xLabels?: string[]
333
+ /** Explicit Y-axis range as `[min, max]`. When omitted the range is
334
+ * auto-computed from the data's lowest `low` and highest `high` values
335
+ * with 5% padding so candles don't touch the top/bottom edges. */
336
+ yRange?: [number, number]
337
+ /** Number of evenly spaced tick labels drawn along the Y-axis. Controls
338
+ * how many price labels appear on the left side of the chart. Default: 5.
339
+ * Minimum effective value is 2 (top and bottom). */
340
+ yTicks?: number
341
+ /** Formatter for Y-axis tick labels. Receives the numeric price value and
342
+ * should return a display string (e.g. `(v) => '$' + v.toFixed(0)`).
343
+ * Default: values >= 1000 use `.toFixed(0)`, otherwise `.toFixed(1)`. */
344
+ yFormat?: (v: number) => string
345
+ /** Color for bullish candles where close >= open. Default: Color.Green. */
346
+ upColor?: Color.ColorLike
347
+ /** Color for bearish candles where close < open. Default: Color.Red. */
348
+ downColor?: Color.ColorLike
349
+ }
350
+
351
+ interface CandleChartType {
352
+ (props: CandleChartProps): any
353
+ }
354
+
355
+ const CandleChart: CandleChartType = (props) => {
356
+ const theme = useTheme()
357
+ const {
358
+ data,
359
+ height = 15,
360
+ xLabels = [],
361
+ yRange,
362
+ yTicks = 5,
363
+ yFormat,
364
+ upColor,
365
+ downColor,
366
+ } = props
367
+
368
+ const resolvedUpColor = resolveColor(upColor) || Color.Green
369
+ const resolvedDownColor = resolveColor(downColor) || Color.Red
370
+
371
+ // Auto-compute Y range from data high/low if not provided
372
+ const computedYRange = useMemo<[number, number]>(() => {
373
+ if (yRange) return yRange
374
+ let min = Infinity
375
+ let max = -Infinity
376
+ for (const candle of data) {
377
+ if (candle.low < min) min = candle.low
378
+ if (candle.high > max) max = candle.high
379
+ }
380
+ if (min === Infinity) return [0, 100]
381
+ // Add padding so candles don't touch edges.
382
+ // When max === min (flat data), use absolute padding to avoid zero range.
383
+ const padding = max === min
384
+ ? Math.max(Math.abs(max) * 0.01, 1)
385
+ : (max - min) * 0.05
386
+ return [min - padding, max + padding]
387
+ }, [data, yRange])
388
+
389
+ // Total height = plot rows + 1 for x-axis labels
390
+ const totalHeight = height + (xLabels.length > 0 ? 1 : 0)
391
+
392
+ return (
393
+ <candle-chart-plot
394
+ width="100%"
395
+ height={totalHeight}
396
+ candles={data}
397
+ xLabels={xLabels}
398
+ yMin={computedYRange[0]}
399
+ yMax={computedYRange[1]}
400
+ yTicks={yTicks}
401
+ yFormat={yFormat}
402
+ axisColor={theme.textMuted}
403
+ upColor={resolvedUpColor}
404
+ downColor={resolvedDownColor}
405
+ wickColor={theme.textMuted}
406
+ />
407
+ )
408
+ }
409
+
410
+ export { CandleChart }
@@ -1970,6 +1970,9 @@ const ListDropdown: ListDropdownType = (props) => {
1970
1970
  </ListDropdownDialog>
1971
1971
  ),
1972
1972
  position: 'center',
1973
+ onClose: () => {
1974
+ setIsDropdownOpen(false)
1975
+ },
1973
1976
  })
1974
1977
  }
1975
1978
  }, [isDropdownOpen, props.children])
@@ -159,6 +159,33 @@ export class TableRenderable extends Renderable {
159
159
  return new StyledText(styledChunks)
160
160
  }
161
161
 
162
+ private getCellContentWidth(content: TableCellContent): number {
163
+ return this.toStyledText(content).chunks.reduce((width, chunk) => {
164
+ return width + chunk.text.length
165
+ }, 0)
166
+ }
167
+
168
+ private getColumnWidths({ colCount }: { colCount: number }): number[] {
169
+ const minColumnWidth = 3
170
+ const maxColumnWidth = 32
171
+ const headerWidths = Array.from({ length: colCount }, (_, col) => {
172
+ const headerContent = this._headers[col] ?? ''
173
+ return this.getCellContentWidth(headerContent) + 2
174
+ })
175
+
176
+ const rowWidths = this._rows.reduce((widths, row) => {
177
+ return widths.map((currentWidth, col) => {
178
+ const cellContent = row[col] ?? ''
179
+ const cellWidth = this.getCellContentWidth(cellContent) + 2
180
+ return Math.max(currentWidth, cellWidth)
181
+ })
182
+ }, headerWidths)
183
+
184
+ return rowWidths.map((width) => {
185
+ return Math.min(maxColumnWidth, Math.max(minColumnWidth, width))
186
+ })
187
+ }
188
+
162
189
  private rebuild(): void {
163
190
  // Remove all existing children (copy array since remove mutates it)
164
191
  const children = [...(this as any)._childrenInLayoutOrder] as Renderable[]
@@ -260,6 +287,8 @@ export class TableRenderable extends Renderable {
260
287
  headerFg: StyleDefinition['fg'],
261
288
  stripeBg: StyleDefinition['fg'],
262
289
  ): void {
290
+ const columnWidths = this.getColumnWidths({ colCount })
291
+
263
292
  if (this._headers.length > 0) {
264
293
  const headerRow = new BoxRenderable(this.ctx, {
265
294
  id: `${this.id}-header-row`,
@@ -267,6 +296,7 @@ export class TableRenderable extends Renderable {
267
296
  backgroundColor: headerBg,
268
297
  })
269
298
  for (let col = 0; col < colCount; col++) {
299
+ const columnWidth = columnWidths[col] ?? 1
270
300
  const headerContent = this._headers[col] ?? ''
271
301
  let headerStyledText = this.toStyledText(headerContent)
272
302
  headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
@@ -275,8 +305,9 @@ export class TableRenderable extends Renderable {
275
305
  new TextRenderable(this.ctx, {
276
306
  id: `${this.id}-header-${col}`,
277
307
  content: headerStyledText,
278
- flexGrow: 1,
279
- flexBasis: 0,
308
+ flexGrow: columnWidth,
309
+ flexShrink: 1,
310
+ flexBasis: columnWidth,
280
311
  paddingLeft: 1,
281
312
  paddingRight: 1,
282
313
  }),
@@ -294,6 +325,7 @@ export class TableRenderable extends Renderable {
294
325
  })
295
326
 
296
327
  for (let col = 0; col < colCount; col++) {
328
+ const columnWidth = columnWidths[col] ?? 1
297
329
  const cell = this._rows[row]?.[col] ?? ''
298
330
  const cellContent = this.toStyledText(cell)
299
331
 
@@ -301,8 +333,9 @@ export class TableRenderable extends Renderable {
301
333
  new TextRenderable(this.ctx, {
302
334
  id: `${this.id}-row-${row}-col-${col}`,
303
335
  content: cellContent,
304
- flexGrow: 1,
305
- flexBasis: 0,
336
+ flexGrow: columnWidth,
337
+ flexShrink: 1,
338
+ flexBasis: columnWidth,
306
339
  paddingLeft: 1,
307
340
  paddingRight: 1,
308
341
  }),
@@ -384,6 +417,7 @@ export class TableRenderable extends Renderable {
384
417
  const allRows = (this as any)._childrenInLayoutOrder as Renderable[]
385
418
  const colCount = this._headers.length || this._rows[0]?.length || 0
386
419
  const hasHeaders = this._headers.length > 0
420
+ const columnWidths = this.getColumnWidths({ colCount })
387
421
 
388
422
  if (hasHeaders) {
389
423
  const headerRow = allRows[0]
@@ -391,11 +425,15 @@ export class TableRenderable extends Renderable {
391
425
  headerRow.backgroundColor = headerBg ?? 'transparent'
392
426
  const headerCells = (headerRow as any)._childrenInLayoutOrder as Renderable[]
393
427
  for (let col = 0; col < colCount; col++) {
428
+ const columnWidth = columnWidths[col] ?? 1
394
429
  const headerText = headerCells[col]
395
430
  if (headerText instanceof TextRenderable) {
396
431
  const headerContent = this._headers[col] ?? ''
397
432
  let headerStyledText = this.toStyledText(headerContent)
398
433
  headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
434
+ headerText.flexGrow = columnWidth
435
+ headerText.flexShrink = 1
436
+ headerText.flexBasis = columnWidth
399
437
  headerText.content = headerStyledText
400
438
  }
401
439
  }
@@ -412,9 +450,13 @@ export class TableRenderable extends Renderable {
412
450
 
413
451
  const rowCells = (rowBox as any)._childrenInLayoutOrder as Renderable[]
414
452
  for (let col = 0; col < colCount; col++) {
453
+ const columnWidth = columnWidths[col] ?? 1
415
454
  const cellText = rowCells[col]
416
455
  if (cellText instanceof TextRenderable) {
417
456
  const cell = this._rows[row]?.[col] ?? ''
457
+ cellText.flexGrow = columnWidth
458
+ cellText.flexShrink = 1
459
+ cellText.flexBasis = columnWidth
418
460
  cellText.content = this.toStyledText(cell)
419
461
  }
420
462
  }