@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
package/dist/app.js ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * App — the top-level shell that owns the entire terminal lifecycle.
3
+ *
4
+ * Creates and wires together all subsystems (PixelBuffer, AnsiOutput,
5
+ * RenderTarget, DrawingContext, InputProcessor) and drives the
6
+ * measure → arrange → render loop in response to input and resize events.
7
+ */
8
+ import * as esc from "./ansi/esc.js";
9
+ import { AnsiOutput } from "./ansi/output.js";
10
+ import { DrawingContext } from "./drawing/context.js";
11
+ import { createInputProcessor } from "./input/processor.js";
12
+ import { disableRawMode, enableRawMode } from "./input/raw-mode.js";
13
+ import { PixelBuffer } from "./pixel/buffer.js";
14
+ import { DirtyRegions } from "./render/regions.js";
15
+ import { RenderTarget } from "./render/render-target.js";
16
+ // ── App ──────────────────────────────────────────────────────────────
17
+ export class App {
18
+ root;
19
+ _alternateScreen;
20
+ _mouse;
21
+ _title;
22
+ // Subsystems — created during run()
23
+ _output;
24
+ _buffer;
25
+ _dirtyRegions;
26
+ _renderTarget;
27
+ _drawingContext;
28
+ _processor;
29
+ _events;
30
+ // Lifecycle bookkeeping
31
+ _running = false;
32
+ _resolve = null;
33
+ _stdinListener = null;
34
+ _resizeListener = null;
35
+ _sigintListener = null;
36
+ _renderScheduled = false;
37
+ constructor(options) {
38
+ this.root = options.root;
39
+ this._alternateScreen = options.alternateScreen ?? true;
40
+ this._mouse = options.mouse ?? false;
41
+ this._title = options.title;
42
+ }
43
+ // ── Public API ───────────────────────────────────────────────────
44
+ /**
45
+ * Start the app — enters raw mode, sets up terminal, runs the event
46
+ * loop. Returns a promise that resolves when the app stops.
47
+ */
48
+ run() {
49
+ if (this._running) {
50
+ return Promise.reject(new Error("App is already running"));
51
+ }
52
+ this._running = true;
53
+ return new Promise((resolve) => {
54
+ this._resolve = resolve;
55
+ try {
56
+ this._setup();
57
+ this._initialRender();
58
+ }
59
+ catch (err) {
60
+ this._teardown();
61
+ throw err;
62
+ }
63
+ });
64
+ }
65
+ /** Stop the app — restores terminal and exits the event loop. */
66
+ stop() {
67
+ if (!this._running)
68
+ return;
69
+ this._teardown();
70
+ }
71
+ /** Force a full re-render. */
72
+ refresh() {
73
+ if (!this._running)
74
+ return;
75
+ this._fullRender();
76
+ }
77
+ // ── Setup ────────────────────────────────────────────────────────
78
+ _setup() {
79
+ const stdout = process.stdout;
80
+ // 1. Enable raw mode
81
+ enableRawMode();
82
+ // 2. Create ANSI output
83
+ this._output = new AnsiOutput(stdout);
84
+ // 3. Prepare terminal (custom sequence instead of prepareTerminal()
85
+ // so we can conditionally enable mouse tracking)
86
+ this._prepareTerminal();
87
+ // 4. Set terminal title
88
+ if (this._title) {
89
+ stdout.write(esc.setTitle(this._title));
90
+ }
91
+ // 5. Create pixel buffer at terminal dimensions
92
+ const cols = stdout.columns || 80;
93
+ const rows = stdout.rows || 24;
94
+ this._createRenderPipeline(cols, rows);
95
+ // 6. Wire up input
96
+ this._setupInput();
97
+ // 7. Wire up resize
98
+ this._resizeListener = () => this._handleResize();
99
+ stdout.on("resize", this._resizeListener);
100
+ // 8. SIGINT fallback
101
+ this._sigintListener = () => this.stop();
102
+ process.on("SIGINT", this._sigintListener);
103
+ }
104
+ _prepareTerminal() {
105
+ const stream = process.stdout;
106
+ let seq = "";
107
+ if (this._alternateScreen) {
108
+ seq += esc.alternateScreenOn;
109
+ }
110
+ seq += esc.hideCursor;
111
+ seq += esc.bracketedPasteOn;
112
+ if (this._mouse) {
113
+ seq += esc.mouseTrackingOn;
114
+ }
115
+ seq += esc.clearScreen;
116
+ stream.write(seq);
117
+ }
118
+ _restoreTerminal() {
119
+ const stream = process.stdout;
120
+ let seq = esc.reset;
121
+ if (this._mouse) {
122
+ seq += esc.mouseTrackingOff;
123
+ }
124
+ seq += esc.bracketedPasteOff;
125
+ seq += esc.showCursor;
126
+ if (this._alternateScreen) {
127
+ seq += esc.alternateScreenOff;
128
+ }
129
+ stream.write(seq);
130
+ }
131
+ _createRenderPipeline(cols, rows) {
132
+ this._buffer = new PixelBuffer(cols, rows);
133
+ this._dirtyRegions = new DirtyRegions();
134
+ this._renderTarget = new RenderTarget(this._buffer, this._output);
135
+ this._drawingContext = new DrawingContext(this._buffer);
136
+ }
137
+ _setupInput() {
138
+ const { processor, events } = createInputProcessor();
139
+ this._processor = processor;
140
+ this._events = events;
141
+ // Listen for parsed input events
142
+ this._events.on("input", (event) => {
143
+ this._handleInput(event);
144
+ });
145
+ // Feed raw stdin data to the processor
146
+ this._stdinListener = (data) => {
147
+ this._processor.feed(data);
148
+ };
149
+ process.stdin.on("data", this._stdinListener);
150
+ }
151
+ // ── Input handling ───────────────────────────────────────────────
152
+ _handleInput(event) {
153
+ // Dispatch to root control (including Ctrl+C — let the app handle it)
154
+ this.root.handleInput(event);
155
+ // Schedule a render if the tree is dirty
156
+ this._scheduleRender();
157
+ }
158
+ // ── Resize handling ──────────────────────────────────────────────
159
+ _handleResize() {
160
+ const stdout = process.stdout;
161
+ const cols = stdout.columns || 80;
162
+ const rows = stdout.rows || 24;
163
+ // Recreate render pipeline at new size
164
+ this._buffer = new PixelBuffer(cols, rows);
165
+ this._dirtyRegions = new DirtyRegions();
166
+ this._renderTarget = new RenderTarget(this._buffer, this._output);
167
+ this._drawingContext = new DrawingContext(this._buffer);
168
+ // Mark root as dirty and do a full render
169
+ this.root.invalidate();
170
+ this._fullRender();
171
+ }
172
+ // ── Render loop ──────────────────────────────────────────────────
173
+ /**
174
+ * Schedule a render pass using setImmediate so multiple rapid events
175
+ * within the same tick coalesce into a single render.
176
+ */
177
+ _scheduleRender() {
178
+ if (this._renderScheduled || !this._running)
179
+ return;
180
+ this._renderScheduled = true;
181
+ setImmediate(() => {
182
+ this._renderScheduled = false;
183
+ if (!this._running)
184
+ return;
185
+ if (this.root.dirty) {
186
+ this._renderFrame();
187
+ }
188
+ });
189
+ }
190
+ /** Perform a full measure → arrange → render cycle (used on init and resize). */
191
+ _fullRender() {
192
+ if (!this._running)
193
+ return;
194
+ const cols = this._buffer.width;
195
+ const rows = this._buffer.height;
196
+ // Clear buffer
197
+ this._buffer.clear();
198
+ // Measure
199
+ const constraint = {
200
+ minWidth: 0,
201
+ maxWidth: cols,
202
+ minHeight: 0,
203
+ maxHeight: rows,
204
+ };
205
+ this.root.measure(constraint);
206
+ // Arrange
207
+ const arrangeRect = { x: 0, y: 0, width: cols, height: rows };
208
+ this.root.arrange(arrangeRect);
209
+ // Render control tree into buffer
210
+ this.root.render(this._drawingContext);
211
+ this._clearDirty(this.root);
212
+ // Mark entire screen dirty and flush to terminal
213
+ this._dirtyRegions.addRect({
214
+ x: 0,
215
+ y: 0,
216
+ width: cols,
217
+ height: rows,
218
+ });
219
+ this._renderTarget.render(this._dirtyRegions);
220
+ }
221
+ /** Perform an incremental render for dirty regions. */
222
+ _renderFrame() {
223
+ const cols = this._buffer.width;
224
+ const rows = this._buffer.height;
225
+ // Clear buffer
226
+ this._buffer.clear();
227
+ // Measure
228
+ const constraint = {
229
+ minWidth: 0,
230
+ maxWidth: cols,
231
+ minHeight: 0,
232
+ maxHeight: rows,
233
+ };
234
+ this.root.measure(constraint);
235
+ // Arrange
236
+ const arrangeRect = { x: 0, y: 0, width: cols, height: rows };
237
+ this.root.arrange(arrangeRect);
238
+ // Mark the full area as dirty (controls re-render their full bounds)
239
+ this._dirtyRegions.addRect({
240
+ x: 0,
241
+ y: 0,
242
+ width: cols,
243
+ height: rows,
244
+ });
245
+ // Render control tree into buffer
246
+ this.root.render(this._drawingContext);
247
+ this._clearDirty(this.root);
248
+ // Diff and flush to terminal
249
+ this._renderTarget.render(this._dirtyRegions);
250
+ }
251
+ /** Recursively clear dirty flags on the entire control tree. */
252
+ _clearDirty(ctrl) {
253
+ ctrl.dirty = false;
254
+ for (const child of ctrl.children) {
255
+ this._clearDirty(child);
256
+ }
257
+ }
258
+ /** Run the initial render after setup. */
259
+ _initialRender() {
260
+ this._fullRender();
261
+ }
262
+ // ── Teardown ─────────────────────────────────────────────────────
263
+ _teardown() {
264
+ if (!this._running)
265
+ return;
266
+ this._running = false;
267
+ // Remove stdin listener
268
+ if (this._stdinListener) {
269
+ process.stdin.removeListener("data", this._stdinListener);
270
+ this._stdinListener = null;
271
+ }
272
+ // Remove resize listener
273
+ if (this._resizeListener) {
274
+ process.stdout.removeListener("resize", this._resizeListener);
275
+ this._resizeListener = null;
276
+ }
277
+ // Remove SIGINT listener
278
+ if (this._sigintListener) {
279
+ process.removeListener("SIGINT", this._sigintListener);
280
+ this._sigintListener = null;
281
+ }
282
+ // Destroy input processor (clears timers)
283
+ if (this._processor) {
284
+ this._processor.destroy();
285
+ }
286
+ // Restore terminal
287
+ this._restoreTerminal();
288
+ // Disable raw mode
289
+ disableRawMode();
290
+ // Resolve the run() promise
291
+ if (this._resolve) {
292
+ const resolve = this._resolve;
293
+ this._resolve = null;
294
+ resolve();
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Clip stack: manages a stack of clip rectangles for restricting drawing regions.
3
+ *
4
+ * Each push intersects the new rect with the current top to produce the effective
5
+ * clip. Pop restores the previous effective clip.
6
+ */
7
+ import type { Rect } from "../layout/types.js";
8
+ export declare class ClipStack {
9
+ /**
10
+ * Stack of effective clip rects. Each entry is the intersection of the
11
+ * pushed rect with its parent. A null entry means the clip is fully
12
+ * degenerate (zero-area) but we still track it so pop() works correctly.
13
+ */
14
+ private readonly stack;
15
+ /** Push a clip rectangle. Drawing outside this rect is ignored. */
16
+ push(rect: Rect): void;
17
+ /** Pop the last clip rectangle. */
18
+ pop(): void;
19
+ /** Check if a point is within the current clip bounds. */
20
+ contains(x: number, y: number): boolean;
21
+ /** Get the current effective clip rect (intersection of all pushed rects). */
22
+ current(): Rect | null;
23
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Clip stack: manages a stack of clip rectangles for restricting drawing regions.
3
+ *
4
+ * Each push intersects the new rect with the current top to produce the effective
5
+ * clip. Pop restores the previous effective clip.
6
+ */
7
+ /**
8
+ * Intersect two rectangles, returning the overlapping region.
9
+ * Returns null if there is no overlap.
10
+ */
11
+ function intersectRects(a, b) {
12
+ const x0 = Math.max(a.x, b.x);
13
+ const y0 = Math.max(a.y, b.y);
14
+ const x1 = Math.min(a.x + a.width, b.x + b.width);
15
+ const y1 = Math.min(a.y + a.height, b.y + b.height);
16
+ if (x1 <= x0 || y1 <= y0)
17
+ return null;
18
+ return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
19
+ }
20
+ export class ClipStack {
21
+ /**
22
+ * Stack of effective clip rects. Each entry is the intersection of the
23
+ * pushed rect with its parent. A null entry means the clip is fully
24
+ * degenerate (zero-area) but we still track it so pop() works correctly.
25
+ */
26
+ stack = [];
27
+ /** Push a clip rectangle. Drawing outside this rect is ignored. */
28
+ push(rect) {
29
+ const top = this.current();
30
+ if (top === null && this.stack.length > 0) {
31
+ // Already fully clipped — intersection stays null
32
+ this.stack.push(null);
33
+ }
34
+ else if (top === null) {
35
+ // First push — use the rect directly
36
+ this.stack.push(rect);
37
+ }
38
+ else {
39
+ this.stack.push(intersectRects(top, rect));
40
+ }
41
+ }
42
+ /** Pop the last clip rectangle. */
43
+ pop() {
44
+ if (this.stack.length === 0) {
45
+ throw new Error("ClipStack underflow: cannot pop from an empty stack");
46
+ }
47
+ this.stack.pop();
48
+ }
49
+ /** Check if a point is within the current clip bounds. */
50
+ contains(x, y) {
51
+ const clip = this.current();
52
+ if (clip === null) {
53
+ // No clip means everything is visible (empty stack) or nothing is (degenerate)
54
+ return this.stack.length === 0;
55
+ }
56
+ return (x >= clip.x &&
57
+ x < clip.x + clip.width &&
58
+ y >= clip.y &&
59
+ y < clip.y + clip.height);
60
+ }
61
+ /** Get the current effective clip rect (intersection of all pushed rects). */
62
+ current() {
63
+ if (this.stack.length === 0)
64
+ return null;
65
+ return this.stack[this.stack.length - 1] ?? null;
66
+ }
67
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * DrawingContext: the main drawing API for rendering to a PixelBuffer.
3
+ *
4
+ * Port of Consolonia's DrawingContextImpl.cs + DrawingContextImpl.Boxes.cs,
5
+ * simplified for the Node.js/TypeScript environment.
6
+ */
7
+ import type { Rect } from "../layout/types.js";
8
+ import type { PixelBuffer } from "../pixel/buffer.js";
9
+ import type { Color } from "../pixel/color.js";
10
+ import type { Pixel } from "../pixel/pixel.js";
11
+ export interface TextStyle {
12
+ fg?: Color;
13
+ bg?: Color;
14
+ bold?: boolean;
15
+ italic?: boolean;
16
+ underline?: boolean;
17
+ strikethrough?: boolean;
18
+ }
19
+ export interface BoxStyle {
20
+ fg?: Color;
21
+ bg?: Color;
22
+ bold?: boolean;
23
+ }
24
+ export declare class DrawingContext {
25
+ private readonly buffer;
26
+ private readonly clipStack;
27
+ private translateStack;
28
+ private offsetX;
29
+ private offsetY;
30
+ constructor(buffer: PixelBuffer);
31
+ /** Push a clip rectangle. All drawing is clipped to this region. */
32
+ pushClip(rect: Rect): void;
33
+ /** Pop the last clip rectangle. */
34
+ popClip(): void;
35
+ /** Push a coordinate translation. All drawing coordinates are offset. */
36
+ pushTranslate(dx: number, dy: number): void;
37
+ /** Pop the last coordinate translation. */
38
+ popTranslate(): void;
39
+ /** Translate a point by the current offset. */
40
+ private tx;
41
+ private ty;
42
+ /** Check if a local-coord point is visible after translate. */
43
+ private isVisible;
44
+ /** Get a pixel at local coordinates (translated to buffer coords). */
45
+ private bufGet;
46
+ /** Set a pixel at local coordinates (translated to buffer coords). */
47
+ private bufSet;
48
+ /** Fill a rectangle with a solid color. */
49
+ fillRect(rect: Rect, color: Color): void;
50
+ /** Draw a single character at (x, y) with styling. */
51
+ drawChar(x: number, y: number, char: string, style?: TextStyle): void;
52
+ /** Draw a text string at (x, y). Handles wide characters and tabs. */
53
+ drawText(x: number, y: number, text: string, style?: TextStyle): void;
54
+ /**
55
+ * Draw an array of styled segments at (x, y).
56
+ * Each segment carries its own TextStyle; segments are drawn sequentially.
57
+ */
58
+ drawStyledText(x: number, y: number, segments: {
59
+ text: string;
60
+ style: TextStyle;
61
+ }[]): void;
62
+ /** Draw a box-drawing rectangle (border) with smart corner merging. */
63
+ drawBox(rect: Rect, style?: BoxStyle): void;
64
+ /**
65
+ * Draw a single box-drawing character at (x, y), merging with any existing
66
+ * box pattern at that position.
67
+ */
68
+ private drawBoxChar;
69
+ /** Draw a horizontal line using box-drawing characters. */
70
+ drawHLine(x: number, y: number, width: number, style?: TextStyle): void;
71
+ /** Draw a vertical line using box-drawing characters. */
72
+ drawVLine(x: number, y: number, height: number, style?: TextStyle): void;
73
+ /** Set a pixel directly. Respects clip. */
74
+ setPixel(x: number, y: number, pixel: Pixel): void;
75
+ /** Blend a pixel at (x, y) with what's already there. Respects clip. */
76
+ blendPixel(x: number, y: number, pixel: Pixel): void;
77
+ }