@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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static text display widget.
|
|
3
|
+
*
|
|
4
|
+
* Supports word wrapping, text alignment (left/center/right), and
|
|
5
|
+
* multi-line content. Automatically invalidates on text or style changes.
|
|
6
|
+
*/
|
|
7
|
+
import { Control } from "../layout/control.js";
|
|
8
|
+
export class Text extends Control {
|
|
9
|
+
_text;
|
|
10
|
+
_style;
|
|
11
|
+
_wrap;
|
|
12
|
+
_align;
|
|
13
|
+
/** Cached wrapped lines from the last measure/render pass. */
|
|
14
|
+
_lines = [];
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
super();
|
|
17
|
+
this._text = options.text ?? "";
|
|
18
|
+
this._style = options.style ?? {};
|
|
19
|
+
this._wrap = options.wrap ?? false;
|
|
20
|
+
this._align = options.align ?? "left";
|
|
21
|
+
}
|
|
22
|
+
// ── Properties ────────────────────────────────────────────────
|
|
23
|
+
get text() {
|
|
24
|
+
return this._text;
|
|
25
|
+
}
|
|
26
|
+
set text(value) {
|
|
27
|
+
if (this._text !== value) {
|
|
28
|
+
this._text = value;
|
|
29
|
+
this.invalidate();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
get style() {
|
|
33
|
+
return this._style;
|
|
34
|
+
}
|
|
35
|
+
set style(value) {
|
|
36
|
+
this._style = value;
|
|
37
|
+
this.invalidate();
|
|
38
|
+
}
|
|
39
|
+
get wrap() {
|
|
40
|
+
return this._wrap;
|
|
41
|
+
}
|
|
42
|
+
set wrap(value) {
|
|
43
|
+
if (this._wrap !== value) {
|
|
44
|
+
this._wrap = value;
|
|
45
|
+
this.invalidate();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
get align() {
|
|
49
|
+
return this._align;
|
|
50
|
+
}
|
|
51
|
+
set align(value) {
|
|
52
|
+
if (this._align !== value) {
|
|
53
|
+
this._align = value;
|
|
54
|
+
this.invalidate();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// ── Layout ────────────────────────────────────────────────────
|
|
58
|
+
measure(constraint) {
|
|
59
|
+
if (this._text.length === 0) {
|
|
60
|
+
this._lines = [];
|
|
61
|
+
return { width: 0, height: 0 };
|
|
62
|
+
}
|
|
63
|
+
const rawLines = this._text.split("\n");
|
|
64
|
+
if (!this._wrap) {
|
|
65
|
+
this._lines = rawLines;
|
|
66
|
+
const longestLine = rawLines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
67
|
+
return {
|
|
68
|
+
width: Math.min(longestLine, constraint.maxWidth),
|
|
69
|
+
height: Math.min(rawLines.length, constraint.maxHeight),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Word-wrap mode: wrap to maxWidth
|
|
73
|
+
const maxW = constraint.maxWidth;
|
|
74
|
+
if (maxW <= 0) {
|
|
75
|
+
this._lines = [];
|
|
76
|
+
return { width: 0, height: 0 };
|
|
77
|
+
}
|
|
78
|
+
const wrapped = wrapLines(rawLines, maxW);
|
|
79
|
+
this._lines = wrapped;
|
|
80
|
+
const longestWrapped = wrapped.reduce((max, line) => Math.max(max, line.length), 0);
|
|
81
|
+
return {
|
|
82
|
+
width: Math.min(longestWrapped, constraint.maxWidth),
|
|
83
|
+
height: Math.min(wrapped.length, constraint.maxHeight),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
render(ctx) {
|
|
87
|
+
const bounds = this.bounds;
|
|
88
|
+
if (!bounds || this._lines.length === 0)
|
|
89
|
+
return;
|
|
90
|
+
const availW = bounds.width;
|
|
91
|
+
for (let i = 0; i < this._lines.length && i < bounds.height; i++) {
|
|
92
|
+
const line = this._lines[i];
|
|
93
|
+
const x = alignOffset(line.length, availW, this._align);
|
|
94
|
+
ctx.drawText(bounds.x + x, bounds.y + i, line, this._style);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// ── Helper: word wrapping ──────────────────────────────────────────
|
|
99
|
+
/**
|
|
100
|
+
* Wrap an array of raw lines to fit within `maxWidth` columns.
|
|
101
|
+
*
|
|
102
|
+
* Rules:
|
|
103
|
+
* - Break on spaces when possible.
|
|
104
|
+
* - If a single word exceeds `maxWidth`, hard-break it at the width boundary.
|
|
105
|
+
* - Preserve explicit line breaks (already split by caller).
|
|
106
|
+
*/
|
|
107
|
+
function wrapLines(rawLines, maxWidth) {
|
|
108
|
+
const result = [];
|
|
109
|
+
for (const rawLine of rawLines) {
|
|
110
|
+
if (rawLine.length === 0) {
|
|
111
|
+
result.push("");
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const words = rawLine.split(" ");
|
|
115
|
+
let current = "";
|
|
116
|
+
for (let wi = 0; wi < words.length; wi++) {
|
|
117
|
+
const word = words[wi];
|
|
118
|
+
if (word.length === 0) {
|
|
119
|
+
// Consecutive spaces: add a space to current if it fits
|
|
120
|
+
if (current.length < maxWidth) {
|
|
121
|
+
current += " ";
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// If the word itself exceeds maxWidth, hard-break it
|
|
126
|
+
if (word.length > maxWidth) {
|
|
127
|
+
// Flush current line first
|
|
128
|
+
if (current.length > 0) {
|
|
129
|
+
result.push(current);
|
|
130
|
+
current = "";
|
|
131
|
+
}
|
|
132
|
+
// Split the long word into chunks
|
|
133
|
+
for (let ci = 0; ci < word.length; ci += maxWidth) {
|
|
134
|
+
const chunk = word.slice(ci, ci + maxWidth);
|
|
135
|
+
if (ci + maxWidth < word.length) {
|
|
136
|
+
result.push(chunk);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
current = chunk;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (current.length === 0) {
|
|
145
|
+
current = word;
|
|
146
|
+
}
|
|
147
|
+
else if (current.length + 1 + word.length <= maxWidth) {
|
|
148
|
+
current += ` ${word}`;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
result.push(current);
|
|
152
|
+
current = word;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
result.push(current);
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
// ── Helper: alignment offset ───────────────────────────────────────
|
|
160
|
+
function alignOffset(textLen, availWidth, align) {
|
|
161
|
+
if (align === "center") {
|
|
162
|
+
return Math.max(0, Math.floor((availWidth - textLen) / 2));
|
|
163
|
+
}
|
|
164
|
+
if (align === "right") {
|
|
165
|
+
return Math.max(0, availWidth - textLen);
|
|
166
|
+
}
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@teammates/consolonia",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Terminal UI rendering engine inspired by Consolonia. Pixel-level compositing with ANSI output.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:coverage": "vitest run --coverage",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"teammates",
|
|
27
|
+
"terminal",
|
|
28
|
+
"tui",
|
|
29
|
+
"ansi",
|
|
30
|
+
"consolonia"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^25.5.0",
|
|
35
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
36
|
+
"typescript": "^5.5.0",
|
|
37
|
+
"vitest": "^4.1.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"marked": "^17.0.4"
|
|
44
|
+
}
|
|
45
|
+
}
|