@effect-tui/react 0.7.0 → 0.8.0
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/console/ConsoleCapture.d.ts +19 -0
- package/dist/src/console/ConsoleCapture.d.ts.map +1 -1
- package/dist/src/console/ConsoleCapture.js +132 -9
- package/dist/src/console/ConsoleCapture.js.map +1 -1
- package/dist/src/renderer.d.ts.map +1 -1
- package/dist/src/renderer.js +9 -1
- package/dist/src/renderer.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/console/ConsoleCapture.ts +151 -9
- package/src/renderer.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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.
|
|
86
|
+
"@effect-tui/core": "^0.8.0",
|
|
87
87
|
"@effect/platform": "^0.94.0",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Console capture singleton for TUI debugging
|
|
2
|
-
// Intercepts console.log/info/warn/error/debug
|
|
2
|
+
// Intercepts console.log/info/warn/error/debug AND process.stdout/stderr.write
|
|
3
|
+
// to capture ALL output including Effect.log, third-party libraries, etc.
|
|
3
4
|
|
|
4
5
|
import { Console } from "node:console"
|
|
5
6
|
import { EventEmitter } from "node:events"
|
|
@@ -105,17 +106,39 @@ export interface ConsoleCaptureEvents {
|
|
|
105
106
|
error: (entry: LogEntry) => void
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// Store the ORIGINAL stdout/stderr write functions at module load time
|
|
110
|
+
// This ensures the renderer can always write to the terminal even when capture is active
|
|
111
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout)
|
|
112
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr)
|
|
113
|
+
|
|
108
114
|
export class ConsoleCapture extends EventEmitter {
|
|
109
115
|
private entries: LogEntry[] = []
|
|
110
116
|
private originalConsole: typeof console | null = null
|
|
111
117
|
private active = false
|
|
112
118
|
private maxEntries: number
|
|
119
|
+
// Track if current write is from our console methods (to avoid double-capture)
|
|
120
|
+
private inConsoleMethod = false
|
|
121
|
+
// Track if current write is from the TUI renderer (should bypass capture)
|
|
122
|
+
private inRendererWrite = false
|
|
113
123
|
|
|
114
124
|
constructor(options?: { maxEntries?: number }) {
|
|
115
125
|
super()
|
|
116
126
|
this.maxEntries = options?.maxEntries ?? 1000
|
|
117
127
|
}
|
|
118
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Write directly to stdout, bypassing capture.
|
|
131
|
+
* Used by the TUI renderer to output to terminal.
|
|
132
|
+
*/
|
|
133
|
+
writeToTerminal(data: string | Uint8Array): boolean {
|
|
134
|
+
this.inRendererWrite = true
|
|
135
|
+
try {
|
|
136
|
+
return originalStdoutWrite(data)
|
|
137
|
+
} finally {
|
|
138
|
+
this.inRendererWrite = false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
get isActive(): boolean {
|
|
120
143
|
return this.active
|
|
121
144
|
}
|
|
@@ -134,6 +157,45 @@ export class ConsoleCapture extends EventEmitter {
|
|
|
134
157
|
// Store original console
|
|
135
158
|
this.originalConsole = globalThis.console
|
|
136
159
|
|
|
160
|
+
// Intercept stdout.write - captures Effect.log, printf, etc.
|
|
161
|
+
process.stdout.write = ((
|
|
162
|
+
chunk: Uint8Array | string,
|
|
163
|
+
encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
|
|
164
|
+
callback?: (err?: Error | null) => void,
|
|
165
|
+
): boolean => {
|
|
166
|
+
// If this write is from renderer or console methods, pass through
|
|
167
|
+
if (this.inRendererWrite) {
|
|
168
|
+
return originalStdoutWrite(chunk, encodingOrCallback as BufferEncoding, callback)
|
|
169
|
+
}
|
|
170
|
+
// Capture if not from our console methods (avoid double-capture)
|
|
171
|
+
if (!this.inConsoleMethod) {
|
|
172
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString()
|
|
173
|
+
this.captureRawOutput(text, "stdout")
|
|
174
|
+
}
|
|
175
|
+
// Don't write to real stdout - TUI owns the terminal
|
|
176
|
+
const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback
|
|
177
|
+
if (cb) cb()
|
|
178
|
+
return true
|
|
179
|
+
}) as typeof process.stdout.write
|
|
180
|
+
|
|
181
|
+
// Intercept stderr.write
|
|
182
|
+
process.stderr.write = ((
|
|
183
|
+
chunk: Uint8Array | string,
|
|
184
|
+
encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
|
|
185
|
+
callback?: (err?: Error | null) => void,
|
|
186
|
+
): boolean => {
|
|
187
|
+
if (this.inRendererWrite) {
|
|
188
|
+
return originalStderrWrite(chunk, encodingOrCallback as BufferEncoding, callback)
|
|
189
|
+
}
|
|
190
|
+
if (!this.inConsoleMethod) {
|
|
191
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString()
|
|
192
|
+
this.captureRawOutput(text, "stderr")
|
|
193
|
+
}
|
|
194
|
+
const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback
|
|
195
|
+
if (cb) cb()
|
|
196
|
+
return true
|
|
197
|
+
}) as typeof process.stdout.write
|
|
198
|
+
|
|
137
199
|
// Create mock streams for the new console
|
|
138
200
|
const mockStdout = new CapturedWritableStream()
|
|
139
201
|
const mockStderr = new CapturedWritableStream()
|
|
@@ -197,37 +259,95 @@ export class ConsoleCapture extends EventEmitter {
|
|
|
197
259
|
deactivate(): void {
|
|
198
260
|
if (!this.active || !this.originalConsole) return
|
|
199
261
|
|
|
262
|
+
// Restore original stdout/stderr write methods
|
|
263
|
+
process.stdout.write = originalStdoutWrite
|
|
264
|
+
process.stderr.write = originalStderrWrite
|
|
265
|
+
|
|
200
266
|
globalThis.console = this.originalConsole
|
|
201
267
|
this.originalConsole = null
|
|
202
268
|
this.active = false
|
|
203
269
|
}
|
|
204
270
|
|
|
205
271
|
private appendToConsole(level: LogLevel, args: unknown[]): void {
|
|
206
|
-
|
|
207
|
-
|
|
272
|
+
// Mark that we're in a console method to avoid double-capture from stdout/stderr
|
|
273
|
+
this.inConsoleMethod = true
|
|
274
|
+
try {
|
|
275
|
+
const location = captureCallerLocation()
|
|
276
|
+
const message = util.format(...args)
|
|
277
|
+
|
|
278
|
+
const entry: LogEntry = {
|
|
279
|
+
timestamp: new Date(),
|
|
280
|
+
level,
|
|
281
|
+
message,
|
|
282
|
+
args,
|
|
283
|
+
location,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Enforce max entries
|
|
287
|
+
if (this.entries.length >= this.maxEntries) {
|
|
288
|
+
this.entries.shift()
|
|
289
|
+
}
|
|
290
|
+
this.entries.push(entry)
|
|
291
|
+
|
|
292
|
+
// Emit events
|
|
293
|
+
this.emit("entry", entry)
|
|
294
|
+
// Only emit error event if there are listeners (avoid unhandled error exception)
|
|
295
|
+
if (level === "ERROR" && this.listenerCount("error") > 0) {
|
|
296
|
+
this.emit("error", entry)
|
|
297
|
+
}
|
|
298
|
+
} finally {
|
|
299
|
+
this.inConsoleMethod = false
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Capture raw output from stdout/stderr (for Effect.log, third-party libs, etc.)
|
|
304
|
+
private captureRawOutput(text: string, stream: "stdout" | "stderr"): void {
|
|
305
|
+
// Skip empty output
|
|
306
|
+
if (!text.trim()) return
|
|
307
|
+
|
|
308
|
+
// Parse Effect log format: timestamp level fiber message
|
|
309
|
+
// Example: "timestamp=2024-01-15T10:30:00.000Z level=INFO fiber=#0 message=Hello"
|
|
310
|
+
// Or the pretty format with ANSI colors
|
|
311
|
+
const level = this.detectLogLevel(text, stream)
|
|
312
|
+
|
|
313
|
+
// Strip ANSI codes for the stored message
|
|
314
|
+
const cleanText = text.replace(/\x1b\[[0-9;]*m/g, "").trim()
|
|
208
315
|
|
|
209
316
|
const entry: LogEntry = {
|
|
210
317
|
timestamp: new Date(),
|
|
211
318
|
level,
|
|
212
|
-
message,
|
|
213
|
-
args,
|
|
214
|
-
location
|
|
319
|
+
message: cleanText,
|
|
320
|
+
args: [text], // Keep original with ANSI for potential future use
|
|
321
|
+
// No location for raw output - we can't get stack trace here
|
|
215
322
|
}
|
|
216
323
|
|
|
217
|
-
// Enforce max entries
|
|
218
324
|
if (this.entries.length >= this.maxEntries) {
|
|
219
325
|
this.entries.shift()
|
|
220
326
|
}
|
|
221
327
|
this.entries.push(entry)
|
|
222
328
|
|
|
223
|
-
// Emit events
|
|
224
329
|
this.emit("entry", entry)
|
|
225
|
-
// Only emit error event if there are listeners (avoid unhandled error exception)
|
|
226
330
|
if (level === "ERROR" && this.listenerCount("error") > 0) {
|
|
227
331
|
this.emit("error", entry)
|
|
228
332
|
}
|
|
229
333
|
}
|
|
230
334
|
|
|
335
|
+
// Detect log level from raw output text
|
|
336
|
+
private detectLogLevel(text: string, stream: "stdout" | "stderr"): LogLevel {
|
|
337
|
+
const upperText = text.toUpperCase()
|
|
338
|
+
|
|
339
|
+
// Effect log format detection
|
|
340
|
+
if (upperText.includes("LEVEL=ERROR") || upperText.includes("[ERROR]")) return "ERROR"
|
|
341
|
+
if (upperText.includes("LEVEL=WARN") || upperText.includes("[WARN]")) return "WARN"
|
|
342
|
+
if (upperText.includes("LEVEL=INFO") || upperText.includes("[INFO]")) return "INFO"
|
|
343
|
+
if (upperText.includes("LEVEL=DEBUG") || upperText.includes("[DEBUG]")) return "DEBUG"
|
|
344
|
+
|
|
345
|
+
// stderr is typically error output
|
|
346
|
+
if (stream === "stderr") return "ERROR"
|
|
347
|
+
|
|
348
|
+
return "LOG"
|
|
349
|
+
}
|
|
350
|
+
|
|
231
351
|
// Also expose write to original console for debugging the capture itself
|
|
232
352
|
writeToOriginal(level: LogLevel, ...args: unknown[]): void {
|
|
233
353
|
if (!this.originalConsole) return
|
|
@@ -264,6 +384,28 @@ export function getConsoleCapture(): ConsoleCapture {
|
|
|
264
384
|
return instance
|
|
265
385
|
}
|
|
266
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Write directly to the terminal, bypassing console capture.
|
|
389
|
+
* Used by the TUI renderer to output to terminal without being captured.
|
|
390
|
+
*/
|
|
391
|
+
export function writeToTerminal(data: string | Uint8Array): boolean {
|
|
392
|
+
return originalStdoutWrite(data)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Create a wrapped stdout that bypasses console capture.
|
|
397
|
+
* Returns a write function compatible with TuiWriteStream.
|
|
398
|
+
*/
|
|
399
|
+
export function createTerminalWriter(): typeof process.stdout.write {
|
|
400
|
+
return ((
|
|
401
|
+
chunk: Uint8Array | string,
|
|
402
|
+
encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
|
|
403
|
+
callback?: (err?: Error | null) => void,
|
|
404
|
+
): boolean => {
|
|
405
|
+
return originalStdoutWrite(chunk, encodingOrCallback as BufferEncoding, callback)
|
|
406
|
+
}) as typeof process.stdout.write
|
|
407
|
+
}
|
|
408
|
+
|
|
267
409
|
// Clean up on process exit
|
|
268
410
|
process.on("exit", () => {
|
|
269
411
|
if (instance?.isActive) {
|
package/src/renderer.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { createTerminalWriter } from "./console/ConsoleCapture.js"
|
|
4
5
|
import { DEFAULT_FPS } from "./constants.js"
|
|
5
6
|
import { requestExit } from "./exit.js"
|
|
6
7
|
import * as Prof from "./profiler.js"
|
|
@@ -30,7 +31,14 @@ type HandledSignal = "SIGINT" | "SIGTERM"
|
|
|
30
31
|
|
|
31
32
|
export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
32
33
|
const fps = options?.fps ?? DEFAULT_FPS
|
|
33
|
-
|
|
34
|
+
// Use custom stdout if provided, otherwise use process.stdout with bypassed capture
|
|
35
|
+
const baseStdout: TuiWriteStream = options?.stdout ?? process.stdout
|
|
36
|
+
// Wrap write method to bypass console capture (so Effect.log etc. don't corrupt TUI)
|
|
37
|
+
const terminalWrite = createTerminalWriter()
|
|
38
|
+
const stdout: TuiWriteStream = {
|
|
39
|
+
...baseStdout,
|
|
40
|
+
write: terminalWrite,
|
|
41
|
+
} as TuiWriteStream
|
|
34
42
|
const stdin: TuiReadStream = options?.stdin ?? process.stdin
|
|
35
43
|
const mode = options?.mode ?? "fullscreen"
|
|
36
44
|
const exitOnCtrlC = options?.exitOnCtrlC ?? true
|