@effect-tui/react 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-tui/react",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "React bindings for @effect-tui/core",
5
5
  "type": "module",
6
6
  "files": [
@@ -83,7 +83,7 @@
83
83
  "prepublishOnly": "bun run typecheck && bun run build"
84
84
  },
85
85
  "dependencies": {
86
- "@effect-tui/core": "^0.15.0",
86
+ "@effect-tui/core": "^0.15.2",
87
87
  "@effect/platform": "^0.94.1",
88
88
  "@effect/platform-bun": "^0.87.0",
89
89
  "@effect/rpc": "^0.73.0",
package/src/hosts/box.ts CHANGED
@@ -59,8 +59,10 @@ export class BoxHost extends SingleChildHost {
59
59
  }
60
60
 
61
61
  private get insetY(): number {
62
- const dividerHeight = this.titleDivider && this.title && this.border !== "none" ? 1 : 0
63
- return this.borderThickness + dividerHeight + this.padding.top + this.padding.bottom + this.borderThickness
62
+ // When titleDivider is enabled: top border + title row + divider line = 3
63
+ // When title but no divider: title embedded in top border = 1
64
+ const titleHeight = this.titleDivider && this.title && this.border !== "none" ? 2 : 0 // title row + divider
65
+ return this.borderThickness + titleHeight + this.padding.top + this.padding.bottom + this.borderThickness
64
66
  }
65
67
 
66
68
  measure(maxW: number, maxH: number): Size {
@@ -93,10 +95,11 @@ export class BoxHost extends SingleChildHost {
93
95
  const layoutRect = this.layoutWithConstraints(rect)
94
96
 
95
97
  const t = this.borderThickness
96
- const dividerHeight = this.titleDivider && this.title && this.border !== "none" ? 1 : 0
98
+ // When titleDivider: title row (1) + divider line (1) = 2
99
+ const titleHeight = this.titleDivider && this.title && this.border !== "none" ? 2 : 0
97
100
  const innerRect: Rect = {
98
101
  x: layoutRect.x + t + this.padding.left,
99
- y: layoutRect.y + t + dividerHeight + this.padding.top,
102
+ y: layoutRect.y + t + titleHeight + this.padding.top,
100
103
  w: Math.max(0, layoutRect.w - this.insetX),
101
104
  h: Math.max(0, layoutRect.h - this.insetY),
102
105
  }
@@ -123,29 +126,40 @@ export class BoxHost extends SingleChildHost {
123
126
  const borderStyle = palette.id({ fg: borderFg })
124
127
  drawBorder(buffer, x, y, w, h, chars, borderStyle)
125
128
 
126
- // Draw title on top border if present
129
+ // Draw title if present
127
130
  if (this.title && w >= 7) {
128
131
  const titleFg = toColorValue(this.titleColor) ?? borderFg
129
132
  const titleStyle = palette.id({ fg: titleFg, bold: this.titleBold })
130
- // Reserve: ┌─ (2) + space before (1) + space after (1) + ─┐ (2) = 6 chars
131
- const maxTitleLen = w - 6
133
+ // Reserve space for borders and padding: 4 chars ( + space + space + )
134
+ const maxTitleLen = w - 4
132
135
  const displayTitle =
133
136
  this.title.length > maxTitleLen ? this.title.slice(0, maxTitleLen - 1) + "…" : this.title
134
- // Draw " Title " starting at x+2, limiting width to just the title text
135
- const titleX = x + 2
136
- const titleText = ` ${displayTitle} `
137
- buffer.drawText(titleX, y, titleText, titleStyle, titleText.length)
138
-
139
- // Draw divider line below title if enabled
140
- if (this.titleDivider && h >= 3) {
141
- const dividerY = y + 1
137
+
138
+ if (this.titleDivider && h >= 4) {
139
+ // Title in its own row: │ Title │
140
+ const titleRowY = y + 1
141
+ const titleText = ` ${displayTitle}`
142
+ buffer.drawText(x, titleRowY, chars.v, borderStyle, 1)
143
+ buffer.drawText(x + 1, titleRowY, titleText, titleStyle, titleText.length)
144
+ // Fill remaining space and draw right border
145
+ for (let dx = 1 + titleText.length; dx < w - 1; dx++) {
146
+ buffer.drawText(x + dx, titleRowY, " ", borderStyle, 1)
147
+ }
148
+ buffer.drawText(x + w - 1, titleRowY, chars.v, borderStyle, 1)
149
+
150
+ // Divider line: ├───┤
151
+ const dividerY = y + 2
142
152
  const tableChars = tableBorderChars(this.border)
143
- // Draw ├ at left, ─ across, ┤ at right
144
153
  buffer.drawText(x, dividerY, tableChars.lt, borderStyle, 1)
145
154
  for (let dx = 1; dx < w - 1; dx++) {
146
155
  buffer.drawText(x + dx, dividerY, tableChars.h, borderStyle, 1)
147
156
  }
148
157
  buffer.drawText(x + w - 1, dividerY, tableChars.rt, borderStyle, 1)
158
+ } else {
159
+ // Title embedded in top border: ┌─ Title ─┐
160
+ const titleX = x + 2
161
+ const titleText = ` ${displayTitle} `
162
+ buffer.drawText(titleX, y, titleText, titleStyle, titleText.length)
149
163
  }
150
164
  }
151
165
  }
@@ -1,6 +1,6 @@
1
1
  import { type CellBuffer, type Color, Colors, displayWidth, type Palette } from "@effect-tui/core"
2
2
  import type { HighlightLine } from "../highlight.js"
3
- import type { CommonProps, HostContext, Rect, Size } from "../reconciler/types.js"
3
+ import type { CommonProps, HostContext, Size } from "../reconciler/types.js"
4
4
  import { type Padding, type PaddingInput, resolveBgStyle, resolvePadding, styleIdFromProps } from "../utils/index.js"
5
5
  import { BaseHost } from "./base.js"
6
6
 
@@ -27,6 +27,7 @@ export class CodeBlockHost extends BaseHost {
27
27
 
28
28
  private cachedLineWidths: number[] = []
29
29
  private gutterWidth = 0
30
+ private prepared = false
30
31
 
31
32
  constructor(props: CodeBlockProps, ctx: HostContext) {
32
33
  super("codeblock", props, ctx)
@@ -49,10 +50,24 @@ export class CodeBlockHost extends BaseHost {
49
50
  return this.padding.top + this.padding.bottom
50
51
  }
51
52
 
53
+ private prepareMetrics(): void {
54
+ this.cachedLineWidths = this.lines.map((line) => lineDisplayWidth(line))
55
+ this.gutterWidth = this.computeGutterWidth()
56
+ this.prepared = true
57
+ }
58
+
59
+ private ensurePrepared(): void {
60
+ if (this.prepared) return
61
+ this.prepareMetrics()
62
+ }
63
+
64
+ protected override prepareSelf(): void {
65
+ this.ensurePrepared()
66
+ }
67
+
52
68
  measure(maxW: number, maxH: number): Size {
53
69
  const constrained = this.constrainProposal(maxW, maxH)
54
- this.cachedLineWidths = this.lines.map((l) => lineDisplayWidth(l))
55
- this.gutterWidth = this.computeGutterWidth()
70
+ this.ensurePrepared()
56
71
 
57
72
  const maxLineW = this.cachedLineWidths.reduce((max, w) => (w > max ? w : max), 0)
58
73
  const contentW = maxLineW + this.insetX
@@ -65,10 +80,6 @@ export class CodeBlockHost extends BaseHost {
65
80
  })
66
81
  }
67
82
 
68
- override layout(rect: Rect): void {
69
- super.layout(rect)
70
- }
71
-
72
83
  render(buffer: CellBuffer, palette: Palette): void {
73
84
  if (!this.rect) return
74
85
 
@@ -120,11 +131,19 @@ export class CodeBlockHost extends BaseHost {
120
131
 
121
132
  override updateProps(props: Record<string, unknown>): void {
122
133
  super.updateProps(props)
123
- if (props.lines !== undefined) this.lines = props.lines as HighlightLine[]
124
- if (props.lineNumbers !== undefined) this.lineNumbers = !!props.lineNumbers
134
+ let invalidate = false
135
+ if (props.lines !== undefined) {
136
+ this.lines = props.lines as HighlightLine[]
137
+ invalidate = true
138
+ }
139
+ if (props.lineNumbers !== undefined) {
140
+ this.lineNumbers = !!props.lineNumbers
141
+ invalidate = true
142
+ }
125
143
  if (props.padding !== undefined) this.padding = resolvePadding(props.padding as CodeBlockProps["padding"])
126
144
  if (props.background !== undefined) this.background = props.background as Color
127
145
  if (props.lineNumberColor !== undefined) this.lineNumberColor = props.lineNumberColor as Color
128
146
  if (props.lineNumberBackground !== undefined) this.lineNumberBackground = props.lineNumberBackground as Color
147
+ if (invalidate) this.prepared = false
129
148
  }
130
149
  }
@@ -1,5 +1,5 @@
1
1
  import type { CellBuffer, Palette } from "@effect-tui/core"
2
- import type { CommonProps, HostContext, Rect, Size } from "../reconciler/types.js"
2
+ import type { CommonProps, HostContext, Size } from "../reconciler/types.js"
3
3
  import { BaseHost } from "./base.js"
4
4
 
5
5
  export interface SpacerProps extends CommonProps {
@@ -26,10 +26,6 @@ export class SpacerHost extends BaseHost {
26
26
  return { w: this.minWidth, h: this.minHeight }
27
27
  }
28
28
 
29
- override layout(rect: Rect): void {
30
- super.layout(rect)
31
- }
32
-
33
29
  render(_buffer: CellBuffer, _palette: Palette): void {
34
30
  // Spacers render nothing
35
31
  }
@@ -57,9 +57,6 @@ const hostConfig = {
57
57
  supportsHydration: false,
58
58
 
59
59
  createInstance(type: Type, props: Props, rootContainer: Container) {
60
- if (type === "vstack") {
61
- console.log("createInstance vstack props", props)
62
- }
63
60
  const instance = createHostInstance(type, props, rootContainer.ctx)
64
61
 
65
62
  // Track static content (from <Static> component)
@@ -67,7 +64,6 @@ const hostConfig = {
67
64
  instance.__static = true
68
65
  rootContainer.staticRoot = instance
69
66
  rootContainer.staticDirty = true
70
- console.log("set staticRoot", Boolean(rootContainer.staticRoot))
71
67
  }
72
68
 
73
69
  return instance
@@ -98,9 +94,6 @@ const hostConfig = {
98
94
  },
99
95
 
100
96
  removeChildFromContainer(container: Container, child: Instance) {
101
- if (child.__static) {
102
- console.log("removeChildFromContainer static")
103
- }
104
97
  if (container.root) {
105
98
  container.root.removeChild(child)
106
99
  child.destroy()
package/src/renderer.ts CHANGED
@@ -2,7 +2,7 @@ import { performance } from "node:perf_hooks"
2
2
  import { fileURLToPath } from "node:url"
3
3
  import { ANSI, bufferToString, type KeyMsg, type MouseMsg } from "@effect-tui/core"
4
4
  import React, { type ReactNode } from "react"
5
- import { createTerminalWriter } from "./console/ConsoleCapture.js"
5
+ import { createTerminalWriter, writeToTerminal } from "./console/ConsoleCapture.js"
6
6
  import { DEFAULT_FPS } from "./constants.js"
7
7
  import { requestExit } from "./exit.js"
8
8
  import * as Prof from "./profiler.js"
@@ -272,6 +272,8 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
272
272
 
273
273
  let onExit: (() => void) | null = null
274
274
  let onSignal: ((signal: NodeJS.Signals) => void) | null = null
275
+ let onUncaughtException: ((err: Error) => void) | null = null
276
+ let onUnhandledRejection: ((reason: unknown) => void) | null = null
275
277
 
276
278
  // Build renderer object
277
279
  const renderer: TuiRenderer = {
@@ -300,6 +302,14 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
300
302
  process.off("SIGTERM", onSignal)
301
303
  onSignal = null
302
304
  }
305
+ if (onUncaughtException) {
306
+ process.off("uncaughtException", onUncaughtException)
307
+ onUncaughtException = null
308
+ }
309
+ if (onUnhandledRejection) {
310
+ process.off("unhandledRejection", onUnhandledRejection)
311
+ onUnhandledRejection = null
312
+ }
303
313
  if (state.loop) {
304
314
  clearInterval(state.loop)
305
315
  state.loop = null
@@ -333,6 +343,7 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
333
343
  if (!manualMode) renderFrame()
334
344
  },
335
345
  dispatchResize(width: number, height: number) {
346
+ renderMode.handleResize(width, height, state.lastWidth)
336
347
  state.updateDimensions(width, height)
337
348
  state.invalidateBuffers()
338
349
  state.markDirty()
@@ -403,6 +414,24 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
403
414
  }
404
415
  process.on("SIGINT", onSignal)
405
416
  process.on("SIGTERM", onSignal)
417
+
418
+ // Handle uncaught exceptions - ensure error is visible before exit
419
+ onUncaughtException = (err: Error) => {
420
+ cleanup()
421
+ // Write directly to terminal, bypassing console capture
422
+ writeToTerminal(`\n[effect-tui] Uncaught exception:\n${err.stack || err.message}\n`)
423
+ process.exit(1)
424
+ }
425
+ process.on("uncaughtException", onUncaughtException)
426
+
427
+ // Handle unhandled promise rejections
428
+ onUnhandledRejection = (reason: unknown) => {
429
+ cleanup()
430
+ const message = reason instanceof Error ? reason.stack || reason.message : String(reason)
431
+ writeToTerminal(`\n[effect-tui] Unhandled rejection:\n${message}\n`)
432
+ process.exit(1)
433
+ }
434
+ process.on("unhandledRejection", onUnhandledRejection)
406
435
  }
407
436
 
408
437
  ;(renderer as TuiRendererInternal)._container = null