@effect-tui/react 0.6.3 → 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/jsx-runtime.d.ts +4 -1
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/src/components/ListView.d.ts +3 -1
- package/dist/src/components/ListView.d.ts.map +1 -1
- package/dist/src/components/ListView.js +38 -11
- package/dist/src/components/ListView.js.map +1 -1
- 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/hooks/use-scroll.d.ts +5 -3
- package/dist/src/hooks/use-scroll.d.ts.map +1 -1
- package/dist/src/hooks/use-scroll.js +4 -2
- package/dist/src/hooks/use-scroll.js.map +1 -1
- package/dist/src/hosts/index.d.ts +1 -1
- package/dist/src/hosts/index.d.ts.map +1 -1
- package/dist/src/hosts/index.js +3 -2
- package/dist/src/hosts/index.js.map +1 -1
- package/dist/src/hosts/scroll.d.ts +5 -0
- package/dist/src/hosts/scroll.d.ts.map +1 -1
- package/dist/src/hosts/scroll.js +10 -0
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/hosts/text.d.ts +48 -1
- package/dist/src/hosts/text.d.ts.map +1 -1
- package/dist/src/hosts/text.js +200 -5
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/remote/Procedures.d.ts +11 -0
- package/dist/src/remote/Procedures.d.ts.map +1 -1
- package/dist/src/remote/Procedures.js +17 -1
- package/dist/src/remote/Procedures.js.map +1 -1
- package/dist/src/remote/Router.d.ts +12 -1
- package/dist/src/remote/Router.d.ts.map +1 -1
- package/dist/src/remote/Router.js +1 -0
- package/dist/src/remote/Router.js.map +1 -1
- package/dist/src/remote/index.d.ts.map +1 -1
- package/dist/src/remote/index.js +14 -0
- package/dist/src/remote/index.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/src/test/render-tui.js +2 -2
- package/dist/src/test/render-tui.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jsx-runtime.ts +2 -1
- package/package.json +2 -2
- package/src/components/ListView.tsx +50 -13
- package/src/console/ConsoleCapture.ts +151 -9
- package/src/hooks/use-scroll.ts +9 -5
- package/src/hosts/index.ts +13 -2
- package/src/hosts/scroll.ts +13 -0
- package/src/hosts/text.ts +242 -5
- package/src/index.ts +1 -1
- package/src/remote/Procedures.ts +19 -1
- package/src/remote/Router.ts +14 -1
- package/src/remote/index.ts +15 -1
- package/src/renderer.ts +9 -1
- package/src/test/render-tui.ts +2 -2
package/src/hosts/text.ts
CHANGED
|
@@ -28,33 +28,164 @@ export class TextHost extends BaseHost {
|
|
|
28
28
|
private cachedWidth = 0
|
|
29
29
|
// Cache content to avoid rescanning children each frame
|
|
30
30
|
private cachedContent: string | null = null
|
|
31
|
+
// Cache for styled mode
|
|
32
|
+
private cachedStyledLines: StyledSpan[][] | null = null
|
|
33
|
+
private hasSpans = false
|
|
31
34
|
|
|
32
35
|
constructor(props: TextProps, ctx: HostContext) {
|
|
33
36
|
super("text", props, ctx)
|
|
34
37
|
this.updateProps(props)
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
/**
|
|
40
|
+
/** Check if we have SpanHost children (requires styled rendering) */
|
|
41
|
+
private checkForSpans(): boolean {
|
|
42
|
+
return this.children.some((c) => c instanceof SpanHost)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Get text content from all children including SpanHost (cached between measure and render) */
|
|
38
46
|
private getContent(): string {
|
|
39
47
|
if (this.cachedContent !== null) {
|
|
40
48
|
return this.cachedContent
|
|
41
49
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
const parts: string[] = []
|
|
51
|
+
for (const child of this.children) {
|
|
52
|
+
if (child instanceof RawTextHost) {
|
|
53
|
+
parts.push(child.content)
|
|
54
|
+
} else if (child instanceof SpanHost) {
|
|
55
|
+
parts.push(child.getContent())
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
this.cachedContent = parts.join("")
|
|
46
59
|
return this.cachedContent
|
|
47
60
|
}
|
|
48
61
|
|
|
62
|
+
/** Collect children as styled spans for multi-style rendering */
|
|
63
|
+
private collectSpans(): StyledSpan[] {
|
|
64
|
+
const spans: StyledSpan[] = []
|
|
65
|
+
|
|
66
|
+
for (const child of this.children) {
|
|
67
|
+
if (child instanceof RawTextHost) {
|
|
68
|
+
if (child.content) {
|
|
69
|
+
spans.push({
|
|
70
|
+
text: child.content,
|
|
71
|
+
// Inherit TextHost's styles
|
|
72
|
+
fg: this.fg,
|
|
73
|
+
bg: this.bg,
|
|
74
|
+
bold: this.bold,
|
|
75
|
+
italic: this.italic,
|
|
76
|
+
underline: this.underline,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
} else if (child instanceof SpanHost) {
|
|
80
|
+
const content = child.getContent()
|
|
81
|
+
if (content) {
|
|
82
|
+
spans.push({
|
|
83
|
+
text: content,
|
|
84
|
+
// Span's styles, falling back to TextHost's
|
|
85
|
+
fg: child.fg ?? this.fg,
|
|
86
|
+
bg: child.bg ?? this.bg,
|
|
87
|
+
bold: child.bold || this.bold,
|
|
88
|
+
italic: child.italic || this.italic,
|
|
89
|
+
underline: child.underline || this.underline,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return spans
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Wrap spans into lines, breaking at word boundaries (same logic as StyledTextHost) */
|
|
99
|
+
private wrapSpans(spans: StyledSpan[], maxWidth: number): StyledSpan[][] {
|
|
100
|
+
const lines: StyledSpan[][] = [[]]
|
|
101
|
+
let lineWidth = 0
|
|
102
|
+
|
|
103
|
+
for (const span of spans) {
|
|
104
|
+
// Split span text into words (keeping whitespace as separate tokens)
|
|
105
|
+
const tokens = span.text.split(/(\s+)/)
|
|
106
|
+
|
|
107
|
+
for (const token of tokens) {
|
|
108
|
+
if (!token) continue
|
|
109
|
+
const tokenWidth = displayWidth(token)
|
|
110
|
+
const isWhitespace = /^\s+$/.test(token)
|
|
111
|
+
|
|
112
|
+
if (lineWidth + tokenWidth <= maxWidth) {
|
|
113
|
+
// Token fits on current line
|
|
114
|
+
lines[lines.length - 1].push({ ...span, text: token })
|
|
115
|
+
lineWidth += tokenWidth
|
|
116
|
+
} else if (isWhitespace) {
|
|
117
|
+
// Skip whitespace at line break
|
|
118
|
+
continue
|
|
119
|
+
} else if (tokenWidth <= maxWidth) {
|
|
120
|
+
// Start new line with this token
|
|
121
|
+
lines.push([{ ...span, text: token }])
|
|
122
|
+
lineWidth = tokenWidth
|
|
123
|
+
} else {
|
|
124
|
+
// Token is longer than maxWidth - break by character
|
|
125
|
+
let charLine = ""
|
|
126
|
+
let charLineWidth = 0
|
|
127
|
+
for (const ch of token) {
|
|
128
|
+
const chWidth = displayWidth(ch)
|
|
129
|
+
if (lineWidth + charLineWidth + chWidth > maxWidth && (charLine || lineWidth > 0)) {
|
|
130
|
+
if (charLine) {
|
|
131
|
+
lines[lines.length - 1].push({ ...span, text: charLine })
|
|
132
|
+
}
|
|
133
|
+
lines.push([])
|
|
134
|
+
lineWidth = 0
|
|
135
|
+
charLine = ch
|
|
136
|
+
charLineWidth = chWidth
|
|
137
|
+
} else {
|
|
138
|
+
charLine += ch
|
|
139
|
+
charLineWidth += chWidth
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (charLine) {
|
|
143
|
+
lines[lines.length - 1].push({ ...span, text: charLine })
|
|
144
|
+
lineWidth += charLineWidth
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Remove empty lines at the end
|
|
151
|
+
while (lines.length > 0 && lines[lines.length - 1].length === 0) {
|
|
152
|
+
lines.pop()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return lines.length > 0 ? lines : [[]]
|
|
156
|
+
}
|
|
157
|
+
|
|
49
158
|
/** Invalidate content cache when children change */
|
|
50
159
|
private invalidateContent(): void {
|
|
51
160
|
this.cachedContent = null
|
|
52
161
|
this.cachedLines = null
|
|
162
|
+
this.cachedStyledLines = null
|
|
53
163
|
}
|
|
54
164
|
|
|
55
165
|
measure(maxW: number, maxH: number): Size {
|
|
56
166
|
// Invalidate content cache at start of measure (will be recomputed on demand)
|
|
57
167
|
this.invalidateContent()
|
|
168
|
+
this.hasSpans = this.checkForSpans()
|
|
169
|
+
|
|
170
|
+
// Styled mode: use span-aware rendering
|
|
171
|
+
if (this.hasSpans) {
|
|
172
|
+
const spans = this.collectSpans()
|
|
173
|
+
if (this.wrap) {
|
|
174
|
+
this.cachedStyledLines = this.wrapSpans(spans, maxW)
|
|
175
|
+
this.cachedWidth = maxW
|
|
176
|
+
const h = Math.min(this.cachedStyledLines.length, maxH)
|
|
177
|
+
const w = this.cachedStyledLines.reduce(
|
|
178
|
+
(max, line) => Math.max(max, line.reduce((sum, span) => sum + displayWidth(span.text), 0)),
|
|
179
|
+
0,
|
|
180
|
+
)
|
|
181
|
+
return { w, h }
|
|
182
|
+
}
|
|
183
|
+
// Non-wrap styled mode
|
|
184
|
+
const totalWidth = spans.reduce((sum, span) => sum + displayWidth(span.text), 0)
|
|
185
|
+
return { w: Math.min(totalWidth, maxW), h: 1 }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Simple mode: single style for all content
|
|
58
189
|
const content = this.getContent()
|
|
59
190
|
const rawLines = content.split("\n")
|
|
60
191
|
|
|
@@ -145,6 +276,37 @@ export class TextHost extends BaseHost {
|
|
|
145
276
|
const { value: bgValue, styleId: bgStyleId } = resolveInheritedBgStyle(palette, this.bg, this.parent)
|
|
146
277
|
const inheritedBg = this.bg ?? getInheritedBg(this.parent)
|
|
147
278
|
|
|
279
|
+
// Styled mode: render with per-span styles
|
|
280
|
+
if (this.hasSpans) {
|
|
281
|
+
const spans = this.collectSpans()
|
|
282
|
+
const lines =
|
|
283
|
+
this.wrap && this.cachedStyledLines && this.cachedWidth === this.rect.w
|
|
284
|
+
? this.cachedStyledLines
|
|
285
|
+
: this.wrap
|
|
286
|
+
? this.wrapSpans(spans, this.rect.w)
|
|
287
|
+
: [spans]
|
|
288
|
+
|
|
289
|
+
for (let y = 0; y < Math.min(lines.length, this.rect.h); y++) {
|
|
290
|
+
let x = this.rect.x
|
|
291
|
+
for (const span of lines[y]) {
|
|
292
|
+
const spanStyleId = styleIdFromProps(palette, {
|
|
293
|
+
fg: span.fg ?? this.fg,
|
|
294
|
+
bg: span.bg ?? inheritedBg,
|
|
295
|
+
bold: span.bold,
|
|
296
|
+
italic: span.italic,
|
|
297
|
+
underline: span.underline,
|
|
298
|
+
})
|
|
299
|
+
const availableWidth = this.rect.w - (x - this.rect.x)
|
|
300
|
+
if (availableWidth <= 0) break
|
|
301
|
+
const textWidth = Math.min(displayWidth(span.text), availableWidth)
|
|
302
|
+
buffer.drawText(x, this.rect.y + y, span.text, spanStyleId, textWidth)
|
|
303
|
+
x += displayWidth(span.text)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Simple mode: single style for all content
|
|
148
310
|
const styleId = styleIdFromProps(palette, {
|
|
149
311
|
fg: this.fg,
|
|
150
312
|
bg: inheritedBg,
|
|
@@ -236,6 +398,81 @@ export class RawTextHost extends BaseHost {
|
|
|
236
398
|
}
|
|
237
399
|
}
|
|
238
400
|
|
|
401
|
+
// ============================================================================
|
|
402
|
+
// Span Host - inline styled text within a TextHost
|
|
403
|
+
// ============================================================================
|
|
404
|
+
|
|
405
|
+
/** Reusable style object for spans */
|
|
406
|
+
export interface SpanStyle {
|
|
407
|
+
fg?: Color
|
|
408
|
+
bg?: Color
|
|
409
|
+
bold?: boolean
|
|
410
|
+
italic?: boolean
|
|
411
|
+
underline?: boolean
|
|
412
|
+
inverse?: boolean
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export interface SpanProps extends CommonProps {
|
|
416
|
+
fg?: Color
|
|
417
|
+
bg?: Color
|
|
418
|
+
bold?: boolean
|
|
419
|
+
italic?: boolean
|
|
420
|
+
underline?: boolean
|
|
421
|
+
inverse?: boolean
|
|
422
|
+
/** Reusable style object. Individual props override textStyle values. */
|
|
423
|
+
textStyle?: SpanStyle
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Host for inline styled spans within a TextHost.
|
|
428
|
+
* Usage: <text>Hello <span fg={GREEN}>World</span></text>
|
|
429
|
+
*
|
|
430
|
+
* Does not render independently - parent TextHost handles rendering.
|
|
431
|
+
*/
|
|
432
|
+
export class SpanHost extends BaseHost {
|
|
433
|
+
fg?: Color
|
|
434
|
+
bg?: Color
|
|
435
|
+
bold = false
|
|
436
|
+
italic = false
|
|
437
|
+
underline = false
|
|
438
|
+
inverse = false
|
|
439
|
+
|
|
440
|
+
constructor(props: SpanProps, ctx: HostContext) {
|
|
441
|
+
super("span", props, ctx)
|
|
442
|
+
this.updateProps(props)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Get text content from RawTextHost children */
|
|
446
|
+
getContent(): string {
|
|
447
|
+
return this.children
|
|
448
|
+
.filter((c): c is RawTextHost => c instanceof RawTextHost)
|
|
449
|
+
.map((c) => c.content)
|
|
450
|
+
.join("")
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
measure(_maxW: number, _maxH: number): Size {
|
|
454
|
+
// Span doesn't measure independently - parent TextHost handles layout
|
|
455
|
+
return { w: 0, h: 0 }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
render(_buffer: CellBuffer, _palette: Palette): void {
|
|
459
|
+
// Span doesn't render independently - parent TextHost handles rendering
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
override updateProps(props: Record<string, unknown>): void {
|
|
463
|
+
super.updateProps(props)
|
|
464
|
+
const textStyle = props.textStyle as SpanStyle | undefined
|
|
465
|
+
|
|
466
|
+
// Individual props override textStyle object
|
|
467
|
+
this.fg = props.fg !== undefined ? (props.fg as Color) : textStyle?.fg
|
|
468
|
+
this.bg = props.bg !== undefined ? (props.bg as Color) : textStyle?.bg
|
|
469
|
+
this.bold = props.bold !== undefined ? Boolean(props.bold) : Boolean(textStyle?.bold)
|
|
470
|
+
this.italic = props.italic !== undefined ? Boolean(props.italic) : Boolean(textStyle?.italic)
|
|
471
|
+
this.underline = props.underline !== undefined ? Boolean(props.underline) : Boolean(textStyle?.underline)
|
|
472
|
+
this.inverse = props.inverse !== undefined ? Boolean(props.inverse) : Boolean(textStyle?.inverse)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
239
476
|
// ============================================================================
|
|
240
477
|
// Styled Text Host - for inline formatted text that wraps as a unit
|
|
241
478
|
// ============================================================================
|
package/src/index.ts
CHANGED
|
@@ -59,7 +59,7 @@ export type { CanvasProps, DrawContext } from "./hosts/canvas.js"
|
|
|
59
59
|
export type { HStackProps } from "./hosts/hstack.js"
|
|
60
60
|
export type { ScrollProps } from "./hosts/scroll.js"
|
|
61
61
|
export type { SpacerProps } from "./hosts/spacer.js"
|
|
62
|
-
export type { TextProps } from "./hosts/text.js"
|
|
62
|
+
export type { SpanProps, SpanStyle, TextProps } from "./hosts/text.js"
|
|
63
63
|
export type { VStackProps } from "./hosts/vstack.js"
|
|
64
64
|
export type { ZStackProps } from "./hosts/zstack.js"
|
|
65
65
|
export type { ColorInput, MotionValue, RGBA, SpringOptions } from "./motion/index.js"
|
package/src/remote/Procedures.ts
CHANGED
|
@@ -46,5 +46,23 @@ const Info = Rpc.make("Info", {
|
|
|
46
46
|
}),
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
+
// Log entry schema
|
|
50
|
+
const LogEntrySchema = Schema.Struct({
|
|
51
|
+
timestamp: Schema.String,
|
|
52
|
+
level: Schema.String,
|
|
53
|
+
message: Schema.String,
|
|
54
|
+
file: Schema.optional(Schema.String),
|
|
55
|
+
line: Schema.optional(Schema.Number),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// GetLogs - get accumulated console logs
|
|
59
|
+
const GetLogs = Rpc.make("GetLogs", {
|
|
60
|
+
payload: { limit: Schema.optional(Schema.Number) },
|
|
61
|
+
success: Schema.Struct({
|
|
62
|
+
entries: Schema.Array(LogEntrySchema),
|
|
63
|
+
total: Schema.Number,
|
|
64
|
+
}),
|
|
65
|
+
})
|
|
66
|
+
|
|
49
67
|
// Group all RPCs together
|
|
50
|
-
export class TuiRpcs extends RpcGroup.make(Screenshot, SendKey, Paste, Resize, Info) {}
|
|
68
|
+
export class TuiRpcs extends RpcGroup.make(Screenshot, SendKey, Paste, Resize, Info, GetLogs) {}
|
package/src/remote/Router.ts
CHANGED
|
@@ -4,6 +4,15 @@ import type { KeyMsg } from "@effect-tui/core"
|
|
|
4
4
|
import { Context, Effect, type Layer } from "effect"
|
|
5
5
|
import { TuiRpcs } from "./Procedures.js"
|
|
6
6
|
|
|
7
|
+
// Log entry result type
|
|
8
|
+
export interface LogEntryResult {
|
|
9
|
+
timestamp: string
|
|
10
|
+
level: string
|
|
11
|
+
message: string
|
|
12
|
+
file?: string
|
|
13
|
+
line?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
// Service interface for the TUI session
|
|
8
17
|
export interface TuiSessionImpl {
|
|
9
18
|
readonly getScreenshot: () => string
|
|
@@ -17,6 +26,7 @@ export interface TuiSessionImpl {
|
|
|
17
26
|
entryPath?: string
|
|
18
27
|
name?: string
|
|
19
28
|
}
|
|
29
|
+
readonly getLogEntries: (limit?: number) => { entries: LogEntryResult[]; total: number }
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export class TuiSession extends Context.Tag("TuiSession")<TuiSession, TuiSessionImpl>() {}
|
|
@@ -27,7 +37,8 @@ export const HandlersLive: Layer.Layer<
|
|
|
27
37
|
| Rpc.Handler<"SendKey">
|
|
28
38
|
| Rpc.Handler<"Paste">
|
|
29
39
|
| Rpc.Handler<"Resize">
|
|
30
|
-
| Rpc.Handler<"Info"
|
|
40
|
+
| Rpc.Handler<"Info">
|
|
41
|
+
| Rpc.Handler<"GetLogs">,
|
|
31
42
|
never,
|
|
32
43
|
TuiSession
|
|
33
44
|
> = TuiRpcs.toLayer(
|
|
@@ -55,6 +66,8 @@ export const HandlersLive: Layer.Layer<
|
|
|
55
66
|
Resize: ({ width, height }) => Effect.sync(() => session.dispatchResize(width, height)),
|
|
56
67
|
|
|
57
68
|
Info: () => Effect.sync(() => session.getInfo()),
|
|
69
|
+
|
|
70
|
+
GetLogs: ({ limit }) => Effect.sync(() => session.getLogEntries(limit)),
|
|
58
71
|
}
|
|
59
72
|
}),
|
|
60
73
|
)
|
package/src/remote/index.ts
CHANGED
|
@@ -13,8 +13,9 @@ export { getSocketPath, makeServerLayer } from "./Server.js"
|
|
|
13
13
|
|
|
14
14
|
import * as fs from "node:fs"
|
|
15
15
|
import { Effect, Exit, Layer, Scope } from "effect"
|
|
16
|
+
import { getConsoleCapture } from "../console/ConsoleCapture.js"
|
|
16
17
|
import type { TuiRenderer } from "../renderer-types.js"
|
|
17
|
-
import type { TuiSessionImpl } from "./Router.js"
|
|
18
|
+
import type { LogEntryResult, TuiSessionImpl } from "./Router.js"
|
|
18
19
|
import { getSocketPath, makeServerLayer } from "./Server.js"
|
|
19
20
|
|
|
20
21
|
export interface EnableRemoteOptions {
|
|
@@ -76,6 +77,19 @@ export function enableRemote(renderer: TuiRenderer, options?: EnableRemoteOption
|
|
|
76
77
|
entryPath: opts.entryPath,
|
|
77
78
|
name,
|
|
78
79
|
}),
|
|
80
|
+
getLogEntries: (limit?: number): { entries: LogEntryResult[]; total: number } => {
|
|
81
|
+
const all = getConsoleCapture().getEntries()
|
|
82
|
+
const total = all.length
|
|
83
|
+
const sliced = limit ? all.slice(-limit) : all
|
|
84
|
+
const entries: LogEntryResult[] = sliced.map((e) => ({
|
|
85
|
+
timestamp: e.timestamp.toISOString(),
|
|
86
|
+
level: e.level,
|
|
87
|
+
message: e.message,
|
|
88
|
+
file: e.location?.file,
|
|
89
|
+
line: e.location?.line,
|
|
90
|
+
}))
|
|
91
|
+
return { entries, total }
|
|
92
|
+
},
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
// Build and run the server layer
|
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
|
package/src/test/render-tui.ts
CHANGED
|
@@ -71,9 +71,9 @@ export function renderTUI(element: ReactElement, options?: RenderTUIOptions): Re
|
|
|
71
71
|
const root = createRoot(renderer)
|
|
72
72
|
let bump: (() => void) | null = null
|
|
73
73
|
const Harness = ({ children }: { children: ReactElement }) => {
|
|
74
|
-
const [, setTick] = useState(0)
|
|
74
|
+
const [tick, setTick] = useState(0)
|
|
75
75
|
bump = () => setTick((value) => value + 1)
|
|
76
|
-
return children
|
|
76
|
+
return React.cloneElement(children, { __renderTick: tick } as Record<string, unknown>)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const harnessed = React.createElement(Harness, null, element)
|