@effect-tui/react 0.2.2 → 0.2.4
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/TextInput.js +1 -1
- package/dist/src/components/TextInput.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.js +1 -1
- 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.d.ts.map +1 -1
- package/dist/src/dev.js +24 -8
- package/dist/src/dev.js.map +1 -1
- 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/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/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.d.ts.map +1 -1
- package/dist/src/renderer.js +3 -1
- package/dist/src/renderer.js.map +1 -1
- package/dist/src/test/mock-streams.d.ts.map +1 -1
- package/dist/src/test/mock-streams.js +2 -1
- package/dist/src/test/mock-streams.js.map +1 -1
- package/dist/src/utils/flex-layout.d.ts.map +1 -1
- package/dist/src/utils/flex-layout.js +2 -4
- 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/TextInput.tsx +1 -1
- package/src/components/text-editing.ts +1 -1
- package/src/console/ConsoleCapture.ts +1 -1
- package/src/console/ConsolePopover.tsx +2 -2
- package/src/debug/DebugOverlay.ts +2 -2
- package/src/dev.tsx +29 -11
- package/src/highlight.ts +5 -7
- package/src/hmr-plugin.ts +2 -1
- package/src/hosts/base.ts +1 -1
- 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.ts +3 -1
- package/src/test/mock-streams.ts +2 -1
- package/src/utils/flex-layout.ts +3 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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.4",
|
|
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
|
|
@@ -451,11 +451,11 @@ export function ConsolePopover({
|
|
|
451
451
|
{/* Title bar */}
|
|
452
452
|
<hstack height={1} bg={TITLE_BG}>
|
|
453
453
|
<text fg={feedback ? Colors.green : TITLE_FG} bg={TITLE_BG}>
|
|
454
|
-
{
|
|
454
|
+
{` ${titleText}`}
|
|
455
455
|
</text>
|
|
456
456
|
<spacer />
|
|
457
457
|
<text fg={Colors.gray(14)} bg={TITLE_BG}>
|
|
458
|
-
{
|
|
458
|
+
{`${mode === "inline" ? inlineHints : hints} `}
|
|
459
459
|
</text>
|
|
460
460
|
</hstack>
|
|
461
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"
|
|
@@ -222,6 +222,20 @@ export interface DevRenderResult {
|
|
|
222
222
|
stop: () => Promise<void>
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Clear require.cache for project files to ensure fresh imports.
|
|
227
|
+
* Bun uses require.cache internally even for ESM modules.
|
|
228
|
+
* This is necessary because query-string cache-busting only affects
|
|
229
|
+
* the entry point, not transitive dependencies.
|
|
230
|
+
*/
|
|
231
|
+
function clearProjectCache(projectRoot: string): void {
|
|
232
|
+
for (const file of Object.keys(require.cache)) {
|
|
233
|
+
if (file.startsWith(projectRoot) && !file.includes("node_modules")) {
|
|
234
|
+
delete require.cache[file]
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
225
239
|
/**
|
|
226
240
|
* Wait for a file to stabilize (no size changes) before proceeding.
|
|
227
241
|
* This avoids importing partially-written files.
|
|
@@ -363,10 +377,10 @@ function DevWrapper({
|
|
|
363
377
|
<ToastProvider>
|
|
364
378
|
<Overlay>
|
|
365
379
|
{/* Base - determines container size */}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
380
|
+
|
|
381
|
+
<ScreenshotHandler />
|
|
382
|
+
{children}
|
|
383
|
+
|
|
370
384
|
{/* Overlays */}
|
|
371
385
|
{visible && (
|
|
372
386
|
<Overlay.Item alignment={{ v: "bottom" }}>
|
|
@@ -401,11 +415,17 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
|
|
|
401
415
|
let version = 0
|
|
402
416
|
let subscription: watcher.AsyncSubscription | null = null
|
|
403
417
|
|
|
418
|
+
// Determine project root for cache clearing
|
|
419
|
+
const projectRoot = options?.watchDirs?.[0] ?? dirname(entryPath)
|
|
420
|
+
|
|
404
421
|
// Import and render the module
|
|
405
422
|
const render = async () => {
|
|
406
423
|
const thisVersion = ++version
|
|
407
424
|
|
|
408
425
|
try {
|
|
426
|
+
// Clear module cache for project files before re-importing
|
|
427
|
+
clearProjectCache(projectRoot)
|
|
428
|
+
|
|
409
429
|
// Cache-bust by adding query string with version
|
|
410
430
|
const mod = await import(`${entryPath}?v=${thisVersion}`)
|
|
411
431
|
|
|
@@ -445,10 +465,8 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
|
|
|
445
465
|
await render()
|
|
446
466
|
|
|
447
467
|
// Watch for changes using @parcel/watcher
|
|
448
|
-
const watchDir = options?.watchDirs?.[0] ?? dirname(entryPath)
|
|
449
|
-
|
|
450
468
|
try {
|
|
451
|
-
subscription = await watcher.subscribe(
|
|
469
|
+
subscription = await watcher.subscribe(projectRoot, async (err, events) => {
|
|
452
470
|
if (err) {
|
|
453
471
|
options?.onError?.(err)
|
|
454
472
|
console.error("[devRender] Watcher error:", err)
|
|
@@ -472,7 +490,7 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
|
|
|
472
490
|
}, debounceMs)
|
|
473
491
|
})
|
|
474
492
|
} catch (err) {
|
|
475
|
-
console.error(`[devRender] Failed to watch ${
|
|
493
|
+
console.error(`[devRender] Failed to watch ${projectRoot}:`, err)
|
|
476
494
|
}
|
|
477
495
|
|
|
478
496
|
const stop = async () => {
|
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/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
|
|
@@ -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.ts
CHANGED
|
@@ -176,7 +176,9 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
176
176
|
})
|
|
177
177
|
|
|
178
178
|
// Combine static + dynamic output for atomic write
|
|
179
|
-
|
|
179
|
+
// Wrap in synchronized output mode (DECSET 2026) to prevent tearing
|
|
180
|
+
// Terminals that don't support it will safely ignore these sequences
|
|
181
|
+
const output = ANSI.sync.begin + staticOutput + modeOutput + state.palette.sgr(0) + ANSI.sync.end
|
|
180
182
|
contentH = contentHeight
|
|
181
183
|
const diffAnsiMs = performance.now() - diffStartMs
|
|
182
184
|
Prof.endPhase("diff+ansi", t)
|
package/src/test/mock-streams.ts
CHANGED
|
@@ -134,7 +134,8 @@ export function encodeKey(key: KeyMsg): Buffer {
|
|
|
134
134
|
*/
|
|
135
135
|
export function stripAnsi(str: string): string {
|
|
136
136
|
// eslint-disable-next-line no-control-regex
|
|
137
|
-
|
|
137
|
+
// Pattern handles: CSI sequences (including DEC private modes like ?2026h), OSC sequences
|
|
138
|
+
return str.replace(/\x1b\[[\?0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "")
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
/**
|
package/src/utils/flex-layout.ts
CHANGED
|
@@ -68,9 +68,8 @@ export function measureFlex(
|
|
|
68
68
|
|
|
69
69
|
if (greedyWeight > 0) {
|
|
70
70
|
// Greedy: measure with proportional share of remaining space
|
|
71
|
-
const greedyMain =
|
|
72
|
-
? (remainingForGreedy * greedyWeight) / totalGreedyWeight
|
|
73
|
-
: remainingForGreedy
|
|
71
|
+
const greedyMain =
|
|
72
|
+
totalGreedyWeight > 0 ? (remainingForGreedy * greedyWeight) / totalGreedyWeight : remainingForGreedy
|
|
74
73
|
const childMaxW = axis === "vertical" ? maxCross : greedyMain
|
|
75
74
|
const childMaxH = axis === "vertical" ? greedyMain : maxCross
|
|
76
75
|
const size = child.measure(childMaxW, childMaxH)
|
|
@@ -82,7 +81,7 @@ export function measureFlex(
|
|
|
82
81
|
|
|
83
82
|
// Calculate total main dimension
|
|
84
83
|
// Use actual measured sizes, not the constraint
|
|
85
|
-
|
|
84
|
+
const totalMain = nonGreedyTotal + greedyMeasuredTotal + totalSpacing
|
|
86
85
|
|
|
87
86
|
// Build total size from main/cross dimensions
|
|
88
87
|
const totalW = axis === "vertical" ? maxChildCross : totalMain
|