@zakmandhro/bunti 0.1.4 → 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/AGENTS.md +65 -0
- package/README.md +145 -34
- package/dist/cli.d.ts +44 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +147 -0
- package/dist/cli.js.map +1 -0
- package/dist/colors.d.ts +144 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +426 -0
- package/dist/colors.js.map +1 -0
- package/dist/components/Box.d.ts +11 -0
- package/dist/components/Box.d.ts.map +1 -0
- package/dist/components/Box.js +9 -0
- package/dist/components/Box.js.map +1 -0
- package/dist/components/Button.d.ts +24 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +102 -0
- package/dist/components/Button.js.map +1 -0
- package/dist/components/Card.d.ts +33 -0
- package/dist/components/Card.d.ts.map +1 -0
- package/dist/components/Card.js +79 -0
- package/dist/components/Card.js.map +1 -0
- package/dist/components/Header.d.ts +23 -0
- package/dist/components/Header.d.ts.map +1 -0
- package/dist/components/Header.js +58 -0
- package/dist/components/Header.js.map +1 -0
- package/dist/components/Input.d.ts +25 -0
- package/dist/components/Input.d.ts.map +1 -0
- package/dist/components/Input.js +232 -0
- package/dist/components/Input.js.map +1 -0
- package/dist/components/Link.d.ts +21 -0
- package/dist/components/Link.d.ts.map +1 -0
- package/dist/components/Link.js +29 -0
- package/dist/components/Link.js.map +1 -0
- package/dist/components/Modal.d.ts +38 -0
- package/dist/components/Modal.d.ts.map +1 -0
- package/dist/components/Modal.js +87 -0
- package/dist/components/Modal.js.map +1 -0
- package/dist/components/Progress.d.ts +27 -0
- package/dist/components/Progress.d.ts.map +1 -0
- package/dist/components/Progress.js +39 -0
- package/dist/components/Progress.js.map +1 -0
- package/dist/components/Spinner.d.ts +19 -0
- package/dist/components/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner.js +27 -0
- package/dist/components/Spinner.js.map +1 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +10 -0
- package/dist/components/index.js.map +1 -0
- package/dist/data/nf-glyphs.d.ts +11 -0
- package/dist/data/nf-glyphs.d.ts.map +1 -0
- package/dist/data/nf-glyphs.js +535 -0
- package/dist/data/nf-glyphs.js.map +1 -0
- package/dist/data/nf-names.d.ts +8 -0
- package/dist/data/nf-names.d.ts.map +1 -0
- package/dist/data/nf-names.js +2 -0
- package/dist/data/nf-names.js.map +1 -0
- package/dist/demo-registry.d.ts +27 -0
- package/dist/demo-registry.d.ts.map +1 -0
- package/dist/demo-registry.js +57 -0
- package/dist/demo-registry.js.map +1 -0
- package/dist/demos/2048.ts +743 -0
- package/dist/demos/animation.ts +246 -0
- package/dist/demos/demo-layout.ts +124 -0
- package/dist/demos/engine.ts +374 -0
- package/dist/demos/interaction.ts +91 -0
- package/dist/demos/login.ts +203 -0
- package/dist/demos/mission-control.ts +558 -0
- package/dist/demos/showcase.ts +224 -0
- package/dist/detect.d.ts +104 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +227 -0
- package/dist/detect.js.map +1 -0
- package/dist/diagnostics.d.ts +73 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +225 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/dsl/context.d.ts +11 -0
- package/dist/dsl/context.d.ts.map +1 -0
- package/dist/dsl/context.js +563 -0
- package/dist/dsl/context.js.map +1 -0
- package/dist/dsl/hooks.d.ts +12 -0
- package/dist/dsl/hooks.d.ts.map +1 -0
- package/dist/dsl/hooks.js +148 -0
- package/dist/dsl/hooks.js.map +1 -0
- package/dist/dsl/interaction.d.ts +16 -0
- package/dist/dsl/interaction.d.ts.map +1 -0
- package/dist/dsl/interaction.js +101 -0
- package/dist/dsl/interaction.js.map +1 -0
- package/dist/dsl/layers.d.ts +14 -0
- package/dist/dsl/layers.d.ts.map +1 -0
- package/dist/dsl/layers.js +87 -0
- package/dist/dsl/layers.js.map +1 -0
- package/dist/dsl/motion.d.ts +10 -0
- package/dist/dsl/motion.d.ts.map +1 -0
- package/dist/dsl/motion.js +150 -0
- package/dist/dsl/motion.js.map +1 -0
- package/dist/dsl/render.d.ts +22 -0
- package/dist/dsl/render.d.ts.map +1 -0
- package/dist/dsl/render.js +69 -0
- package/dist/dsl/render.js.map +1 -0
- package/dist/dsl/types.d.ts +510 -0
- package/dist/dsl/types.d.ts.map +1 -0
- package/dist/dsl/types.js +33 -0
- package/dist/dsl/types.js.map +1 -0
- package/dist/dsl.d.ts +18 -0
- package/dist/dsl.d.ts.map +1 -0
- package/dist/dsl.js +17 -0
- package/dist/dsl.js.map +1 -0
- package/dist/easing.d.ts +45 -0
- package/dist/easing.d.ts.map +1 -0
- package/dist/easing.js +121 -0
- package/dist/easing.js.map +1 -0
- package/dist/geometry.d.ts +77 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +130 -0
- package/dist/geometry.js.map +1 -0
- package/dist/icons-full.d.ts +5 -0
- package/dist/icons-full.d.ts.map +1 -0
- package/dist/icons-full.js +31 -0
- package/dist/icons-full.js.map +1 -0
- package/dist/icons.d.ts +442 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +314 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +228 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +177 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +533 -0
- package/dist/input.js.map +1 -0
- package/dist/layout.d.ts +194 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +673 -0
- package/dist/layout.js.map +1 -0
- package/dist/render.d.ts +49 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +650 -0
- package/dist/render.js.map +1 -0
- package/dist/state.d.ts +226 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +130 -0
- package/dist/state.js.map +1 -0
- package/dist/theme.d.ts +129 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +167 -0
- package/dist/theme.js.map +1 -0
- package/dist/themes/catppuccin-mocha.d.ts +19 -0
- package/dist/themes/catppuccin-mocha.d.ts.map +1 -0
- package/dist/themes/catppuccin-mocha.js +36 -0
- package/dist/themes/catppuccin-mocha.js.map +1 -0
- package/dist/themes/dracula.d.ts +21 -0
- package/dist/themes/dracula.d.ts.map +1 -0
- package/dist/themes/dracula.js +37 -0
- package/dist/themes/dracula.js.map +1 -0
- package/dist/themes/github-light.d.ts +21 -0
- package/dist/themes/github-light.d.ts.map +1 -0
- package/dist/themes/github-light.js +36 -0
- package/dist/themes/github-light.js.map +1 -0
- package/dist/themes/index.d.ts +19 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +17 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/themes/nord.d.ts +23 -0
- package/dist/themes/nord.d.ts.map +1 -0
- package/dist/themes/nord.js +38 -0
- package/dist/themes/nord.js.map +1 -0
- package/dist/themes/one-dark-pro.d.ts +21 -0
- package/dist/themes/one-dark-pro.d.ts.map +1 -0
- package/dist/themes/one-dark-pro.js +36 -0
- package/dist/themes/one-dark-pro.js.map +1 -0
- package/dist/themes/tokyo-night.d.ts +20 -0
- package/dist/themes/tokyo-night.d.ts.map +1 -0
- package/dist/themes/tokyo-night.js +37 -0
- package/dist/themes/tokyo-night.js.map +1 -0
- package/dist/utils.d.ts +28 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +158 -0
- package/dist/utils.js.map +1 -0
- package/dist/vendor/colors.d.ts +45 -0
- package/dist/vendor/colors.d.ts.map +1 -0
- package/dist/vendor/colors.js +72 -0
- package/dist/vendor/colors.js.map +1 -0
- package/llms.txt +79 -0
- package/package.json +52 -11
- package/src/colors.ts +0 -255
- package/src/components/Button.ts +0 -104
- package/src/components/Card.ts +0 -53
- package/src/components/Header.ts +0 -65
- package/src/components/Input.ts +0 -124
- package/src/components/index.ts +0 -4
- package/src/data/glyphs.ts +0 -30
- package/src/detect.ts +0 -60
- package/src/dsl.ts +0 -661
- package/src/icons.ts +0 -186
- package/src/index.ts +0 -72
- package/src/layout.ts +0 -640
- package/src/render.ts +0 -302
- package/src/state.ts +0 -148
- package/src/utils.ts +0 -165
package/dist/layout.js
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunti Functional Layout & Drawing
|
|
3
|
+
* Strictly functional primitives for buffer manipulation and layout generation.
|
|
4
|
+
*/
|
|
5
|
+
import { fg as fgColor, isThemeColor, resolveColor, resolveColorToRGB, } from './colors';
|
|
6
|
+
import { colorTier } from './detect';
|
|
7
|
+
import { replaceEmojis } from './icons';
|
|
8
|
+
import { charWidth, truncate, visibleWidth, wrapText } from './utils';
|
|
9
|
+
// --- Functional Primitives (Buffer Manipulation) ---
|
|
10
|
+
/**
|
|
11
|
+
* Writes one cell into the back buffer (bounds-checked). Wide glyphs mark
|
|
12
|
+
* their tail cells as skip so the differ keeps columns aligned.
|
|
13
|
+
*/
|
|
14
|
+
export function setCell(state, x, y, cell) {
|
|
15
|
+
if (x >= 0 && x < state.width && y >= 0 && y < state.height) {
|
|
16
|
+
const target = state.backBuffer[y * state.width + x];
|
|
17
|
+
let displayWidth = 1;
|
|
18
|
+
if (cell.char !== undefined) {
|
|
19
|
+
target.char = cell.raw ? cell.char : replaceEmojis(cell.char);
|
|
20
|
+
// Single-ASCII-char cells (fills, borders' spaces, plain text) skip width math.
|
|
21
|
+
displayWidth =
|
|
22
|
+
target.char.length === 1 && target.char.charCodeAt(0) < 0x80
|
|
23
|
+
? 1
|
|
24
|
+
: charWidth(target.char);
|
|
25
|
+
target.fg = undefined;
|
|
26
|
+
target.fgCode = undefined;
|
|
27
|
+
target.bold = false;
|
|
28
|
+
target.italic = false;
|
|
29
|
+
target.underline = false;
|
|
30
|
+
target.dim = false;
|
|
31
|
+
target.strike = false;
|
|
32
|
+
}
|
|
33
|
+
if (cell.fg !== undefined) {
|
|
34
|
+
target.fg = cell.fg;
|
|
35
|
+
target.fgCode = resolveColor(cell.fg);
|
|
36
|
+
}
|
|
37
|
+
if (cell.bg !== undefined) {
|
|
38
|
+
target.bg = cell.bg;
|
|
39
|
+
target.bgCode = resolveColor(cell.bg);
|
|
40
|
+
}
|
|
41
|
+
if (cell.bold !== undefined)
|
|
42
|
+
target.bold = cell.bold;
|
|
43
|
+
if (cell.italic !== undefined)
|
|
44
|
+
target.italic = cell.italic;
|
|
45
|
+
if (cell.underline !== undefined)
|
|
46
|
+
target.underline = cell.underline;
|
|
47
|
+
if (cell.dim !== undefined)
|
|
48
|
+
target.dim = cell.dim;
|
|
49
|
+
if (cell.strike !== undefined)
|
|
50
|
+
target.strike = cell.strike;
|
|
51
|
+
target.skip = cell.skip ?? false;
|
|
52
|
+
for (let dx = 1; dx < displayWidth; dx++) {
|
|
53
|
+
const skipX = x + dx;
|
|
54
|
+
if (skipX >= state.width)
|
|
55
|
+
break;
|
|
56
|
+
const skip = state.backBuffer[y * state.width + skipX];
|
|
57
|
+
skip.char = '';
|
|
58
|
+
skip.fg = target.fg;
|
|
59
|
+
skip.bg = target.bg;
|
|
60
|
+
skip.fgCode = target.fgCode;
|
|
61
|
+
skip.bgCode = target.bgCode;
|
|
62
|
+
skip.bold = target.bold;
|
|
63
|
+
skip.italic = target.italic;
|
|
64
|
+
skip.underline = target.underline;
|
|
65
|
+
skip.dim = target.dim;
|
|
66
|
+
skip.strike = target.strike;
|
|
67
|
+
skip.skip = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Repaints existing back-buffer cells inside `area` at reduced luminance.
|
|
73
|
+
* Each cell's current fg/bg is resolved to exact RGB (numeric ANSI-256 codes
|
|
74
|
+
* via the xterm table) and scaled toward black by `amount` (0 = unchanged,
|
|
75
|
+
* 1 = fully black). Cells with an undefined bg keep their transparent
|
|
76
|
+
* background (fg-only dim); undefined fg is left alone (there is no color to
|
|
77
|
+
* scale). Wide-glyph tail cells (skip) are dimmed too so they stay in sync
|
|
78
|
+
* with their head cell. No-op on the mono tier — there is no color to dim.
|
|
79
|
+
*/
|
|
80
|
+
export function dimRect(state, area, amount) {
|
|
81
|
+
if (colorTier() === 'mono')
|
|
82
|
+
return;
|
|
83
|
+
const t = Math.max(0, Math.min(1, amount));
|
|
84
|
+
if (t === 0)
|
|
85
|
+
return;
|
|
86
|
+
const keep = 1 - t;
|
|
87
|
+
const x0 = Math.max(0, area.x);
|
|
88
|
+
const y0 = Math.max(0, area.y);
|
|
89
|
+
const x1 = Math.min(state.width, area.x + area.width);
|
|
90
|
+
const y1 = Math.min(state.height, area.y + area.height);
|
|
91
|
+
for (let y = y0; y < y1; y++) {
|
|
92
|
+
const rowOffset = y * state.width;
|
|
93
|
+
for (let x = x0; x < x1; x++) {
|
|
94
|
+
const cell = state.backBuffer[rowOffset + x];
|
|
95
|
+
if (cell.fg !== undefined) {
|
|
96
|
+
const rgb = resolveColorToRGB(cell.fg);
|
|
97
|
+
const dimmed = {
|
|
98
|
+
r: Math.round(rgb.r * keep),
|
|
99
|
+
g: Math.round(rgb.g * keep),
|
|
100
|
+
b: Math.round(rgb.b * keep),
|
|
101
|
+
};
|
|
102
|
+
cell.fg = dimmed;
|
|
103
|
+
cell.fgCode = resolveColor(dimmed);
|
|
104
|
+
}
|
|
105
|
+
if (cell.bg !== undefined) {
|
|
106
|
+
const rgb = resolveColorToRGB(cell.bg);
|
|
107
|
+
const dimmed = {
|
|
108
|
+
r: Math.round(rgb.r * keep),
|
|
109
|
+
g: Math.round(rgb.g * keep),
|
|
110
|
+
b: Math.round(rgb.b * keep),
|
|
111
|
+
};
|
|
112
|
+
cell.bg = dimmed;
|
|
113
|
+
cell.bgCode = resolveColor(dimmed);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fills a w×h cell rectangle at absolute coordinates — bars, panels,
|
|
120
|
+
* charts, backgrounds. Pure-background fills (no `char`) preserve the
|
|
121
|
+
* characters already painted underneath.
|
|
122
|
+
* @example rect(state, 0, 0, state.width, 3, { bg: 'midnight' });
|
|
123
|
+
*/
|
|
124
|
+
export function rect(state, x, y, w, h, style) {
|
|
125
|
+
const isGradient = style.bg && typeof style.bg === 'object' && 'colors' in style.bg;
|
|
126
|
+
for (let dy = 0; dy < h; dy++) {
|
|
127
|
+
const row = y + dy;
|
|
128
|
+
if (row < 0 || row >= state.height)
|
|
129
|
+
continue;
|
|
130
|
+
for (let dx = 0; dx < w; dx++) {
|
|
131
|
+
const col = x + dx;
|
|
132
|
+
if (col < 0 || col >= state.width)
|
|
133
|
+
continue;
|
|
134
|
+
let resolvedBg;
|
|
135
|
+
if (isGradient) {
|
|
136
|
+
const grad = style.bg;
|
|
137
|
+
if (grad.direction === 'horizontal') {
|
|
138
|
+
resolvedBg = grad.colors[Math.floor((dx / w) * grad.colors.length)];
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
resolvedBg = grad.colors[Math.floor((dy / h) * grad.colors.length)];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
resolvedBg =
|
|
146
|
+
style.bg !== undefined
|
|
147
|
+
? resolveColor(style.bg)
|
|
148
|
+
: undefined;
|
|
149
|
+
}
|
|
150
|
+
const cell = { bg: resolvedBg };
|
|
151
|
+
if (style.char)
|
|
152
|
+
cell.char = style.char;
|
|
153
|
+
if (style.fg)
|
|
154
|
+
cell.fg = resolveColor(style.fg);
|
|
155
|
+
const existing = state.backBuffer[row * state.width + col];
|
|
156
|
+
if (existing.char !== ' ' && !style.char) {
|
|
157
|
+
// Keep existing char/fg if drawing a pure background
|
|
158
|
+
cell.char = existing.char;
|
|
159
|
+
cell.fg = existing.fg;
|
|
160
|
+
}
|
|
161
|
+
setCell(state, col, row, cell);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Paints a multi-line, ANSI-styled string into the back buffer at absolute
|
|
167
|
+
* cell coordinates, parsing SGR codes into per-cell attributes. Grapheme-
|
|
168
|
+
* aware: emoji and wide glyphs occupy their real column width.
|
|
169
|
+
* @example blit(state, 2, 1, fg('gold', 'FPS 60'));
|
|
170
|
+
*/
|
|
171
|
+
export function blit(state, startX, startY, content, style = {}) {
|
|
172
|
+
const lines = content.split('\n');
|
|
173
|
+
for (let row = 0; row < lines.length; row++) {
|
|
174
|
+
const line = lines[row];
|
|
175
|
+
let x = startX;
|
|
176
|
+
let currentFg, currentBg;
|
|
177
|
+
let currentBold = false;
|
|
178
|
+
let currentItalic = false;
|
|
179
|
+
let currentUnderline = false;
|
|
180
|
+
let currentDim = false;
|
|
181
|
+
let currentStrike = false;
|
|
182
|
+
const regex = /\x1B\[([0-9;]*)m|([^\x1B]+)/g;
|
|
183
|
+
let match = null;
|
|
184
|
+
while ((match = regex.exec(line)) !== null) {
|
|
185
|
+
if (match[1] !== undefined) {
|
|
186
|
+
const codes = match[1].split(';');
|
|
187
|
+
for (let i = 0; i < codes.length; i++) {
|
|
188
|
+
const code = parseInt(codes[i] || '0', 10);
|
|
189
|
+
if (code === 0) {
|
|
190
|
+
currentFg = undefined;
|
|
191
|
+
currentBg = undefined;
|
|
192
|
+
currentBold = false;
|
|
193
|
+
currentItalic = false;
|
|
194
|
+
currentUnderline = false;
|
|
195
|
+
currentDim = false;
|
|
196
|
+
currentStrike = false;
|
|
197
|
+
}
|
|
198
|
+
else if (code === 1)
|
|
199
|
+
currentBold = true;
|
|
200
|
+
else if (code === 2)
|
|
201
|
+
currentDim = true;
|
|
202
|
+
else if (code === 3)
|
|
203
|
+
currentItalic = true;
|
|
204
|
+
else if (code === 4)
|
|
205
|
+
currentUnderline = true;
|
|
206
|
+
else if (code === 9)
|
|
207
|
+
currentStrike = true;
|
|
208
|
+
else if (code === 22) {
|
|
209
|
+
// SGR 22 resets intensity: clears BOTH bold and dim.
|
|
210
|
+
currentBold = false;
|
|
211
|
+
currentDim = false;
|
|
212
|
+
}
|
|
213
|
+
else if (code === 23)
|
|
214
|
+
currentItalic = false;
|
|
215
|
+
else if (code === 24)
|
|
216
|
+
currentUnderline = false;
|
|
217
|
+
else if (code === 29)
|
|
218
|
+
currentStrike = false;
|
|
219
|
+
else if (code >= 30 && code <= 37)
|
|
220
|
+
currentFg = (code - 30).toString();
|
|
221
|
+
else if (code >= 40 && code <= 47)
|
|
222
|
+
currentBg = (code - 40).toString();
|
|
223
|
+
else if (code >= 90 && code <= 97)
|
|
224
|
+
currentFg = (code - 90 + 8).toString();
|
|
225
|
+
else if (code >= 100 && code <= 107)
|
|
226
|
+
currentBg = (code - 100 + 8).toString();
|
|
227
|
+
else if (code === 38 && codes[i + 1] === '5') {
|
|
228
|
+
currentFg = parseInt(codes[i + 2], 10);
|
|
229
|
+
i += 2;
|
|
230
|
+
}
|
|
231
|
+
else if (code === 38 && codes[i + 1] === '2') {
|
|
232
|
+
currentFg = {
|
|
233
|
+
r: parseInt(codes[i + 2], 10),
|
|
234
|
+
g: parseInt(codes[i + 3], 10),
|
|
235
|
+
b: parseInt(codes[i + 4], 10),
|
|
236
|
+
};
|
|
237
|
+
i += 4;
|
|
238
|
+
}
|
|
239
|
+
else if (code === 48 && codes[i + 1] === '5') {
|
|
240
|
+
currentBg = parseInt(codes[i + 2], 10);
|
|
241
|
+
i += 2;
|
|
242
|
+
}
|
|
243
|
+
else if (code === 48 && codes[i + 1] === '2') {
|
|
244
|
+
currentBg = {
|
|
245
|
+
r: parseInt(codes[i + 2], 10),
|
|
246
|
+
g: parseInt(codes[i + 3], 10),
|
|
247
|
+
b: parseInt(codes[i + 4], 10),
|
|
248
|
+
};
|
|
249
|
+
i += 4;
|
|
250
|
+
}
|
|
251
|
+
else if (code === 39)
|
|
252
|
+
currentFg = undefined;
|
|
253
|
+
else if (code === 49)
|
|
254
|
+
currentBg = undefined;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (match[2]) {
|
|
258
|
+
const processedText = replaceEmojis(match[2]);
|
|
259
|
+
const segmenter = new Intl.Segmenter('en', {
|
|
260
|
+
granularity: 'grapheme',
|
|
261
|
+
});
|
|
262
|
+
for (const { segment } of segmenter.segment(processedText)) {
|
|
263
|
+
const w = charWidth(segment);
|
|
264
|
+
if (w === 0)
|
|
265
|
+
continue;
|
|
266
|
+
const cell = { char: segment, ...style };
|
|
267
|
+
// Only overwrite buffer colors if explicitly set in the string or style
|
|
268
|
+
if (currentFg !== undefined)
|
|
269
|
+
cell.fg = currentFg;
|
|
270
|
+
else if (style.fg !== undefined)
|
|
271
|
+
cell.fg = style.fg;
|
|
272
|
+
else if (state.options.defaultFg !== undefined)
|
|
273
|
+
cell.fg = state.options.defaultFg;
|
|
274
|
+
if (currentBg !== undefined)
|
|
275
|
+
cell.bg = currentBg;
|
|
276
|
+
else if (style.bg !== undefined)
|
|
277
|
+
cell.bg = style.bg;
|
|
278
|
+
if (currentBold)
|
|
279
|
+
cell.bold = true;
|
|
280
|
+
if (currentItalic)
|
|
281
|
+
cell.italic = true;
|
|
282
|
+
if (currentUnderline)
|
|
283
|
+
cell.underline = true;
|
|
284
|
+
if (currentDim)
|
|
285
|
+
cell.dim = true;
|
|
286
|
+
if (currentStrike)
|
|
287
|
+
cell.strike = true;
|
|
288
|
+
setCell(state, x, startY + row, cell);
|
|
289
|
+
x += w;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const BORDERS = {
|
|
296
|
+
default: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' },
|
|
297
|
+
rounded: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' },
|
|
298
|
+
double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
|
|
299
|
+
dashed: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '╌', v: '╎' },
|
|
300
|
+
dotted: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '┈', v: '┊' },
|
|
301
|
+
frame: {
|
|
302
|
+
tl: '█',
|
|
303
|
+
tr: '█',
|
|
304
|
+
bl: '█',
|
|
305
|
+
br: '█',
|
|
306
|
+
top: '▀',
|
|
307
|
+
bottom: '▄',
|
|
308
|
+
left: '█',
|
|
309
|
+
right: '█',
|
|
310
|
+
},
|
|
311
|
+
'thick-frame': { tl: '█', tr: '█', bl: '█', br: '█', h: '█', v: '█' },
|
|
312
|
+
classic: { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' },
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Resolves a SizeUnit to an absolute integer based on parent dimensions.
|
|
316
|
+
*/
|
|
317
|
+
export function resolveSize(unit, parentDim, contentDim) {
|
|
318
|
+
if (unit === undefined || unit === 'auto')
|
|
319
|
+
return contentDim;
|
|
320
|
+
if (typeof unit === 'number')
|
|
321
|
+
return unit;
|
|
322
|
+
if (typeof unit === 'string') {
|
|
323
|
+
if (unit.endsWith('%')) {
|
|
324
|
+
const pct = parseFloat(unit) / 100;
|
|
325
|
+
return Math.floor(parentDim * pct);
|
|
326
|
+
}
|
|
327
|
+
if (unit.endsWith('fr')) {
|
|
328
|
+
return parentDim;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return contentDim;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Generates a styled box string with perfect alignment and optional
|
|
335
|
+
* wrapping. This is the pure string-building form; ctx.box() adds
|
|
336
|
+
* placement and painting on top of it.
|
|
337
|
+
* @example box('hello', { width: 20, border: 'rounded', padding: [0, 1] });
|
|
338
|
+
*/
|
|
339
|
+
export function box(content, options = {}, parentW, parentH) {
|
|
340
|
+
// Ensure we operate on the swapped glyphs for all layout math
|
|
341
|
+
content = replaceEmojis(content);
|
|
342
|
+
const px = options.padding?.[1] ?? 0;
|
|
343
|
+
const py = options.padding?.[0] ?? 0;
|
|
344
|
+
const borderStyle = options.border || 'none';
|
|
345
|
+
const borderOffset = borderStyle === 'none' ? 0 : 2;
|
|
346
|
+
const rawLines = content.split('\n');
|
|
347
|
+
const maxRawW = Math.max(...rawLines.map((l) => visibleWidth(l)), 0);
|
|
348
|
+
const intrinsicW = maxRawW + px * 2 + borderOffset;
|
|
349
|
+
// 1. Resolve Target Width (Outer)
|
|
350
|
+
const resolvedW = resolveSize(options.width, parentW || 0, intrinsicW);
|
|
351
|
+
// 2. Calculate Inner Width (between borders, including padding)
|
|
352
|
+
let targetInnerW = Math.max(0, resolvedW - borderOffset);
|
|
353
|
+
if (options.minWidth)
|
|
354
|
+
targetInnerW = Math.max(targetInnerW, options.minWidth - borderOffset);
|
|
355
|
+
if (options.maxWidth)
|
|
356
|
+
targetInnerW = Math.min(targetInnerW, options.maxWidth - borderOffset);
|
|
357
|
+
let lines = [];
|
|
358
|
+
const contentWidth = Math.max(0, targetInnerW - px * 2);
|
|
359
|
+
if (options.wrap && contentWidth > 0) {
|
|
360
|
+
lines = wrapText(content, contentWidth);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
lines = content.split('\n').map((l) => truncate(l, contentWidth));
|
|
364
|
+
}
|
|
365
|
+
const contentH = lines.length + py * 2;
|
|
366
|
+
const resolvedH = resolveSize(options.height, parentH || 0, contentH);
|
|
367
|
+
let finalInnerH = resolvedH ? resolvedH - borderOffset : contentH;
|
|
368
|
+
if (options.minHeight)
|
|
369
|
+
finalInnerH = Math.max(finalInnerH, options.minHeight - borderOffset);
|
|
370
|
+
if (options.maxHeight)
|
|
371
|
+
finalInnerH = Math.min(finalInnerH, options.maxHeight - borderOffset);
|
|
372
|
+
const b = borderStyle === 'none'
|
|
373
|
+
? null
|
|
374
|
+
: BORDERS[borderStyle] || BORDERS.default;
|
|
375
|
+
// Color Resolution
|
|
376
|
+
// ThemeColors are callable, so they must be detected BEFORE the
|
|
377
|
+
// function-style border-wrapper path (brand check via isThemeColor).
|
|
378
|
+
const resolveSide = (color) => isThemeColor(color)
|
|
379
|
+
? (s) => fgColor(color.rgb, s)
|
|
380
|
+
: typeof color === 'function'
|
|
381
|
+
? color
|
|
382
|
+
: (s) => fgColor(color, s);
|
|
383
|
+
const wrapBg = (s) => {
|
|
384
|
+
if (!options.bgColor)
|
|
385
|
+
return s;
|
|
386
|
+
if (typeof options.bgColor === 'object' && 'colors' in options.bgColor) {
|
|
387
|
+
// Gradients cannot be wrapped as a single ANSI string because they change per-cell.
|
|
388
|
+
// In string-building mode, we must ignore gradients. They are handled in absolute rect mode.
|
|
389
|
+
return s;
|
|
390
|
+
}
|
|
391
|
+
const code = resolveColor(options.bgColor);
|
|
392
|
+
if (code === undefined)
|
|
393
|
+
return s; // mono tier: no color output
|
|
394
|
+
const codeStr = String(code);
|
|
395
|
+
const prefix = codeStr.startsWith('2;') ? '48' : '48;5';
|
|
396
|
+
const bgOn = `\x1b[${prefix};${codeStr}m`;
|
|
397
|
+
// Apply bg, and if the string contains resets, re-apply the bg right after the reset.
|
|
398
|
+
// We add a final reset at the very end to clean up.
|
|
399
|
+
return `${bgOn + s.replace(/\x1b\[0m/g, `\x1b[0m${bgOn}`)}\x1b[0m`;
|
|
400
|
+
};
|
|
401
|
+
const bc = options.borderColor;
|
|
402
|
+
const colors = typeof bc === 'object' && !('r' in bc)
|
|
403
|
+
? {
|
|
404
|
+
top: resolveSide(bc.top ||
|
|
405
|
+
bc.left ||
|
|
406
|
+
bc.right),
|
|
407
|
+
bottom: resolveSide(bc.bottom ||
|
|
408
|
+
bc.left ||
|
|
409
|
+
bc.right),
|
|
410
|
+
left: resolveSide(bc.left || bc.top),
|
|
411
|
+
right: resolveSide(bc.right || bc.top),
|
|
412
|
+
}
|
|
413
|
+
: {
|
|
414
|
+
top: resolveSide(bc || ((s) => s)),
|
|
415
|
+
bottom: resolveSide(bc || ((s) => s)),
|
|
416
|
+
left: resolveSide(bc || ((s) => s)),
|
|
417
|
+
right: resolveSide(bc || ((s) => s)),
|
|
418
|
+
};
|
|
419
|
+
const hTop = b?.top || b?.h || ' ';
|
|
420
|
+
const hBottom = b?.bottom || b?.h || ' ';
|
|
421
|
+
const vLeft = b?.left || b?.v || ' ';
|
|
422
|
+
const vRight = b?.right || b?.v || ' ';
|
|
423
|
+
const vSpace = Math.max(0, finalInnerH - lines.length - py * 2);
|
|
424
|
+
let topS = 0, bottomS = vSpace;
|
|
425
|
+
if (options.valign === 'middle') {
|
|
426
|
+
topS = Math.floor(vSpace / 2);
|
|
427
|
+
bottomS = Math.ceil(vSpace / 2);
|
|
428
|
+
}
|
|
429
|
+
else if (options.valign === 'bottom') {
|
|
430
|
+
topS = vSpace;
|
|
431
|
+
bottomS = 0;
|
|
432
|
+
}
|
|
433
|
+
const out = [];
|
|
434
|
+
// Top Border
|
|
435
|
+
if (b) {
|
|
436
|
+
const title = options.title && targetInnerW > 2
|
|
437
|
+
? truncate(` ${options.title.trim()} `, targetInnerW)
|
|
438
|
+
: '';
|
|
439
|
+
const styledTitle = options.titleStyle ? options.titleStyle(title) : title;
|
|
440
|
+
const titleW = visibleWidth(title);
|
|
441
|
+
const beforeTitle = title ? 1 : 0;
|
|
442
|
+
const afterTitle = Math.max(0, targetInnerW - beforeTitle - titleW);
|
|
443
|
+
out.push(wrapBg(colors.top(b.tl) +
|
|
444
|
+
colors.top(hTop.repeat(beforeTitle)) +
|
|
445
|
+
styledTitle +
|
|
446
|
+
colors.top(hTop.repeat(afterTitle)) +
|
|
447
|
+
colors.top(b.tr)));
|
|
448
|
+
}
|
|
449
|
+
// Top Padding
|
|
450
|
+
for (let i = 0; i < py + topS; i++) {
|
|
451
|
+
let row = b ? colors.left(vLeft) : '';
|
|
452
|
+
row += wrapBg(' '.repeat(targetInnerW));
|
|
453
|
+
if (b)
|
|
454
|
+
row += colors.right(vRight);
|
|
455
|
+
out.push(row);
|
|
456
|
+
}
|
|
457
|
+
// Content Lines
|
|
458
|
+
for (let i = 0; i < lines.length; i++) {
|
|
459
|
+
const line = lines[i].trimEnd();
|
|
460
|
+
const lineW = visibleWidth(line);
|
|
461
|
+
const extra = Math.max(0, targetInnerW - lineW - px * 2);
|
|
462
|
+
let left = 0, right = 0;
|
|
463
|
+
const align = options.align || 'left';
|
|
464
|
+
if (align === 'center') {
|
|
465
|
+
left = px + Math.floor(extra / 2);
|
|
466
|
+
right = px + Math.ceil(extra / 2);
|
|
467
|
+
}
|
|
468
|
+
else if (align === 'right') {
|
|
469
|
+
left = px + extra;
|
|
470
|
+
right = px;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
left = px;
|
|
474
|
+
right = px + extra;
|
|
475
|
+
}
|
|
476
|
+
let row = b ? colors.left(vLeft) : '';
|
|
477
|
+
row += wrapBg(' '.repeat(Math.max(0, left)) + line + ' '.repeat(Math.max(0, right)));
|
|
478
|
+
if (b)
|
|
479
|
+
row += colors.right(vRight);
|
|
480
|
+
out.push(row);
|
|
481
|
+
}
|
|
482
|
+
// Bottom Padding
|
|
483
|
+
for (let i = 0; i < py + bottomS; i++) {
|
|
484
|
+
let row = b ? colors.left(vLeft) : '';
|
|
485
|
+
row += wrapBg(' '.repeat(targetInnerW));
|
|
486
|
+
if (b)
|
|
487
|
+
row += colors.right(vRight);
|
|
488
|
+
out.push(row);
|
|
489
|
+
}
|
|
490
|
+
// Bottom Border
|
|
491
|
+
if (b) {
|
|
492
|
+
out.push(wrapBg(colors.bottom(b.bl) +
|
|
493
|
+
colors.bottom(hBottom.repeat(targetInnerW)) +
|
|
494
|
+
colors.bottom(b.br)));
|
|
495
|
+
}
|
|
496
|
+
return out.join('\n');
|
|
497
|
+
}
|
|
498
|
+
/** Joins rendered blocks side-by-side, padding each to its own width. */
|
|
499
|
+
export function joinHorizontal(...blocks) {
|
|
500
|
+
const parsed = blocks.map((b) => b.split('\n'));
|
|
501
|
+
const maxH = Math.max(...parsed.map((p) => p.length));
|
|
502
|
+
const widths = parsed.map((p) => Math.max(...p.map(visibleWidth)));
|
|
503
|
+
const out = [];
|
|
504
|
+
for (let i = 0; i < maxH; i++) {
|
|
505
|
+
let row = '';
|
|
506
|
+
for (let j = 0; j < parsed.length; j++) {
|
|
507
|
+
const block = parsed[j];
|
|
508
|
+
const targetW = widths[j];
|
|
509
|
+
if (block[i] !== undefined) {
|
|
510
|
+
row +=
|
|
511
|
+
block[i] +
|
|
512
|
+
' '.repeat(Math.max(0, targetW - visibleWidth(block[i])));
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
row += ' '.repeat(targetW);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
out.push(row);
|
|
519
|
+
}
|
|
520
|
+
return out.join('\n');
|
|
521
|
+
}
|
|
522
|
+
/** Stacks rendered blocks vertically (skips empty strings). */
|
|
523
|
+
export function joinVertical(...blocks) {
|
|
524
|
+
return blocks.filter(Boolean).join('\n');
|
|
525
|
+
}
|
|
526
|
+
/** Returns a box() partially applied with default StyleOptions. */
|
|
527
|
+
export function createStyle(defaults) {
|
|
528
|
+
return (content, overrides = {}) => {
|
|
529
|
+
return box(content, { ...defaults, ...overrides });
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
/** Uppercased ` TEXT ` chip, wrapped by the given styler. */
|
|
533
|
+
export function badge(text, colorFn = (s) => s) {
|
|
534
|
+
return colorFn(` ${text.toUpperCase()} `);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Renders a data table with perfectly aligned columns and shared borders.
|
|
538
|
+
*/
|
|
539
|
+
export function table(rows, options = {}, parentW) {
|
|
540
|
+
const borderStyle = options.border || 'default';
|
|
541
|
+
const _px = options.padding?.[1] ?? 1;
|
|
542
|
+
const _py = options.padding?.[0] ?? 0;
|
|
543
|
+
const colCount = rows[0]?.length || 0;
|
|
544
|
+
if (colCount === 0)
|
|
545
|
+
return '';
|
|
546
|
+
// 1. Resolve Column Widths
|
|
547
|
+
const resolvedWidth = resolveSize(options.width, parentW || 0, 80);
|
|
548
|
+
const gutterW = 1;
|
|
549
|
+
const colWidth = Math.floor((resolvedWidth - (colCount - 1) * gutterW) / colCount);
|
|
550
|
+
// 2. Render each cell as a rigid block
|
|
551
|
+
const renderedRows = rows.map((row) => {
|
|
552
|
+
const cells = row.map((content, i) => {
|
|
553
|
+
// Explicitly pad empty content to ensure it occupies the full column width
|
|
554
|
+
const safeContent = content || ' '.repeat(colWidth);
|
|
555
|
+
const cellAlign = options.columns?.[i]?.align
|
|
556
|
+
? options.columns[i].align
|
|
557
|
+
: 'left';
|
|
558
|
+
return box(safeContent, {
|
|
559
|
+
width: colWidth,
|
|
560
|
+
border: 'none',
|
|
561
|
+
// Strict internal zero-padding; let the gutter handle spacing
|
|
562
|
+
padding: [0, 0],
|
|
563
|
+
align: cellAlign,
|
|
564
|
+
}, colWidth, 0);
|
|
565
|
+
});
|
|
566
|
+
// Join with gutter
|
|
567
|
+
return joinHorizontal(...cells.flatMap((c, i) => i < cells.length - 1 ? [c, ' '.repeat(gutterW)] : [c]));
|
|
568
|
+
});
|
|
569
|
+
return box(joinVertical(...renderedRows), {
|
|
570
|
+
border: borderStyle,
|
|
571
|
+
padding: [0, 0],
|
|
572
|
+
width: resolvedWidth,
|
|
573
|
+
align: 'left',
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Computes the scroll window that keeps `selectedIndex` visible in
|
|
578
|
+
* `maxVisible` rows (used by list windowing).
|
|
579
|
+
*/
|
|
580
|
+
export function getWindow(items, selectedIndex, maxVisible) {
|
|
581
|
+
const start = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), items.length - maxVisible));
|
|
582
|
+
const visible = items.slice(start, start + maxVisible);
|
|
583
|
+
return {
|
|
584
|
+
visible,
|
|
585
|
+
start,
|
|
586
|
+
hasMoreAbove: start > 0,
|
|
587
|
+
hasMoreBelow: start + maxVisible < items.length,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Renders items as list rows with selection/hover washes and optional
|
|
592
|
+
* maxVisible windowing. Pure string form; ctx.list() adds the keyboard
|
|
593
|
+
* and mouse behavior on top.
|
|
594
|
+
*/
|
|
595
|
+
export function list(items, options = {}, parentW) {
|
|
596
|
+
const bullet = options.bullet || '';
|
|
597
|
+
const indent = ' '.repeat(options.indent || 0);
|
|
598
|
+
const resolvedW = resolveSize(options.width, parentW || 0, 0);
|
|
599
|
+
const wrapLineBg = (line, bg) => {
|
|
600
|
+
const code = resolveColor(bg);
|
|
601
|
+
if (code === undefined)
|
|
602
|
+
return line; // mono tier: no color output
|
|
603
|
+
const codeStr = String(code);
|
|
604
|
+
const prefix = codeStr.startsWith('2;') ? '48' : '48;5';
|
|
605
|
+
const bgOn = `\x1b[${prefix};${codeStr}m`;
|
|
606
|
+
return `${bgOn + line.replace(/\x1b\[0m/g, `\x1b[0m${bgOn}`)}\x1b[0m`;
|
|
607
|
+
};
|
|
608
|
+
const padToWidth = (line) => resolvedW > 0
|
|
609
|
+
? line + ' '.repeat(Math.max(0, resolvedW - visibleWidth(line)))
|
|
610
|
+
: line;
|
|
611
|
+
let targetItems = items, offset = 0, hasMoreAbove = false, hasMoreBelow = false;
|
|
612
|
+
if (options.maxVisible && items.length > options.maxVisible) {
|
|
613
|
+
const win = getWindow(items, options.focusedIndex || 0, options.maxVisible);
|
|
614
|
+
targetItems = win.visible;
|
|
615
|
+
offset = win.start;
|
|
616
|
+
hasMoreAbove = win.hasMoreAbove;
|
|
617
|
+
hasMoreBelow = win.hasMoreBelow;
|
|
618
|
+
}
|
|
619
|
+
const rendered = targetItems
|
|
620
|
+
.map((item, idx) => {
|
|
621
|
+
const actualIdx = offset + idx;
|
|
622
|
+
const line = `${indent}${bullet}${item}`;
|
|
623
|
+
if (options.focusedIndex === actualIdx) {
|
|
624
|
+
const styledLine = options.focusStyle ? options.focusStyle(line) : line;
|
|
625
|
+
if (!options.selectedBg)
|
|
626
|
+
return styledLine;
|
|
627
|
+
return wrapLineBg(padToWidth(styledLine), options.selectedBg);
|
|
628
|
+
}
|
|
629
|
+
// Hover wash (selection wins: checked above).
|
|
630
|
+
if (options.hoveredIndex === actualIdx && options.hoverBg) {
|
|
631
|
+
return wrapLineBg(padToWidth(line), options.hoverBg);
|
|
632
|
+
}
|
|
633
|
+
return line;
|
|
634
|
+
})
|
|
635
|
+
.join('\n');
|
|
636
|
+
let out = rendered;
|
|
637
|
+
if (hasMoreAbove)
|
|
638
|
+
out = `${indent} ↑ more…\n${out}`;
|
|
639
|
+
if (hasMoreBelow)
|
|
640
|
+
out = `${out}\n${indent} ↓ more…`;
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Crops a multi-line string to a width×height window starting at
|
|
645
|
+
* `scrollY` — the primitive for scrollable content.
|
|
646
|
+
*/
|
|
647
|
+
export function viewport(content, width, height, scrollY = 0) {
|
|
648
|
+
const lines = content.split('\n');
|
|
649
|
+
const visibleLines = lines.slice(scrollY, scrollY + height);
|
|
650
|
+
return visibleLines
|
|
651
|
+
.map((line) => {
|
|
652
|
+
return truncate(line, width, '');
|
|
653
|
+
})
|
|
654
|
+
.join('\n');
|
|
655
|
+
}
|
|
656
|
+
/** Fills the whole screen background with a solid color. */
|
|
657
|
+
export function wallpaper(state, options) {
|
|
658
|
+
const { bg } = options;
|
|
659
|
+
rect(state, 0, 0, state.width, state.height, { char: ' ', bg });
|
|
660
|
+
}
|
|
661
|
+
/** Fills the whole screen background with a multi-stop gradient. */
|
|
662
|
+
export function gradient(state, colors, options = {}) {
|
|
663
|
+
const { direction = 'vertical' } = options;
|
|
664
|
+
rect(state, 0, 0, state.width, state.height, {
|
|
665
|
+
char: ' ',
|
|
666
|
+
bg: {
|
|
667
|
+
colors,
|
|
668
|
+
direction,
|
|
669
|
+
steps: direction === 'vertical' ? state.height : state.width,
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
//# sourceMappingURL=layout.js.map
|