@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,275 @@
|
|
|
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 { background } from "../pixel/background.js";
|
|
8
|
+
import { boxChar, DOWN, LEFT, mergeBoxPatterns, RIGHT, UP, } from "../pixel/box-pattern.js";
|
|
9
|
+
import { foreground } from "../pixel/foreground.js";
|
|
10
|
+
import { blendPixel } from "../pixel/pixel.js";
|
|
11
|
+
import { charWidth, isZeroWidth, sym } from "../pixel/symbol.js";
|
|
12
|
+
import { ClipStack } from "./clip.js";
|
|
13
|
+
// ── Defaults ──────────────────────────────────────────────────────
|
|
14
|
+
const DEFAULT_FG = { r: 255, g: 255, b: 255, a: 255 };
|
|
15
|
+
const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 };
|
|
16
|
+
const TAB_WIDTH = 4;
|
|
17
|
+
// ── DrawingContext ────────────────────────────────────────────────
|
|
18
|
+
export class DrawingContext {
|
|
19
|
+
buffer;
|
|
20
|
+
clipStack;
|
|
21
|
+
translateStack = [];
|
|
22
|
+
offsetX = 0;
|
|
23
|
+
offsetY = 0;
|
|
24
|
+
constructor(buffer) {
|
|
25
|
+
this.buffer = buffer;
|
|
26
|
+
this.clipStack = new ClipStack();
|
|
27
|
+
}
|
|
28
|
+
// ── Clip management ──────────────────────────────────────────
|
|
29
|
+
/** Push a clip rectangle. All drawing is clipped to this region. */
|
|
30
|
+
pushClip(rect) {
|
|
31
|
+
this.clipStack.push(rect);
|
|
32
|
+
}
|
|
33
|
+
/** Pop the last clip rectangle. */
|
|
34
|
+
popClip() {
|
|
35
|
+
this.clipStack.pop();
|
|
36
|
+
}
|
|
37
|
+
// ── Translate management ───────────────────────────────────
|
|
38
|
+
/** Push a coordinate translation. All drawing coordinates are offset. */
|
|
39
|
+
pushTranslate(dx, dy) {
|
|
40
|
+
this.translateStack.push({ dx: this.offsetX, dy: this.offsetY });
|
|
41
|
+
this.offsetX += dx;
|
|
42
|
+
this.offsetY += dy;
|
|
43
|
+
}
|
|
44
|
+
/** Pop the last coordinate translation. */
|
|
45
|
+
popTranslate() {
|
|
46
|
+
const prev = this.translateStack.pop();
|
|
47
|
+
if (prev) {
|
|
48
|
+
this.offsetX = prev.dx;
|
|
49
|
+
this.offsetY = prev.dy;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.offsetX = 0;
|
|
53
|
+
this.offsetY = 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ── Visibility check ────────────────────────────────────────
|
|
57
|
+
/** Translate a point by the current offset. */
|
|
58
|
+
tx(x) {
|
|
59
|
+
return x + this.offsetX;
|
|
60
|
+
}
|
|
61
|
+
ty(y) {
|
|
62
|
+
return y + this.offsetY;
|
|
63
|
+
}
|
|
64
|
+
/** Check if a local-coord point is visible after translate. */
|
|
65
|
+
isVisible(x, y) {
|
|
66
|
+
const wx = this.tx(x);
|
|
67
|
+
const wy = this.ty(y);
|
|
68
|
+
if (wx < 0 ||
|
|
69
|
+
wx >= this.buffer.width ||
|
|
70
|
+
wy < 0 ||
|
|
71
|
+
wy >= this.buffer.height) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return this.clipStack.contains(wx, wy);
|
|
75
|
+
}
|
|
76
|
+
/** Get a pixel at local coordinates (translated to buffer coords). */
|
|
77
|
+
bufGet(x, y) {
|
|
78
|
+
return this.buffer.get(this.tx(x), this.ty(y));
|
|
79
|
+
}
|
|
80
|
+
/** Set a pixel at local coordinates (translated to buffer coords). */
|
|
81
|
+
bufSet(x, y, px) {
|
|
82
|
+
this.buffer.set(this.tx(x), this.ty(y), px);
|
|
83
|
+
}
|
|
84
|
+
// ── Fill ─────────────────────────────────────────────────────
|
|
85
|
+
/** Fill a rectangle with a solid color. */
|
|
86
|
+
fillRect(rect, color) {
|
|
87
|
+
const bg = background(color);
|
|
88
|
+
const fg = foreground(sym(" "), TRANSPARENT);
|
|
89
|
+
const px = { foreground: fg, background: bg };
|
|
90
|
+
for (let y = rect.y; y < rect.y + rect.height; y++) {
|
|
91
|
+
for (let x = rect.x; x < rect.x + rect.width; x++) {
|
|
92
|
+
if (this.isVisible(x, y)) {
|
|
93
|
+
this.bufSet(x, y, blendPixel(px, this.bufGet(x, y)));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// ── Single character ─────────────────────────────────────────
|
|
99
|
+
/** Draw a single character at (x, y) with styling. */
|
|
100
|
+
drawChar(x, y, char, style) {
|
|
101
|
+
if (!this.isVisible(x, y))
|
|
102
|
+
return;
|
|
103
|
+
const fgColor = style?.fg ?? DEFAULT_FG;
|
|
104
|
+
const bgColor = style?.bg ?? TRANSPARENT;
|
|
105
|
+
const s = sym(char);
|
|
106
|
+
const fg = foreground(s, fgColor, {
|
|
107
|
+
bold: style?.bold ?? false,
|
|
108
|
+
italic: style?.italic ?? false,
|
|
109
|
+
underline: style?.underline ?? false,
|
|
110
|
+
strikethrough: style?.strikethrough ?? false,
|
|
111
|
+
});
|
|
112
|
+
const bg = background(bgColor);
|
|
113
|
+
const px = { foreground: fg, background: bg };
|
|
114
|
+
this.bufSet(x, y, blendPixel(px, this.bufGet(x, y)));
|
|
115
|
+
// Wide characters need a continuation marker in the next cell
|
|
116
|
+
if (s.width === 2 && this.isVisible(x + 1, y)) {
|
|
117
|
+
const contFg = foreground(sym(""), TRANSPARENT);
|
|
118
|
+
const contPx = { foreground: contFg, background: bg };
|
|
119
|
+
this.bufSet(x + 1, y, blendPixel(contPx, this.bufGet(x + 1, y)));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ── Text string ──────────────────────────────────────────────
|
|
123
|
+
/** Draw a text string at (x, y). Handles wide characters and tabs. */
|
|
124
|
+
drawText(x, y, text, style) {
|
|
125
|
+
let cx = x;
|
|
126
|
+
for (const char of text) {
|
|
127
|
+
// Handle tab characters as spaces
|
|
128
|
+
if (char === "\t") {
|
|
129
|
+
for (let i = 0; i < TAB_WIDTH; i++) {
|
|
130
|
+
this.drawChar(cx, y, " ", style);
|
|
131
|
+
cx++;
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// Skip control characters and zero-width characters (variation
|
|
136
|
+
// selectors, ZWJ, combining marks, etc.) — they have no visual
|
|
137
|
+
// representation and would render as "missing glyph" boxes.
|
|
138
|
+
const cp = char.codePointAt(0);
|
|
139
|
+
if (cp < 0x20 && cp !== 0x09)
|
|
140
|
+
continue;
|
|
141
|
+
if (isZeroWidth(cp))
|
|
142
|
+
continue;
|
|
143
|
+
const w = charWidth(cp);
|
|
144
|
+
this.drawChar(cx, y, char, style);
|
|
145
|
+
cx += w;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ── Styled text (segment arrays) ────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* Draw an array of styled segments at (x, y).
|
|
151
|
+
* Each segment carries its own TextStyle; segments are drawn sequentially.
|
|
152
|
+
*/
|
|
153
|
+
drawStyledText(x, y, segments) {
|
|
154
|
+
let cx = x;
|
|
155
|
+
for (const seg of segments) {
|
|
156
|
+
for (const char of seg.text) {
|
|
157
|
+
if (char === "\t") {
|
|
158
|
+
for (let i = 0; i < TAB_WIDTH; i++) {
|
|
159
|
+
this.drawChar(cx, y, " ", seg.style);
|
|
160
|
+
cx++;
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const cp = char.codePointAt(0);
|
|
165
|
+
if (cp < 0x20 && cp !== 0x09)
|
|
166
|
+
continue;
|
|
167
|
+
if (isZeroWidth(cp))
|
|
168
|
+
continue;
|
|
169
|
+
const w = charWidth(cp);
|
|
170
|
+
this.drawChar(cx, y, char, seg.style);
|
|
171
|
+
cx += w;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// ── Box drawing ──────────────────────────────────────────────
|
|
176
|
+
/** Draw a box-drawing rectangle (border) with smart corner merging. */
|
|
177
|
+
drawBox(rect, style) {
|
|
178
|
+
const { x, y, width, height } = rect;
|
|
179
|
+
if (width < 1 || height < 1)
|
|
180
|
+
return;
|
|
181
|
+
const fgColor = style?.fg ?? DEFAULT_FG;
|
|
182
|
+
const bgColor = style?.bg ?? TRANSPARENT;
|
|
183
|
+
const bold = style?.bold ?? false;
|
|
184
|
+
// Single cell box — draw a cross
|
|
185
|
+
if (width === 1 && height === 1) {
|
|
186
|
+
this.drawBoxChar(x, y, UP | RIGHT | DOWN | LEFT, fgColor, bgColor, bold);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Single column box
|
|
190
|
+
if (width === 1) {
|
|
191
|
+
this.drawBoxChar(x, y, DOWN, fgColor, bgColor, bold); // top
|
|
192
|
+
for (let dy = 1; dy < height - 1; dy++) {
|
|
193
|
+
this.drawBoxChar(x, y + dy, UP | DOWN, fgColor, bgColor, bold);
|
|
194
|
+
}
|
|
195
|
+
this.drawBoxChar(x, y + height - 1, UP, fgColor, bgColor, bold); // bottom
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Single row box
|
|
199
|
+
if (height === 1) {
|
|
200
|
+
this.drawBoxChar(x, y, RIGHT, fgColor, bgColor, bold); // left
|
|
201
|
+
for (let dx = 1; dx < width - 1; dx++) {
|
|
202
|
+
this.drawBoxChar(x + dx, y, LEFT | RIGHT, fgColor, bgColor, bold);
|
|
203
|
+
}
|
|
204
|
+
this.drawBoxChar(x + width - 1, y, LEFT, fgColor, bgColor, bold); // right
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// Corners
|
|
208
|
+
this.drawBoxChar(x, y, DOWN | RIGHT, fgColor, bgColor, bold); // top-left
|
|
209
|
+
this.drawBoxChar(x + width - 1, y, DOWN | LEFT, fgColor, bgColor, bold); // top-right
|
|
210
|
+
this.drawBoxChar(x, y + height - 1, UP | RIGHT, fgColor, bgColor, bold); // bottom-left
|
|
211
|
+
this.drawBoxChar(x + width - 1, y + height - 1, UP | LEFT, fgColor, bgColor, bold); // bottom-right
|
|
212
|
+
// Top and bottom edges
|
|
213
|
+
for (let dx = 1; dx < width - 1; dx++) {
|
|
214
|
+
this.drawBoxChar(x + dx, y, LEFT | RIGHT, fgColor, bgColor, bold); // top
|
|
215
|
+
this.drawBoxChar(x + dx, y + height - 1, LEFT | RIGHT, fgColor, bgColor, bold); // bottom
|
|
216
|
+
}
|
|
217
|
+
// Left and right edges
|
|
218
|
+
for (let dy = 1; dy < height - 1; dy++) {
|
|
219
|
+
this.drawBoxChar(x, y + dy, UP | DOWN, fgColor, bgColor, bold); // left
|
|
220
|
+
this.drawBoxChar(x + width - 1, y + dy, UP | DOWN, fgColor, bgColor, bold); // right
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Draw a single box-drawing character at (x, y), merging with any existing
|
|
225
|
+
* box pattern at that position.
|
|
226
|
+
*/
|
|
227
|
+
drawBoxChar(x, y, pattern, fgColor, bgColor, bold) {
|
|
228
|
+
if (!this.isVisible(x, y))
|
|
229
|
+
return;
|
|
230
|
+
// Check if there's already a box pattern at this position
|
|
231
|
+
const existing = this.bufGet(x, y);
|
|
232
|
+
const existingPattern = existing.foreground.symbol.pattern;
|
|
233
|
+
const mergedPattern = existingPattern !== 0
|
|
234
|
+
? mergeBoxPatterns(existingPattern, pattern)
|
|
235
|
+
: pattern;
|
|
236
|
+
const char = boxChar(mergedPattern);
|
|
237
|
+
const s = sym(char, mergedPattern);
|
|
238
|
+
const fg = foreground(s, fgColor, { bold });
|
|
239
|
+
const bg = background(bgColor);
|
|
240
|
+
const px = { foreground: fg, background: bg };
|
|
241
|
+
this.bufSet(x, y, blendPixel(px, this.bufGet(x, y)));
|
|
242
|
+
}
|
|
243
|
+
// ── Lines ────────────────────────────────────────────────────
|
|
244
|
+
/** Draw a horizontal line using box-drawing characters. */
|
|
245
|
+
drawHLine(x, y, width, style) {
|
|
246
|
+
const fgColor = style?.fg ?? DEFAULT_FG;
|
|
247
|
+
const bgColor = style?.bg ?? TRANSPARENT;
|
|
248
|
+
const bold = style?.bold ?? false;
|
|
249
|
+
for (let dx = 0; dx < width; dx++) {
|
|
250
|
+
this.drawBoxChar(x + dx, y, LEFT | RIGHT, fgColor, bgColor, bold);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Draw a vertical line using box-drawing characters. */
|
|
254
|
+
drawVLine(x, y, height, style) {
|
|
255
|
+
const fgColor = style?.fg ?? DEFAULT_FG;
|
|
256
|
+
const bgColor = style?.bg ?? TRANSPARENT;
|
|
257
|
+
const bold = style?.bold ?? false;
|
|
258
|
+
for (let dy = 0; dy < height; dy++) {
|
|
259
|
+
this.drawBoxChar(x, y + dy, UP | DOWN, fgColor, bgColor, bold);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// ── Raw pixel operations ─────────────────────────────────────
|
|
263
|
+
/** Set a pixel directly. Respects clip. */
|
|
264
|
+
setPixel(x, y, pixel) {
|
|
265
|
+
if (!this.isVisible(x, y))
|
|
266
|
+
return;
|
|
267
|
+
this.bufSet(x, y, pixel);
|
|
268
|
+
}
|
|
269
|
+
/** Blend a pixel at (x, y) with what's already there. Respects clip. */
|
|
270
|
+
blendPixel(x, y, pixel) {
|
|
271
|
+
if (!this.isVisible(x, y))
|
|
272
|
+
return;
|
|
273
|
+
this.bufSet(x, y, blendPixel(pixel, this.bufGet(x, y)));
|
|
274
|
+
}
|
|
275
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @teammates/consolonia — Terminal UI rendering engine for Node.js.
|
|
3
|
+
*
|
|
4
|
+
* A TypeScript port of Consolonia (.NET console UI framework).
|
|
5
|
+
* Provides pixel-buffer rendering, raw input handling, layout engine,
|
|
6
|
+
* and widgets for building interactive terminal applications.
|
|
7
|
+
*/
|
|
8
|
+
export { background, blendBackground, EMPTY_BACKGROUND, type PixelBackground, } from "./pixel/background.js";
|
|
9
|
+
export { BOX_CHARS, BOX_NONE, type BoxPattern, boxChar, DOWN, LEFT, mergeBoxPatterns, RIGHT, UP, } from "./pixel/box-pattern.js";
|
|
10
|
+
export { PixelBuffer } from "./pixel/buffer.js";
|
|
11
|
+
export { BLACK, BLACK_BRIGHT, BLUE, BLUE_BRIGHT, type Color, CYAN, CYAN_BRIGHT, color, colorBlend, colorBrighten, colorShade, DARK_GRAY, GRAY, GREEN, GREEN_BRIGHT, GREY, LIGHT_GRAY, MAGENTA, MAGENTA_BRIGHT, RED, RED_BRIGHT, TRANSPARENT, WHITE, WHITE_BRIGHT, YELLOW, YELLOW_BRIGHT, } from "./pixel/color.js";
|
|
12
|
+
export { blendForeground, EMPTY_FOREGROUND, foreground, type PixelForeground, } from "./pixel/foreground.js";
|
|
13
|
+
export { blendPixel, PIXEL_EMPTY, PIXEL_SPACE, type Pixel, pixel, } from "./pixel/pixel.js";
|
|
14
|
+
export { charWidth, EMPTY_SYMBOL, isZeroWidth, type Symbol, stringDisplayWidth, sym, } from "./pixel/symbol.js";
|
|
15
|
+
export type { Constraint, Point, Rect, Size, } from "./layout/types.js";
|
|
16
|
+
export * as esc from "./ansi/esc.js";
|
|
17
|
+
export { AnsiOutput } from "./ansi/output.js";
|
|
18
|
+
export { stripAnsi, truncateAnsi, visibleLength, } from "./ansi/strip.js";
|
|
19
|
+
export { DirtyRegions, DirtySnapshot } from "./render/regions.js";
|
|
20
|
+
export { RenderTarget } from "./render/render-target.js";
|
|
21
|
+
export { EscapeMatcher } from "./input/escape-matcher.js";
|
|
22
|
+
export type { InputEvent, KeyEvent, MouseEvent, PasteEvent, } from "./input/events.js";
|
|
23
|
+
export { keyEvent, mouseEvent, pasteEvent, resizeEvent, } from "./input/events.js";
|
|
24
|
+
export { type IMatcher, MatchResult } from "./input/matcher.js";
|
|
25
|
+
export { MouseMatcher } from "./input/mouse-matcher.js";
|
|
26
|
+
export { PasteMatcher } from "./input/paste-matcher.js";
|
|
27
|
+
export { createInputProcessor, InputProcessor } from "./input/processor.js";
|
|
28
|
+
export { disableRawMode, enableRawMode } from "./input/raw-mode.js";
|
|
29
|
+
export { TextMatcher } from "./input/text-matcher.js";
|
|
30
|
+
export { ClipStack } from "./drawing/clip.js";
|
|
31
|
+
export { type BoxStyle, DrawingContext, type TextStyle, } from "./drawing/context.js";
|
|
32
|
+
export { Box, type BoxOptions } from "./layout/box.js";
|
|
33
|
+
export { Column, type ColumnOptions } from "./layout/column.js";
|
|
34
|
+
export { Control } from "./layout/control.js";
|
|
35
|
+
export { Row, type RowOptions } from "./layout/row.js";
|
|
36
|
+
export { Stack, type StackOptions } from "./layout/stack.js";
|
|
37
|
+
export { Border, type BorderOptions } from "./widgets/border.js";
|
|
38
|
+
export { ChatView, type ChatViewOptions, type DropdownItem, type FeedActionItem, } from "./widgets/chat-view.js";
|
|
39
|
+
export { Interview, type InterviewOptions, type InterviewQuestion, } from "./widgets/interview.js";
|
|
40
|
+
export { Panel, type PanelOptions } from "./widgets/panel.js";
|
|
41
|
+
export { ScrollView, type ScrollViewOptions } from "./widgets/scroll-view.js";
|
|
42
|
+
export { type StyledLine, StyledText, type StyledTextOptions, } from "./widgets/styled-text.js";
|
|
43
|
+
export { Text, type TextOptions } from "./widgets/text.js";
|
|
44
|
+
export { type DeleteSizer, type InputColorizer, TextInput, type TextInputOptions, } from "./widgets/text-input.js";
|
|
45
|
+
export { type MarkdownOptions, type MarkdownTheme, renderMarkdown, } from "./widgets/markdown.js";
|
|
46
|
+
export { DEFAULT_SYNTAX_THEME, getHighlighter, highlightLine, registerHighlighter, type SyntaxHighlighter, type SyntaxTheme, type SyntaxToken, type SyntaxTokenType, } from "./widgets/syntax.js";
|
|
47
|
+
export { concat, isStyledSpan, pen, type StyledSegment, type StyledSpan, spanLength, spanText, } from "./styled.js";
|
|
48
|
+
export { App, type AppOptions } from "./app.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @teammates/consolonia — Terminal UI rendering engine for Node.js.
|
|
3
|
+
*
|
|
4
|
+
* A TypeScript port of Consolonia (.NET console UI framework).
|
|
5
|
+
* Provides pixel-buffer rendering, raw input handling, layout engine,
|
|
6
|
+
* and widgets for building interactive terminal applications.
|
|
7
|
+
*/
|
|
8
|
+
// ── Pixel types ─────────────────────────────────────────────────────
|
|
9
|
+
export { background, blendBackground, EMPTY_BACKGROUND, } from "./pixel/background.js";
|
|
10
|
+
export { BOX_CHARS, BOX_NONE, boxChar, DOWN, LEFT, mergeBoxPatterns, RIGHT, UP, } from "./pixel/box-pattern.js";
|
|
11
|
+
export { PixelBuffer } from "./pixel/buffer.js";
|
|
12
|
+
export {
|
|
13
|
+
// Standard ANSI colors
|
|
14
|
+
BLACK,
|
|
15
|
+
// Bright ANSI colors
|
|
16
|
+
BLACK_BRIGHT, BLUE, BLUE_BRIGHT, CYAN, CYAN_BRIGHT, color, colorBlend, colorBrighten, colorShade, DARK_GRAY,
|
|
17
|
+
// Aliases
|
|
18
|
+
GRAY, GREEN, GREEN_BRIGHT, GREY, LIGHT_GRAY, MAGENTA, MAGENTA_BRIGHT, RED, RED_BRIGHT, TRANSPARENT, WHITE, WHITE_BRIGHT, YELLOW, YELLOW_BRIGHT, } from "./pixel/color.js";
|
|
19
|
+
export { blendForeground, EMPTY_FOREGROUND, foreground, } from "./pixel/foreground.js";
|
|
20
|
+
export { blendPixel, PIXEL_EMPTY, PIXEL_SPACE, pixel, } from "./pixel/pixel.js";
|
|
21
|
+
export { charWidth, EMPTY_SYMBOL, isZeroWidth, stringDisplayWidth, sym, } from "./pixel/symbol.js";
|
|
22
|
+
// ── ANSI output ─────────────────────────────────────────────────────
|
|
23
|
+
export * as esc from "./ansi/esc.js";
|
|
24
|
+
export { AnsiOutput } from "./ansi/output.js";
|
|
25
|
+
export { stripAnsi, truncateAnsi, visibleLength, } from "./ansi/strip.js";
|
|
26
|
+
// ── Render pipeline ─────────────────────────────────────────────────
|
|
27
|
+
export { DirtyRegions, DirtySnapshot } from "./render/regions.js";
|
|
28
|
+
export { RenderTarget } from "./render/render-target.js";
|
|
29
|
+
// ── Input system ────────────────────────────────────────────────────
|
|
30
|
+
export { EscapeMatcher } from "./input/escape-matcher.js";
|
|
31
|
+
export { keyEvent, mouseEvent, pasteEvent, resizeEvent, } from "./input/events.js";
|
|
32
|
+
export { MatchResult } from "./input/matcher.js";
|
|
33
|
+
export { MouseMatcher } from "./input/mouse-matcher.js";
|
|
34
|
+
export { PasteMatcher } from "./input/paste-matcher.js";
|
|
35
|
+
export { createInputProcessor, InputProcessor } from "./input/processor.js";
|
|
36
|
+
export { disableRawMode, enableRawMode } from "./input/raw-mode.js";
|
|
37
|
+
export { TextMatcher } from "./input/text-matcher.js";
|
|
38
|
+
// ── Drawing context ─────────────────────────────────────────────────
|
|
39
|
+
export { ClipStack } from "./drawing/clip.js";
|
|
40
|
+
export { DrawingContext, } from "./drawing/context.js";
|
|
41
|
+
// ── Layout engine ───────────────────────────────────────────────────
|
|
42
|
+
export { Box } from "./layout/box.js";
|
|
43
|
+
export { Column } from "./layout/column.js";
|
|
44
|
+
export { Control } from "./layout/control.js";
|
|
45
|
+
export { Row } from "./layout/row.js";
|
|
46
|
+
export { Stack } from "./layout/stack.js";
|
|
47
|
+
// ── Widgets ─────────────────────────────────────────────────────────
|
|
48
|
+
export { Border } from "./widgets/border.js";
|
|
49
|
+
export { ChatView, } from "./widgets/chat-view.js";
|
|
50
|
+
export { Interview, } from "./widgets/interview.js";
|
|
51
|
+
export { Panel } from "./widgets/panel.js";
|
|
52
|
+
export { ScrollView } from "./widgets/scroll-view.js";
|
|
53
|
+
export { StyledText, } from "./widgets/styled-text.js";
|
|
54
|
+
export { Text } from "./widgets/text.js";
|
|
55
|
+
export { TextInput, } from "./widgets/text-input.js";
|
|
56
|
+
// ── Markdown ─────────────────────────────────────────────────────────
|
|
57
|
+
export { renderMarkdown, } from "./widgets/markdown.js";
|
|
58
|
+
// ── Syntax highlighting ──────────────────────────────────────────────
|
|
59
|
+
export { DEFAULT_SYNTAX_THEME, getHighlighter, highlightLine, registerHighlighter, } from "./widgets/syntax.js";
|
|
60
|
+
// ── Styled text (chalk-like API) ─────────────────────────────────────
|
|
61
|
+
export { concat, isStyledSpan, pen, spanLength, spanText, } from "./styled.js";
|
|
62
|
+
// ── App shell ────────────────────────────────────────────────────────
|
|
63
|
+
export { App } from "./app.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses ANSI escape sequences into KeyEvents.
|
|
3
|
+
* Handles CSI sequences, SS3 sequences, and alt+char combinations.
|
|
4
|
+
*/
|
|
5
|
+
import { type InputEvent } from "./events.js";
|
|
6
|
+
import { type IMatcher, MatchResult } from "./matcher.js";
|
|
7
|
+
export declare class EscapeMatcher implements IMatcher {
|
|
8
|
+
private state;
|
|
9
|
+
/** Accumulated parameter bytes for CSI sequences (digits and semicolons). */
|
|
10
|
+
private params;
|
|
11
|
+
/** The completed event ready for flushing. */
|
|
12
|
+
private result;
|
|
13
|
+
append(char: string): MatchResult;
|
|
14
|
+
flush(): InputEvent | null;
|
|
15
|
+
reset(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Called externally when an ESC timeout fires — emit the standalone
|
|
18
|
+
* escape key event if we are still in the GotEsc state.
|
|
19
|
+
*/
|
|
20
|
+
flushEscapeTimeout(): InputEvent | null;
|
|
21
|
+
private handleControlChar;
|
|
22
|
+
private handleAfterEsc;
|
|
23
|
+
private handleCsi;
|
|
24
|
+
private handleTildeSequence;
|
|
25
|
+
private handleSs3;
|
|
26
|
+
private parseModifier;
|
|
27
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses ANSI escape sequences into KeyEvents.
|
|
3
|
+
* Handles CSI sequences, SS3 sequences, and alt+char combinations.
|
|
4
|
+
*/
|
|
5
|
+
import { keyEvent } from "./events.js";
|
|
6
|
+
import { MatchResult } from "./matcher.js";
|
|
7
|
+
const ESC = "\x1b";
|
|
8
|
+
/** Internal state of the escape sequence parser. */
|
|
9
|
+
var State;
|
|
10
|
+
(function (State) {
|
|
11
|
+
/** Waiting for ESC to start a sequence. */
|
|
12
|
+
State[State["Idle"] = 0] = "Idle";
|
|
13
|
+
/** Received ESC, waiting to see what follows. */
|
|
14
|
+
State[State["GotEsc"] = 1] = "GotEsc";
|
|
15
|
+
/** Inside a CSI sequence (\x1b[). */
|
|
16
|
+
State[State["Csi"] = 2] = "Csi";
|
|
17
|
+
/** Inside an SS3 sequence (\x1bO). */
|
|
18
|
+
State[State["Ss3"] = 3] = "Ss3";
|
|
19
|
+
})(State || (State = {}));
|
|
20
|
+
/** Decode the xterm modifier parameter (1-based) into shift/alt/ctrl flags. */
|
|
21
|
+
function decodeModifier(mod) {
|
|
22
|
+
// modifier = 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0)
|
|
23
|
+
const m = mod - 1;
|
|
24
|
+
return {
|
|
25
|
+
shift: (m & 1) !== 0,
|
|
26
|
+
alt: (m & 2) !== 0,
|
|
27
|
+
ctrl: (m & 4) !== 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Map CSI final byte to key name for cursor/editing keys.
|
|
32
|
+
* Final byte is the letter that terminates the sequence.
|
|
33
|
+
*/
|
|
34
|
+
const CSI_FINAL_KEYS = {
|
|
35
|
+
A: "up",
|
|
36
|
+
B: "down",
|
|
37
|
+
C: "right",
|
|
38
|
+
D: "left",
|
|
39
|
+
H: "home",
|
|
40
|
+
F: "end",
|
|
41
|
+
Z: "tab", // shift+tab (backtab) produces CSI Z
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Map CSI numeric code (before ~) to key name.
|
|
45
|
+
* These are the "tilde" sequences like \x1b[3~.
|
|
46
|
+
*/
|
|
47
|
+
const CSI_TILDE_KEYS = {
|
|
48
|
+
1: "home",
|
|
49
|
+
2: "insert",
|
|
50
|
+
3: "delete",
|
|
51
|
+
4: "end",
|
|
52
|
+
5: "pageup",
|
|
53
|
+
6: "pagedown",
|
|
54
|
+
// Function keys (F5-F12)
|
|
55
|
+
15: "f5",
|
|
56
|
+
17: "f6",
|
|
57
|
+
18: "f7",
|
|
58
|
+
19: "f8",
|
|
59
|
+
20: "f9",
|
|
60
|
+
21: "f10",
|
|
61
|
+
23: "f11",
|
|
62
|
+
24: "f12",
|
|
63
|
+
};
|
|
64
|
+
/** SS3 final bytes map to F1-F4. */
|
|
65
|
+
const SS3_KEYS = {
|
|
66
|
+
P: "f1",
|
|
67
|
+
Q: "f2",
|
|
68
|
+
R: "f3",
|
|
69
|
+
S: "f4",
|
|
70
|
+
};
|
|
71
|
+
/** Control character code points mapped to key names. */
|
|
72
|
+
const CTRL_KEYS = {
|
|
73
|
+
0: { key: "space", char: "", ctrl: true }, // Ctrl+Space / Ctrl+@
|
|
74
|
+
8: { key: "backspace", char: "", ctrl: false }, // Ctrl+H (some terminals)
|
|
75
|
+
9: { key: "tab", char: "\t", ctrl: false },
|
|
76
|
+
10: { key: "enter", char: "\n", ctrl: false }, // Ctrl+J
|
|
77
|
+
13: { key: "enter", char: "\r", ctrl: false },
|
|
78
|
+
127: { key: "backspace", char: "", ctrl: false },
|
|
79
|
+
};
|
|
80
|
+
export class EscapeMatcher {
|
|
81
|
+
state = State.Idle;
|
|
82
|
+
/** Accumulated parameter bytes for CSI sequences (digits and semicolons). */
|
|
83
|
+
params = "";
|
|
84
|
+
/** The completed event ready for flushing. */
|
|
85
|
+
result = null;
|
|
86
|
+
append(char) {
|
|
87
|
+
const code = char.charCodeAt(0);
|
|
88
|
+
switch (this.state) {
|
|
89
|
+
case State.Idle:
|
|
90
|
+
if (char === ESC) {
|
|
91
|
+
this.state = State.GotEsc;
|
|
92
|
+
return MatchResult.Partial;
|
|
93
|
+
}
|
|
94
|
+
// Handle control characters (Ctrl+A through Ctrl+Z, etc.)
|
|
95
|
+
if (code < 32 || code === 127) {
|
|
96
|
+
return this.handleControlChar(code);
|
|
97
|
+
}
|
|
98
|
+
return MatchResult.NoMatch;
|
|
99
|
+
case State.GotEsc:
|
|
100
|
+
return this.handleAfterEsc(char, code);
|
|
101
|
+
case State.Csi:
|
|
102
|
+
return this.handleCsi(char, code);
|
|
103
|
+
case State.Ss3:
|
|
104
|
+
return this.handleSs3(char);
|
|
105
|
+
default:
|
|
106
|
+
return MatchResult.NoMatch;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
flush() {
|
|
110
|
+
const ev = this.result;
|
|
111
|
+
this.result = null;
|
|
112
|
+
return ev;
|
|
113
|
+
}
|
|
114
|
+
reset() {
|
|
115
|
+
this.state = State.Idle;
|
|
116
|
+
this.params = "";
|
|
117
|
+
this.result = null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Called externally when an ESC timeout fires — emit the standalone
|
|
121
|
+
* escape key event if we are still in the GotEsc state.
|
|
122
|
+
*/
|
|
123
|
+
flushEscapeTimeout() {
|
|
124
|
+
if (this.state === State.GotEsc) {
|
|
125
|
+
this.state = State.Idle;
|
|
126
|
+
this.params = "";
|
|
127
|
+
return keyEvent("escape", "", false, false, false);
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
handleControlChar(code) {
|
|
132
|
+
const mapped = CTRL_KEYS[code];
|
|
133
|
+
if (mapped) {
|
|
134
|
+
this.result = keyEvent(mapped.key, mapped.char, false, mapped.ctrl, false);
|
|
135
|
+
return MatchResult.Complete;
|
|
136
|
+
}
|
|
137
|
+
// Ctrl+A (1) through Ctrl+Z (26)
|
|
138
|
+
if (code >= 1 && code <= 26) {
|
|
139
|
+
const letter = String.fromCharCode(code + 96); // 1 -> 'a', 2 -> 'b', etc.
|
|
140
|
+
this.result = keyEvent(letter, "", false, true, false);
|
|
141
|
+
return MatchResult.Complete;
|
|
142
|
+
}
|
|
143
|
+
// Other control chars (28-31)
|
|
144
|
+
this.result = keyEvent(`ctrl-${code}`, "", false, true, false);
|
|
145
|
+
return MatchResult.Complete;
|
|
146
|
+
}
|
|
147
|
+
handleAfterEsc(char, code) {
|
|
148
|
+
if (char === "[") {
|
|
149
|
+
this.state = State.Csi;
|
|
150
|
+
this.params = "";
|
|
151
|
+
return MatchResult.Partial;
|
|
152
|
+
}
|
|
153
|
+
if (char === "O") {
|
|
154
|
+
this.state = State.Ss3;
|
|
155
|
+
return MatchResult.Partial;
|
|
156
|
+
}
|
|
157
|
+
if (char === ESC) {
|
|
158
|
+
// Double ESC — emit first escape and stay in GotEsc for the second
|
|
159
|
+
this.result = keyEvent("escape", "", false, false, false);
|
|
160
|
+
this.state = State.GotEsc;
|
|
161
|
+
return MatchResult.Complete;
|
|
162
|
+
}
|
|
163
|
+
// Alt+char
|
|
164
|
+
this.state = State.Idle;
|
|
165
|
+
if (code < 32 || code === 127) {
|
|
166
|
+
// Alt+control char, e.g. Alt+Enter = \x1b\r
|
|
167
|
+
const mapped = CTRL_KEYS[code];
|
|
168
|
+
if (mapped) {
|
|
169
|
+
this.result = keyEvent(mapped.key, mapped.char, false, mapped.ctrl, true);
|
|
170
|
+
return MatchResult.Complete;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Alt + printable character
|
|
174
|
+
const isUpper = code >= 65 && code <= 90;
|
|
175
|
+
const key = char.toLowerCase() || char;
|
|
176
|
+
this.result = keyEvent(key, char, isUpper, false, true);
|
|
177
|
+
return MatchResult.Complete;
|
|
178
|
+
}
|
|
179
|
+
handleCsi(char, code) {
|
|
180
|
+
// Parameter bytes: digits, semicolons, and '<' (for SGR mouse, but
|
|
181
|
+
// mouse-matcher handles that — if we see '<' at start, bail out so
|
|
182
|
+
// mouse-matcher can handle it)
|
|
183
|
+
if (this.params.length === 0 && char === "<") {
|
|
184
|
+
// This is the start of an SGR mouse sequence; we should not match it.
|
|
185
|
+
this.state = State.Idle;
|
|
186
|
+
this.params = "";
|
|
187
|
+
return MatchResult.NoMatch;
|
|
188
|
+
}
|
|
189
|
+
if (code >= 0x30 && code <= 0x3b /* 0-9, :, ; */) {
|
|
190
|
+
this.params += char;
|
|
191
|
+
return MatchResult.Partial;
|
|
192
|
+
}
|
|
193
|
+
// Final byte — the actual command character
|
|
194
|
+
this.state = State.Idle;
|
|
195
|
+
if (char === "~") {
|
|
196
|
+
return this.handleTildeSequence();
|
|
197
|
+
}
|
|
198
|
+
const keyName = CSI_FINAL_KEYS[char];
|
|
199
|
+
if (keyName) {
|
|
200
|
+
const mods = this.parseModifier();
|
|
201
|
+
// CSI Z (shift+tab/backtab) always implies shift
|
|
202
|
+
const shift = char === "Z" ? true : mods.shift;
|
|
203
|
+
this.params = "";
|
|
204
|
+
this.result = keyEvent(keyName, "", shift, mods.ctrl, mods.alt);
|
|
205
|
+
return MatchResult.Complete;
|
|
206
|
+
}
|
|
207
|
+
// Unrecognized CSI sequence — discard
|
|
208
|
+
this.params = "";
|
|
209
|
+
return MatchResult.NoMatch;
|
|
210
|
+
}
|
|
211
|
+
handleTildeSequence() {
|
|
212
|
+
const parts = this.params.split(";");
|
|
213
|
+
const num = parseInt(parts[0], 10);
|
|
214
|
+
this.params = "";
|
|
215
|
+
const keyName = CSI_TILDE_KEYS[num];
|
|
216
|
+
if (!keyName) {
|
|
217
|
+
return MatchResult.NoMatch;
|
|
218
|
+
}
|
|
219
|
+
const mods = parts.length >= 2
|
|
220
|
+
? decodeModifier(parseInt(parts[1], 10))
|
|
221
|
+
: { shift: false, ctrl: false, alt: false };
|
|
222
|
+
this.result = keyEvent(keyName, "", mods.shift, mods.ctrl, mods.alt);
|
|
223
|
+
return MatchResult.Complete;
|
|
224
|
+
}
|
|
225
|
+
handleSs3(char) {
|
|
226
|
+
this.state = State.Idle;
|
|
227
|
+
const keyName = SS3_KEYS[char];
|
|
228
|
+
if (keyName) {
|
|
229
|
+
this.result = keyEvent(keyName, "", false, false, false);
|
|
230
|
+
return MatchResult.Complete;
|
|
231
|
+
}
|
|
232
|
+
// Some terminals send SS3 A/B/C/D for arrow keys
|
|
233
|
+
const arrowKey = CSI_FINAL_KEYS[char];
|
|
234
|
+
if (arrowKey) {
|
|
235
|
+
this.result = keyEvent(arrowKey, "", false, false, false);
|
|
236
|
+
return MatchResult.Complete;
|
|
237
|
+
}
|
|
238
|
+
return MatchResult.NoMatch;
|
|
239
|
+
}
|
|
240
|
+
parseModifier() {
|
|
241
|
+
if (!this.params) {
|
|
242
|
+
return { shift: false, ctrl: false, alt: false };
|
|
243
|
+
}
|
|
244
|
+
const parts = this.params.split(";");
|
|
245
|
+
if (parts.length >= 2) {
|
|
246
|
+
const mod = parseInt(parts[1], 10);
|
|
247
|
+
if (!Number.isNaN(mod)) {
|
|
248
|
+
return decodeModifier(mod);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { shift: false, ctrl: false, alt: false };
|
|
252
|
+
}
|
|
253
|
+
}
|