@effect-tui/core 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/anim.d.ts +4 -0
- package/dist/anim.d.ts.map +1 -0
- package/dist/anim.js +5 -0
- package/dist/anim.js.map +1 -0
- package/dist/ansi.d.ts +69 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +72 -0
- package/dist/ansi.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +18 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +247 -0
- package/dist/keys.js.map +1 -0
- package/dist/layout/linearStack.d.ts +17 -0
- package/dist/layout/linearStack.d.ts.map +1 -0
- package/dist/layout/linearStack.js +86 -0
- package/dist/layout/linearStack.js.map +1 -0
- package/dist/motion-value.d.ts +58 -0
- package/dist/motion-value.d.ts.map +1 -0
- package/dist/motion-value.js +250 -0
- package/dist/motion-value.js.map +1 -0
- package/dist/present/display.d.ts +58 -0
- package/dist/present/display.d.ts.map +1 -0
- package/dist/present/display.js +168 -0
- package/dist/present/display.js.map +1 -0
- package/dist/present/writers/fullscreen.d.ts +19 -0
- package/dist/present/writers/fullscreen.d.ts.map +1 -0
- package/dist/present/writers/fullscreen.js +55 -0
- package/dist/present/writers/fullscreen.js.map +1 -0
- package/dist/present/writers/inline.d.ts +20 -0
- package/dist/present/writers/inline.d.ts.map +1 -0
- package/dist/present/writers/inline.js +92 -0
- package/dist/present/writers/inline.js.map +1 -0
- package/dist/render/buffer.d.ts +31 -0
- package/dist/render/buffer.d.ts.map +1 -0
- package/dist/render/buffer.js +183 -0
- package/dist/render/buffer.js.map +1 -0
- package/dist/render/color-utils.d.ts +18 -0
- package/dist/render/color-utils.d.ts.map +1 -0
- package/dist/render/color-utils.js +58 -0
- package/dist/render/color-utils.js.map +1 -0
- package/dist/render/diff.d.ts +30 -0
- package/dist/render/diff.d.ts.map +1 -0
- package/dist/render/diff.js +83 -0
- package/dist/render/diff.js.map +1 -0
- package/dist/render/measure.d.ts +15 -0
- package/dist/render/measure.d.ts.map +1 -0
- package/dist/render/measure.js +65 -0
- package/dist/render/measure.js.map +1 -0
- package/dist/render/palette.d.ts +46 -0
- package/dist/render/palette.d.ts.map +1 -0
- package/dist/render/palette.js +108 -0
- package/dist/render/palette.js.map +1 -0
- package/dist/render/surface.d.ts +77 -0
- package/dist/render/surface.d.ts.map +1 -0
- package/dist/render/surface.js +198 -0
- package/dist/render/surface.js.map +1 -0
- package/dist/runtime/backend_node.d.ts +36 -0
- package/dist/runtime/backend_node.d.ts.map +1 -0
- package/dist/runtime/backend_node.js +66 -0
- package/dist/runtime/backend_node.js.map +1 -0
- package/dist/spring-physics.d.ts +36 -0
- package/dist/spring-physics.d.ts.map +1 -0
- package/dist/spring-physics.js +113 -0
- package/dist/spring-physics.js.map +1 -0
- package/dist/spring.d.ts +73 -0
- package/dist/spring.d.ts.map +1 -0
- package/dist/spring.js +136 -0
- package/dist/spring.js.map +1 -0
- package/dist/ui/containers/canvas.d.ts +13 -0
- package/dist/ui/containers/canvas.d.ts.map +1 -0
- package/dist/ui/containers/canvas.js +16 -0
- package/dist/ui/containers/canvas.js.map +1 -0
- package/dist/ui/containers/geometry-reader.d.ts +17 -0
- package/dist/ui/containers/geometry-reader.d.ts.map +1 -0
- package/dist/ui/containers/geometry-reader.js +24 -0
- package/dist/ui/containers/geometry-reader.js.map +1 -0
- package/dist/ui/containers/hstack.d.ts +12 -0
- package/dist/ui/containers/hstack.d.ts.map +1 -0
- package/dist/ui/containers/hstack.js +28 -0
- package/dist/ui/containers/hstack.js.map +1 -0
- package/dist/ui/containers/scroll.d.ts +28 -0
- package/dist/ui/containers/scroll.d.ts.map +1 -0
- package/dist/ui/containers/scroll.js +97 -0
- package/dist/ui/containers/scroll.js.map +1 -0
- package/dist/ui/containers/shared.d.ts +12 -0
- package/dist/ui/containers/shared.d.ts.map +1 -0
- package/dist/ui/containers/shared.js +19 -0
- package/dist/ui/containers/shared.js.map +1 -0
- package/dist/ui/containers/vstack.d.ts +12 -0
- package/dist/ui/containers/vstack.d.ts.map +1 -0
- package/dist/ui/containers/vstack.js +28 -0
- package/dist/ui/containers/vstack.js.map +1 -0
- package/dist/ui/containers/zstack.d.ts +14 -0
- package/dist/ui/containers/zstack.d.ts.map +1 -0
- package/dist/ui/containers/zstack.js +36 -0
- package/dist/ui/containers/zstack.js.map +1 -0
- package/dist/ui/core/geometry-store.d.ts +22 -0
- package/dist/ui/core/geometry-store.d.ts.map +1 -0
- package/dist/ui/core/geometry-store.js +29 -0
- package/dist/ui/core/geometry-store.js.map +1 -0
- package/dist/ui/core/geometry.d.ts +34 -0
- package/dist/ui/core/geometry.d.ts.map +1 -0
- package/dist/ui/core/geometry.js +14 -0
- package/dist/ui/core/geometry.js.map +1 -0
- package/dist/ui/core/view.d.ts +25 -0
- package/dist/ui/core/view.d.ts.map +1 -0
- package/dist/ui/core/view.js +34 -0
- package/dist/ui/core/view.js.map +1 -0
- package/dist/ui/index.d.ts +44 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +39 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/inlinetext.d.ts +24 -0
- package/dist/ui/inlinetext.d.ts.map +1 -0
- package/dist/ui/inlinetext.js +131 -0
- package/dist/ui/inlinetext.js.map +1 -0
- package/dist/ui/install.d.ts +22 -0
- package/dist/ui/install.d.ts.map +1 -0
- package/dist/ui/install.js +66 -0
- package/dist/ui/install.js.map +1 -0
- package/dist/ui/markdown.d.ts +40 -0
- package/dist/ui/markdown.d.ts.map +1 -0
- package/dist/ui/markdown.js +351 -0
- package/dist/ui/markdown.js.map +1 -0
- package/dist/ui/modifiers/border.d.ts +33 -0
- package/dist/ui/modifiers/border.d.ts.map +1 -0
- package/dist/ui/modifiers/border.js +82 -0
- package/dist/ui/modifiers/border.js.map +1 -0
- package/dist/ui/modifiers/fill.d.ts +14 -0
- package/dist/ui/modifiers/fill.d.ts.map +1 -0
- package/dist/ui/modifiers/fill.js +25 -0
- package/dist/ui/modifiers/fill.js.map +1 -0
- package/dist/ui/modifiers/frame.d.ts +23 -0
- package/dist/ui/modifiers/frame.d.ts.map +1 -0
- package/dist/ui/modifiers/frame.js +54 -0
- package/dist/ui/modifiers/frame.js.map +1 -0
- package/dist/ui/modifiers/offset.d.ts +15 -0
- package/dist/ui/modifiers/offset.d.ts.map +1 -0
- package/dist/ui/modifiers/offset.js +21 -0
- package/dist/ui/modifiers/offset.js.map +1 -0
- package/dist/ui/modifiers/opacity.d.ts +15 -0
- package/dist/ui/modifiers/opacity.d.ts.map +1 -0
- package/dist/ui/modifiers/opacity.js +95 -0
- package/dist/ui/modifiers/opacity.js.map +1 -0
- package/dist/ui/modifiers/padding.d.ts +20 -0
- package/dist/ui/modifiers/padding.d.ts.map +1 -0
- package/dist/ui/modifiers/padding.js +36 -0
- package/dist/ui/modifiers/padding.js.map +1 -0
- package/dist/ui/modifiers/styled.d.ts +14 -0
- package/dist/ui/modifiers/styled.d.ts.map +1 -0
- package/dist/ui/modifiers/styled.js +26 -0
- package/dist/ui/modifiers/styled.js.map +1 -0
- package/dist/ui/primitives/rectangle.d.ts +15 -0
- package/dist/ui/primitives/rectangle.d.ts.map +1 -0
- package/dist/ui/primitives/rectangle.js +23 -0
- package/dist/ui/primitives/rectangle.js.map +1 -0
- package/dist/ui/primitives/spacer.d.ts +13 -0
- package/dist/ui/primitives/spacer.d.ts.map +1 -0
- package/dist/ui/primitives/spacer.js +16 -0
- package/dist/ui/primitives/spacer.js.map +1 -0
- package/dist/ui/primitives/text.d.ts +15 -0
- package/dist/ui/primitives/text.d.ts.map +1 -0
- package/dist/ui/primitives/text.js +79 -0
- package/dist/ui/primitives/text.js.map +1 -0
- package/dist/ui/primitives/wrapped-text.d.ts +30 -0
- package/dist/ui/primitives/wrapped-text.d.ts.map +1 -0
- package/dist/ui/primitives/wrapped-text.js +117 -0
- package/dist/ui/primitives/wrapped-text.js.map +1 -0
- package/dist/ui/shinytext.d.ts +66 -0
- package/dist/ui/shinytext.d.ts.map +1 -0
- package/dist/ui/shinytext.js +99 -0
- package/dist/ui/shinytext.js.map +1 -0
- package/dist/ui/text/layout.d.ts +35 -0
- package/dist/ui/text/layout.d.ts.map +1 -0
- package/dist/ui/text/layout.js +102 -0
- package/dist/ui/text/layout.js.map +1 -0
- package/dist/ui/textinput.d.ts +140 -0
- package/dist/ui/textinput.d.ts.map +1 -0
- package/dist/ui/textinput.js +402 -0
- package/dist/ui/textinput.js.map +1 -0
- package/dist/ui/view-constructors.d.ts +72 -0
- package/dist/ui/view-constructors.d.ts.map +1 -0
- package/dist/ui/view-constructors.js +74 -0
- package/dist/ui/view-constructors.js.map +1 -0
- package/package.json +57 -0
- package/src/anim.ts +5 -0
- package/src/ansi.ts +83 -0
- package/src/index.ts +21 -0
- package/src/keys.ts +302 -0
- package/src/layout/linearStack.ts +115 -0
- package/src/motion-value.ts +335 -0
- package/src/present/display.ts +206 -0
- package/src/present/writers/fullscreen.ts +58 -0
- package/src/present/writers/inline.ts +101 -0
- package/src/render/buffer.ts +200 -0
- package/src/render/color-utils.ts +60 -0
- package/src/render/diff.ts +95 -0
- package/src/render/measure.ts +74 -0
- package/src/render/palette.ts +113 -0
- package/src/render/surface.ts +238 -0
- package/src/runtime/backend_node.ts +80 -0
- package/src/spring-physics.ts +151 -0
- package/src/spring.ts +234 -0
- package/src/ui/__snapshots__/wrappedtext.test.ts.snap +57 -0
- package/src/ui/containers/canvas.ts +18 -0
- package/src/ui/containers/geometry-reader.ts +32 -0
- package/src/ui/containers/hstack.ts +33 -0
- package/src/ui/containers/scroll.ts +106 -0
- package/src/ui/containers/shared.ts +27 -0
- package/src/ui/containers/vstack.ts +34 -0
- package/src/ui/containers/zstack.ts +37 -0
- package/src/ui/core/geometry-store.ts +42 -0
- package/src/ui/core/geometry.ts +30 -0
- package/src/ui/core/view.ts +49 -0
- package/src/ui/index.ts +84 -0
- package/src/ui/inlinetext.ts +135 -0
- package/src/ui/install.ts +110 -0
- package/src/ui/markdown.test.ts +74 -0
- package/src/ui/markdown.ts +388 -0
- package/src/ui/modifiers/border.ts +100 -0
- package/src/ui/modifiers/fill.ts +28 -0
- package/src/ui/modifiers/frame.ts +74 -0
- package/src/ui/modifiers/offset.ts +23 -0
- package/src/ui/modifiers/opacity.ts +93 -0
- package/src/ui/modifiers/padding.ts +53 -0
- package/src/ui/modifiers/styled.ts +31 -0
- package/src/ui/primitives/rectangle.ts +25 -0
- package/src/ui/primitives/spacer.ts +18 -0
- package/src/ui/primitives/text.ts +85 -0
- package/src/ui/primitives/wrapped-text.ts +131 -0
- package/src/ui/shinytext.ts +159 -0
- package/src/ui/text/layout.ts +119 -0
- package/src/ui/textinput.ts +496 -0
- package/src/ui/view-constructors.ts +96 -0
- package/src/ui/wrappedtext.test.ts +138 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/* buffer.ts — 2D cell buffer with drawing, clipping, and offsets */
|
|
2
|
+
|
|
3
|
+
export type Wcwidth = (cp: number) => 0 | 1 | 2
|
|
4
|
+
|
|
5
|
+
// basic ASCII/Latin fallback; plug a real wcwidth for CJK/emoji later
|
|
6
|
+
export const defaultWcwidth: Wcwidth = (cp) => (cp === 0 ? 0 : cp < 32 ? 0 : cp === 0x7f ? 0 : 1)
|
|
7
|
+
|
|
8
|
+
type ClipRect = { x: number; y: number; w: number; h: number }
|
|
9
|
+
|
|
10
|
+
// Represents a single frame of cells (glyph + style + cell width)
|
|
11
|
+
export class CellBuffer {
|
|
12
|
+
readonly w: number
|
|
13
|
+
readonly h: number
|
|
14
|
+
|
|
15
|
+
// cell data
|
|
16
|
+
readonly g: Uint32Array // glyph code points
|
|
17
|
+
readonly s: Uint32Array // style ids
|
|
18
|
+
readonly cw: Uint8Array // cell width: 0 (continuation), 1, 2
|
|
19
|
+
|
|
20
|
+
private wcwidth: Wcwidth
|
|
21
|
+
private clip: ClipRect
|
|
22
|
+
private clipStack: ClipRect[] = []
|
|
23
|
+
private offX = 0
|
|
24
|
+
private offY = 0
|
|
25
|
+
private offStack: { x: number; y: number }[] = []
|
|
26
|
+
|
|
27
|
+
constructor(width: number, height: number, wcwidth: Wcwidth = defaultWcwidth) {
|
|
28
|
+
this.w = Math.max(1, width | 0)
|
|
29
|
+
this.h = Math.max(1, height | 0)
|
|
30
|
+
const n = this.w * this.h
|
|
31
|
+
this.g = new Uint32Array(n)
|
|
32
|
+
this.s = new Uint32Array(n)
|
|
33
|
+
this.cw = new Uint8Array(n)
|
|
34
|
+
this.wcwidth = wcwidth
|
|
35
|
+
this.clip = { x: 0, y: 0, w: this.w, h: this.h }
|
|
36
|
+
this.clear(0)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Clear the buffer to spaces in the given style. */
|
|
40
|
+
clear(styleId = 0): void {
|
|
41
|
+
this.g.fill(32)
|
|
42
|
+
this.s.fill(styleId)
|
|
43
|
+
this.cw.fill(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Draw a single code point at (x,y). If width=2, marks following cell as continuation. */
|
|
47
|
+
drawCP(x: number, y: number, cp: number, styleId = 0): void {
|
|
48
|
+
const ax = x + this.offX
|
|
49
|
+
const ay = y + this.offY
|
|
50
|
+
if (ax < 0 || ay < 0 || ax >= this.w || ay >= this.h) return
|
|
51
|
+
if (ax < this.clip.x || ay < this.clip.y || ax >= this.clip.x + this.clip.w || ay >= this.clip.y + this.clip.h)
|
|
52
|
+
return
|
|
53
|
+
const idx = ay * this.w + ax
|
|
54
|
+
const w = this.wcwidth(cp)
|
|
55
|
+
this.g[idx] = cp
|
|
56
|
+
this.s[idx] = styleId
|
|
57
|
+
this.cw[idx] = w === 0 ? 1 : w // treat nonspacing as width 1 here
|
|
58
|
+
|
|
59
|
+
if (w === 2) {
|
|
60
|
+
// mark the continuation cell
|
|
61
|
+
if (x + 1 < this.w) {
|
|
62
|
+
const cidx = idx + 1
|
|
63
|
+
this.g[cidx] = 0 // not printed
|
|
64
|
+
this.s[cidx] = styleId
|
|
65
|
+
this.cw[cidx] = 0 // continuation slot
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Draw plain text (no wrapping) clipped to width. Uses Intl.Segmenter if available. */
|
|
71
|
+
drawText(x: number, y: number, text: string, styleId = 0, maxWidth?: number): void {
|
|
72
|
+
const ax = x + this.offX
|
|
73
|
+
const ay = y + this.offY
|
|
74
|
+
if (ay < 0 || ay >= this.h) return
|
|
75
|
+
// vertical clip
|
|
76
|
+
if (ay < this.clip.y || ay >= this.clip.y + this.clip.h) return
|
|
77
|
+
const limit = Math.min(this.w - ax, maxWidth ?? this.w, this.clip.x + this.clip.w - ax)
|
|
78
|
+
if (limit <= 0) return
|
|
79
|
+
|
|
80
|
+
// iterate graphemes (so emoji / ZWJ don’t split). Fallback: naive code points.
|
|
81
|
+
// Use Intl.Segmenter if available without importing its types to satisfy TS in Node
|
|
82
|
+
type GraphemeSegmenter = {
|
|
83
|
+
segment(input: string): Iterable<{ segment: string }>
|
|
84
|
+
}
|
|
85
|
+
const SegCtor: { new (...args: any[]): GraphemeSegmenter } | undefined = (globalThis as any).Intl?.Segmenter
|
|
86
|
+
const seg: GraphemeSegmenter | null =
|
|
87
|
+
typeof SegCtor === "function"
|
|
88
|
+
? (new (SegCtor as new (...args: any[]) => GraphemeSegmenter)(undefined, {
|
|
89
|
+
granularity: "grapheme",
|
|
90
|
+
}) as GraphemeSegmenter)
|
|
91
|
+
: null
|
|
92
|
+
|
|
93
|
+
let col = 0
|
|
94
|
+
if (seg) {
|
|
95
|
+
for (const { segment } of seg.segment(text)) {
|
|
96
|
+
const cp = segment.codePointAt(0) ?? 32 // first code point of grapheme
|
|
97
|
+
const w = this.wcwidth(cp) || 1 // nonspacing combining: treat as 1 (simple path)
|
|
98
|
+
if (col + w > limit) break
|
|
99
|
+
this.drawCP(x + col, y, cp, styleId)
|
|
100
|
+
col += w
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
for (const ch of text) {
|
|
104
|
+
const cp = ch.codePointAt(0) ?? 32
|
|
105
|
+
const w = this.wcwidth(cp) || 1
|
|
106
|
+
if (col + w > limit) break
|
|
107
|
+
this.drawCP(x + col, y, cp, styleId)
|
|
108
|
+
col += w
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// pad remainder within the visible limit with spaces in the same style (erases leftovers)
|
|
113
|
+
for (; col < limit; col++) {
|
|
114
|
+
this.drawCP(x + col, y, 32, styleId)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Fill a rectangle with a code point + style. */
|
|
119
|
+
fillRect(x: number, y: number, w: number, h: number, cp = 32, styleId = 0): void {
|
|
120
|
+
const ax = x + this.offX
|
|
121
|
+
const ay = y + this.offY
|
|
122
|
+
let x0 = Math.max(0, ax | 0),
|
|
123
|
+
y0 = Math.max(0, ay | 0)
|
|
124
|
+
let x1 = Math.min(this.w, x0 + Math.max(0, w | 0))
|
|
125
|
+
let y1 = Math.min(this.h, y0 + Math.max(0, h | 0))
|
|
126
|
+
// intersect with current clip
|
|
127
|
+
x0 = Math.max(x0, this.clip.x)
|
|
128
|
+
y0 = Math.max(y0, this.clip.y)
|
|
129
|
+
x1 = Math.min(x1, this.clip.x + this.clip.w)
|
|
130
|
+
y1 = Math.min(y1, this.clip.y + this.clip.h)
|
|
131
|
+
const width = x1 - x0
|
|
132
|
+
if (width <= 0 || y1 <= y0) return
|
|
133
|
+
for (let yy = y0; yy < y1; yy++) {
|
|
134
|
+
const base = yy * this.w + x0
|
|
135
|
+
this.g.fill(cp, base, base + width)
|
|
136
|
+
this.s.fill(styleId, base, base + width)
|
|
137
|
+
this.cw.fill(1, base, base + width)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Clipping API
|
|
142
|
+
pushClip(x: number, y: number, w: number, h: number): void {
|
|
143
|
+
const nx = Math.max(0, x | 0)
|
|
144
|
+
const ny = Math.max(0, y | 0)
|
|
145
|
+
const nw = Math.max(0, w | 0)
|
|
146
|
+
const nh = Math.max(0, h | 0)
|
|
147
|
+
const cur = this.clip
|
|
148
|
+
const ix = Math.max(cur.x, nx)
|
|
149
|
+
const iy = Math.max(cur.y, ny)
|
|
150
|
+
const ix2 = Math.min(cur.x + cur.w, nx + nw)
|
|
151
|
+
const iy2 = Math.min(cur.y + cur.h, ny + nh)
|
|
152
|
+
this.clipStack.push(cur)
|
|
153
|
+
this.clip = { x: ix, y: iy, w: Math.max(0, ix2 - ix), h: Math.max(0, iy2 - iy) }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
popClip(): void {
|
|
157
|
+
if (this.clipStack.length > 0) {
|
|
158
|
+
const prev = this.clipStack.pop() as ClipRect
|
|
159
|
+
this.clip = prev
|
|
160
|
+
} else {
|
|
161
|
+
this.clip = { x: 0, y: 0, w: this.w, h: this.h }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
withClip(x: number, y: number, w: number, h: number, fn: () => void): void {
|
|
166
|
+
this.pushClip(x, y, w, h)
|
|
167
|
+
try {
|
|
168
|
+
fn()
|
|
169
|
+
} finally {
|
|
170
|
+
this.popClip()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Translation API
|
|
175
|
+
pushOffset(dx: number, dy: number): void {
|
|
176
|
+
this.offStack.push({ x: this.offX, y: this.offY })
|
|
177
|
+
this.offX += dx | 0
|
|
178
|
+
this.offY += dy | 0
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
popOffset(): void {
|
|
182
|
+
if (this.offStack.length > 0) {
|
|
183
|
+
const prev = this.offStack.pop() as { x: number; y: number }
|
|
184
|
+
this.offX = prev.x
|
|
185
|
+
this.offY = prev.y
|
|
186
|
+
} else {
|
|
187
|
+
this.offX = 0
|
|
188
|
+
this.offY = 0
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
withOffset(dx: number, dy: number, fn: () => void): void {
|
|
193
|
+
this.pushOffset(dx, dy)
|
|
194
|
+
try {
|
|
195
|
+
fn()
|
|
196
|
+
} finally {
|
|
197
|
+
this.popOffset()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// color-utils.ts - Shared color conversion utilities
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clamp a number to the 0-255 range and convert to integer.
|
|
5
|
+
*/
|
|
6
|
+
export function clamp255(n: number): number {
|
|
7
|
+
return n < 0 ? 0 : n > 255 ? 255 : n | 0
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert a 256-color index to RGB values (approximate xterm palette).
|
|
12
|
+
*
|
|
13
|
+
* The 256-color palette is divided into:
|
|
14
|
+
* - 0-15: Standard ANSI colors (system colors)
|
|
15
|
+
* - 16-231: 6x6x6 RGB cube
|
|
16
|
+
* - 232-255: Grayscale ramp (24 shades)
|
|
17
|
+
*/
|
|
18
|
+
export function idxToRGB(idx: number): { r: number; g: number; b: number } {
|
|
19
|
+
if (idx < 0) idx = 0
|
|
20
|
+
if (idx > 255) idx = 255
|
|
21
|
+
|
|
22
|
+
// Standard ANSI colors (0-15)
|
|
23
|
+
if (idx < 16) {
|
|
24
|
+
const base: Array<[number, number, number]> = [
|
|
25
|
+
[0x00, 0x00, 0x00], // 0: black
|
|
26
|
+
[0x80, 0x00, 0x00], // 1: red
|
|
27
|
+
[0x00, 0x80, 0x00], // 2: green
|
|
28
|
+
[0x80, 0x80, 0x00], // 3: yellow
|
|
29
|
+
[0x00, 0x00, 0x80], // 4: blue
|
|
30
|
+
[0x80, 0x00, 0x80], // 5: magenta
|
|
31
|
+
[0x00, 0x80, 0x80], // 6: cyan
|
|
32
|
+
[0xc0, 0xc0, 0xc0], // 7: white
|
|
33
|
+
[0x80, 0x80, 0x80], // 8: bright black (gray)
|
|
34
|
+
[0xff, 0x00, 0x00], // 9: bright red
|
|
35
|
+
[0x00, 0xff, 0x00], // 10: bright green
|
|
36
|
+
[0xff, 0xff, 0x00], // 11: bright yellow
|
|
37
|
+
[0x00, 0x00, 0xff], // 12: bright blue
|
|
38
|
+
[0xff, 0x00, 0xff], // 13: bright magenta
|
|
39
|
+
[0x00, 0xff, 0xff], // 14: bright cyan
|
|
40
|
+
[0xff, 0xff, 0xff], // 15: bright white
|
|
41
|
+
]
|
|
42
|
+
const [r, g, b] = base[idx] ?? [0, 0, 0]
|
|
43
|
+
return { r, g, b }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Grayscale ramp (232-255)
|
|
47
|
+
if (idx >= 232) {
|
|
48
|
+
const n = idx - 232 // 0..23
|
|
49
|
+
const v = 8 + n * 10
|
|
50
|
+
return { r: v, g: v, b: v }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 6x6x6 RGB cube (16-231)
|
|
54
|
+
const n = idx - 16 // 0..215
|
|
55
|
+
const r = Math.floor(n / 36) % 6
|
|
56
|
+
const g = Math.floor(n / 6) % 6
|
|
57
|
+
const b = n % 6
|
|
58
|
+
const steps = [0, 95, 135, 175, 215, 255]
|
|
59
|
+
return { r: steps[r] ?? 0, g: steps[g] ?? 0, b: steps[b] ?? 0 }
|
|
60
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Surface } from "./surface.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Writer that consumes style runs produced by diffing two frames.
|
|
5
|
+
* - Coordinates are 0-based: `row` in [0..rows), `col` in [0..cols).
|
|
6
|
+
* - `begin` is called once with the viewport size, then `run` zero or more times,
|
|
7
|
+
* then `end`. Implementations may buffer and expose the result via `flush`.
|
|
8
|
+
*/
|
|
9
|
+
export interface RunWriter {
|
|
10
|
+
begin(viewport: { cols: number; rows: number }): void
|
|
11
|
+
run(row: number, col: number, styleId: number, text: string): void
|
|
12
|
+
end(): void
|
|
13
|
+
flush(): string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stream a minimal set of style-runs for changes from prev → next.
|
|
18
|
+
*
|
|
19
|
+
* Algorithm (row-wise):
|
|
20
|
+
* - For each row, compute a tight change window [left, right]. Skip entirely if equal.
|
|
21
|
+
* - Within the window, emit contiguous runs grouped by style. A new run begins when:
|
|
22
|
+
* - A cell changes, and
|
|
23
|
+
* - Its style differs from the current run (or the run just started).
|
|
24
|
+
* - Wide glyph continuation cells (cw=0) are skipped so they are not reprinted.
|
|
25
|
+
*
|
|
26
|
+
* Assumes both Surfaces are same size and compares their back buffers (B).
|
|
27
|
+
*/
|
|
28
|
+
export function diffFrames(prev: Surface, next: Surface, w: RunWriter): void {
|
|
29
|
+
const width = next.w
|
|
30
|
+
const height = next.h
|
|
31
|
+
// Access back buffers (like Surface.flush does). B is private on Surface, so use `any`.
|
|
32
|
+
const gA = (prev as any).B.g as Uint32Array
|
|
33
|
+
const sA = (prev as any).B.s as Uint32Array
|
|
34
|
+
const gB = (next as any).B.g as Uint32Array
|
|
35
|
+
const sB = (next as any).B.s as Uint32Array
|
|
36
|
+
const cwB = (next as any).B.cw as Uint8Array
|
|
37
|
+
|
|
38
|
+
w.begin({ cols: width, rows: height })
|
|
39
|
+
|
|
40
|
+
for (let y = 0; y < height; y++) {
|
|
41
|
+
const row = y * width
|
|
42
|
+
|
|
43
|
+
// Fast skip window: [left..right] bounds that changed
|
|
44
|
+
let left = 0,
|
|
45
|
+
right = width - 1
|
|
46
|
+
while (left <= right) {
|
|
47
|
+
const i = row + left
|
|
48
|
+
if (gA[i] === gB[i] && sA[i] === sB[i]) left++
|
|
49
|
+
else break
|
|
50
|
+
}
|
|
51
|
+
if (left > right) continue
|
|
52
|
+
|
|
53
|
+
while (right >= left) {
|
|
54
|
+
const i = row + right
|
|
55
|
+
if (gA[i] === gB[i] && sA[i] === sB[i]) right--
|
|
56
|
+
else break
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Emit contiguous style runs within [left..right]
|
|
60
|
+
let x = left
|
|
61
|
+
while (x <= right) {
|
|
62
|
+
// Skip unchanged within window
|
|
63
|
+
while (x <= right) {
|
|
64
|
+
const i = row + x
|
|
65
|
+
if (gA[i] !== gB[i] || sA[i] !== sB[i]) break
|
|
66
|
+
x++
|
|
67
|
+
}
|
|
68
|
+
if (x > right) break
|
|
69
|
+
|
|
70
|
+
const style = sB[row + x]
|
|
71
|
+
const runX = x
|
|
72
|
+
let text = ""
|
|
73
|
+
|
|
74
|
+
while (x <= right) {
|
|
75
|
+
const i = row + x
|
|
76
|
+
if (sB[i] !== style) break
|
|
77
|
+
const cp = gB[i]
|
|
78
|
+
const ww = cwB[i]
|
|
79
|
+
if (ww !== 0) text += cp === 32 ? " " : String.fromCodePoint(cp)
|
|
80
|
+
x++
|
|
81
|
+
// skip continuation cells of wide glyphs from the next buffer
|
|
82
|
+
while (x <= right && cwB[row + x] === 0) x++
|
|
83
|
+
|
|
84
|
+
// Stop run if next cell is unchanged; diff window will reposition
|
|
85
|
+
if (x <= right) {
|
|
86
|
+
const j = row + x
|
|
87
|
+
if (gA[j] === gB[j] && sA[j] === sB[j]) break
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (text.length > 0) w.run(y, runX, style, text)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
w.end()
|
|
95
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* measure.ts — shared grapheme segmentation and display width helpers
|
|
2
|
+
*
|
|
3
|
+
* These helpers intentionally mirror CellBuffer.drawText semantics:
|
|
4
|
+
* - Prefer Intl.Segmenter('grapheme') when available; otherwise iterate code points.
|
|
5
|
+
* - Width is derived from wcwidth(first code point) with a minimum of 1.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Wcwidth } from "./buffer.js"
|
|
9
|
+
import { defaultWcwidth } from "./buffer.js"
|
|
10
|
+
|
|
11
|
+
type GraphemeSegmenter = {
|
|
12
|
+
segment(input: string): Iterable<{ segment: string }>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getGraphemeSegmenter(): GraphemeSegmenter | null {
|
|
16
|
+
const SegCtor: { new (...args: any[]): GraphemeSegmenter } | undefined = (globalThis as any).Intl?.Segmenter
|
|
17
|
+
if (typeof SegCtor === "function") {
|
|
18
|
+
try {
|
|
19
|
+
return new (SegCtor as new (...args: any[]) => GraphemeSegmenter)(undefined, {
|
|
20
|
+
granularity: "grapheme",
|
|
21
|
+
}) as GraphemeSegmenter
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore and fall back
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Split a string into grapheme clusters (ZWJ sequences kept together when supported). */
|
|
30
|
+
export function graphemes(text: string): string[] {
|
|
31
|
+
const seg = getGraphemeSegmenter()
|
|
32
|
+
if (seg) {
|
|
33
|
+
const out: string[] = []
|
|
34
|
+
for (const { segment } of seg.segment(text)) out.push(segment)
|
|
35
|
+
return out
|
|
36
|
+
}
|
|
37
|
+
// Fallback: iterate code points (may split ZWJ sequences)
|
|
38
|
+
return Array.from(text)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Compute display width using wcwidth of the first code point of each grapheme. */
|
|
42
|
+
export function displayWidth(text: string, wc: Wcwidth = defaultWcwidth): number {
|
|
43
|
+
let w = 0
|
|
44
|
+
const gs = graphemes(text)
|
|
45
|
+
for (const g of gs) {
|
|
46
|
+
const cp = g.codePointAt(0) ?? 32
|
|
47
|
+
const ww = wc(cp) || 1
|
|
48
|
+
w += ww
|
|
49
|
+
}
|
|
50
|
+
return w
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Take as many graphemes as fit within maxWidth cells.
|
|
55
|
+
* Returns the substring, its display width, and whether the whole input fit.
|
|
56
|
+
*/
|
|
57
|
+
export function sliceByWidth(
|
|
58
|
+
text: string,
|
|
59
|
+
maxWidth: number,
|
|
60
|
+
wc: Wcwidth = defaultWcwidth,
|
|
61
|
+
): { text: string; width: number; complete: boolean } {
|
|
62
|
+
if (maxWidth <= 0) return { text: "", width: 0, complete: text.length === 0 }
|
|
63
|
+
const gs = graphemes(text)
|
|
64
|
+
const out: string[] = []
|
|
65
|
+
let used = 0
|
|
66
|
+
for (const g of gs) {
|
|
67
|
+
const cp = g.codePointAt(0) ?? 32
|
|
68
|
+
const ww = wc(cp) || 1
|
|
69
|
+
if (used + ww > maxWidth) break
|
|
70
|
+
out.push(g)
|
|
71
|
+
used += ww
|
|
72
|
+
}
|
|
73
|
+
return { text: out.join(""), width: used, complete: out.length === gs.length }
|
|
74
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const ESC = "\x1b["
|
|
2
|
+
const CSI = ESC // alias for clarity
|
|
3
|
+
|
|
4
|
+
// Style palette: intern style specs -> small numeric ids -> prebuilt SGR strings
|
|
5
|
+
// Color can be an xterm-256 index (0..255) or a truecolor triplet.
|
|
6
|
+
export type ColorValue = number | { r: number; g: number; b: number }
|
|
7
|
+
|
|
8
|
+
// Minimal set of attributes we support. Additional flags can be added as needed.
|
|
9
|
+
export type StyleSpec = {
|
|
10
|
+
fg?: ColorValue // foreground: 256 index or RGB
|
|
11
|
+
bg?: ColorValue // background: 256 index or RGB
|
|
12
|
+
bold?: boolean
|
|
13
|
+
italic?: boolean
|
|
14
|
+
underline?: boolean
|
|
15
|
+
inverse?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Palette interns "style specs" into small numeric ids and caches their
|
|
20
|
+
* ANSI SGR escape strings. Id 0 always represents the default terminal style.
|
|
21
|
+
*/
|
|
22
|
+
export class Palette {
|
|
23
|
+
private nextId = 1 // 0 = default terminal style
|
|
24
|
+
private idByKey = new Map<string, number>()
|
|
25
|
+
private sgrById = new Map<number, string>()
|
|
26
|
+
|
|
27
|
+
/** Get or create a numeric style id for a given style spec. */
|
|
28
|
+
id(spec?: StyleSpec): number {
|
|
29
|
+
if (!spec) return 0
|
|
30
|
+
const key = JSON.stringify(spec)
|
|
31
|
+
let id = this.idByKey.get(key)
|
|
32
|
+
if (id) return id
|
|
33
|
+
id = this.nextId++
|
|
34
|
+
this.idByKey.set(key, id)
|
|
35
|
+
this.sgrById.set(id, this.buildSGR(spec))
|
|
36
|
+
return id
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Look up the ANSI SGR sequence for a style id. Id 0 resets attributes. */
|
|
40
|
+
sgr(id: number): string {
|
|
41
|
+
if (id === 0) return `${ESC}0m`
|
|
42
|
+
const s = this.sgrById.get(id)
|
|
43
|
+
return s ?? `${ESC}0m`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build an ANSI SGR sequence for a style spec.
|
|
48
|
+
* We always begin with reset (0) to avoid style drift across runs,
|
|
49
|
+
* then add only the requested attributes.
|
|
50
|
+
*/
|
|
51
|
+
private buildSGR(spec: StyleSpec): string {
|
|
52
|
+
const parts: string[] = []
|
|
53
|
+
parts.push("0") // reset, then set exact attrs to avoid drift
|
|
54
|
+
if (spec.bold) parts.push("1")
|
|
55
|
+
if (spec.italic) parts.push("3")
|
|
56
|
+
if (spec.underline) parts.push("4")
|
|
57
|
+
if (spec.inverse) parts.push("7")
|
|
58
|
+
|
|
59
|
+
const colorToCodes = (c: ColorValue | undefined, isBg: boolean): string | undefined => {
|
|
60
|
+
if (c == null) return undefined
|
|
61
|
+
if (typeof c === "number") return (isBg ? "48;5;" : "38;5;") + String(c | 0)
|
|
62
|
+
const clamp = (n: number) => (n < 0 ? 0 : n > 255 ? 255 : n | 0)
|
|
63
|
+
return `${isBg ? "48;2;" : "38;2;"}${clamp(c.r)};${clamp(c.g)};${clamp(c.b)}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const fgc = colorToCodes(spec.fg, false)
|
|
67
|
+
const bgc = colorToCodes(spec.bg, true)
|
|
68
|
+
if (fgc) parts.push(fgc)
|
|
69
|
+
if (bgc) parts.push(bgc)
|
|
70
|
+
|
|
71
|
+
return `${CSI}${parts.join(";")}m`
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* ScopedPalette provides style scoping: it merges a base style with any
|
|
77
|
+
* requested style, while delegating id/sgr generation to a shared Palette.
|
|
78
|
+
*/
|
|
79
|
+
export class ScopedPalette extends Palette {
|
|
80
|
+
constructor(
|
|
81
|
+
private readonly base: Palette,
|
|
82
|
+
private readonly baseStyle?: StyleSpec,
|
|
83
|
+
) {
|
|
84
|
+
super()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override id(spec?: StyleSpec): number {
|
|
88
|
+
return this.base.id(mergeStyle(this.baseStyle, spec))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override sgr(id: number): string {
|
|
92
|
+
return this.base.sgr(id)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Merge two style specs (b overrides a). Returns undefined if nothing to set. */
|
|
97
|
+
export function mergeStyle(a?: StyleSpec, b?: StyleSpec): StyleSpec | undefined {
|
|
98
|
+
if (!a && !b) return undefined
|
|
99
|
+
const merged: StyleSpec = {}
|
|
100
|
+
const fg = b?.fg ?? a?.fg
|
|
101
|
+
const bg = b?.bg ?? a?.bg
|
|
102
|
+
const bold = b?.bold ?? a?.bold
|
|
103
|
+
const italic = b?.italic ?? a?.italic
|
|
104
|
+
const underline = b?.underline ?? a?.underline
|
|
105
|
+
const inverse = b?.inverse ?? a?.inverse
|
|
106
|
+
if (fg !== undefined) merged.fg = fg
|
|
107
|
+
if (bg !== undefined) merged.bg = bg
|
|
108
|
+
if (bold !== undefined) merged.bold = bold
|
|
109
|
+
if (italic !== undefined) merged.italic = italic
|
|
110
|
+
if (underline !== undefined) merged.underline = underline
|
|
111
|
+
if (inverse !== undefined) merged.inverse = inverse
|
|
112
|
+
return Object.keys(merged).length > 0 ? merged : undefined
|
|
113
|
+
}
|