@teammates/consolonia 0.2.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 +48 -0
- package/dist/__tests__/ansi.test.d.ts +1 -0
- package/dist/__tests__/ansi.test.js +520 -0
- package/dist/__tests__/chat-view.test.d.ts +4 -0
- package/dist/__tests__/chat-view.test.js +480 -0
- package/dist/__tests__/drawing.test.d.ts +4 -0
- package/dist/__tests__/drawing.test.js +426 -0
- package/dist/__tests__/input.test.d.ts +5 -0
- package/dist/__tests__/input.test.js +911 -0
- package/dist/__tests__/layout.test.d.ts +4 -0
- package/dist/__tests__/layout.test.js +689 -0
- package/dist/__tests__/pixel.test.d.ts +1 -0
- package/dist/__tests__/pixel.test.js +674 -0
- package/dist/__tests__/render.test.d.ts +1 -0
- package/dist/__tests__/render.test.js +400 -0
- package/dist/__tests__/styled.test.d.ts +4 -0
- package/dist/__tests__/styled.test.js +149 -0
- package/dist/__tests__/widgets.test.d.ts +5 -0
- package/dist/__tests__/widgets.test.js +924 -0
- package/dist/ansi/esc.d.ts +61 -0
- package/dist/ansi/esc.js +85 -0
- package/dist/ansi/output.d.ts +66 -0
- package/dist/ansi/output.js +192 -0
- package/dist/ansi/strip.d.ts +16 -0
- package/dist/ansi/strip.js +74 -0
- package/dist/app.d.ts +68 -0
- package/dist/app.js +297 -0
- package/dist/drawing/clip.d.ts +23 -0
- package/dist/drawing/clip.js +67 -0
- package/dist/drawing/context.d.ts +77 -0
- package/dist/drawing/context.js +275 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +63 -0
- package/dist/input/escape-matcher.d.ts +27 -0
- package/dist/input/escape-matcher.js +253 -0
- package/dist/input/events.d.ts +49 -0
- package/dist/input/events.js +17 -0
- package/dist/input/index.d.ts +15 -0
- package/dist/input/index.js +14 -0
- package/dist/input/matcher.d.ts +23 -0
- package/dist/input/matcher.js +14 -0
- package/dist/input/mouse-matcher.d.ts +27 -0
- package/dist/input/mouse-matcher.js +142 -0
- package/dist/input/paste-matcher.d.ts +23 -0
- package/dist/input/paste-matcher.js +104 -0
- package/dist/input/processor.d.ts +51 -0
- package/dist/input/processor.js +145 -0
- package/dist/input/raw-mode.d.ts +13 -0
- package/dist/input/raw-mode.js +24 -0
- package/dist/input/text-matcher.d.ts +14 -0
- package/dist/input/text-matcher.js +32 -0
- package/dist/layout/box.d.ts +33 -0
- package/dist/layout/box.js +92 -0
- package/dist/layout/column.d.ts +21 -0
- package/dist/layout/column.js +90 -0
- package/dist/layout/control.d.ts +73 -0
- package/dist/layout/control.js +215 -0
- package/dist/layout/row.d.ts +21 -0
- package/dist/layout/row.js +95 -0
- package/dist/layout/stack.d.ts +18 -0
- package/dist/layout/stack.js +64 -0
- package/dist/layout/types.d.ts +27 -0
- package/dist/layout/types.js +4 -0
- package/dist/pixel/background.d.ts +16 -0
- package/dist/pixel/background.js +16 -0
- package/dist/pixel/box-pattern.d.ts +38 -0
- package/dist/pixel/box-pattern.js +57 -0
- package/dist/pixel/buffer.d.ts +25 -0
- package/dist/pixel/buffer.js +51 -0
- package/dist/pixel/color.d.ts +48 -0
- package/dist/pixel/color.js +92 -0
- package/dist/pixel/foreground.d.ts +31 -0
- package/dist/pixel/foreground.js +64 -0
- package/dist/pixel/pixel.d.ts +21 -0
- package/dist/pixel/pixel.js +38 -0
- package/dist/pixel/symbol.d.ts +38 -0
- package/dist/pixel/symbol.js +192 -0
- package/dist/render/regions.d.ts +54 -0
- package/dist/render/regions.js +102 -0
- package/dist/render/render-target.d.ts +42 -0
- package/dist/render/render-target.js +118 -0
- package/dist/styled.d.ts +113 -0
- package/dist/styled.js +176 -0
- package/dist/widgets/border.d.ts +34 -0
- package/dist/widgets/border.js +121 -0
- package/dist/widgets/chat-view.d.ts +239 -0
- package/dist/widgets/chat-view.js +993 -0
- package/dist/widgets/interview.d.ts +87 -0
- package/dist/widgets/interview.js +187 -0
- package/dist/widgets/markdown.d.ts +87 -0
- package/dist/widgets/markdown.js +611 -0
- package/dist/widgets/panel.d.ts +19 -0
- package/dist/widgets/panel.js +35 -0
- package/dist/widgets/scroll-view.d.ts +43 -0
- package/dist/widgets/scroll-view.js +182 -0
- package/dist/widgets/styled-text.d.ts +38 -0
- package/dist/widgets/styled-text.js +183 -0
- package/dist/widgets/syntax.d.ts +37 -0
- package/dist/widgets/syntax.js +670 -0
- package/dist/widgets/text-input.d.ts +121 -0
- package/dist/widgets/text-input.js +618 -0
- package/dist/widgets/text.d.ts +34 -0
- package/dist/widgets/text.js +168 -0
- package/package.json +45 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a single cell's character content.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Check if a code point is zero-width (invisible) and should be skipped
|
|
6
|
+
* during rendering. These characters have no visual representation and
|
|
7
|
+
* occupy zero terminal columns. If drawn as individual cells, terminals
|
|
8
|
+
* typically show them as "missing glyph" boxes.
|
|
9
|
+
*/
|
|
10
|
+
export function isZeroWidth(codePoint) {
|
|
11
|
+
// Soft hyphen
|
|
12
|
+
if (codePoint === 0x00ad)
|
|
13
|
+
return true;
|
|
14
|
+
// Combining diacritical marks (U+0300–U+036F)
|
|
15
|
+
if (codePoint >= 0x0300 && codePoint <= 0x036f)
|
|
16
|
+
return true;
|
|
17
|
+
// Zero-width space, non-joiner, joiner
|
|
18
|
+
if (codePoint >= 0x200b && codePoint <= 0x200d)
|
|
19
|
+
return true;
|
|
20
|
+
// Left-to-right / right-to-left marks
|
|
21
|
+
if (codePoint >= 0x200e && codePoint <= 0x200f)
|
|
22
|
+
return true;
|
|
23
|
+
// LRE, RLE, PDF, LRO, RLO
|
|
24
|
+
if (codePoint >= 0x202a && codePoint <= 0x202e)
|
|
25
|
+
return true;
|
|
26
|
+
// Word joiner
|
|
27
|
+
if (codePoint === 0x2060)
|
|
28
|
+
return true;
|
|
29
|
+
// LRI, RLI, FSI, PDI
|
|
30
|
+
if (codePoint >= 0x2066 && codePoint <= 0x2069)
|
|
31
|
+
return true;
|
|
32
|
+
// Combining grapheme joiner
|
|
33
|
+
if (codePoint === 0x034f)
|
|
34
|
+
return true;
|
|
35
|
+
// Variation selectors (VS1-VS16)
|
|
36
|
+
if (codePoint >= 0xfe00 && codePoint <= 0xfe0f)
|
|
37
|
+
return true;
|
|
38
|
+
// BOM / zero-width no-break space
|
|
39
|
+
if (codePoint === 0xfeff)
|
|
40
|
+
return true;
|
|
41
|
+
// Variation selectors supplement (VS17-VS256)
|
|
42
|
+
if (codePoint >= 0xe0100 && codePoint <= 0xe01ef)
|
|
43
|
+
return true;
|
|
44
|
+
// Tags block (used in flag sequences)
|
|
45
|
+
if (codePoint >= 0xe0001 && codePoint <= 0xe007f)
|
|
46
|
+
return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Determine the display width of a single character.
|
|
51
|
+
* Returns 2 for wide characters (CJK, fullwidth forms, emoji), 1 otherwise.
|
|
52
|
+
*/
|
|
53
|
+
export function charWidth(codePoint) {
|
|
54
|
+
// CJK Unified Ideographs
|
|
55
|
+
if (codePoint >= 0x4e00 && codePoint <= 0x9fff)
|
|
56
|
+
return 2;
|
|
57
|
+
// CJK Unified Ideographs Extension A
|
|
58
|
+
if (codePoint >= 0x3400 && codePoint <= 0x4dbf)
|
|
59
|
+
return 2;
|
|
60
|
+
// CJK Unified Ideographs Extension B
|
|
61
|
+
if (codePoint >= 0x20000 && codePoint <= 0x2a6df)
|
|
62
|
+
return 2;
|
|
63
|
+
// CJK Compatibility Ideographs
|
|
64
|
+
if (codePoint >= 0xf900 && codePoint <= 0xfaff)
|
|
65
|
+
return 2;
|
|
66
|
+
// Fullwidth Forms (excluding halfwidth range)
|
|
67
|
+
if (codePoint >= 0xff01 && codePoint <= 0xff60)
|
|
68
|
+
return 2;
|
|
69
|
+
// Fullwidth Forms (extra)
|
|
70
|
+
if (codePoint >= 0xffe0 && codePoint <= 0xffe6)
|
|
71
|
+
return 2;
|
|
72
|
+
// CJK Radicals Supplement
|
|
73
|
+
if (codePoint >= 0x2e80 && codePoint <= 0x2eff)
|
|
74
|
+
return 2;
|
|
75
|
+
// Kangxi Radicals
|
|
76
|
+
if (codePoint >= 0x2f00 && codePoint <= 0x2fdf)
|
|
77
|
+
return 2;
|
|
78
|
+
// CJK Symbols and Punctuation
|
|
79
|
+
if (codePoint >= 0x3000 && codePoint <= 0x303f)
|
|
80
|
+
return 2;
|
|
81
|
+
// Hiragana
|
|
82
|
+
if (codePoint >= 0x3040 && codePoint <= 0x309f)
|
|
83
|
+
return 2;
|
|
84
|
+
// Katakana
|
|
85
|
+
if (codePoint >= 0x30a0 && codePoint <= 0x30ff)
|
|
86
|
+
return 2;
|
|
87
|
+
// Bopomofo
|
|
88
|
+
if (codePoint >= 0x3100 && codePoint <= 0x312f)
|
|
89
|
+
return 2;
|
|
90
|
+
// Hangul Compatibility Jamo
|
|
91
|
+
if (codePoint >= 0x3130 && codePoint <= 0x318f)
|
|
92
|
+
return 2;
|
|
93
|
+
// Kanbun
|
|
94
|
+
if (codePoint >= 0x3190 && codePoint <= 0x319f)
|
|
95
|
+
return 2;
|
|
96
|
+
// Bopomofo Extended
|
|
97
|
+
if (codePoint >= 0x31a0 && codePoint <= 0x31bf)
|
|
98
|
+
return 2;
|
|
99
|
+
// CJK Strokes
|
|
100
|
+
if (codePoint >= 0x31c0 && codePoint <= 0x31ef)
|
|
101
|
+
return 2;
|
|
102
|
+
// Katakana Phonetic Extensions
|
|
103
|
+
if (codePoint >= 0x31f0 && codePoint <= 0x31ff)
|
|
104
|
+
return 2;
|
|
105
|
+
// Enclosed CJK Letters and Months
|
|
106
|
+
if (codePoint >= 0x3200 && codePoint <= 0x32ff)
|
|
107
|
+
return 2;
|
|
108
|
+
// CJK Compatibility
|
|
109
|
+
if (codePoint >= 0x3300 && codePoint <= 0x33ff)
|
|
110
|
+
return 2;
|
|
111
|
+
// Hangul Syllables
|
|
112
|
+
if (codePoint >= 0xac00 && codePoint <= 0xd7af)
|
|
113
|
+
return 2;
|
|
114
|
+
// CJK Compatibility Ideographs Supplement
|
|
115
|
+
if (codePoint >= 0x2f800 && codePoint <= 0x2fa1f)
|
|
116
|
+
return 2;
|
|
117
|
+
// ── Emoji ranges (rendered as width 2 on modern terminals) ─────
|
|
118
|
+
// Hourglass + Watch
|
|
119
|
+
if (codePoint === 0x231a || codePoint === 0x231b)
|
|
120
|
+
return 2;
|
|
121
|
+
// Player controls (⏩-⏳)
|
|
122
|
+
if (codePoint >= 0x23e9 && codePoint <= 0x23f3)
|
|
123
|
+
return 2;
|
|
124
|
+
// Media controls (⏸-⏺)
|
|
125
|
+
if (codePoint >= 0x23f8 && codePoint <= 0x23fa)
|
|
126
|
+
return 2;
|
|
127
|
+
// Play / reverse play buttons
|
|
128
|
+
if (codePoint === 0x25b6 || codePoint === 0x25c0)
|
|
129
|
+
return 2;
|
|
130
|
+
// Geometric shapes used as emoji (◻◼◽◾)
|
|
131
|
+
if (codePoint >= 0x25fb && codePoint <= 0x25fe)
|
|
132
|
+
return 2;
|
|
133
|
+
// Miscellaneous Symbols — most have emoji presentation (☀-⛿)
|
|
134
|
+
if (codePoint >= 0x2600 && codePoint <= 0x26ff)
|
|
135
|
+
return 2;
|
|
136
|
+
// Dingbats with emoji presentation (✂-➿)
|
|
137
|
+
if (codePoint >= 0x2702 && codePoint <= 0x27b0)
|
|
138
|
+
return 2;
|
|
139
|
+
// Curly loop
|
|
140
|
+
if (codePoint === 0x27bf)
|
|
141
|
+
return 2;
|
|
142
|
+
// Supplemental arrows used as emoji
|
|
143
|
+
if (codePoint === 0x2934 || codePoint === 0x2935)
|
|
144
|
+
return 2;
|
|
145
|
+
// Misc symbols used as emoji (⬛⬜⭐⭕)
|
|
146
|
+
if (codePoint >= 0x2b05 && codePoint <= 0x2b07)
|
|
147
|
+
return 2;
|
|
148
|
+
if (codePoint === 0x2b1b || codePoint === 0x2b1c)
|
|
149
|
+
return 2;
|
|
150
|
+
if (codePoint === 0x2b50 || codePoint === 0x2b55)
|
|
151
|
+
return 2;
|
|
152
|
+
// Copyright / Registered / TM (when emoji-styled)
|
|
153
|
+
if (codePoint === 0x00a9 || codePoint === 0x00ae)
|
|
154
|
+
return 2;
|
|
155
|
+
// Wavy dash, part alternation mark
|
|
156
|
+
if (codePoint === 0x3030 || codePoint === 0x303d)
|
|
157
|
+
return 2;
|
|
158
|
+
// SMP Emoji: Mahjong through Symbols & Pictographs Extended-A
|
|
159
|
+
// Covers emoticons, transport, flags, supplemental symbols, etc.
|
|
160
|
+
if (codePoint >= 0x1f000 && codePoint <= 0x1faff)
|
|
161
|
+
return 2;
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Calculate the display width of a string (sum of charWidth per code point).
|
|
166
|
+
* Zero-width characters (variation selectors, ZWJ, etc.) are excluded.
|
|
167
|
+
* Useful for layout and wrapping where terminal column count matters.
|
|
168
|
+
*/
|
|
169
|
+
export function stringDisplayWidth(text) {
|
|
170
|
+
let width = 0;
|
|
171
|
+
for (const char of text) {
|
|
172
|
+
const cp = char.codePointAt(0);
|
|
173
|
+
if (!isZeroWidth(cp)) {
|
|
174
|
+
width += charWidth(cp);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return width;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Create a Symbol from a text string.
|
|
181
|
+
* Width is auto-detected from the first code point.
|
|
182
|
+
*/
|
|
183
|
+
export function sym(text, pattern = 0) {
|
|
184
|
+
const cp = text.codePointAt(0) ?? 0;
|
|
185
|
+
return {
|
|
186
|
+
text,
|
|
187
|
+
width: pattern !== 0 ? 1 : charWidth(cp),
|
|
188
|
+
pattern,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** An empty symbol (space character). */
|
|
192
|
+
export const EMPTY_SYMBOL = { text: " ", width: 1, pattern: 0 };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dirty-region tracking for incremental rendering.
|
|
3
|
+
* Port of Consolonia's Regions.cs / Snapshot pattern.
|
|
4
|
+
*
|
|
5
|
+
* Maintains a list of dirty rectangles and provides a snapshot mechanism
|
|
6
|
+
* so the renderer can consume the current set while new rects accumulate.
|
|
7
|
+
*/
|
|
8
|
+
import type { Rect } from "../layout/types.js";
|
|
9
|
+
/**
|
|
10
|
+
* A frozen snapshot of dirty rectangles at the moment it was taken.
|
|
11
|
+
* The renderer iterates cells and calls `contains(x, y)` to decide
|
|
12
|
+
* whether a given cell needs to be re-drawn.
|
|
13
|
+
*/
|
|
14
|
+
export declare class DirtySnapshot {
|
|
15
|
+
private readonly _rects;
|
|
16
|
+
constructor(rects: readonly Rect[]);
|
|
17
|
+
/**
|
|
18
|
+
* Returns true if (x, y) falls inside any of the snapshot's rectangles.
|
|
19
|
+
* Uses exclusive upper bounds: a Rect {x:0, y:0, width:10, height:5}
|
|
20
|
+
* contains columns 0-9 and rows 0-4.
|
|
21
|
+
*/
|
|
22
|
+
contains(x: number, y: number): boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Accumulates dirty rectangles. Before each render pass the renderer
|
|
26
|
+
* calls `getSnapshotAndClear()` which atomically captures the current
|
|
27
|
+
* set and resets the internal list so new mutations can be collected
|
|
28
|
+
* while the frame is being drawn.
|
|
29
|
+
*/
|
|
30
|
+
export declare class DirtyRegions {
|
|
31
|
+
private _rects;
|
|
32
|
+
/**
|
|
33
|
+
* Register a rectangle as dirty.
|
|
34
|
+
*
|
|
35
|
+
* Optimisations (mirroring the C# Regions.AddRect):
|
|
36
|
+
* - If `rect` is empty (width or height <= 0), it is ignored.
|
|
37
|
+
* - If an existing rect already fully contains the new one, skip.
|
|
38
|
+
* - If the new rect fully contains an existing one, remove the existing one.
|
|
39
|
+
*/
|
|
40
|
+
addRect(rect: Rect): void;
|
|
41
|
+
/**
|
|
42
|
+
* Check whether a point is inside any tracked dirty region.
|
|
43
|
+
* Uses exclusive upper bounds (same semantics as DirtySnapshot.contains).
|
|
44
|
+
*/
|
|
45
|
+
contains(x: number, y: number): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Capture a snapshot of the current dirty rects and reset the list.
|
|
48
|
+
*/
|
|
49
|
+
getSnapshotAndClear(): DirtySnapshot;
|
|
50
|
+
/**
|
|
51
|
+
* Discard all tracked regions without creating a snapshot.
|
|
52
|
+
*/
|
|
53
|
+
clear(): void;
|
|
54
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dirty-region tracking for incremental rendering.
|
|
3
|
+
* Port of Consolonia's Regions.cs / Snapshot pattern.
|
|
4
|
+
*
|
|
5
|
+
* Maintains a list of dirty rectangles and provides a snapshot mechanism
|
|
6
|
+
* so the renderer can consume the current set while new rects accumulate.
|
|
7
|
+
*/
|
|
8
|
+
// ── Snapshot ────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* A frozen snapshot of dirty rectangles at the moment it was taken.
|
|
11
|
+
* The renderer iterates cells and calls `contains(x, y)` to decide
|
|
12
|
+
* whether a given cell needs to be re-drawn.
|
|
13
|
+
*/
|
|
14
|
+
export class DirtySnapshot {
|
|
15
|
+
_rects;
|
|
16
|
+
constructor(rects) {
|
|
17
|
+
this._rects = rects;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Returns true if (x, y) falls inside any of the snapshot's rectangles.
|
|
21
|
+
* Uses exclusive upper bounds: a Rect {x:0, y:0, width:10, height:5}
|
|
22
|
+
* contains columns 0-9 and rows 0-4.
|
|
23
|
+
*/
|
|
24
|
+
contains(x, y) {
|
|
25
|
+
for (const r of this._rects) {
|
|
26
|
+
if (x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ── DirtyRegions ────────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Accumulates dirty rectangles. Before each render pass the renderer
|
|
36
|
+
* calls `getSnapshotAndClear()` which atomically captures the current
|
|
37
|
+
* set and resets the internal list so new mutations can be collected
|
|
38
|
+
* while the frame is being drawn.
|
|
39
|
+
*/
|
|
40
|
+
export class DirtyRegions {
|
|
41
|
+
_rects = [];
|
|
42
|
+
/**
|
|
43
|
+
* Register a rectangle as dirty.
|
|
44
|
+
*
|
|
45
|
+
* Optimisations (mirroring the C# Regions.AddRect):
|
|
46
|
+
* - If `rect` is empty (width or height <= 0), it is ignored.
|
|
47
|
+
* - If an existing rect already fully contains the new one, skip.
|
|
48
|
+
* - If the new rect fully contains an existing one, remove the existing one.
|
|
49
|
+
*/
|
|
50
|
+
addRect(rect) {
|
|
51
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
52
|
+
return;
|
|
53
|
+
for (let i = 0; i < this._rects.length; i++) {
|
|
54
|
+
const existing = this._rects[i];
|
|
55
|
+
// Existing rect contains the new one — nothing to add.
|
|
56
|
+
if (rectContains(existing, rect))
|
|
57
|
+
return;
|
|
58
|
+
// New rect contains the existing one — remove existing.
|
|
59
|
+
if (rectContains(rect, existing)) {
|
|
60
|
+
this._rects.splice(i, 1);
|
|
61
|
+
i--;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this._rects.push(rect);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check whether a point is inside any tracked dirty region.
|
|
68
|
+
* Uses exclusive upper bounds (same semantics as DirtySnapshot.contains).
|
|
69
|
+
*/
|
|
70
|
+
contains(x, y) {
|
|
71
|
+
for (const r of this._rects) {
|
|
72
|
+
if (x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Capture a snapshot of the current dirty rects and reset the list.
|
|
80
|
+
*/
|
|
81
|
+
getSnapshotAndClear() {
|
|
82
|
+
const snapshot = new DirtySnapshot([...this._rects]);
|
|
83
|
+
this._rects = [];
|
|
84
|
+
return snapshot;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Discard all tracked regions without creating a snapshot.
|
|
88
|
+
*/
|
|
89
|
+
clear() {
|
|
90
|
+
this._rects = [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
94
|
+
/**
|
|
95
|
+
* Returns true when `outer` fully contains `inner` (exclusive upper bound).
|
|
96
|
+
*/
|
|
97
|
+
function rectContains(outer, inner) {
|
|
98
|
+
return (inner.x >= outer.x &&
|
|
99
|
+
inner.y >= outer.y &&
|
|
100
|
+
inner.x + inner.width <= outer.x + outer.width &&
|
|
101
|
+
inner.y + inner.height <= outer.y + outer.height);
|
|
102
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RenderTarget — incremental pixel renderer.
|
|
3
|
+
* Port of Consolonia's RenderTarget.cs.
|
|
4
|
+
*
|
|
5
|
+
* Compares the current PixelBuffer against a cached copy of what was
|
|
6
|
+
* last written to output, and only emits ANSI sequences for cells that
|
|
7
|
+
* have actually changed *and* fall inside the current dirty regions.
|
|
8
|
+
*/
|
|
9
|
+
import type { AnsiOutput } from "../ansi/output.js";
|
|
10
|
+
import type { PixelBuffer } from "../pixel/buffer.js";
|
|
11
|
+
import type { Pixel } from "../pixel/pixel.js";
|
|
12
|
+
import { DirtyRegions } from "./regions.js";
|
|
13
|
+
export declare class RenderTarget {
|
|
14
|
+
private readonly _buffer;
|
|
15
|
+
private readonly _output;
|
|
16
|
+
/** 2-D cache indexed [y][x]. null means "never rendered at this cell". */
|
|
17
|
+
private _cache;
|
|
18
|
+
constructor(buffer: PixelBuffer, output: AnsiOutput);
|
|
19
|
+
/**
|
|
20
|
+
* Render only the cells that are both inside a dirty region *and*
|
|
21
|
+
* differ from the cached version last written to output.
|
|
22
|
+
*/
|
|
23
|
+
render(dirtyRegions: DirtyRegions): void;
|
|
24
|
+
/**
|
|
25
|
+
* Recreate the cache at new dimensions. Every cell is set to null
|
|
26
|
+
* so the next render pass will treat everything as dirty.
|
|
27
|
+
*/
|
|
28
|
+
resize(width: number, height: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* Mark the entire buffer as dirty and perform a full render pass.
|
|
31
|
+
*/
|
|
32
|
+
fullRender(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Retrieve the cached pixel at (x, y) — useful for tests.
|
|
35
|
+
* Returns null if the cell has never been rendered.
|
|
36
|
+
*/
|
|
37
|
+
getCachePixel(x: number, y: number): Pixel | null;
|
|
38
|
+
/**
|
|
39
|
+
* Build a fresh cache grid filled with null (meaning "unknown / never rendered").
|
|
40
|
+
*/
|
|
41
|
+
private _initCache;
|
|
42
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RenderTarget — incremental pixel renderer.
|
|
3
|
+
* Port of Consolonia's RenderTarget.cs.
|
|
4
|
+
*
|
|
5
|
+
* Compares the current PixelBuffer against a cached copy of what was
|
|
6
|
+
* last written to output, and only emits ANSI sequences for cells that
|
|
7
|
+
* have actually changed *and* fall inside the current dirty regions.
|
|
8
|
+
*/
|
|
9
|
+
import { DirtyRegions } from "./regions.js";
|
|
10
|
+
export class RenderTarget {
|
|
11
|
+
_buffer;
|
|
12
|
+
_output;
|
|
13
|
+
/** 2-D cache indexed [y][x]. null means "never rendered at this cell". */
|
|
14
|
+
_cache;
|
|
15
|
+
constructor(buffer, output) {
|
|
16
|
+
this._buffer = buffer;
|
|
17
|
+
this._output = output;
|
|
18
|
+
this._cache = this._initCache(buffer.width, buffer.height);
|
|
19
|
+
}
|
|
20
|
+
// ── Public API ──────────────────────────────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* Render only the cells that are both inside a dirty region *and*
|
|
23
|
+
* differ from the cached version last written to output.
|
|
24
|
+
*/
|
|
25
|
+
render(dirtyRegions) {
|
|
26
|
+
const snapshot = dirtyRegions.getSnapshotAndClear();
|
|
27
|
+
this._output.hideCursor();
|
|
28
|
+
for (let y = 0; y < this._buffer.height; y++) {
|
|
29
|
+
for (let x = 0; x < this._buffer.width; x++) {
|
|
30
|
+
if (!snapshot.contains(x, y))
|
|
31
|
+
continue;
|
|
32
|
+
const pixel = this._buffer.get(x, y);
|
|
33
|
+
const cached = this._cache[y][x];
|
|
34
|
+
if (cached !== null && pixelsEqual(pixel, cached))
|
|
35
|
+
continue;
|
|
36
|
+
this._output.writePixel(x, y, pixel);
|
|
37
|
+
this._cache[y][x] = pixel;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this._output.flush();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Recreate the cache at new dimensions. Every cell is set to null
|
|
44
|
+
* so the next render pass will treat everything as dirty.
|
|
45
|
+
*/
|
|
46
|
+
resize(width, height) {
|
|
47
|
+
this._cache = this._initCache(width, height);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mark the entire buffer as dirty and perform a full render pass.
|
|
51
|
+
*/
|
|
52
|
+
fullRender() {
|
|
53
|
+
const regions = new DirtyRegions();
|
|
54
|
+
regions.addRect({
|
|
55
|
+
x: 0,
|
|
56
|
+
y: 0,
|
|
57
|
+
width: this._buffer.width,
|
|
58
|
+
height: this._buffer.height,
|
|
59
|
+
});
|
|
60
|
+
this.render(regions);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Retrieve the cached pixel at (x, y) — useful for tests.
|
|
64
|
+
* Returns null if the cell has never been rendered.
|
|
65
|
+
*/
|
|
66
|
+
getCachePixel(x, y) {
|
|
67
|
+
if (y < 0 || y >= this._cache.length)
|
|
68
|
+
return null;
|
|
69
|
+
if (x < 0 || x >= this._cache[y].length)
|
|
70
|
+
return null;
|
|
71
|
+
return this._cache[y][x];
|
|
72
|
+
}
|
|
73
|
+
// ── Internal ────────────────────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* Build a fresh cache grid filled with null (meaning "unknown / never rendered").
|
|
76
|
+
*/
|
|
77
|
+
_initCache(width, height) {
|
|
78
|
+
const cache = [];
|
|
79
|
+
for (let y = 0; y < height; y++) {
|
|
80
|
+
const row = new Array(width).fill(null);
|
|
81
|
+
cache.push(row);
|
|
82
|
+
}
|
|
83
|
+
return cache;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
87
|
+
/**
|
|
88
|
+
* Shallow structural equality for two Pixel values.
|
|
89
|
+
*
|
|
90
|
+
* Pixels are plain data objects so reference equality is almost never
|
|
91
|
+
* true. We compare every leaf field instead. The implementation is
|
|
92
|
+
* kept deliberately simple — if the Pixel type grows, this function
|
|
93
|
+
* must be updated.
|
|
94
|
+
*/
|
|
95
|
+
function pixelsEqual(a, b) {
|
|
96
|
+
// Fast path: same reference.
|
|
97
|
+
if (a === b)
|
|
98
|
+
return true;
|
|
99
|
+
// Compare foreground
|
|
100
|
+
const af = a.foreground;
|
|
101
|
+
const bf = b.foreground;
|
|
102
|
+
if (af.symbol.text !== bf.symbol.text)
|
|
103
|
+
return false;
|
|
104
|
+
if (af.symbol.width !== bf.symbol.width)
|
|
105
|
+
return false;
|
|
106
|
+
if (!colorsEq(af.color, bf.color))
|
|
107
|
+
return false;
|
|
108
|
+
// Compare background
|
|
109
|
+
const ab = a.background;
|
|
110
|
+
const bb = b.background;
|
|
111
|
+
if (!colorsEq(ab.color, bb.color))
|
|
112
|
+
return false;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
/** Fast inline color equality (avoids importing colorsEqual). */
|
|
116
|
+
function colorsEq(a, b) {
|
|
117
|
+
return a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a;
|
|
118
|
+
}
|
package/dist/styled.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styled — a chalk-like fluent API for building styled text segments.
|
|
3
|
+
*
|
|
4
|
+
* Instead of producing ANSI escape codes (which only work on raw stdout),
|
|
5
|
+
* Styled produces {text, style} segment arrays that can be rendered into
|
|
6
|
+
* a pixel buffer via DrawingContext.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { pen } from "@teammates/consolonia";
|
|
10
|
+
*
|
|
11
|
+
* // Single styled segment
|
|
12
|
+
* pen.cyan("hello") → [{ text: "hello", style: { fg: CYAN } }]
|
|
13
|
+
*
|
|
14
|
+
* // Chained styles
|
|
15
|
+
* pen.bold.red("error") → [{ text: "error", style: { fg: RED, bold: true } }]
|
|
16
|
+
*
|
|
17
|
+
* // Concatenation with +
|
|
18
|
+
* pen.green("✔ ") + pen.white("done")
|
|
19
|
+
* → [{ text: "✔ ", style: { fg: GREEN } }, { text: "done", style: { fg: WHITE } }]
|
|
20
|
+
*
|
|
21
|
+
* // Mixed plain + styled
|
|
22
|
+
* pen("prefix ") + pen.cyan("@name")
|
|
23
|
+
* → [{ text: "prefix ", style: {} }, { text: "@name", style: { fg: CYAN } }]
|
|
24
|
+
*
|
|
25
|
+
* // Gray is dim white (like chalk.gray)
|
|
26
|
+
* pen.gray("muted") → [{ text: "muted", style: { fg: GRAY } }]
|
|
27
|
+
*/
|
|
28
|
+
import type { TextStyle } from "./drawing/context.js";
|
|
29
|
+
import type { Color } from "./pixel/color.js";
|
|
30
|
+
/** A piece of text with an associated style. */
|
|
31
|
+
export interface StyledSegment {
|
|
32
|
+
text: string;
|
|
33
|
+
style: TextStyle;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An array of styled segments that supports + concatenation with
|
|
37
|
+
* other StyledSpan values or plain strings.
|
|
38
|
+
*/
|
|
39
|
+
export type StyledSpan = StyledSegment[] & {
|
|
40
|
+
__brand: "StyledSpan";
|
|
41
|
+
};
|
|
42
|
+
/** Check if a value is a StyledSpan. */
|
|
43
|
+
export declare function isStyledSpan(v: unknown): v is StyledSpan;
|
|
44
|
+
/** Concatenate styled spans and/or plain strings. */
|
|
45
|
+
export declare function concat(...parts: (StyledSpan | string)[]): StyledSpan;
|
|
46
|
+
/** Get the visible (unstyled) text from a StyledSpan. */
|
|
47
|
+
export declare function spanText(s: StyledSpan): string;
|
|
48
|
+
/** Get the visible display width of a StyledSpan (accounts for wide characters). */
|
|
49
|
+
export declare function spanLength(s: StyledSpan): number;
|
|
50
|
+
interface PenCallable {
|
|
51
|
+
/** Create an unstyled span from a plain string. */
|
|
52
|
+
(text: string): StyledSpan;
|
|
53
|
+
}
|
|
54
|
+
type Pen = PenCallable & {
|
|
55
|
+
readonly black: Pen;
|
|
56
|
+
readonly red: Pen;
|
|
57
|
+
readonly green: Pen;
|
|
58
|
+
readonly yellow: Pen;
|
|
59
|
+
readonly blue: Pen;
|
|
60
|
+
readonly magenta: Pen;
|
|
61
|
+
readonly cyan: Pen;
|
|
62
|
+
readonly white: Pen;
|
|
63
|
+
readonly blackBright: Pen;
|
|
64
|
+
readonly redBright: Pen;
|
|
65
|
+
readonly greenBright: Pen;
|
|
66
|
+
readonly yellowBright: Pen;
|
|
67
|
+
readonly blueBright: Pen;
|
|
68
|
+
readonly magentaBright: Pen;
|
|
69
|
+
readonly cyanBright: Pen;
|
|
70
|
+
readonly whiteBright: Pen;
|
|
71
|
+
readonly gray: Pen;
|
|
72
|
+
readonly grey: Pen;
|
|
73
|
+
readonly darkGray: Pen;
|
|
74
|
+
readonly lightGray: Pen;
|
|
75
|
+
readonly bgBlack: Pen;
|
|
76
|
+
readonly bgRed: Pen;
|
|
77
|
+
readonly bgGreen: Pen;
|
|
78
|
+
readonly bgYellow: Pen;
|
|
79
|
+
readonly bgBlue: Pen;
|
|
80
|
+
readonly bgMagenta: Pen;
|
|
81
|
+
readonly bgCyan: Pen;
|
|
82
|
+
readonly bgWhite: Pen;
|
|
83
|
+
readonly bgBlackBright: Pen;
|
|
84
|
+
readonly bgRedBright: Pen;
|
|
85
|
+
readonly bgGreenBright: Pen;
|
|
86
|
+
readonly bgYellowBright: Pen;
|
|
87
|
+
readonly bgBlueBright: Pen;
|
|
88
|
+
readonly bgMagentaBright: Pen;
|
|
89
|
+
readonly bgCyanBright: Pen;
|
|
90
|
+
readonly bgWhiteBright: Pen;
|
|
91
|
+
readonly bgGray: Pen;
|
|
92
|
+
readonly bgGrey: Pen;
|
|
93
|
+
/** Set foreground to an arbitrary Color. */
|
|
94
|
+
readonly fg: (c: Color) => Pen;
|
|
95
|
+
/** Set background to an arbitrary Color. */
|
|
96
|
+
readonly bg: (c: Color) => Pen;
|
|
97
|
+
readonly bold: Pen;
|
|
98
|
+
readonly italic: Pen;
|
|
99
|
+
readonly underline: Pen;
|
|
100
|
+
readonly strikethrough: Pen;
|
|
101
|
+
readonly dim: Pen;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The default pen — starting point for building styled text.
|
|
105
|
+
*
|
|
106
|
+
* Usage:
|
|
107
|
+
* pen("plain text")
|
|
108
|
+
* pen.cyan("colored")
|
|
109
|
+
* pen.bold.red("bold red")
|
|
110
|
+
* pen.bgBlue.white("white on blue")
|
|
111
|
+
*/
|
|
112
|
+
export declare const pen: Pen;
|
|
113
|
+
export {};
|