@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.
Files changed (104) hide show
  1. package/README.md +48 -0
  2. package/dist/__tests__/ansi.test.d.ts +1 -0
  3. package/dist/__tests__/ansi.test.js +520 -0
  4. package/dist/__tests__/chat-view.test.d.ts +4 -0
  5. package/dist/__tests__/chat-view.test.js +480 -0
  6. package/dist/__tests__/drawing.test.d.ts +4 -0
  7. package/dist/__tests__/drawing.test.js +426 -0
  8. package/dist/__tests__/input.test.d.ts +5 -0
  9. package/dist/__tests__/input.test.js +911 -0
  10. package/dist/__tests__/layout.test.d.ts +4 -0
  11. package/dist/__tests__/layout.test.js +689 -0
  12. package/dist/__tests__/pixel.test.d.ts +1 -0
  13. package/dist/__tests__/pixel.test.js +674 -0
  14. package/dist/__tests__/render.test.d.ts +1 -0
  15. package/dist/__tests__/render.test.js +400 -0
  16. package/dist/__tests__/styled.test.d.ts +4 -0
  17. package/dist/__tests__/styled.test.js +149 -0
  18. package/dist/__tests__/widgets.test.d.ts +5 -0
  19. package/dist/__tests__/widgets.test.js +924 -0
  20. package/dist/ansi/esc.d.ts +61 -0
  21. package/dist/ansi/esc.js +85 -0
  22. package/dist/ansi/output.d.ts +66 -0
  23. package/dist/ansi/output.js +192 -0
  24. package/dist/ansi/strip.d.ts +16 -0
  25. package/dist/ansi/strip.js +74 -0
  26. package/dist/app.d.ts +68 -0
  27. package/dist/app.js +297 -0
  28. package/dist/drawing/clip.d.ts +23 -0
  29. package/dist/drawing/clip.js +67 -0
  30. package/dist/drawing/context.d.ts +77 -0
  31. package/dist/drawing/context.js +275 -0
  32. package/dist/index.d.ts +48 -0
  33. package/dist/index.js +63 -0
  34. package/dist/input/escape-matcher.d.ts +27 -0
  35. package/dist/input/escape-matcher.js +253 -0
  36. package/dist/input/events.d.ts +49 -0
  37. package/dist/input/events.js +17 -0
  38. package/dist/input/index.d.ts +15 -0
  39. package/dist/input/index.js +14 -0
  40. package/dist/input/matcher.d.ts +23 -0
  41. package/dist/input/matcher.js +14 -0
  42. package/dist/input/mouse-matcher.d.ts +27 -0
  43. package/dist/input/mouse-matcher.js +142 -0
  44. package/dist/input/paste-matcher.d.ts +23 -0
  45. package/dist/input/paste-matcher.js +104 -0
  46. package/dist/input/processor.d.ts +51 -0
  47. package/dist/input/processor.js +145 -0
  48. package/dist/input/raw-mode.d.ts +13 -0
  49. package/dist/input/raw-mode.js +24 -0
  50. package/dist/input/text-matcher.d.ts +14 -0
  51. package/dist/input/text-matcher.js +32 -0
  52. package/dist/layout/box.d.ts +33 -0
  53. package/dist/layout/box.js +92 -0
  54. package/dist/layout/column.d.ts +21 -0
  55. package/dist/layout/column.js +90 -0
  56. package/dist/layout/control.d.ts +73 -0
  57. package/dist/layout/control.js +215 -0
  58. package/dist/layout/row.d.ts +21 -0
  59. package/dist/layout/row.js +95 -0
  60. package/dist/layout/stack.d.ts +18 -0
  61. package/dist/layout/stack.js +64 -0
  62. package/dist/layout/types.d.ts +27 -0
  63. package/dist/layout/types.js +4 -0
  64. package/dist/pixel/background.d.ts +16 -0
  65. package/dist/pixel/background.js +16 -0
  66. package/dist/pixel/box-pattern.d.ts +38 -0
  67. package/dist/pixel/box-pattern.js +57 -0
  68. package/dist/pixel/buffer.d.ts +25 -0
  69. package/dist/pixel/buffer.js +51 -0
  70. package/dist/pixel/color.d.ts +48 -0
  71. package/dist/pixel/color.js +92 -0
  72. package/dist/pixel/foreground.d.ts +31 -0
  73. package/dist/pixel/foreground.js +64 -0
  74. package/dist/pixel/pixel.d.ts +21 -0
  75. package/dist/pixel/pixel.js +38 -0
  76. package/dist/pixel/symbol.d.ts +38 -0
  77. package/dist/pixel/symbol.js +192 -0
  78. package/dist/render/regions.d.ts +54 -0
  79. package/dist/render/regions.js +102 -0
  80. package/dist/render/render-target.d.ts +42 -0
  81. package/dist/render/render-target.js +118 -0
  82. package/dist/styled.d.ts +113 -0
  83. package/dist/styled.js +176 -0
  84. package/dist/widgets/border.d.ts +34 -0
  85. package/dist/widgets/border.js +121 -0
  86. package/dist/widgets/chat-view.d.ts +239 -0
  87. package/dist/widgets/chat-view.js +993 -0
  88. package/dist/widgets/interview.d.ts +87 -0
  89. package/dist/widgets/interview.js +187 -0
  90. package/dist/widgets/markdown.d.ts +87 -0
  91. package/dist/widgets/markdown.js +611 -0
  92. package/dist/widgets/panel.d.ts +19 -0
  93. package/dist/widgets/panel.js +35 -0
  94. package/dist/widgets/scroll-view.d.ts +43 -0
  95. package/dist/widgets/scroll-view.js +182 -0
  96. package/dist/widgets/styled-text.d.ts +38 -0
  97. package/dist/widgets/styled-text.js +183 -0
  98. package/dist/widgets/syntax.d.ts +37 -0
  99. package/dist/widgets/syntax.js +670 -0
  100. package/dist/widgets/text-input.d.ts +121 -0
  101. package/dist/widgets/text-input.js +618 -0
  102. package/dist/widgets/text.d.ts +34 -0
  103. package/dist/widgets/text.js +168 -0
  104. 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
+ }
@@ -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 {};