@effect-tui/react 0.15.1 → 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.1",
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.1",
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",
@@ -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
  }
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