@effect-tui/core 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -11
- package/dist/ansi.d.ts +127 -32
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +159 -37
- package/dist/ansi.js.map +1 -1
- package/dist/colors.d.ts +139 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +339 -0
- package/dist/colors.js.map +1 -0
- package/dist/index.d.ts +6 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +21 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +199 -58
- package/dist/keys.js.map +1 -1
- package/dist/layout/axis-helpers.d.ts +19 -0
- package/dist/layout/axis-helpers.d.ts.map +1 -0
- package/dist/layout/axis-helpers.js +19 -0
- package/dist/layout/axis-helpers.js.map +1 -0
- package/dist/output.d.ts +59 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +142 -0
- package/dist/output.js.map +1 -0
- package/dist/render/buffer.d.ts.map +1 -1
- package/dist/render/buffer.js +6 -25
- package/dist/render/buffer.js.map +1 -1
- package/dist/render/graphemes.d.ts +15 -0
- package/dist/render/graphemes.d.ts.map +1 -0
- package/dist/render/graphemes.js +28 -0
- package/dist/render/graphemes.js.map +1 -0
- package/dist/render/measure.d.ts +1 -0
- package/dist/render/measure.d.ts.map +1 -1
- package/dist/render/measure.js +14 -36
- package/dist/render/measure.js.map +1 -1
- package/dist/render/palette.d.ts.map +1 -1
- package/dist/render/palette.js +26 -1
- package/dist/render/palette.js.map +1 -1
- package/dist/render/segmenter.d.ts +8 -0
- package/dist/render/segmenter.d.ts.map +1 -0
- package/dist/render/segmenter.js +23 -0
- package/dist/render/segmenter.js.map +1 -0
- package/dist/render/surface.d.ts +6 -32
- package/dist/render/surface.d.ts.map +1 -1
- package/dist/render/surface.js +11 -80
- package/dist/render/surface.js.map +1 -1
- package/dist/runtime/backend_node.d.ts.map +1 -1
- package/dist/runtime/backend_node.js.map +1 -1
- package/dist/tailwind-colors.d.ts +291 -0
- package/dist/tailwind-colors.d.ts.map +1 -0
- package/dist/tailwind-colors.js +291 -0
- package/dist/tailwind-colors.js.map +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -55
- package/src/ansi.ts +201 -73
- package/src/colors.ts +468 -0
- package/src/index.ts +28 -14
- package/src/keys.ts +467 -287
- package/src/layout/axis-helpers.ts +33 -0
- package/src/output.ts +175 -0
- package/src/render/buffer.ts +161 -184
- package/src/render/graphemes.ts +34 -0
- package/src/render/measure.ts +15 -38
- package/src/render/palette.ts +98 -77
- package/src/render/segmenter.ts +27 -0
- package/src/render/surface.ts +139 -225
- package/src/runtime/backend_node.ts +71 -71
- package/src/tailwind-colors.ts +295 -0
- package/src/types.ts +18 -0
- package/dist/anim.d.ts +0 -4
- package/dist/anim.d.ts.map +0 -1
- package/dist/anim.js +0 -5
- package/dist/anim.js.map +0 -1
- package/dist/layout/linearStack.d.ts +0 -17
- package/dist/layout/linearStack.d.ts.map +0 -1
- package/dist/layout/linearStack.js +0 -86
- package/dist/layout/linearStack.js.map +0 -1
- package/dist/motion-value.d.ts +0 -58
- package/dist/motion-value.d.ts.map +0 -1
- package/dist/motion-value.js +0 -250
- package/dist/motion-value.js.map +0 -1
- package/dist/present/display.d.ts +0 -58
- package/dist/present/display.d.ts.map +0 -1
- package/dist/present/display.js +0 -168
- package/dist/present/display.js.map +0 -1
- package/dist/present/writers/fullscreen.d.ts +0 -19
- package/dist/present/writers/fullscreen.d.ts.map +0 -1
- package/dist/present/writers/fullscreen.js +0 -55
- package/dist/present/writers/fullscreen.js.map +0 -1
- package/dist/present/writers/inline.d.ts +0 -20
- package/dist/present/writers/inline.d.ts.map +0 -1
- package/dist/present/writers/inline.js +0 -92
- package/dist/present/writers/inline.js.map +0 -1
- package/dist/render/color-utils.d.ts +0 -18
- package/dist/render/color-utils.d.ts.map +0 -1
- package/dist/render/color-utils.js +0 -58
- package/dist/render/color-utils.js.map +0 -1
- package/dist/render/diff.d.ts +0 -30
- package/dist/render/diff.d.ts.map +0 -1
- package/dist/render/diff.js +0 -83
- package/dist/render/diff.js.map +0 -1
- package/dist/spring-physics.d.ts +0 -36
- package/dist/spring-physics.d.ts.map +0 -1
- package/dist/spring-physics.js +0 -113
- package/dist/spring-physics.js.map +0 -1
- package/dist/spring.d.ts +0 -73
- package/dist/spring.d.ts.map +0 -1
- package/dist/spring.js +0 -136
- package/dist/spring.js.map +0 -1
- package/dist/ui/containers/canvas.d.ts +0 -13
- package/dist/ui/containers/canvas.d.ts.map +0 -1
- package/dist/ui/containers/canvas.js +0 -16
- package/dist/ui/containers/canvas.js.map +0 -1
- package/dist/ui/containers/geometry-reader.d.ts +0 -17
- package/dist/ui/containers/geometry-reader.d.ts.map +0 -1
- package/dist/ui/containers/geometry-reader.js +0 -24
- package/dist/ui/containers/geometry-reader.js.map +0 -1
- package/dist/ui/containers/hstack.d.ts +0 -12
- package/dist/ui/containers/hstack.d.ts.map +0 -1
- package/dist/ui/containers/hstack.js +0 -28
- package/dist/ui/containers/hstack.js.map +0 -1
- package/dist/ui/containers/scroll.d.ts +0 -28
- package/dist/ui/containers/scroll.d.ts.map +0 -1
- package/dist/ui/containers/scroll.js +0 -97
- package/dist/ui/containers/scroll.js.map +0 -1
- package/dist/ui/containers/shared.d.ts +0 -12
- package/dist/ui/containers/shared.d.ts.map +0 -1
- package/dist/ui/containers/shared.js +0 -19
- package/dist/ui/containers/shared.js.map +0 -1
- package/dist/ui/containers/vstack.d.ts +0 -12
- package/dist/ui/containers/vstack.d.ts.map +0 -1
- package/dist/ui/containers/vstack.js +0 -28
- package/dist/ui/containers/vstack.js.map +0 -1
- package/dist/ui/containers/zstack.d.ts +0 -14
- package/dist/ui/containers/zstack.d.ts.map +0 -1
- package/dist/ui/containers/zstack.js +0 -36
- package/dist/ui/containers/zstack.js.map +0 -1
- package/dist/ui/core/geometry-store.d.ts +0 -22
- package/dist/ui/core/geometry-store.d.ts.map +0 -1
- package/dist/ui/core/geometry-store.js +0 -29
- package/dist/ui/core/geometry-store.js.map +0 -1
- package/dist/ui/core/geometry.d.ts +0 -34
- package/dist/ui/core/geometry.d.ts.map +0 -1
- package/dist/ui/core/geometry.js +0 -14
- package/dist/ui/core/geometry.js.map +0 -1
- package/dist/ui/core/view.d.ts +0 -25
- package/dist/ui/core/view.d.ts.map +0 -1
- package/dist/ui/core/view.js +0 -34
- package/dist/ui/core/view.js.map +0 -1
- package/dist/ui/index.d.ts +0 -44
- package/dist/ui/index.d.ts.map +0 -1
- package/dist/ui/index.js +0 -39
- package/dist/ui/index.js.map +0 -1
- package/dist/ui/inlinetext.d.ts +0 -24
- package/dist/ui/inlinetext.d.ts.map +0 -1
- package/dist/ui/inlinetext.js +0 -131
- package/dist/ui/inlinetext.js.map +0 -1
- package/dist/ui/install.d.ts +0 -22
- package/dist/ui/install.d.ts.map +0 -1
- package/dist/ui/install.js +0 -66
- package/dist/ui/install.js.map +0 -1
- package/dist/ui/markdown.d.ts +0 -40
- package/dist/ui/markdown.d.ts.map +0 -1
- package/dist/ui/markdown.js +0 -351
- package/dist/ui/markdown.js.map +0 -1
- package/dist/ui/modifiers/border.d.ts +0 -33
- package/dist/ui/modifiers/border.d.ts.map +0 -1
- package/dist/ui/modifiers/border.js +0 -82
- package/dist/ui/modifiers/border.js.map +0 -1
- package/dist/ui/modifiers/fill.d.ts +0 -14
- package/dist/ui/modifiers/fill.d.ts.map +0 -1
- package/dist/ui/modifiers/fill.js +0 -25
- package/dist/ui/modifiers/fill.js.map +0 -1
- package/dist/ui/modifiers/frame.d.ts +0 -23
- package/dist/ui/modifiers/frame.d.ts.map +0 -1
- package/dist/ui/modifiers/frame.js +0 -54
- package/dist/ui/modifiers/frame.js.map +0 -1
- package/dist/ui/modifiers/offset.d.ts +0 -15
- package/dist/ui/modifiers/offset.d.ts.map +0 -1
- package/dist/ui/modifiers/offset.js +0 -21
- package/dist/ui/modifiers/offset.js.map +0 -1
- package/dist/ui/modifiers/opacity.d.ts +0 -15
- package/dist/ui/modifiers/opacity.d.ts.map +0 -1
- package/dist/ui/modifiers/opacity.js +0 -95
- package/dist/ui/modifiers/opacity.js.map +0 -1
- package/dist/ui/modifiers/padding.d.ts +0 -20
- package/dist/ui/modifiers/padding.d.ts.map +0 -1
- package/dist/ui/modifiers/padding.js +0 -36
- package/dist/ui/modifiers/padding.js.map +0 -1
- package/dist/ui/modifiers/styled.d.ts +0 -14
- package/dist/ui/modifiers/styled.d.ts.map +0 -1
- package/dist/ui/modifiers/styled.js +0 -26
- package/dist/ui/modifiers/styled.js.map +0 -1
- package/dist/ui/primitives/rectangle.d.ts +0 -15
- package/dist/ui/primitives/rectangle.d.ts.map +0 -1
- package/dist/ui/primitives/rectangle.js +0 -23
- package/dist/ui/primitives/rectangle.js.map +0 -1
- package/dist/ui/primitives/spacer.d.ts +0 -13
- package/dist/ui/primitives/spacer.d.ts.map +0 -1
- package/dist/ui/primitives/spacer.js +0 -16
- package/dist/ui/primitives/spacer.js.map +0 -1
- package/dist/ui/primitives/text.d.ts +0 -15
- package/dist/ui/primitives/text.d.ts.map +0 -1
- package/dist/ui/primitives/text.js +0 -79
- package/dist/ui/primitives/text.js.map +0 -1
- package/dist/ui/primitives/wrapped-text.d.ts +0 -30
- package/dist/ui/primitives/wrapped-text.d.ts.map +0 -1
- package/dist/ui/primitives/wrapped-text.js +0 -117
- package/dist/ui/primitives/wrapped-text.js.map +0 -1
- package/dist/ui/shinytext.d.ts +0 -66
- package/dist/ui/shinytext.d.ts.map +0 -1
- package/dist/ui/shinytext.js +0 -99
- package/dist/ui/shinytext.js.map +0 -1
- package/dist/ui/text/layout.d.ts +0 -35
- package/dist/ui/text/layout.d.ts.map +0 -1
- package/dist/ui/text/layout.js +0 -102
- package/dist/ui/text/layout.js.map +0 -1
- package/dist/ui/textinput.d.ts +0 -140
- package/dist/ui/textinput.d.ts.map +0 -1
- package/dist/ui/textinput.js +0 -402
- package/dist/ui/textinput.js.map +0 -1
- package/dist/ui/view-constructors.d.ts +0 -72
- package/dist/ui/view-constructors.d.ts.map +0 -1
- package/dist/ui/view-constructors.js +0 -74
- package/dist/ui/view-constructors.js.map +0 -1
- package/src/anim.ts +0 -5
- package/src/layout/linearStack.ts +0 -115
- package/src/motion-value.ts +0 -335
- package/src/present/display.ts +0 -206
- package/src/present/writers/fullscreen.ts +0 -58
- package/src/present/writers/inline.ts +0 -101
- package/src/render/color-utils.ts +0 -60
- package/src/render/diff.ts +0 -95
- package/src/spring-physics.ts +0 -151
- package/src/spring.ts +0 -234
- package/src/ui/__snapshots__/wrappedtext.test.ts.snap +0 -57
- package/src/ui/containers/canvas.ts +0 -18
- package/src/ui/containers/geometry-reader.ts +0 -32
- package/src/ui/containers/hstack.ts +0 -33
- package/src/ui/containers/scroll.ts +0 -106
- package/src/ui/containers/shared.ts +0 -27
- package/src/ui/containers/vstack.ts +0 -34
- package/src/ui/containers/zstack.ts +0 -37
- package/src/ui/core/geometry-store.ts +0 -42
- package/src/ui/core/geometry.ts +0 -30
- package/src/ui/core/view.ts +0 -49
- package/src/ui/index.ts +0 -84
- package/src/ui/inlinetext.ts +0 -135
- package/src/ui/install.ts +0 -110
- package/src/ui/markdown.test.ts +0 -74
- package/src/ui/markdown.ts +0 -388
- package/src/ui/modifiers/border.ts +0 -100
- package/src/ui/modifiers/fill.ts +0 -28
- package/src/ui/modifiers/frame.ts +0 -74
- package/src/ui/modifiers/offset.ts +0 -23
- package/src/ui/modifiers/opacity.ts +0 -93
- package/src/ui/modifiers/padding.ts +0 -53
- package/src/ui/modifiers/styled.ts +0 -31
- package/src/ui/primitives/rectangle.ts +0 -25
- package/src/ui/primitives/spacer.ts +0 -18
- package/src/ui/primitives/text.ts +0 -85
- package/src/ui/primitives/wrapped-text.ts +0 -131
- package/src/ui/shinytext.ts +0 -159
- package/src/ui/text/layout.ts +0 -119
- package/src/ui/textinput.ts +0 -496
- package/src/ui/view-constructors.ts +0 -96
- package/src/ui/wrappedtext.test.ts +0 -138
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// axis-helpers.ts — Utility functions for axis-aware layout calculations
|
|
2
|
+
// Reduces repeated `axis === "vertical" ? size.h : size.w` patterns
|
|
3
|
+
|
|
4
|
+
import type { Axis, Size, Rect } from "../types.js"
|
|
5
|
+
|
|
6
|
+
// Re-export for backwards compatibility
|
|
7
|
+
export type { Axis, Size, Rect } from "../types.js"
|
|
8
|
+
|
|
9
|
+
/** Get the main-axis size (height for vertical, width for horizontal) */
|
|
10
|
+
export const mainSize = (axis: Axis, s: Size): number => (axis === "vertical" ? s.h : s.w)
|
|
11
|
+
|
|
12
|
+
/** Get the cross-axis size (width for vertical, height for horizontal) */
|
|
13
|
+
export const crossSize = (axis: Axis, s: Size): number => (axis === "vertical" ? s.w : s.h)
|
|
14
|
+
|
|
15
|
+
/** Get the main-axis position (y for vertical, x for horizontal) */
|
|
16
|
+
export const mainPos = (axis: Axis, r: Rect): number => (axis === "vertical" ? r.y : r.x)
|
|
17
|
+
|
|
18
|
+
/** Get the cross-axis position (x for vertical, y for horizontal) */
|
|
19
|
+
export const crossPos = (axis: Axis, r: Rect): number => (axis === "vertical" ? r.x : r.y)
|
|
20
|
+
|
|
21
|
+
/** Get the main-axis dimension (height for vertical, width for horizontal) */
|
|
22
|
+
export const mainDim = (axis: Axis, r: Rect): number => (axis === "vertical" ? r.h : r.w)
|
|
23
|
+
|
|
24
|
+
/** Get the cross-axis dimension (width for vertical, height for horizontal) */
|
|
25
|
+
export const crossDim = (axis: Axis, r: Rect): number => (axis === "vertical" ? r.w : r.h)
|
|
26
|
+
|
|
27
|
+
/** Create a rect from main/cross coordinates and dimensions */
|
|
28
|
+
export const makeRect = (axis: Axis, mainP: number, crossP: number, mainD: number, crossD: number): Rect =>
|
|
29
|
+
axis === "vertical" ? { x: crossP, y: mainP, w: crossD, h: mainD } : { x: mainP, y: crossP, w: mainD, h: crossD }
|
|
30
|
+
|
|
31
|
+
/** Split max constraints into [maxMain, maxCross] */
|
|
32
|
+
export const splitConstraints = (axis: Axis, maxW: number, maxH: number): [number, number] =>
|
|
33
|
+
axis === "vertical" ? [maxH, maxW] : [maxW, maxH]
|
package/src/output.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output helpers for rendering CellBuffer to ANSI strings.
|
|
3
|
+
* Provides diffing, row emission, and buffer-to-string conversion.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CellBuffer } from "./render/buffer.js"
|
|
7
|
+
import type { Palette } from "./render/palette.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Emit a row of cells as an ANSI string with run-length SGR encoding.
|
|
11
|
+
* Handles wide characters (cellWidth=0 continuations) and style changes.
|
|
12
|
+
*
|
|
13
|
+
* @param buffer - The cell buffer to read from
|
|
14
|
+
* @param palette - Palette for SGR code generation
|
|
15
|
+
* @param y - Row index
|
|
16
|
+
* @param width - Terminal width
|
|
17
|
+
* @param startX - Start column (default 0)
|
|
18
|
+
* @param endX - End column exclusive (default width)
|
|
19
|
+
* @returns ANSI string for the row (no cursor positioning, no trailing reset)
|
|
20
|
+
*/
|
|
21
|
+
export function emitRow(
|
|
22
|
+
buffer: CellBuffer,
|
|
23
|
+
palette: Palette,
|
|
24
|
+
y: number,
|
|
25
|
+
width: number,
|
|
26
|
+
startX = 0,
|
|
27
|
+
endX = width,
|
|
28
|
+
): { output: string; lastStyle: number } {
|
|
29
|
+
const row = y * width
|
|
30
|
+
let output = ""
|
|
31
|
+
let visualCol = startX
|
|
32
|
+
let currentStyle = -1
|
|
33
|
+
|
|
34
|
+
for (let x = startX; x < endX && visualCol < width; x++) {
|
|
35
|
+
const idx = row + x
|
|
36
|
+
const glyph = buffer.g[idx]
|
|
37
|
+
const styleId = buffer.s[idx]
|
|
38
|
+
const cellWidth = buffer.cw[idx] || 1
|
|
39
|
+
|
|
40
|
+
// Skip continuation cells (wide char second half)
|
|
41
|
+
if (cellWidth === 0) continue
|
|
42
|
+
|
|
43
|
+
// Stop if this char would overflow
|
|
44
|
+
if (visualCol + cellWidth > width) break
|
|
45
|
+
|
|
46
|
+
// Emit SGR only when style changes (run-length encoding)
|
|
47
|
+
if (styleId !== currentStyle) {
|
|
48
|
+
output += palette.sgr(styleId)
|
|
49
|
+
currentStyle = styleId
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
output += glyph === 32 ? " " : String.fromCodePoint(glyph)
|
|
53
|
+
visualCol += cellWidth
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { output, lastStyle: currentStyle }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Emit a row and reset style if needed.
|
|
61
|
+
*/
|
|
62
|
+
export function emitRowWithReset(
|
|
63
|
+
buffer: CellBuffer,
|
|
64
|
+
palette: Palette,
|
|
65
|
+
y: number,
|
|
66
|
+
width: number,
|
|
67
|
+
startX = 0,
|
|
68
|
+
endX = width,
|
|
69
|
+
): string {
|
|
70
|
+
const { output, lastStyle } = emitRow(buffer, palette, y, width, startX, endX)
|
|
71
|
+
return lastStyle !== 0 ? output + palette.sgr(0) : output
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if a row changed between two buffers.
|
|
76
|
+
*/
|
|
77
|
+
export function rowChanged(prev: CellBuffer, next: CellBuffer, y: number, width: number): boolean {
|
|
78
|
+
const row = y * width
|
|
79
|
+
for (let x = 0; x < width; x++) {
|
|
80
|
+
const idx = row + x
|
|
81
|
+
if (next.g[idx] !== prev.g[idx] || next.s[idx] !== prev.s[idx] || next.cw[idx] !== prev.cw[idx]) {
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find the rightmost column with content (non-space or styled).
|
|
90
|
+
* Returns 0 if row is empty.
|
|
91
|
+
*/
|
|
92
|
+
export function rowContentWidth(buffer: CellBuffer, y: number, width: number): number {
|
|
93
|
+
const row = y * width
|
|
94
|
+
for (let x = width - 1; x >= 0; x--) {
|
|
95
|
+
const idx = row + x
|
|
96
|
+
// Skip continuation cells
|
|
97
|
+
if (buffer.cw[idx] === 0) continue
|
|
98
|
+
// Found content if non-space or has style
|
|
99
|
+
if (buffer.g[idx] !== 32 || buffer.s[idx] !== 0) {
|
|
100
|
+
return x + buffer.cw[idx] // account for wide char width
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return 0
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Find the change window between two buffers for a row.
|
|
108
|
+
* Returns the leftmost and rightmost changed columns, or null if no changes.
|
|
109
|
+
*/
|
|
110
|
+
export function findChangeWindow(
|
|
111
|
+
prev: CellBuffer,
|
|
112
|
+
next: CellBuffer,
|
|
113
|
+
y: number,
|
|
114
|
+
width: number,
|
|
115
|
+
): { left: number; right: number } | null {
|
|
116
|
+
const row = y * width
|
|
117
|
+
let left = 0
|
|
118
|
+
let right = width - 1
|
|
119
|
+
|
|
120
|
+
// Find leftmost change
|
|
121
|
+
while (left <= right) {
|
|
122
|
+
const i = row + left
|
|
123
|
+
if (next.g[i] !== prev.g[i] || next.s[i] !== prev.s[i] || next.cw[i] !== prev.cw[i]) {
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
left++
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// No changes found
|
|
130
|
+
if (left > right) return null
|
|
131
|
+
|
|
132
|
+
// Find rightmost change
|
|
133
|
+
while (right >= left) {
|
|
134
|
+
const i = row + right
|
|
135
|
+
if (next.g[i] !== prev.g[i] || next.s[i] !== prev.s[i] || next.cw[i] !== prev.cw[i]) {
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
right--
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { left, right }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Find the last row with content in a buffer.
|
|
146
|
+
*/
|
|
147
|
+
export function contentHeight(buffer: CellBuffer, width: number, height: number): number {
|
|
148
|
+
for (let y = height - 1; y >= 0; y--) {
|
|
149
|
+
const row = y * width
|
|
150
|
+
for (let x = 0; x < width; x++) {
|
|
151
|
+
const glyph = buffer.g[row + x]
|
|
152
|
+
const style = buffer.s[row + x]
|
|
153
|
+
if (glyph !== 32 || style !== 0) return y + 1
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Convert a CellBuffer to a complete ANSI string (for static output).
|
|
161
|
+
* Each row is emitted with proper styling and joined with newlines.
|
|
162
|
+
*
|
|
163
|
+
* @param buffer - The cell buffer to convert
|
|
164
|
+
* @param palette - Palette for SGR code generation
|
|
165
|
+
* @param width - Buffer width
|
|
166
|
+
* @param height - Number of rows to emit
|
|
167
|
+
* @returns ANSI string with newlines between rows
|
|
168
|
+
*/
|
|
169
|
+
export function bufferToString(buffer: CellBuffer, palette: Palette, width: number, height: number): string {
|
|
170
|
+
const lines: string[] = []
|
|
171
|
+
for (let y = 0; y < height; y++) {
|
|
172
|
+
lines.push(emitRowWithReset(buffer, palette, y, width))
|
|
173
|
+
}
|
|
174
|
+
return lines.join("\n")
|
|
175
|
+
}
|
package/src/render/buffer.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* buffer.ts — 2D cell buffer with drawing, clipping, and offsets */
|
|
2
2
|
|
|
3
|
+
import { iterateGraphemeCells } from "./graphemes.js"
|
|
4
|
+
|
|
3
5
|
export type Wcwidth = (cp: number) => 0 | 1 | 2
|
|
4
6
|
|
|
5
7
|
// basic ASCII/Latin fallback; plug a real wcwidth for CJK/emoji later
|
|
@@ -9,192 +11,167 @@ type ClipRect = { x: number; y: number; w: number; h: number }
|
|
|
9
11
|
|
|
10
12
|
// Represents a single frame of cells (glyph + style + cell width)
|
|
11
13
|
export class CellBuffer {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
14
|
+
readonly w: number
|
|
15
|
+
readonly h: number
|
|
16
|
+
|
|
17
|
+
// cell data
|
|
18
|
+
readonly g: Uint32Array // glyph code points
|
|
19
|
+
readonly s: Uint32Array // style ids
|
|
20
|
+
readonly cw: Uint8Array // cell width: 0 (continuation), 1, 2
|
|
21
|
+
|
|
22
|
+
private wcwidth: Wcwidth
|
|
23
|
+
private clip: ClipRect
|
|
24
|
+
private clipStack: ClipRect[] = []
|
|
25
|
+
private offX = 0
|
|
26
|
+
private offY = 0
|
|
27
|
+
private offStack: { x: number; y: number }[] = []
|
|
28
|
+
|
|
29
|
+
constructor(width: number, height: number, wcwidth: Wcwidth = defaultWcwidth) {
|
|
30
|
+
this.w = Math.max(1, width | 0)
|
|
31
|
+
this.h = Math.max(1, height | 0)
|
|
32
|
+
const n = this.w * this.h
|
|
33
|
+
this.g = new Uint32Array(n)
|
|
34
|
+
this.s = new Uint32Array(n)
|
|
35
|
+
this.cw = new Uint8Array(n)
|
|
36
|
+
this.wcwidth = wcwidth
|
|
37
|
+
this.clip = { x: 0, y: 0, w: this.w, h: this.h }
|
|
38
|
+
this.clear(0)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Clear the buffer to spaces in the given style. */
|
|
42
|
+
clear(styleId = 0): void {
|
|
43
|
+
this.g.fill(32)
|
|
44
|
+
this.s.fill(styleId)
|
|
45
|
+
this.cw.fill(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Draw a single code point at (x,y). If width=2, marks following cell as continuation. */
|
|
49
|
+
drawCP(x: number, y: number, cp: number, styleId = 0): void {
|
|
50
|
+
const ax = x + this.offX
|
|
51
|
+
const ay = y + this.offY
|
|
52
|
+
if (ax < 0 || ay < 0 || ax >= this.w || ay >= this.h) return
|
|
53
|
+
if (ax < this.clip.x || ay < this.clip.y || ax >= this.clip.x + this.clip.w || ay >= this.clip.y + this.clip.h)
|
|
54
|
+
return
|
|
55
|
+
const idx = ay * this.w + ax
|
|
56
|
+
const w = this.wcwidth(cp)
|
|
57
|
+
this.g[idx] = cp
|
|
58
|
+
this.s[idx] = styleId
|
|
59
|
+
this.cw[idx] = w === 0 ? 1 : w // treat nonspacing as width 1 here
|
|
60
|
+
|
|
61
|
+
if (w === 2) {
|
|
62
|
+
// mark the continuation cell
|
|
63
|
+
if (x + 1 < this.w) {
|
|
64
|
+
const cidx = idx + 1
|
|
65
|
+
this.g[cidx] = 0 // not printed
|
|
66
|
+
this.s[cidx] = styleId
|
|
67
|
+
this.cw[cidx] = 0 // continuation slot
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Draw plain text (no wrapping) clipped to width. Uses Intl.Segmenter if available. */
|
|
73
|
+
drawText(x: number, y: number, text: string, styleId = 0, maxWidth?: number): void {
|
|
74
|
+
const ax = x + this.offX
|
|
75
|
+
const ay = y + this.offY
|
|
76
|
+
if (ay < 0 || ay >= this.h) return
|
|
77
|
+
// vertical clip
|
|
78
|
+
if (ay < this.clip.y || ay >= this.clip.y + this.clip.h) return
|
|
79
|
+
const limit = Math.min(this.w - ax, maxWidth ?? this.w, this.clip.x + this.clip.w - ax)
|
|
80
|
+
if (limit <= 0) return
|
|
92
81
|
|
|
93
82
|
let col = 0
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
}
|
|
83
|
+
for (const { cp, width } of iterateGraphemeCells(text, this.wcwidth)) {
|
|
84
|
+
if (col + width > limit) break
|
|
85
|
+
this.drawCP(x + col, y, cp, styleId)
|
|
86
|
+
col += width
|
|
110
87
|
}
|
|
111
88
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
89
|
+
// pad remainder within the visible limit with spaces in the same style (erases leftovers)
|
|
90
|
+
for (; col < limit; col++) {
|
|
91
|
+
this.drawCP(x + col, y, 32, styleId)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Fill a rectangle with a code point + style. */
|
|
96
|
+
fillRect(x: number, y: number, w: number, h: number, cp = 32, styleId = 0): void {
|
|
97
|
+
const ax = x + this.offX
|
|
98
|
+
const ay = y + this.offY
|
|
99
|
+
let x0 = Math.max(0, ax | 0),
|
|
100
|
+
y0 = Math.max(0, ay | 0)
|
|
101
|
+
let x1 = Math.min(this.w, x0 + Math.max(0, w | 0))
|
|
102
|
+
let y1 = Math.min(this.h, y0 + Math.max(0, h | 0))
|
|
103
|
+
// intersect with current clip
|
|
104
|
+
x0 = Math.max(x0, this.clip.x)
|
|
105
|
+
y0 = Math.max(y0, this.clip.y)
|
|
106
|
+
x1 = Math.min(x1, this.clip.x + this.clip.w)
|
|
107
|
+
y1 = Math.min(y1, this.clip.y + this.clip.h)
|
|
108
|
+
const width = x1 - x0
|
|
109
|
+
if (width <= 0 || y1 <= y0) return
|
|
110
|
+
for (let yy = y0; yy < y1; yy++) {
|
|
111
|
+
const base = yy * this.w + x0
|
|
112
|
+
this.g.fill(cp, base, base + width)
|
|
113
|
+
this.s.fill(styleId, base, base + width)
|
|
114
|
+
this.cw.fill(1, base, base + width)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Clipping API
|
|
119
|
+
pushClip(x: number, y: number, w: number, h: number): void {
|
|
120
|
+
const nx = Math.max(0, x | 0)
|
|
121
|
+
const ny = Math.max(0, y | 0)
|
|
122
|
+
const nw = Math.max(0, w | 0)
|
|
123
|
+
const nh = Math.max(0, h | 0)
|
|
124
|
+
const cur = this.clip
|
|
125
|
+
const ix = Math.max(cur.x, nx)
|
|
126
|
+
const iy = Math.max(cur.y, ny)
|
|
127
|
+
const ix2 = Math.min(cur.x + cur.w, nx + nw)
|
|
128
|
+
const iy2 = Math.min(cur.y + cur.h, ny + nh)
|
|
129
|
+
this.clipStack.push(cur)
|
|
130
|
+
this.clip = { x: ix, y: iy, w: Math.max(0, ix2 - ix), h: Math.max(0, iy2 - iy) }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
popClip(): void {
|
|
134
|
+
if (this.clipStack.length > 0) {
|
|
135
|
+
const prev = this.clipStack.pop() as ClipRect
|
|
136
|
+
this.clip = prev
|
|
137
|
+
} else {
|
|
138
|
+
this.clip = { x: 0, y: 0, w: this.w, h: this.h }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
withClip(x: number, y: number, w: number, h: number, fn: () => void): void {
|
|
143
|
+
this.pushClip(x, y, w, h)
|
|
144
|
+
try {
|
|
145
|
+
fn()
|
|
146
|
+
} finally {
|
|
147
|
+
this.popClip()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Translation API
|
|
152
|
+
pushOffset(dx: number, dy: number): void {
|
|
153
|
+
this.offStack.push({ x: this.offX, y: this.offY })
|
|
154
|
+
this.offX += dx | 0
|
|
155
|
+
this.offY += dy | 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
popOffset(): void {
|
|
159
|
+
if (this.offStack.length > 0) {
|
|
160
|
+
const prev = this.offStack.pop() as { x: number; y: number }
|
|
161
|
+
this.offX = prev.x
|
|
162
|
+
this.offY = prev.y
|
|
163
|
+
} else {
|
|
164
|
+
this.offX = 0
|
|
165
|
+
this.offY = 0
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
withOffset(dx: number, dy: number, fn: () => void): void {
|
|
170
|
+
this.pushOffset(dx, dy)
|
|
171
|
+
try {
|
|
172
|
+
fn()
|
|
173
|
+
} finally {
|
|
174
|
+
this.popOffset()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
200
177
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getGraphemeSegmenter } from "./segmenter.js"
|
|
2
|
+
import type { Wcwidth } from "./buffer.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Iterate grapheme clusters using Intl.Segmenter when available.
|
|
6
|
+
* Falls back to code point iteration when not supported.
|
|
7
|
+
*/
|
|
8
|
+
export function* iterateGraphemes(text: string): IterableIterator<string> {
|
|
9
|
+
const seg = getGraphemeSegmenter()
|
|
10
|
+
if (seg) {
|
|
11
|
+
for (const { segment } of seg.segment(text)) {
|
|
12
|
+
yield segment
|
|
13
|
+
}
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const ch of text) {
|
|
18
|
+
yield ch
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Iterate grapheme clusters and provide their first code point + display width.
|
|
24
|
+
*/
|
|
25
|
+
export function* iterateGraphemeCells(
|
|
26
|
+
text: string,
|
|
27
|
+
wcwidth: Wcwidth,
|
|
28
|
+
): IterableIterator<{ segment: string; cp: number; width: number }> {
|
|
29
|
+
for (const segment of iterateGraphemes(text)) {
|
|
30
|
+
const cp = segment.codePointAt(0) ?? 32
|
|
31
|
+
const width = wcwidth(cp) || 1
|
|
32
|
+
yield { segment, cp, width }
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/render/measure.ts
CHANGED
|
@@ -7,45 +7,21 @@
|
|
|
7
7
|
|
|
8
8
|
import type { Wcwidth } from "./buffer.js"
|
|
9
9
|
import { defaultWcwidth } from "./buffer.js"
|
|
10
|
+
import { iterateGraphemes, iterateGraphemeCells } from "./graphemes.js"
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
}
|
|
12
|
+
// Re-export for backwards compatibility
|
|
13
|
+
export { getGraphemeSegmenter, type GraphemeSegmenter } from "./segmenter.js"
|
|
28
14
|
|
|
29
15
|
/** Split a string into grapheme clusters (ZWJ sequences kept together when supported). */
|
|
30
16
|
export function graphemes(text: string): string[] {
|
|
31
|
-
|
|
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)
|
|
17
|
+
return Array.from(iterateGraphemes(text))
|
|
39
18
|
}
|
|
40
19
|
|
|
41
20
|
/** Compute display width using wcwidth of the first code point of each grapheme. */
|
|
42
21
|
export function displayWidth(text: string, wc: Wcwidth = defaultWcwidth): number {
|
|
43
22
|
let w = 0
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const cp = g.codePointAt(0) ?? 32
|
|
47
|
-
const ww = wc(cp) || 1
|
|
48
|
-
w += ww
|
|
23
|
+
for (const { width } of iterateGraphemeCells(text, wc)) {
|
|
24
|
+
w += width
|
|
49
25
|
}
|
|
50
26
|
return w
|
|
51
27
|
}
|
|
@@ -60,15 +36,16 @@ export function sliceByWidth(
|
|
|
60
36
|
wc: Wcwidth = defaultWcwidth,
|
|
61
37
|
): { text: string; width: number; complete: boolean } {
|
|
62
38
|
if (maxWidth <= 0) return { text: "", width: 0, complete: text.length === 0 }
|
|
63
|
-
const gs = graphemes(text)
|
|
64
39
|
const out: string[] = []
|
|
65
40
|
let used = 0
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
41
|
+
let complete = true
|
|
42
|
+
for (const { segment, width } of iterateGraphemeCells(text, wc)) {
|
|
43
|
+
if (used + width > maxWidth) {
|
|
44
|
+
complete = false
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
out.push(segment)
|
|
48
|
+
used += width
|
|
72
49
|
}
|
|
73
|
-
return { text: out.join(""), width: used, complete
|
|
50
|
+
return { text: out.join(""), width: used, complete }
|
|
74
51
|
}
|