@effect-tui/react 0.16.0 → 2.0.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/README.md +9 -0
- package/dist/src/codeblock.d.ts +1 -1
- package/dist/src/codeblock.d.ts.map +1 -1
- package/dist/src/codeblock.js +2 -2
- package/dist/src/codeblock.js.map +1 -1
- package/dist/src/components/Markdown.js +3 -3
- package/dist/src/components/Markdown.js.map +1 -1
- package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
- package/dist/src/components/MultilineTextInput.js +133 -305
- package/dist/src/components/MultilineTextInput.js.map +1 -1
- package/dist/src/components/TextInput.d.ts.map +1 -1
- package/dist/src/components/TextInput.js +51 -98
- package/dist/src/components/TextInput.js.map +1 -1
- package/dist/src/components/text-editing.d.ts +61 -0
- package/dist/src/components/text-editing.d.ts.map +1 -1
- package/dist/src/components/text-editing.js +131 -0
- package/dist/src/components/text-editing.js.map +1 -1
- package/dist/src/hosts/base.d.ts +13 -2
- package/dist/src/hosts/base.d.ts.map +1 -1
- package/dist/src/hosts/base.js +74 -2
- package/dist/src/hosts/base.js.map +1 -1
- package/dist/src/hosts/box.d.ts +2 -2
- package/dist/src/hosts/box.d.ts.map +1 -1
- package/dist/src/hosts/box.js +29 -2
- package/dist/src/hosts/box.js.map +1 -1
- package/dist/src/hosts/canvas.d.ts +22 -2
- package/dist/src/hosts/canvas.d.ts.map +1 -1
- package/dist/src/hosts/canvas.js +99 -31
- package/dist/src/hosts/canvas.js.map +1 -1
- package/dist/src/hosts/codeblock.d.ts +8 -10
- package/dist/src/hosts/codeblock.d.ts.map +1 -1
- package/dist/src/hosts/codeblock.js +36 -33
- package/dist/src/hosts/codeblock.js.map +1 -1
- package/dist/src/hosts/flex-container.d.ts +2 -2
- package/dist/src/hosts/flex-container.d.ts.map +1 -1
- package/dist/src/hosts/flex-container.js +17 -2
- package/dist/src/hosts/flex-container.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.map +1 -1
- package/dist/src/hosts/overlay-item.d.ts +2 -2
- package/dist/src/hosts/overlay-item.d.ts.map +1 -1
- package/dist/src/hosts/overlay-item.js +7 -2
- package/dist/src/hosts/overlay-item.js.map +1 -1
- package/dist/src/hosts/overlay.d.ts +2 -2
- package/dist/src/hosts/overlay.d.ts.map +1 -1
- package/dist/src/hosts/overlay.js +2 -2
- package/dist/src/hosts/overlay.js.map +1 -1
- package/dist/src/hosts/scroll.d.ts +7 -2
- package/dist/src/hosts/scroll.d.ts.map +1 -1
- package/dist/src/hosts/scroll.js +126 -45
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/hosts/single-child.d.ts.map +1 -1
- package/dist/src/hosts/single-child.js +2 -0
- package/dist/src/hosts/single-child.js.map +1 -1
- package/dist/src/hosts/spacer.d.ts +1 -1
- package/dist/src/hosts/spacer.d.ts.map +1 -1
- package/dist/src/hosts/spacer.js +6 -1
- package/dist/src/hosts/spacer.js.map +1 -1
- package/dist/src/hosts/text.d.ts +20 -15
- package/dist/src/hosts/text.d.ts.map +1 -1
- package/dist/src/hosts/text.js +104 -71
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/hosts/zstack.d.ts +2 -2
- package/dist/src/hosts/zstack.d.ts.map +1 -1
- package/dist/src/hosts/zstack.js +7 -2
- package/dist/src/hosts/zstack.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/internal/renderer/index.d.ts.map +1 -1
- package/dist/src/internal/renderer/index.js +41 -16
- package/dist/src/internal/renderer/index.js.map +1 -1
- package/dist/src/internal/renderer/types.d.ts +4 -0
- package/dist/src/internal/renderer/types.d.ts.map +1 -1
- package/dist/src/motion/hooks.d.ts +1 -1
- package/dist/src/motion/hooks.js +1 -1
- package/dist/src/reconciler/host-config.js +2 -2
- package/dist/src/reconciler/host-config.js.map +1 -1
- package/dist/src/reconciler/types.d.ts +5 -1
- package/dist/src/reconciler/types.d.ts.map +1 -1
- package/dist/src/utils/border.d.ts +1 -1
- package/dist/src/utils/border.d.ts.map +1 -1
- package/dist/src/utils/border.js +2 -0
- package/dist/src/utils/border.js.map +1 -1
- package/dist/src/utils/index.d.ts +2 -1
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +2 -1
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/text-layout.d.ts +22 -0
- package/dist/src/utils/text-layout.d.ts.map +1 -0
- package/dist/src/utils/text-layout.js +37 -0
- package/dist/src/utils/text-layout.js.map +1 -0
- package/dist/src/utils/text-wrap.d.ts +26 -1
- package/dist/src/utils/text-wrap.d.ts.map +1 -1
- package/dist/src/utils/text-wrap.js +106 -11
- package/dist/src/utils/text-wrap.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/codeblock.tsx +2 -2
- package/src/components/Markdown.tsx +3 -3
- package/src/components/MultilineTextInput.tsx +138 -344
- package/src/components/TextInput.tsx +54 -99
- package/src/components/text-editing.ts +180 -0
- package/src/hosts/base.ts +86 -3
- package/src/hosts/box.ts +37 -2
- package/src/hosts/canvas.ts +120 -31
- package/src/hosts/codeblock.ts +46 -33
- package/src/hosts/flex-container.ts +21 -2
- package/src/hosts/index.ts +1 -1
- package/src/hosts/overlay-item.ts +8 -2
- package/src/hosts/overlay.ts +2 -2
- package/src/hosts/scroll.ts +142 -45
- package/src/hosts/single-child.ts +2 -0
- package/src/hosts/spacer.ts +6 -1
- package/src/hosts/text.ts +122 -75
- package/src/hosts/zstack.ts +7 -2
- package/src/index.ts +1 -1
- package/src/internal/renderer/index.ts +53 -20
- package/src/internal/renderer/types.ts +4 -0
- package/src/motion/hooks.ts +1 -1
- package/src/reconciler/host-config.ts +2 -2
- package/src/reconciler/types.ts +7 -1
- package/src/utils/border.ts +11 -1
- package/src/utils/index.ts +15 -1
- package/src/utils/text-layout.ts +65 -0
- package/src/utils/text-wrap.ts +135 -13
package/src/hosts/text.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { type CellBuffer, type Color, displayWidth, type Palette } from "@effect-tui/core"
|
|
2
2
|
import type { ColorMotionValue } from "../motion/color-motion-value.js"
|
|
3
3
|
import type { CommonProps, HostContext, HostInstance, Rect, Size } from "../reconciler/types.js"
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
resolveInheritedBgStyle,
|
|
6
|
+
splitSpansByNewline,
|
|
7
|
+
spansDisplayWidth,
|
|
8
|
+
styleIdFromProps,
|
|
9
|
+
wrapSpansByLine,
|
|
10
|
+
wrapText,
|
|
11
|
+
} from "../utils/index.js"
|
|
5
12
|
import { BaseHost, getInheritedBg } from "./base.js"
|
|
6
13
|
import { LeafHost } from "./leaf.js"
|
|
7
14
|
|
|
@@ -50,6 +57,7 @@ export class TextHost extends BaseHost {
|
|
|
50
57
|
// Cache wrapped lines between measure() and render()
|
|
51
58
|
private cachedLines: string[] | null = null
|
|
52
59
|
private cachedWidth = 0
|
|
60
|
+
private cachedStyledWrap = false
|
|
53
61
|
// Cache content to avoid rescanning children each frame
|
|
54
62
|
private cachedContent: string | null = null
|
|
55
63
|
// Cache for styled mode
|
|
@@ -57,7 +65,6 @@ export class TextHost extends BaseHost {
|
|
|
57
65
|
private cachedSpans: StyledSpan[] | null = null
|
|
58
66
|
private hasSpans = false
|
|
59
67
|
private explicitSpans: StyledSpan[] | null = null
|
|
60
|
-
private prepared = false
|
|
61
68
|
|
|
62
69
|
constructor(props: TextProps, ctx: HostContext) {
|
|
63
70
|
super("text", props, ctx)
|
|
@@ -95,15 +102,6 @@ export class TextHost extends BaseHost {
|
|
|
95
102
|
if (child.content) {
|
|
96
103
|
spans.push({
|
|
97
104
|
text: child.content,
|
|
98
|
-
// Inherit TextHost's styles
|
|
99
|
-
fg: this.fg,
|
|
100
|
-
bg: this.bg,
|
|
101
|
-
bold: this.bold,
|
|
102
|
-
dimmed: this.dimmed,
|
|
103
|
-
italic: this.italic,
|
|
104
|
-
underline: this.underline,
|
|
105
|
-
strikethrough: this.strikethrough,
|
|
106
|
-
inverse: this.inverse,
|
|
107
105
|
})
|
|
108
106
|
}
|
|
109
107
|
} else if (child instanceof SpanHost) {
|
|
@@ -111,15 +109,15 @@ export class TextHost extends BaseHost {
|
|
|
111
109
|
if (content) {
|
|
112
110
|
spans.push({
|
|
113
111
|
text: content,
|
|
114
|
-
// Span's styles
|
|
115
|
-
fg: child.fg
|
|
116
|
-
bg: child.bg
|
|
117
|
-
bold: child.bold
|
|
118
|
-
dimmed: child.dimmed
|
|
119
|
-
italic: child.italic
|
|
120
|
-
underline: child.underline
|
|
121
|
-
strikethrough: child.strikethrough
|
|
122
|
-
inverse: child.inverse
|
|
112
|
+
// Span's styles (TextHost applies fallbacks at render time)
|
|
113
|
+
fg: child.fg,
|
|
114
|
+
bg: child.bg,
|
|
115
|
+
bold: child.bold,
|
|
116
|
+
dimmed: child.dimmed,
|
|
117
|
+
italic: child.italic,
|
|
118
|
+
underline: child.underline,
|
|
119
|
+
strikethrough: child.strikethrough,
|
|
120
|
+
inverse: child.inverse,
|
|
123
121
|
})
|
|
124
122
|
}
|
|
125
123
|
}
|
|
@@ -129,54 +127,51 @@ export class TextHost extends BaseHost {
|
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
private prepareContent(): void {
|
|
132
|
-
this.invalidateContent()
|
|
133
130
|
const useExplicitSpans = this.explicitSpans !== null
|
|
134
131
|
if (useExplicitSpans) {
|
|
135
132
|
this.hasSpans = false
|
|
136
133
|
this.cachedSpans = null
|
|
137
|
-
this.prepared = true
|
|
138
134
|
return
|
|
139
135
|
}
|
|
140
136
|
|
|
141
137
|
this.hasSpans = this.checkForSpans()
|
|
142
138
|
this.cachedSpans = this.hasSpans ? this.collectSpans() : null
|
|
143
|
-
this.prepared = true
|
|
144
139
|
}
|
|
145
140
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.prepareContent()
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Invalidate content cache when children change */
|
|
152
|
-
private invalidateContent(): void {
|
|
141
|
+
/** Reset content-related caches (text/spans/wrap). */
|
|
142
|
+
private resetContentCaches(): void {
|
|
153
143
|
this.cachedContent = null
|
|
154
144
|
this.cachedLines = null
|
|
155
145
|
this.cachedStyledLines = null
|
|
146
|
+
this.cachedStyledWrap = false
|
|
156
147
|
this.cachedSpans = null
|
|
157
|
-
this.prepared = false
|
|
158
148
|
}
|
|
159
149
|
|
|
160
|
-
protected override prepareSelf(): void {
|
|
150
|
+
protected override prepareSelf(_layoutDirty: boolean, _renderDirty: boolean): void {
|
|
161
151
|
this.prepareContent()
|
|
162
152
|
}
|
|
163
153
|
|
|
154
|
+
override invalidateLayout(): void {
|
|
155
|
+
this.resetContentCaches()
|
|
156
|
+
super.invalidateLayout()
|
|
157
|
+
}
|
|
158
|
+
|
|
164
159
|
override appendChild(child: HostInstance): void {
|
|
160
|
+
this.resetContentCaches()
|
|
165
161
|
super.appendChild(child)
|
|
166
|
-
this.invalidateContent()
|
|
167
162
|
}
|
|
168
163
|
|
|
169
164
|
override removeChild(child: HostInstance): void {
|
|
165
|
+
this.resetContentCaches()
|
|
170
166
|
super.removeChild(child)
|
|
171
|
-
this.invalidateContent()
|
|
172
167
|
}
|
|
173
168
|
|
|
174
169
|
override insertBefore(child: HostInstance, before: HostInstance): void {
|
|
170
|
+
this.resetContentCaches()
|
|
175
171
|
super.insertBefore(child, before)
|
|
176
|
-
this.invalidateContent()
|
|
177
172
|
}
|
|
178
173
|
|
|
179
|
-
|
|
174
|
+
protected measureSelf(maxW: number, maxH: number): Size {
|
|
180
175
|
const constrained = this.constrainProposal(maxW, maxH)
|
|
181
176
|
this.ensurePrepared()
|
|
182
177
|
const useExplicitSpans = this.explicitSpans !== null
|
|
@@ -185,18 +180,21 @@ export class TextHost extends BaseHost {
|
|
|
185
180
|
if (useExplicitSpans || this.hasSpans) {
|
|
186
181
|
const spans = useExplicitSpans ? this.explicitSpans! : (this.cachedSpans ?? this.collectSpans())
|
|
187
182
|
if (this.wrap) {
|
|
188
|
-
this.cachedStyledLines =
|
|
183
|
+
this.cachedStyledLines = wrapSpansByLine(spans, constrained.w)
|
|
189
184
|
this.cachedWidth = constrained.w
|
|
185
|
+
this.cachedStyledWrap = true
|
|
190
186
|
const h = Math.min(this.cachedStyledLines.length, constrained.h)
|
|
191
|
-
const w = this.cachedStyledLines.reduce(
|
|
192
|
-
(max, line) => Math.max(max, line.reduce((sum, span) => sum + displayWidth(span.text), 0)),
|
|
193
|
-
0,
|
|
194
|
-
)
|
|
187
|
+
const w = this.cachedStyledLines.reduce((max, line) => Math.max(max, spansDisplayWidth(line)), 0)
|
|
195
188
|
return this.constrainResult({ w, h })
|
|
196
189
|
}
|
|
197
|
-
// Non-wrap styled mode
|
|
198
|
-
const
|
|
199
|
-
|
|
190
|
+
// Non-wrap styled mode (preserve explicit newlines)
|
|
191
|
+
const lines = splitSpansByNewline(spans)
|
|
192
|
+
this.cachedStyledLines = lines
|
|
193
|
+
this.cachedWidth = constrained.w
|
|
194
|
+
this.cachedStyledWrap = false
|
|
195
|
+
const maxLineWidth = lines.reduce((max, line) => Math.max(max, spansDisplayWidth(line)), 0)
|
|
196
|
+
const h = Math.min(lines.length, constrained.h)
|
|
197
|
+
return this.constrainResult({ w: Math.min(maxLineWidth, constrained.w), h })
|
|
200
198
|
}
|
|
201
199
|
|
|
202
200
|
// Simple mode: single style for all content
|
|
@@ -221,7 +219,7 @@ export class TextHost extends BaseHost {
|
|
|
221
219
|
return this.constrainResult({ w, h })
|
|
222
220
|
}
|
|
223
221
|
|
|
224
|
-
override
|
|
222
|
+
protected override layoutSelf(rect: Rect): void {
|
|
225
223
|
const layoutRect = this.layoutWithConstraints(rect)
|
|
226
224
|
// Layout children (RawTextHost nodes) at same position
|
|
227
225
|
for (const child of this.children) {
|
|
@@ -230,10 +228,7 @@ export class TextHost extends BaseHost {
|
|
|
230
228
|
}
|
|
231
229
|
|
|
232
230
|
render(buffer: CellBuffer, palette: Palette): void {
|
|
233
|
-
if (!this.rect)
|
|
234
|
-
this.prepared = false
|
|
235
|
-
return
|
|
236
|
-
}
|
|
231
|
+
if (!this.rect) return
|
|
237
232
|
this.ensurePrepared()
|
|
238
233
|
|
|
239
234
|
// If text has no bg, inherit from parent box for proper highlight rendering
|
|
@@ -244,11 +239,11 @@ export class TextHost extends BaseHost {
|
|
|
244
239
|
// Styled mode: render with per-span styles
|
|
245
240
|
if (useExplicitSpans || this.hasSpans) {
|
|
246
241
|
const lines =
|
|
247
|
-
this.
|
|
242
|
+
this.cachedStyledLines && this.cachedWidth === this.rect.w && this.cachedStyledWrap === this.wrap
|
|
248
243
|
? this.cachedStyledLines
|
|
249
244
|
: (() => {
|
|
250
245
|
const spans = useExplicitSpans ? this.explicitSpans! : (this.cachedSpans ?? this.collectSpans())
|
|
251
|
-
return this.wrap ?
|
|
246
|
+
return this.wrap ? wrapSpansByLine(spans, this.rect.w) : splitSpansByNewline(spans)
|
|
252
247
|
})()
|
|
253
248
|
|
|
254
249
|
for (let y = 0; y < Math.min(lines.length, this.rect.h); y++) {
|
|
@@ -257,12 +252,12 @@ export class TextHost extends BaseHost {
|
|
|
257
252
|
const spanStyleId = styleIdFromProps(palette, {
|
|
258
253
|
fg: span.fg ?? this.fg,
|
|
259
254
|
bg: span.bg ?? inheritedBg,
|
|
260
|
-
bold: span.bold,
|
|
261
|
-
dimmed: span.dimmed,
|
|
262
|
-
italic: span.italic,
|
|
263
|
-
underline: span.underline,
|
|
264
|
-
strikethrough: span.strikethrough,
|
|
265
|
-
inverse: span.inverse,
|
|
255
|
+
bold: span.bold ?? this.bold,
|
|
256
|
+
dimmed: span.dimmed ?? this.dimmed,
|
|
257
|
+
italic: span.italic ?? this.italic,
|
|
258
|
+
underline: span.underline ?? this.underline,
|
|
259
|
+
strikethrough: span.strikethrough ?? this.strikethrough,
|
|
260
|
+
inverse: span.inverse ?? this.inverse,
|
|
266
261
|
})
|
|
267
262
|
const availableWidth = this.rect.w - (x - this.rect.x)
|
|
268
263
|
if (availableWidth <= 0) break
|
|
@@ -271,7 +266,6 @@ export class TextHost extends BaseHost {
|
|
|
271
266
|
x += displayWidth(span.text)
|
|
272
267
|
}
|
|
273
268
|
}
|
|
274
|
-
this.prepared = false
|
|
275
269
|
return
|
|
276
270
|
}
|
|
277
271
|
|
|
@@ -308,7 +302,6 @@ export class TextHost extends BaseHost {
|
|
|
308
302
|
const lineWidth = Math.min(displayWidth(lines[i]), this.rect.w)
|
|
309
303
|
buffer.drawText(this.rect.x, this.rect.y + i, lines[i], styleId, lineWidth)
|
|
310
304
|
}
|
|
311
|
-
this.prepared = false
|
|
312
305
|
return
|
|
313
306
|
}
|
|
314
307
|
|
|
@@ -322,11 +315,21 @@ export class TextHost extends BaseHost {
|
|
|
322
315
|
const lineWidth = Math.min(displayWidth(rawLines[i]), this.rect.w)
|
|
323
316
|
buffer.drawText(this.rect.x, this.rect.y + i, rawLines[i], styleId, lineWidth)
|
|
324
317
|
}
|
|
325
|
-
this.prepared = false
|
|
326
318
|
}
|
|
327
319
|
|
|
328
320
|
override updateProps(props: Record<string, unknown>): void {
|
|
329
321
|
super.updateProps(props)
|
|
322
|
+
const prevFg = this.fg
|
|
323
|
+
const prevBg = this.bg
|
|
324
|
+
const prevBold = this.bold
|
|
325
|
+
const prevDimmed = this.dimmed
|
|
326
|
+
const prevItalic = this.italic
|
|
327
|
+
const prevUnderline = this.underline
|
|
328
|
+
const prevStrikethrough = this.strikethrough
|
|
329
|
+
const prevInverse = this.inverse
|
|
330
|
+
const prevWrap = this.wrap
|
|
331
|
+
const prevExplicitSpans = this.explicitSpans
|
|
332
|
+
|
|
330
333
|
// Color props support MotionValue/ColorMotionValue - auto-subscribe and animate
|
|
331
334
|
this.fg = this.resolveSpringProp("fg", props.fg, (v) => {
|
|
332
335
|
this.fg = v as Color
|
|
@@ -342,6 +345,26 @@ export class TextHost extends BaseHost {
|
|
|
342
345
|
this.inverse = Boolean(props.inverse)
|
|
343
346
|
this.wrap = Boolean(props.wrap)
|
|
344
347
|
this.explicitSpans = "spans" in props ? ((props.spans as StyledSpan[] | undefined) ?? []) : null
|
|
348
|
+
|
|
349
|
+
const layoutChanged = prevWrap !== this.wrap || prevExplicitSpans !== this.explicitSpans
|
|
350
|
+
if (layoutChanged) {
|
|
351
|
+
this.invalidateLayout()
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const renderChanged =
|
|
356
|
+
prevFg !== this.fg ||
|
|
357
|
+
prevBg !== this.bg ||
|
|
358
|
+
prevBold !== this.bold ||
|
|
359
|
+
prevDimmed !== this.dimmed ||
|
|
360
|
+
prevItalic !== this.italic ||
|
|
361
|
+
prevUnderline !== this.underline ||
|
|
362
|
+
prevStrikethrough !== this.strikethrough ||
|
|
363
|
+
prevInverse !== this.inverse
|
|
364
|
+
|
|
365
|
+
if (renderChanged) {
|
|
366
|
+
this.invalidateRender()
|
|
367
|
+
}
|
|
345
368
|
}
|
|
346
369
|
}
|
|
347
370
|
|
|
@@ -354,7 +377,7 @@ export class RawTextHost extends LeafHost {
|
|
|
354
377
|
this.content = text
|
|
355
378
|
}
|
|
356
379
|
|
|
357
|
-
|
|
380
|
+
protected measureSelf(maxW: number, _maxH: number): Size {
|
|
358
381
|
const w = Math.min(displayWidth(this.content), maxW)
|
|
359
382
|
return { w, h: 1 }
|
|
360
383
|
}
|
|
@@ -371,6 +394,7 @@ export class RawTextHost extends LeafHost {
|
|
|
371
394
|
|
|
372
395
|
updateText(text: string): void {
|
|
373
396
|
this.content = text
|
|
397
|
+
this.parent?.invalidateLayout?.()
|
|
374
398
|
}
|
|
375
399
|
|
|
376
400
|
override updateProps(_props: Record<string, unknown>): void {
|
|
@@ -416,12 +440,12 @@ export interface SpanProps extends CommonProps {
|
|
|
416
440
|
export class SpanHost extends BaseHost {
|
|
417
441
|
fg?: Color
|
|
418
442
|
bg?: Color
|
|
419
|
-
bold
|
|
420
|
-
dimmed
|
|
421
|
-
italic
|
|
422
|
-
underline
|
|
423
|
-
strikethrough
|
|
424
|
-
inverse
|
|
443
|
+
bold?: boolean
|
|
444
|
+
dimmed?: boolean
|
|
445
|
+
italic?: boolean
|
|
446
|
+
underline?: boolean
|
|
447
|
+
strikethrough?: boolean
|
|
448
|
+
inverse?: boolean
|
|
425
449
|
|
|
426
450
|
constructor(props: SpanProps, ctx: HostContext) {
|
|
427
451
|
super("span", props, ctx)
|
|
@@ -436,7 +460,22 @@ export class SpanHost extends BaseHost {
|
|
|
436
460
|
.join("")
|
|
437
461
|
}
|
|
438
462
|
|
|
439
|
-
|
|
463
|
+
override appendChild(child: HostInstance): void {
|
|
464
|
+
super.appendChild(child)
|
|
465
|
+
this.parent?.invalidateLayout?.()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
override removeChild(child: HostInstance): void {
|
|
469
|
+
super.removeChild(child)
|
|
470
|
+
this.parent?.invalidateLayout?.()
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
override insertBefore(child: HostInstance, before: HostInstance): void {
|
|
474
|
+
super.insertBefore(child, before)
|
|
475
|
+
this.parent?.invalidateLayout?.()
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
protected measureSelf(_maxW: number, _maxH: number): Size {
|
|
440
479
|
// Span doesn't measure independently - parent TextHost handles layout
|
|
441
480
|
return { w: 0, h: 0 }
|
|
442
481
|
}
|
|
@@ -452,12 +491,20 @@ export class SpanHost extends BaseHost {
|
|
|
452
491
|
// Individual props override textStyle object
|
|
453
492
|
this.fg = props.fg !== undefined ? (props.fg as Color) : textStyle?.fg
|
|
454
493
|
this.bg = props.bg !== undefined ? (props.bg as Color) : textStyle?.bg
|
|
455
|
-
this.bold = props.bold !== undefined ? Boolean(props.bold) :
|
|
456
|
-
this.dimmed = props.dimmed !== undefined ? Boolean(props.dimmed) :
|
|
457
|
-
this.italic = props.italic !== undefined ? Boolean(props.italic) :
|
|
458
|
-
this.underline = props.underline !== undefined ? Boolean(props.underline) :
|
|
459
|
-
this.strikethrough =
|
|
460
|
-
|
|
461
|
-
this.
|
|
494
|
+
this.bold = props.bold !== undefined ? Boolean(props.bold) : textStyle?.bold
|
|
495
|
+
this.dimmed = props.dimmed !== undefined ? Boolean(props.dimmed) : textStyle?.dimmed
|
|
496
|
+
this.italic = props.italic !== undefined ? Boolean(props.italic) : textStyle?.italic
|
|
497
|
+
this.underline = props.underline !== undefined ? Boolean(props.underline) : textStyle?.underline
|
|
498
|
+
this.strikethrough = props.strikethrough !== undefined ? Boolean(props.strikethrough) : textStyle?.strikethrough
|
|
499
|
+
this.inverse = props.inverse !== undefined ? Boolean(props.inverse) : textStyle?.inverse
|
|
500
|
+
this.invalidateLayout()
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
override invalidateLayout(): void {
|
|
504
|
+
this.parent?.invalidateLayout?.()
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
override invalidateRender(): void {
|
|
508
|
+
this.parent?.invalidateRender?.()
|
|
462
509
|
}
|
|
463
510
|
}
|
package/src/hosts/zstack.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class ZStackHost extends BaseHost {
|
|
|
19
19
|
this.updateProps(props as unknown as Record<string, unknown>)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
protected measureSelf(maxW: number, maxH: number): Size {
|
|
23
23
|
// Apply frame constraints to what we propose to children
|
|
24
24
|
const constrained = this.constrainProposal(maxW, maxH)
|
|
25
25
|
|
|
@@ -43,7 +43,7 @@ export class ZStackHost extends BaseHost {
|
|
|
43
43
|
return this.constrainResult(naturalSize)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
override
|
|
46
|
+
protected override layoutSelf(rect: Rect): void {
|
|
47
47
|
const layoutRect = this.layoutWithConstraints(rect)
|
|
48
48
|
|
|
49
49
|
layoutAlignedChildren(layoutRect, this.children, this.cachedSizes, () => ({
|
|
@@ -60,10 +60,15 @@ export class ZStackHost extends BaseHost {
|
|
|
60
60
|
|
|
61
61
|
override updateProps(props: Record<string, unknown>): void {
|
|
62
62
|
super.updateProps(props)
|
|
63
|
+
const prevH = this.alignmentH
|
|
64
|
+
const prevV = this.alignmentV
|
|
63
65
|
if (props.alignment !== undefined) {
|
|
64
66
|
const a = props.alignment as ZStackProps["alignment"]
|
|
65
67
|
if (a?.h) this.alignmentH = a.h
|
|
66
68
|
if (a?.v) this.alignmentV = a.v
|
|
67
69
|
}
|
|
70
|
+
if (prevH !== this.alignmentH || prevV !== this.alignmentV) {
|
|
71
|
+
this.invalidateLayout()
|
|
72
|
+
}
|
|
68
73
|
}
|
|
69
74
|
}
|
package/src/index.ts
CHANGED
|
@@ -65,7 +65,7 @@ export { useKeyboard, useMouse, usePaste, useQuit, useScroll, useShortcut, useTi
|
|
|
65
65
|
export { isKey } from "./shortcuts.js"
|
|
66
66
|
export { useFrameStats } from "./hooks/useFrameStats.js"
|
|
67
67
|
export type { BorderKind, BoxProps } from "./hosts/box.js"
|
|
68
|
-
export type { CanvasProps, DrawContext } from "./hosts/canvas.js"
|
|
68
|
+
export type { CanvasCell, CanvasProps, DrawContext } from "./hosts/canvas.js"
|
|
69
69
|
export type { HStackProps } from "./hosts/hstack.js"
|
|
70
70
|
export type { ScrollAlign, ScrollAlignX, ScrollAlignY, ScrollAxis, ScrollLayoutChange, ScrollProps } from "./hosts/scroll.js"
|
|
71
71
|
export type { SpacerProps } from "./hosts/spacer.js"
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks"
|
|
2
2
|
import { fileURLToPath } from "node:url"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ANSI,
|
|
5
|
+
bufferToString,
|
|
6
|
+
setEmojiWidth,
|
|
7
|
+
type KeyMsg,
|
|
8
|
+
type MouseMsg,
|
|
9
|
+
} from "@effect-tui/core"
|
|
4
10
|
import React, { type ReactNode } from "react"
|
|
5
11
|
import { createTerminalWriter, writeToTerminal } from "../../console/ConsoleCapture.js"
|
|
6
12
|
import { DEFAULT_FPS } from "../../constants.js"
|
|
@@ -42,6 +48,7 @@ type HandledSignal = "SIGINT" | "SIGTERM"
|
|
|
42
48
|
|
|
43
49
|
export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
44
50
|
const fps = options?.fps ?? DEFAULT_FPS
|
|
51
|
+
const exitOnRenderError = options?.exitOnRenderError ?? true
|
|
45
52
|
// Use custom stdout if provided, otherwise use process.stdout with bypassed capture
|
|
46
53
|
let stdout: TuiWriteStream
|
|
47
54
|
if (options?.stdout) {
|
|
@@ -82,6 +89,9 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
82
89
|
const enableMouse = options?.enableMouse ?? mode === "fullscreen"
|
|
83
90
|
const enableKittyKeyboard = options?.enableKittyKeyboard
|
|
84
91
|
const debugHook = options?.debug?.onFrame
|
|
92
|
+
const emojiWidthOption = options?.emojiWidth
|
|
93
|
+
const envEmojiWidth =
|
|
94
|
+
process.env.TUI_EMOJI_WIDTH ?? process.env.EFFECT_TUI_EMOJI_WIDTH ?? process.env.EMOJI_WIDTH
|
|
85
95
|
|
|
86
96
|
const keyboardProbe =
|
|
87
97
|
!skipTerminalSetup && enableKittyKeyboard !== false
|
|
@@ -98,6 +108,12 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
98
108
|
})
|
|
99
109
|
: null
|
|
100
110
|
|
|
111
|
+
if (emojiWidthOption === 1 || emojiWidthOption === 2) {
|
|
112
|
+
setEmojiWidth(emojiWidthOption)
|
|
113
|
+
} else if (envEmojiWidth === "1" || envEmojiWidth === "2") {
|
|
114
|
+
setEmojiWidth(envEmojiWidth === "1" ? 1 : 2)
|
|
115
|
+
}
|
|
116
|
+
|
|
101
117
|
// Initialize state
|
|
102
118
|
const state = new RendererState(stdout.columns || 80, stdout.rows || 24)
|
|
103
119
|
const events = new EventBus()
|
|
@@ -117,6 +133,29 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
117
133
|
// Static content renderer (inline mode only)
|
|
118
134
|
const staticRenderer = mode === "inline" ? new StaticContentRenderer(stdout, state.palette) : null
|
|
119
135
|
|
|
136
|
+
let renderer!: TuiRenderer
|
|
137
|
+
let cleanedUp = false
|
|
138
|
+
const cleanup = () => {
|
|
139
|
+
if (cleanedUp) return
|
|
140
|
+
cleanedUp = true
|
|
141
|
+
renderer.stop()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const handleRenderError = (err: unknown) => {
|
|
145
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
146
|
+
cleanup()
|
|
147
|
+
writeToTerminal(`\n[effect-tui] Render error:\n${error.stack || error.message}\n`)
|
|
148
|
+
if (exitOnRenderError) process.exit(1)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const filterInput = keyboardProbe
|
|
152
|
+
? (input: string) => {
|
|
153
|
+
let output = input
|
|
154
|
+
if (keyboardProbe) output = keyboardProbe.handleInput(output)
|
|
155
|
+
return output
|
|
156
|
+
}
|
|
157
|
+
: undefined
|
|
158
|
+
|
|
120
159
|
// Input processing
|
|
121
160
|
const inputProcessor = new InputProcessor({
|
|
122
161
|
exitOnCtrlC,
|
|
@@ -135,7 +174,7 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
135
174
|
renderer.stop()
|
|
136
175
|
requestExit(0)
|
|
137
176
|
},
|
|
138
|
-
filterInput
|
|
177
|
+
filterInput,
|
|
139
178
|
})
|
|
140
179
|
|
|
141
180
|
const handleInlineFullRerender = (): string => {
|
|
@@ -201,9 +240,9 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
201
240
|
const container = (renderer as TuiRendererInternal)._container
|
|
202
241
|
const root = container?.root ?? null
|
|
203
242
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
243
|
+
// Must render if dirty OR if static content needs flushing
|
|
244
|
+
if ((!state.dirty && !container?.staticDirty) || !root) return
|
|
245
|
+
state.dirty = false
|
|
207
246
|
|
|
208
247
|
try {
|
|
209
248
|
// Handle full rerender on resize (Ink-style: clear everything + replay static)
|
|
@@ -294,8 +333,7 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
294
333
|
if (debugHook) debugHook(stats)
|
|
295
334
|
if (events.hasFrameHandlers) events.dispatchFrame(stats)
|
|
296
335
|
} catch (err) {
|
|
297
|
-
|
|
298
|
-
state.markDirty()
|
|
336
|
+
handleRenderError(err)
|
|
299
337
|
}
|
|
300
338
|
}
|
|
301
339
|
|
|
@@ -318,7 +356,7 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
318
356
|
let unregisterProcessHandlers: (() => void) | null = null
|
|
319
357
|
|
|
320
358
|
// Build renderer object
|
|
321
|
-
|
|
359
|
+
renderer = {
|
|
322
360
|
get width() {
|
|
323
361
|
return state.width
|
|
324
362
|
},
|
|
@@ -383,15 +421,17 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
383
421
|
|
|
384
422
|
// Terminal setup
|
|
385
423
|
terminal.setup()
|
|
386
|
-
keyboardProbe?.start()
|
|
387
|
-
if (!keyboardProbe && !skipTerminalSetup) {
|
|
388
|
-
stdout.write(ANSI.modifyOtherKeys.enable)
|
|
389
|
-
}
|
|
390
424
|
|
|
391
425
|
// Input handling
|
|
392
426
|
state.inputHandler = (data: Buffer) => inputProcessor.process(data)
|
|
393
427
|
stdin.on("data", state.inputHandler)
|
|
394
428
|
|
|
429
|
+
keyboardProbe?.start()
|
|
430
|
+
// No emoji width probing.
|
|
431
|
+
if (!keyboardProbe && !skipTerminalSetup) {
|
|
432
|
+
stdout.write(ANSI.modifyOtherKeys.enable)
|
|
433
|
+
}
|
|
434
|
+
|
|
395
435
|
// Resize handling
|
|
396
436
|
state.resizeHandler = () => {
|
|
397
437
|
const newWidth = stdout.columns || 80
|
|
@@ -415,13 +455,6 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
|
415
455
|
|
|
416
456
|
// Process exit handlers - ensure terminal is restored on any exit
|
|
417
457
|
// These handlers are critical for proper cleanup when process.exit() is called
|
|
418
|
-
let cleanedUp = false
|
|
419
|
-
const cleanup = () => {
|
|
420
|
-
if (cleanedUp) return
|
|
421
|
-
cleanedUp = true
|
|
422
|
-
renderer.stop()
|
|
423
|
-
}
|
|
424
|
-
|
|
425
458
|
if (handleSignals) {
|
|
426
459
|
// Handle normal process exit (synchronous - runs before exit completes)
|
|
427
460
|
onExit = () => cleanup()
|
|
@@ -470,7 +503,7 @@ export interface Root {
|
|
|
470
503
|
export function createRoot(renderer: TuiRenderer): Root {
|
|
471
504
|
const hostContext: HostContext = {
|
|
472
505
|
requestRender: () => renderer.requestRender(),
|
|
473
|
-
|
|
506
|
+
requestRenderNow: () => renderer.renderNow(),
|
|
474
507
|
}
|
|
475
508
|
|
|
476
509
|
const container: Container = {
|
|
@@ -87,6 +87,8 @@ export interface RendererOptions {
|
|
|
87
87
|
handleSignals?: boolean
|
|
88
88
|
/** Exit the process after handling SIGINT/SIGTERM. Defaults to true. */
|
|
89
89
|
exitOnSignal?: boolean
|
|
90
|
+
/** Exit the process on render errors after logging to terminal. Defaults to true. */
|
|
91
|
+
exitOnRenderError?: boolean
|
|
90
92
|
/** Override exit codes for signals. Defaults: SIGINT=130, SIGTERM=143. */
|
|
91
93
|
signalExitCodes?: Partial<Record<"SIGINT" | "SIGTERM", number>>
|
|
92
94
|
/** Enable diffed rendering (per-line). Defaults to true in runtime, false in manualMode (tests). */
|
|
@@ -101,6 +103,8 @@ export interface RendererOptions {
|
|
|
101
103
|
enableMouse?: boolean
|
|
102
104
|
/** Enable Kitty/xterm keyboard protocols for enhanced modifier detection (default true). */
|
|
103
105
|
enableKittyKeyboard?: boolean
|
|
106
|
+
/** Emoji width handling: 1 or 2 forces width. */
|
|
107
|
+
emojiWidth?: 1 | 2
|
|
104
108
|
/** Optional per-frame diagnostics hook. Called after each frame is written. */
|
|
105
109
|
debug?: {
|
|
106
110
|
onFrame?: (stats: FrameStats) => void
|
package/src/motion/hooks.ts
CHANGED
|
@@ -221,7 +221,7 @@ export function useColorMotionValue(initial: ColorInput): ColorMotionValue {
|
|
|
221
221
|
* // In canvas draw callback
|
|
222
222
|
* <canvas draw={(ctx) => {
|
|
223
223
|
* const { r, g, b } = colorMv.get()
|
|
224
|
-
* ctx.
|
|
224
|
+
* ctx.fillRect(0, 0, 10, 5, "█", { fg: Colors.rgb(r, g, b) })
|
|
225
225
|
* }} />
|
|
226
226
|
*/
|
|
227
227
|
export function useColorSpring(
|
|
@@ -110,8 +110,8 @@ const hostConfig = {
|
|
|
110
110
|
|
|
111
111
|
resetAfterCommit(container: Container) {
|
|
112
112
|
// If static content is dirty, flush immediately (bypassing throttle)
|
|
113
|
-
if (container.staticDirty && container.ctx.
|
|
114
|
-
container.ctx.
|
|
113
|
+
if (container.staticDirty && container.ctx.requestRenderNow) {
|
|
114
|
+
container.ctx.requestRenderNow()
|
|
115
115
|
} else {
|
|
116
116
|
container.ctx.requestRender()
|
|
117
117
|
}
|
package/src/reconciler/types.ts
CHANGED
|
@@ -32,6 +32,12 @@ export interface HostInstance {
|
|
|
32
32
|
/** Optional pre-frame hook for cache prep (called before measure/layout/render). */
|
|
33
33
|
prepareFrame?(): void
|
|
34
34
|
|
|
35
|
+
/** @internal Mark layout-related caches dirty. */
|
|
36
|
+
invalidateLayout?(): void
|
|
37
|
+
|
|
38
|
+
/** @internal Mark render-related caches dirty. */
|
|
39
|
+
invalidateRender?(): void
|
|
40
|
+
|
|
35
41
|
/** Update props from React */
|
|
36
42
|
updateProps(props: Record<string, unknown>): void
|
|
37
43
|
|
|
@@ -51,7 +57,7 @@ export interface HostInstance {
|
|
|
51
57
|
export interface HostContext {
|
|
52
58
|
requestRender(): void
|
|
53
59
|
/** @internal Trigger immediate render (bypasses throttling) for static content */
|
|
54
|
-
|
|
60
|
+
requestRenderNow?(): void
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
/**
|
package/src/utils/border.ts
CHANGED
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import type { CellBuffer } from "@effect-tui/core"
|
|
6
6
|
|
|
7
|
-
export type BorderKind =
|
|
7
|
+
export type BorderKind =
|
|
8
|
+
| "none"
|
|
9
|
+
| "rounded"
|
|
10
|
+
| "round"
|
|
11
|
+
| "square"
|
|
12
|
+
| "double"
|
|
13
|
+
| "heavy"
|
|
14
|
+
| "dashed"
|
|
15
|
+
| "ascii"
|
|
8
16
|
|
|
9
17
|
export interface BorderChars {
|
|
10
18
|
tl: string // top-left
|
|
@@ -33,6 +41,7 @@ export interface TableBorderChars extends BorderChars {
|
|
|
33
41
|
export function borderChars(kind: BorderKind): BorderChars {
|
|
34
42
|
switch (kind) {
|
|
35
43
|
case "rounded":
|
|
44
|
+
case "round":
|
|
36
45
|
return { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" }
|
|
37
46
|
case "square":
|
|
38
47
|
return { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" }
|
|
@@ -55,6 +64,7 @@ export function borderChars(kind: BorderKind): BorderChars {
|
|
|
55
64
|
export function tableBorderChars(kind: BorderKind): TableBorderChars {
|
|
56
65
|
switch (kind) {
|
|
57
66
|
case "rounded":
|
|
67
|
+
case "round":
|
|
58
68
|
return { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", tt: "┬", bt: "┴", lt: "├", rt: "┤", cross: "┼" }
|
|
59
69
|
case "square":
|
|
60
70
|
return { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", tt: "┬", bt: "┴", lt: "├", rt: "┤", cross: "┼" }
|