@effect-tui/react 0.2.1 → 0.2.3
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/dist/src/components/MultilineTextInput.js +1 -1
- package/dist/src/components/MultilineTextInput.js.map +1 -1
- package/dist/src/components/text-editing.js +1 -1
- package/dist/src/components/text-editing.js.map +1 -1
- package/dist/src/console/ConsoleCapture.d.ts +1 -1
- package/dist/src/console/ConsoleCapture.d.ts.map +1 -1
- package/dist/src/console/ConsoleCapture.js +1 -1
- package/dist/src/console/ConsoleCapture.js.map +1 -1
- package/dist/src/console/ConsolePopover.d.ts.map +1 -1
- package/dist/src/console/ConsolePopover.js +7 -7
- package/dist/src/console/ConsolePopover.js.map +1 -1
- package/dist/src/debug/DebugOverlay.d.ts +2 -2
- package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
- package/dist/src/debug/DebugOverlay.js +2 -2
- package/dist/src/debug/DebugOverlay.js.map +1 -1
- package/dist/src/dev.js +5 -5
- package/dist/src/dev.js.map +1 -1
- package/dist/src/exit.d.ts +7 -0
- package/dist/src/exit.d.ts.map +1 -0
- package/dist/src/exit.js +9 -0
- package/dist/src/exit.js.map +1 -0
- package/dist/src/highlight.d.ts.map +1 -1
- package/dist/src/highlight.js +2 -4
- package/dist/src/highlight.js.map +1 -1
- package/dist/src/hmr-plugin.d.ts +14 -0
- package/dist/src/hmr-plugin.d.ts.map +1 -1
- package/dist/src/hmr-plugin.js +1 -1
- package/dist/src/hmr-plugin.js.map +1 -1
- package/dist/src/hooks/use-quit.d.ts.map +1 -1
- package/dist/src/hooks/use-quit.js +2 -1
- package/dist/src/hooks/use-quit.js.map +1 -1
- package/dist/src/hosts/base.d.ts +1 -1
- package/dist/src/hosts/base.d.ts.map +1 -1
- package/dist/src/hosts/base.js +1 -1
- package/dist/src/hosts/base.js.map +1 -1
- package/dist/src/hosts/canvas.d.ts +3 -0
- package/dist/src/hosts/canvas.d.ts.map +1 -1
- package/dist/src/hosts/canvas.js +9 -1
- package/dist/src/hosts/canvas.js.map +1 -1
- package/dist/src/hosts/single-child.d.ts +1 -2
- package/dist/src/hosts/single-child.d.ts.map +1 -1
- package/dist/src/hosts/single-child.js +0 -3
- package/dist/src/hosts/single-child.js.map +1 -1
- package/dist/src/motion/hooks.d.ts.map +1 -1
- package/dist/src/motion/hooks.js +4 -2
- package/dist/src/motion/hooks.js.map +1 -1
- package/dist/src/renderer/modes/InlineRenderer.js +1 -1
- package/dist/src/renderer/modes/InlineRenderer.js.map +1 -1
- package/dist/src/renderer/modes/StaticContentRenderer.js +1 -1
- package/dist/src/renderer/modes/StaticContentRenderer.js.map +1 -1
- package/dist/src/renderer-types.d.ts +6 -0
- package/dist/src/renderer-types.d.ts.map +1 -1
- package/dist/src/renderer.d.ts.map +1 -1
- package/dist/src/renderer.js +35 -10
- package/dist/src/renderer.js.map +1 -1
- package/dist/src/utils/flex-layout.d.ts +14 -0
- package/dist/src/utils/flex-layout.d.ts.map +1 -1
- package/dist/src/utils/flex-layout.js +76 -23
- package/dist/src/utils/flex-layout.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/MultilineTextInput.tsx +1 -1
- package/src/components/text-editing.ts +1 -1
- package/src/console/ConsoleCapture.ts +1 -1
- package/src/console/ConsolePopover.tsx +87 -95
- package/src/debug/DebugOverlay.ts +2 -2
- package/src/dev.tsx +7 -7
- package/src/exit.ts +8 -0
- package/src/highlight.ts +5 -7
- package/src/hmr-plugin.ts +2 -1
- package/src/hooks/use-quit.ts +2 -1
- package/src/hosts/base.ts +1 -1
- package/src/hosts/canvas.ts +11 -0
- package/src/hosts/single-child.ts +1 -5
- package/src/motion/hooks.ts +5 -2
- package/src/renderer/modes/InlineRenderer.ts +1 -1
- package/src/renderer/modes/StaticContentRenderer.ts +1 -1
- package/src/renderer-types.ts +6 -0
- package/src/renderer.ts +35 -9
- package/src/utils/flex-layout.ts +80 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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.2.
|
|
86
|
+
"@effect-tui/core": "^0.2.3",
|
|
87
87
|
"@effect/platform": "^0.94.0",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
|
@@ -335,7 +335,7 @@ export function MultilineTextInput({
|
|
|
335
335
|
if (newRow !== cursor.row || newCol !== cursor.col) {
|
|
336
336
|
setCursor({ row: newRow, col: newCol })
|
|
337
337
|
}
|
|
338
|
-
}, [
|
|
338
|
+
}, [logicalLines, layout.lines, cursor.row, cursor.col])
|
|
339
339
|
|
|
340
340
|
// Keep scroll in bounds (visual lines)
|
|
341
341
|
useEffect(() => {
|
|
@@ -219,7 +219,7 @@ export function deleteWordBackwardMultiline(state: MultilineState): EditResult<M
|
|
|
219
219
|
const match = matchPrevWord(prevLine)
|
|
220
220
|
|
|
221
221
|
if (match) {
|
|
222
|
-
const killed = match
|
|
222
|
+
const killed = `${match}\n`
|
|
223
223
|
const newPrevLine = prevLine.slice(0, prevLine.length - match.length)
|
|
224
224
|
const newLines = [...lines]
|
|
225
225
|
newLines[cursor.row - 1] = newPrevLine + currentLine
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Intercepts console.log/info/warn/error/debug and stores entries for display
|
|
3
3
|
|
|
4
4
|
import { Console } from "node:console"
|
|
5
|
+
import { EventEmitter } from "node:events"
|
|
5
6
|
import { Writable } from "node:stream"
|
|
6
7
|
import * as util from "node:util"
|
|
7
|
-
import { EventEmitter } from "events"
|
|
8
8
|
|
|
9
9
|
// ─────────────────────────────────────────────────────────────
|
|
10
10
|
// Types
|
|
@@ -300,107 +300,99 @@ export function ConsolePopover({
|
|
|
300
300
|
[contentStartY, scrollState.offset, displayLines.length],
|
|
301
301
|
)
|
|
302
302
|
|
|
303
|
-
// Keyboard handler
|
|
304
|
-
useKeyboard(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return
|
|
311
|
-
}
|
|
303
|
+
// Keyboard handler - no useCallback needed, useKeyboard uses ref internally
|
|
304
|
+
useKeyboard((key) => {
|
|
305
|
+
// Ctrl+Y or Ctrl+C - Copy selection
|
|
306
|
+
if (key.ctrl && !key.shift && key.name === "char" && (key.text === "y" || key.text === "c")) {
|
|
307
|
+
handleCopy()
|
|
308
|
+
return
|
|
309
|
+
}
|
|
312
310
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
311
|
+
// Ctrl+S - Save logs to file
|
|
312
|
+
if (key.ctrl && !key.shift && key.name === "char" && key.text === "s") {
|
|
313
|
+
handleSave()
|
|
314
|
+
return
|
|
315
|
+
}
|
|
318
316
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
317
|
+
// Size controls
|
|
318
|
+
if (key.name === "char" && (key.text === "+" || key.text === "=")) {
|
|
319
|
+
if (mode === "inline") {
|
|
320
|
+
setInlineHeight((prev) => Math.min(30, prev + 2))
|
|
321
|
+
} else {
|
|
322
|
+
setSizePercent((prev) => Math.min(maxHeightPercent, prev + 5))
|
|
323
|
+
}
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
if (key.name === "char" && key.text === "-") {
|
|
327
|
+
if (mode === "inline") {
|
|
328
|
+
setInlineHeight((prev) => Math.max(4, prev - 2))
|
|
329
|
+
} else {
|
|
330
|
+
setSizePercent((prev) => Math.max(minHeightPercent, prev - 5))
|
|
331
|
+
}
|
|
332
|
+
return
|
|
333
|
+
}
|
|
336
334
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
335
|
+
// Shift+up/down - jump to top/bottom
|
|
336
|
+
if (key.shift && key.name === "up") {
|
|
337
|
+
scrollToStart()
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
if (key.shift && key.name === "down") {
|
|
341
|
+
scrollToEnd()
|
|
342
|
+
return
|
|
343
|
+
}
|
|
346
344
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
345
|
+
// Regular up/down - scroll
|
|
346
|
+
if (key.name === "up") {
|
|
347
|
+
scrollBy(-1)
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
if (key.name === "down") {
|
|
351
|
+
scrollBy(1)
|
|
352
|
+
return
|
|
353
|
+
}
|
|
356
354
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
[handleCopy, handleSave, maxHeightPercent, minHeightPercent, scrollToStart, scrollToEnd, scrollBy],
|
|
368
|
-
),
|
|
369
|
-
)
|
|
355
|
+
// Home/End
|
|
356
|
+
if (key.name === "home") {
|
|
357
|
+
scrollToStart()
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
if (key.name === "end") {
|
|
361
|
+
scrollToEnd()
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
})
|
|
370
365
|
|
|
371
|
-
// Mouse handler for text selection
|
|
366
|
+
// Mouse handler for text selection - no useCallback needed, useMouse uses ref internally
|
|
372
367
|
useMouse(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
scrollBy(1)
|
|
397
|
-
}
|
|
398
|
-
} else if (mouse.action === "release") {
|
|
399
|
-
isSelectingRef.current = false
|
|
368
|
+
(mouse) => {
|
|
369
|
+
// Only handle events in content area
|
|
370
|
+
if (mouse.y < contentStartY || mouse.y >= termHeight) return
|
|
371
|
+
|
|
372
|
+
const point = mouseToSelectionPoint(mouse.x, mouse.y)
|
|
373
|
+
if (!point) return
|
|
374
|
+
|
|
375
|
+
if (mouse.action === "press") {
|
|
376
|
+
// Start new selection
|
|
377
|
+
setSelectionAnchor(point)
|
|
378
|
+
setSelectionHead(point)
|
|
379
|
+
isSelectingRef.current = true
|
|
380
|
+
} else if (mouse.action === "drag" && isSelectingRef.current) {
|
|
381
|
+
// Extend selection
|
|
382
|
+
setSelectionHead(point)
|
|
383
|
+
|
|
384
|
+
// Auto-scroll when dragging near edges
|
|
385
|
+
const relativeY = mouse.y - contentStartY
|
|
386
|
+
const viewportHeight = popoverHeight - 1 // -1 for title
|
|
387
|
+
if (relativeY <= 1) {
|
|
388
|
+
scrollBy(-1)
|
|
389
|
+
} else if (relativeY >= viewportHeight - 2) {
|
|
390
|
+
scrollBy(1)
|
|
400
391
|
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
392
|
+
} else if (mouse.action === "release") {
|
|
393
|
+
isSelectingRef.current = false
|
|
394
|
+
}
|
|
395
|
+
},
|
|
404
396
|
{ action: "any", button: "left" },
|
|
405
397
|
)
|
|
406
398
|
|
|
@@ -459,11 +451,11 @@ export function ConsolePopover({
|
|
|
459
451
|
{/* Title bar */}
|
|
460
452
|
<hstack height={1} bg={TITLE_BG}>
|
|
461
453
|
<text fg={feedback ? Colors.green : TITLE_FG} bg={TITLE_BG}>
|
|
462
|
-
{
|
|
454
|
+
{` ${titleText}`}
|
|
463
455
|
</text>
|
|
464
456
|
<spacer />
|
|
465
457
|
<text fg={Colors.gray(14)} bg={TITLE_BG}>
|
|
466
|
-
{
|
|
458
|
+
{`${mode === "inline" ? inlineHints : hints} `}
|
|
467
459
|
</text>
|
|
468
460
|
</hstack>
|
|
469
461
|
|
|
@@ -162,7 +162,7 @@ export class DebugOverlay {
|
|
|
162
162
|
// Key handling - returns true if event was consumed
|
|
163
163
|
// ─────────────────────────────────────────────────────────────
|
|
164
164
|
|
|
165
|
-
handleKey(key: KeyMsg,
|
|
165
|
+
handleKey(key: KeyMsg, _width: number, height: number): boolean {
|
|
166
166
|
// Ctrl+Shift+D - toggle overlay (handled even when hidden)
|
|
167
167
|
if (key.ctrl && key.shift && key.name === "char" && key.text === "d") {
|
|
168
168
|
this.toggle()
|
|
@@ -246,7 +246,7 @@ export class DebugOverlay {
|
|
|
246
246
|
// Mouse handling - returns true if event was consumed
|
|
247
247
|
// ─────────────────────────────────────────────────────────────
|
|
248
248
|
|
|
249
|
-
handleMouse(mouse: MouseMsg,
|
|
249
|
+
handleMouse(mouse: MouseMsg, _width: number, height: number): boolean {
|
|
250
250
|
if (!this._visible) return false
|
|
251
251
|
|
|
252
252
|
const popoverHeight = this.getPopoverHeight(height)
|
package/src/dev.tsx
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* - Remote control when EFFECT_TUI_REMOTE=1
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { readFileSync } from "node:fs"
|
|
13
|
+
import { stat } from "node:fs/promises"
|
|
14
|
+
import { dirname } from "node:path"
|
|
12
15
|
import { Colors } from "@effect-tui/core"
|
|
13
16
|
import * as watcher from "@parcel/watcher"
|
|
14
17
|
import { globalValue } from "effect/GlobalValue"
|
|
15
|
-
import { readFileSync } from "fs"
|
|
16
|
-
import { stat } from "fs/promises"
|
|
17
|
-
import { dirname } from "path"
|
|
18
18
|
import React from "react"
|
|
19
19
|
import { Overlay } from "./components/Overlay.js"
|
|
20
20
|
import { ConsolePopover } from "./console/ConsolePopover.js"
|
|
@@ -363,10 +363,10 @@ function DevWrapper({
|
|
|
363
363
|
<ToastProvider>
|
|
364
364
|
<Overlay>
|
|
365
365
|
{/* Base - determines container size */}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
366
|
+
|
|
367
|
+
<ScreenshotHandler />
|
|
368
|
+
{children}
|
|
369
|
+
|
|
370
370
|
{/* Overlays */}
|
|
371
371
|
{visible && (
|
|
372
372
|
<Overlay.Item alignment={{ v: "bottom" }}>
|
package/src/exit.ts
ADDED
package/src/highlight.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Color } from "@effect-tui/core"
|
|
2
|
-
import { type BundledLanguage, type BundledTheme, createHighlighter, type Highlighter } from "shiki"
|
|
2
|
+
import { type BundledLanguage, type BundledTheme, createHighlighter, type Highlighter, type ThemedToken } from "shiki"
|
|
3
3
|
|
|
4
4
|
export interface HighlightTokenStyle {
|
|
5
5
|
fg?: Color
|
|
@@ -48,17 +48,15 @@ export async function highlightCode(
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const tokensResult = await highlighter.codeToTokens(code, { lang, theme })
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
: // Shiki v3 returns { tokens, theme }
|
|
54
|
-
(tokensResult as any).tokens
|
|
51
|
+
// Shiki v3 returns { tokens: ThemedToken[][], ... }
|
|
52
|
+
const tokenLines: ThemedToken[][] = tokensResult.tokens
|
|
55
53
|
|
|
56
54
|
if (!Array.isArray(tokenLines)) return toPlainLines(code)
|
|
57
55
|
|
|
58
|
-
return tokenLines.map((line
|
|
56
|
+
return tokenLines.map((line) =>
|
|
59
57
|
line.map((token) => {
|
|
60
58
|
const style: HighlightTokenStyle = {}
|
|
61
|
-
if (token.color) style.fg = token.color
|
|
59
|
+
if (token.color) style.fg = token.color as Color
|
|
62
60
|
const fs = token.fontStyle ?? 0
|
|
63
61
|
// fontStyle bitmask is: 1 = Italic, 2 = Bold, 4 = Underline
|
|
64
62
|
if (fs & 2) style.bold = true
|
package/src/hmr-plugin.ts
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* [run]
|
|
13
13
|
* preload = ["./node_modules/@effect-tui/react/dist/hmr-plugin.js"]
|
|
14
14
|
*/
|
|
15
|
+
|
|
16
|
+
import { relative } from "node:path"
|
|
15
17
|
import { plugin } from "bun"
|
|
16
|
-
import { relative } from "path"
|
|
17
18
|
|
|
18
19
|
// Only enable in development
|
|
19
20
|
if (process.env.NODE_ENV !== "production") {
|
package/src/hooks/use-quit.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback } from "react"
|
|
2
|
+
import { requestExit } from "../exit.js"
|
|
2
3
|
import { useRenderer } from "../renderer-context.js"
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -26,7 +27,7 @@ export function useQuit(): (code?: number) => void {
|
|
|
26
27
|
return useCallback(
|
|
27
28
|
(code = 0) => {
|
|
28
29
|
renderer.stop()
|
|
29
|
-
|
|
30
|
+
requestExit(code)
|
|
30
31
|
},
|
|
31
32
|
[renderer],
|
|
32
33
|
)
|
package/src/hosts/base.ts
CHANGED
|
@@ -54,7 +54,7 @@ export abstract class BaseHost implements HostInstance {
|
|
|
54
54
|
|
|
55
55
|
protected ctx: HostContext
|
|
56
56
|
|
|
57
|
-
constructor(type: string,
|
|
57
|
+
constructor(type: string, _props: CommonProps, ctx: HostContext) {
|
|
58
58
|
this.id = `${type}-${idCounter++}`
|
|
59
59
|
this.type = type
|
|
60
60
|
this.ctx = ctx
|
package/src/hosts/canvas.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
borderChars,
|
|
7
7
|
drawBorder,
|
|
8
8
|
resolveBgStyle,
|
|
9
|
+
resolveInheritedBgStyle,
|
|
9
10
|
styleIdFromProps,
|
|
10
11
|
toColorValue,
|
|
11
12
|
} from "../utils/index.js"
|
|
@@ -62,12 +63,15 @@ export interface CanvasProps extends CommonProps {
|
|
|
62
63
|
width?: number
|
|
63
64
|
/** Fixed height (default: fill available) */
|
|
64
65
|
height?: number
|
|
66
|
+
/** Inherit background color from parent and pre-fill canvas (default: false) */
|
|
67
|
+
inheritBg?: boolean
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export class CanvasHost extends BaseHost {
|
|
68
71
|
draw: CanvasProps["draw"] = () => {}
|
|
69
72
|
fixedWidth?: number
|
|
70
73
|
fixedHeight?: number
|
|
74
|
+
inheritBg = false
|
|
71
75
|
|
|
72
76
|
constructor(props: CanvasProps, ctx: HostContext) {
|
|
73
77
|
super("canvas", props, ctx)
|
|
@@ -85,6 +89,12 @@ export class CanvasHost extends BaseHost {
|
|
|
85
89
|
if (!this.rect) return
|
|
86
90
|
const { x: ox, y: oy, w, h } = this.rect
|
|
87
91
|
|
|
92
|
+
// Pre-fill with inherited background if requested
|
|
93
|
+
if (this.inheritBg) {
|
|
94
|
+
const { styleId } = resolveInheritedBgStyle(palette, undefined, this.parent)
|
|
95
|
+
buffer.fillRect(ox, oy, w, h, " ".codePointAt(0)!, styleId)
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
// Create draw context
|
|
89
99
|
const ctx: DrawContext = {
|
|
90
100
|
width: w,
|
|
@@ -180,5 +190,6 @@ export class CanvasHost extends BaseHost {
|
|
|
180
190
|
}
|
|
181
191
|
if (props.width !== undefined) this.fixedWidth = props.width as number
|
|
182
192
|
if (props.height !== undefined) this.fixedHeight = props.height as number
|
|
193
|
+
if (props.inheritBg !== undefined) this.inheritBg = props.inheritBg as boolean
|
|
183
194
|
}
|
|
184
195
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HostInstance } from "../reconciler/types.js"
|
|
2
2
|
import { BaseHost } from "./base.js"
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -8,10 +8,6 @@ import { BaseHost } from "./base.js"
|
|
|
8
8
|
export abstract class SingleChildHost extends BaseHost {
|
|
9
9
|
private warned = false
|
|
10
10
|
|
|
11
|
-
constructor(type: string, props: CommonProps, ctx: HostContext) {
|
|
12
|
-
super(type, props, ctx)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
11
|
protected get child(): HostInstance | null {
|
|
16
12
|
return this.children[0] ?? null
|
|
17
13
|
}
|
package/src/motion/hooks.ts
CHANGED
|
@@ -34,8 +34,11 @@ export function useMotionValue<T>(initial: T): MotionValue<T> {
|
|
|
34
34
|
* If no renderer is passed, it will use the nearest RendererContext (useRenderer()).
|
|
35
35
|
*/
|
|
36
36
|
export function useSpringRenderer(renderer?: { requestRender: () => void }) {
|
|
37
|
-
//
|
|
38
|
-
const
|
|
37
|
+
// Always call useRenderer to satisfy rules of hooks
|
|
38
|
+
const contextRenderer = useRenderer()
|
|
39
|
+
// Prefer explicit renderer; otherwise use context
|
|
40
|
+
const inferred = renderer ?? contextRenderer
|
|
41
|
+
|
|
39
42
|
useEffect(() => {
|
|
40
43
|
if (!inferred) return
|
|
41
44
|
// Store the bound function so we can compare for cleanup
|
|
@@ -139,7 +139,7 @@ export class InlineRenderer implements RendererMode {
|
|
|
139
139
|
|
|
140
140
|
// Clear any extra lines if content shrank
|
|
141
141
|
for (let screenY = rowCount; screenY < this.previousHeight; screenY++) {
|
|
142
|
-
output += palette.sgr(0) + ANSI.line.clear
|
|
142
|
+
output += `${palette.sgr(0) + ANSI.line.clear}\r\n`
|
|
143
143
|
this.printedWidths[screenY] = 0
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -45,7 +45,7 @@ export class StaticContentRenderer {
|
|
|
45
45
|
for (let y = 0; y < staticSize.h; y++) {
|
|
46
46
|
const trimmedWidth = rowContentWidth(staticBuffer, y, frameWidth)
|
|
47
47
|
const line = emitRowWithReset(staticBuffer, this.palette, y, frameWidth, 0, trimmedWidth)
|
|
48
|
-
contentLines += line
|
|
48
|
+
contentLines += `${line}\n`
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Cache the content for replay on resize
|
package/src/renderer-types.ts
CHANGED
|
@@ -75,6 +75,12 @@ export interface RendererOptions {
|
|
|
75
75
|
mode?: "fullscreen" | "inline"
|
|
76
76
|
/** Exit the process on Ctrl+C unless preventDefault was called. Defaults to true. */
|
|
77
77
|
exitOnCtrlC?: boolean
|
|
78
|
+
/** Handle SIGINT/SIGTERM and restore terminal. Defaults to true. */
|
|
79
|
+
handleSignals?: boolean
|
|
80
|
+
/** Exit the process after handling SIGINT/SIGTERM. Defaults to true. */
|
|
81
|
+
exitOnSignal?: boolean
|
|
82
|
+
/** Override exit codes for signals. Defaults: SIGINT=130, SIGTERM=143. */
|
|
83
|
+
signalExitCodes?: Partial<Record<"SIGINT" | "SIGTERM", number>>
|
|
78
84
|
/** Enable diffed rendering (per-line). Defaults to true in runtime, false in manualMode (tests). */
|
|
79
85
|
diff?: boolean
|
|
80
86
|
/** Skip automatic render loop. Call flush() manually to render frames. */
|
package/src/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { performance } from "node:perf_hooks"
|
|
|
2
2
|
import { ANSI, bufferToString, type KeyMsg, type MouseMsg } from "@effect-tui/core"
|
|
3
3
|
import React, { type ReactNode } from "react"
|
|
4
4
|
import { DEFAULT_FPS } from "./constants.js"
|
|
5
|
+
import { requestExit } from "./exit.js"
|
|
5
6
|
import * as Prof from "./profiler.js"
|
|
6
7
|
import { flushSync, reconciler } from "./reconciler/host-config.js"
|
|
7
8
|
import type { HostContext } from "./reconciler/types.js"
|
|
@@ -25,12 +26,21 @@ export { RendererContext, useRenderer, useTerminalSize } from "./renderer-contex
|
|
|
25
26
|
// Re-export types and context for backwards compatibility
|
|
26
27
|
export type { FrameStats, RendererOptions, TuiReadStream, TuiRenderer, TuiWriteStream } from "./renderer-types.js"
|
|
27
28
|
|
|
29
|
+
type HandledSignal = "SIGINT" | "SIGTERM"
|
|
30
|
+
|
|
28
31
|
export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
29
32
|
const fps = options?.fps ?? DEFAULT_FPS
|
|
30
33
|
const stdout: TuiWriteStream = options?.stdout ?? process.stdout
|
|
31
34
|
const stdin: TuiReadStream = options?.stdin ?? process.stdin
|
|
32
35
|
const mode = options?.mode ?? "fullscreen"
|
|
33
36
|
const exitOnCtrlC = options?.exitOnCtrlC ?? true
|
|
37
|
+
const handleSignals = options?.handleSignals ?? true
|
|
38
|
+
const exitOnSignal = options?.exitOnSignal ?? true
|
|
39
|
+
const signalExitCodes: Record<HandledSignal, number> = {
|
|
40
|
+
SIGINT: 130,
|
|
41
|
+
SIGTERM: 143,
|
|
42
|
+
...options?.signalExitCodes,
|
|
43
|
+
}
|
|
34
44
|
const manualMode = options?.manualMode ?? false
|
|
35
45
|
const enableDiff = options?.diff ?? !manualMode
|
|
36
46
|
const skipTerminalSetup = options?.skipTerminalSetup ?? false
|
|
@@ -73,7 +83,7 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
73
83
|
onQuit: () => {
|
|
74
84
|
// Clean up terminal state before exiting
|
|
75
85
|
renderer.stop()
|
|
76
|
-
|
|
86
|
+
requestExit(0)
|
|
77
87
|
},
|
|
78
88
|
})
|
|
79
89
|
|
|
@@ -210,6 +220,9 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
210
220
|
}
|
|
211
221
|
}
|
|
212
222
|
|
|
223
|
+
let onExit: (() => void) | null = null
|
|
224
|
+
let onSignal: ((signal: NodeJS.Signals) => void) | null = null
|
|
225
|
+
|
|
213
226
|
// Build renderer object
|
|
214
227
|
const renderer: TuiRenderer = {
|
|
215
228
|
get width() {
|
|
@@ -228,6 +241,15 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
228
241
|
onFrameStats: (handler: (stats: FrameStats) => void) => events.onFrameStats(handler),
|
|
229
242
|
stop() {
|
|
230
243
|
state.running = false
|
|
244
|
+
if (onExit) {
|
|
245
|
+
process.off("exit", onExit)
|
|
246
|
+
onExit = null
|
|
247
|
+
}
|
|
248
|
+
if (onSignal) {
|
|
249
|
+
process.off("SIGINT", onSignal)
|
|
250
|
+
process.off("SIGTERM", onSignal)
|
|
251
|
+
onSignal = null
|
|
252
|
+
}
|
|
231
253
|
if (state.loop) {
|
|
232
254
|
clearInterval(state.loop)
|
|
233
255
|
state.loop = null
|
|
@@ -314,16 +336,20 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
314
336
|
}
|
|
315
337
|
|
|
316
338
|
// Handle normal process exit (synchronous - runs before exit completes)
|
|
317
|
-
|
|
339
|
+
onExit = () => cleanup()
|
|
318
340
|
process.on("exit", onExit)
|
|
319
341
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
342
|
+
if (handleSignals) {
|
|
343
|
+
// Handle SIGINT (Ctrl+C from shell, not from TUI input) and SIGTERM
|
|
344
|
+
onSignal = (signal: NodeJS.Signals) => {
|
|
345
|
+
cleanup()
|
|
346
|
+
if (!exitOnSignal) return
|
|
347
|
+
const code = signalExitCodes[signal as HandledSignal] ?? 0
|
|
348
|
+
requestExit(code)
|
|
349
|
+
}
|
|
350
|
+
process.on("SIGINT", onSignal)
|
|
351
|
+
process.on("SIGTERM", onSignal)
|
|
324
352
|
}
|
|
325
|
-
process.on("SIGINT", onSignal)
|
|
326
|
-
process.on("SIGTERM", onSignal)
|
|
327
353
|
|
|
328
354
|
;(renderer as TuiRendererInternal)._container = null
|
|
329
355
|
return renderer
|
|
@@ -435,7 +461,7 @@ export function render(element: ReactNode, options?: RendererOptions): RenderIns
|
|
|
435
461
|
// The createRenderer exit handler will also run, but it's idempotent
|
|
436
462
|
const quit = (code = 0) => {
|
|
437
463
|
renderer.stop()
|
|
438
|
-
|
|
464
|
+
requestExit(code)
|
|
439
465
|
}
|
|
440
466
|
|
|
441
467
|
return {
|