archymedes-cli 2.6.0 → 3.1.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.
@@ -1,465 +0,0 @@
1
- import {
2
- padToWidth,
3
- progressBar,
4
- scrollIndicator,
5
- scrollPercent,
6
- sliceToWidth
7
- } from "./archymedes-kn6wtq32.mjs";
8
- import {
9
- clip
10
- } from "./archymedes-twanftqk.mjs";
11
- import {
12
- ASCII_GLYPHS,
13
- UNICODE_GLYPHS,
14
- visibleWidth
15
- } from "./archymedes-s4qcy0q4.mjs";
16
-
17
- // packages/archymedes-cli/src/render/canvas.ts
18
- var BRAILLE_DOTS = [
19
- [1, 2, 4, 64],
20
- [8, 16, 32, 128]
21
- ];
22
- var BRAILLE_BLANK = 10240;
23
-
24
- class BrailleCanvas {
25
- columns;
26
- rows;
27
- cellColumns;
28
- cellRows;
29
- bits;
30
- ascii;
31
- constructor(options) {
32
- this.ascii = (options.glyphs ?? UNICODE_GLYPHS) === ASCII_GLYPHS;
33
- this.columns = Math.max(1, Math.floor(options.columns));
34
- this.rows = Math.max(1, Math.floor(options.rows));
35
- this.cellColumns = this.ascii ? 1 : 2;
36
- this.cellRows = this.ascii ? 1 : 4;
37
- this.bits = Array.from({ length: this.rows }, () => new Array(this.columns).fill(0));
38
- }
39
- get width() {
40
- return this.columns * this.cellColumns;
41
- }
42
- get height() {
43
- return this.rows * this.cellRows;
44
- }
45
- set(x, y) {
46
- const dotX = Math.round(x);
47
- const dotY = Math.round(y);
48
- if (!Number.isFinite(dotX) || !Number.isFinite(dotY))
49
- return;
50
- if (dotX < 0 || dotY < 0 || dotX >= this.width || dotY >= this.height)
51
- return;
52
- const cellX = Math.floor(dotX / this.cellColumns);
53
- const cellY = Math.floor(dotY / this.cellRows);
54
- if (this.ascii)
55
- this.bits[cellY][cellX] = 1;
56
- else
57
- this.bits[cellY][cellX] |= BRAILLE_DOTS[dotX % this.cellColumns][dotY % this.cellRows];
58
- }
59
- line(x0, y0, x1, y1) {
60
- let x = Math.round(x0);
61
- let y = Math.round(y0);
62
- const endX = Math.round(x1);
63
- const endY = Math.round(y1);
64
- if (![x, y, endX, endY].every(Number.isFinite))
65
- return;
66
- const deltaX = Math.abs(endX - x);
67
- const deltaY = -Math.abs(endY - y);
68
- const stepX = x < endX ? 1 : -1;
69
- const stepY = y < endY ? 1 : -1;
70
- let error = deltaX + deltaY;
71
- for (let guard = 0;guard <= this.width + this.height; guard += 1) {
72
- this.set(x, y);
73
- if (x === endX && y === endY)
74
- return;
75
- const doubled = 2 * error;
76
- if (doubled >= deltaY) {
77
- error += deltaY;
78
- x += stepX;
79
- }
80
- if (doubled <= deltaX) {
81
- error += deltaX;
82
- y += stepY;
83
- }
84
- }
85
- }
86
- render() {
87
- return this.bits.map((row) => row.map((bits) => this.ascii ? bits ? "*" : " " : String.fromCharCode(BRAILLE_BLANK + bits)).join(""));
88
- }
89
- }
90
- function dotRowFor(value, low, span, dotRows) {
91
- const normalized = (value - low) / (span === 0 ? 1 : span);
92
- return Math.min(dotRows - 1, Math.max(0, Math.round((1 - normalized) * (dotRows - 1))));
93
- }
94
- function seriesBounds(values) {
95
- const finite = values.filter((value) => Number.isFinite(value));
96
- if (finite.length === 0)
97
- return { low: 0, high: 0, span: 1 };
98
- const low = Math.min(...finite);
99
- const high = Math.max(...finite);
100
- return { low, high, span: high - low === 0 ? 1 : high - low };
101
- }
102
-
103
- // packages/archymedes-cli/src/render/charts.ts
104
- function barChart(data, options) {
105
- if (data.length === 0)
106
- return [];
107
- const glyphs = options.glyphs ?? UNICODE_GLYPHS;
108
- const format = options.format ?? ((value) => `${Math.round(value)}`);
109
- const width = Math.max(20, Math.floor(options.width));
110
- const labels = data.map((datum) => datum.label);
111
- const values = data.map((datum) => format(datum.value));
112
- const labelWidth = Math.min(Math.max(...labels.map(visibleWidth)), Math.floor(width * 0.35));
113
- const valueWidth = Math.min(Math.max(...values.map(visibleWidth)), Math.floor(width * 0.2));
114
- const barWidth = Math.max(1, width - labelWidth - valueWidth - 2);
115
- const ceiling = options.max ?? Math.max(...data.map((datum) => datum.value));
116
- return data.map((datum, index) => {
117
- const fraction = ceiling > 0 ? datum.value / ceiling : 0;
118
- const bar = progressBar(fraction, barWidth, {
119
- depth: options.depth,
120
- glyphs,
121
- ...options.from === undefined ? {} : { from: options.from },
122
- ...options.to === undefined ? {} : { to: options.to }
123
- });
124
- return `${padToWidth(sliceToWidth(labels[index], labelWidth), labelWidth)} ${bar} ${padToWidth(values[index], valueWidth, "right")}`;
125
- });
126
- }
127
- function lineChart(values, options) {
128
- return plotSeries([{ values }], options);
129
- }
130
- function plotSeries(series, options) {
131
- const glyphs = options.glyphs ?? UNICODE_GLYPHS;
132
- const height = Math.max(1, Math.floor(options.height));
133
- const format = options.format ?? ((value) => `${Math.round(value)}`);
134
- const all = series.flatMap((entry) => entry.values.filter((value) => Number.isFinite(value)));
135
- if (all.length === 0)
136
- return Array.from({ length: height }, () => "");
137
- const { low, high, span } = seriesBounds(all);
138
- const gutter = options.bare ? 0 : Math.max(visibleWidth(format(high)), visibleWidth(format(low))) + 1;
139
- const plotWidth = Math.max(1, Math.floor(options.width) - gutter);
140
- const canvas = new BrailleCanvas({ columns: plotWidth, rows: height, glyphs });
141
- for (const entry of series) {
142
- const finite = entry.values.filter((value) => Number.isFinite(value));
143
- if (finite.length === 0)
144
- continue;
145
- let previous;
146
- for (let x = 0;x < canvas.width; x += 1) {
147
- const position = canvas.width === 1 ? 0 : x / (canvas.width - 1) * (finite.length - 1);
148
- const left = Math.floor(position);
149
- const right = Math.min(finite.length - 1, left + 1);
150
- const value = finite[left] + (finite[right] - finite[left]) * (position - left);
151
- const y = dotRowFor(value, low, span, canvas.height);
152
- if (previous)
153
- canvas.line(previous.x, previous.y, x, y);
154
- else
155
- canvas.set(x, y);
156
- previous = { x, y };
157
- }
158
- }
159
- return canvas.render().map((drawn, index) => {
160
- if (options.bare)
161
- return drawn;
162
- const label = index === 0 ? format(high) : index === height - 1 ? format(low) : "";
163
- return `${padToWidth(label, gutter - 1)} ${drawn}`;
164
- });
165
- }
166
- function scatterChart(points, options) {
167
- const glyphs = options.glyphs ?? UNICODE_GLYPHS;
168
- const height = Math.max(1, Math.floor(options.height));
169
- const format = options.format ?? ((value) => `${Math.round(value)}`);
170
- const finite = points.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
171
- if (finite.length === 0)
172
- return Array.from({ length: height }, () => "");
173
- const vertical = seriesBounds(finite.map((point) => point.y));
174
- const horizontal = seriesBounds(finite.map((point) => point.x));
175
- const gutter = options.bare ? 0 : Math.max(visibleWidth(format(vertical.high)), visibleWidth(format(vertical.low))) + 1;
176
- const plotWidth = Math.max(1, Math.floor(options.width) - gutter);
177
- const canvas = new BrailleCanvas({ columns: plotWidth, rows: height, glyphs });
178
- for (const point of finite) {
179
- const x = Math.round((point.x - horizontal.low) / horizontal.span * (canvas.width - 1));
180
- canvas.set(x, dotRowFor(point.y, vertical.low, vertical.span, canvas.height));
181
- }
182
- return canvas.render().map((drawn, index) => {
183
- if (options.bare)
184
- return drawn;
185
- const label = index === 0 ? format(vertical.high) : index === height - 1 ? format(vertical.low) : "";
186
- return `${padToWidth(label, gutter - 1)} ${drawn}`;
187
- });
188
- }
189
- function waveLine(values, options) {
190
- const glyphs = options.glyphs ?? UNICODE_GLYPHS;
191
- const width = Math.max(1, Math.floor(options.width));
192
- const finite = values.filter((value) => Number.isFinite(value));
193
- if (finite.length === 0)
194
- return "";
195
- const { low, span } = seriesBounds(finite);
196
- const ascii = glyphs === ASCII_GLYPHS;
197
- const level = ascii ? ["_", "-", "^"] : ["▁", "─", "▔"];
198
- const rising = ascii ? "/" : "╱";
199
- const falling = ascii ? "\\" : "╲";
200
- const sampled = Array.from({ length: width }, (_, column) => {
201
- const position = width === 1 ? 0 : column / (width - 1) * (finite.length - 1);
202
- const left = Math.floor(position);
203
- const right = Math.min(finite.length - 1, left + 1);
204
- return finite[left] + (finite[right] - finite[left]) * (position - left);
205
- });
206
- const bandOf = (value) => Math.min(2, Math.max(0, Math.round((value - low) / span * 2)));
207
- return sampled.map((value, column) => {
208
- const band = bandOf(value);
209
- if (column === 0)
210
- return level[band];
211
- const previous = bandOf(sampled[column - 1]);
212
- if (band > previous)
213
- return rising;
214
- if (band < previous)
215
- return falling;
216
- return level[band];
217
- }).join("");
218
- }
219
-
220
- class StreamSeries {
221
- capacity;
222
- samples = [];
223
- constructor(capacity) {
224
- this.capacity = capacity;
225
- if (!Number.isInteger(capacity) || capacity < 1)
226
- throw new Error("A stream needs room for at least one sample");
227
- }
228
- push(value) {
229
- if (!Number.isFinite(value))
230
- return;
231
- this.samples.push(value);
232
- if (this.samples.length > this.capacity)
233
- this.samples.splice(0, this.samples.length - this.capacity);
234
- }
235
- values() {
236
- return [...this.samples];
237
- }
238
- get length() {
239
- return this.samples.length;
240
- }
241
- clear() {
242
- this.samples.length = 0;
243
- }
244
- }
245
- function heatStrip(data, options) {
246
- if (data.length === 0)
247
- return [];
248
- const glyphs = options.glyphs ?? UNICODE_GLYPHS;
249
- const levels = glyphs.sparkLevels;
250
- const width = Math.max(10, Math.floor(options.width));
251
- const labelWidth = Math.min(Math.max(...data.map((datum) => visibleWidth(datum.label))), Math.floor(width * 0.4));
252
- const cells = Math.max(1, width - labelWidth - 1);
253
- const ceiling = options.max ?? Math.max(...data.map((datum) => datum.value));
254
- return data.map((datum) => {
255
- const fraction = ceiling > 0 ? Math.max(0, Math.min(1, datum.value / ceiling)) : 0;
256
- const level = levels[Math.min(levels.length - 1, Math.floor(fraction * levels.length))];
257
- return `${padToWidth(sliceToWidth(datum.label, labelWidth), labelWidth)} ${level.repeat(cells)}`;
258
- });
259
- }
260
-
261
- // packages/archymedes-cli/src/ui/workspace-model.ts
262
- function paneHeight(rows) {
263
- return Math.max(1, rows - 4);
264
- }
265
- function selectedPane(snapshot) {
266
- if (snapshot.panes.length === 0)
267
- return;
268
- const index = Math.max(0, Math.min(snapshot.selected, snapshot.panes.length - 1));
269
- return snapshot.panes[index];
270
- }
271
- function visibleLines(pane, height, scroll) {
272
- const clamped = Math.max(0, Math.min(scroll, Math.max(0, pane.lines.length - height)));
273
- const end = pane.lines.length - clamped;
274
- return pane.lines.slice(Math.max(0, end - height), end);
275
- }
276
- function atLiveEdge(pane, height, scroll) {
277
- return Math.min(scroll, Math.max(0, pane.lines.length - height)) === 0;
278
- }
279
- function keyToAction(key, character) {
280
- const name = key.name ?? "";
281
- const isEscape = name === "escape" || name === "\x1B";
282
- const isEnter = name === "return" || name === "enter" || name === "\r" || name === `
283
- `;
284
- if (isEscape || name === "q" && !key.ctrl || key.ctrl && name === "c")
285
- return { kind: "exit" };
286
- if (isEnter)
287
- return { kind: "focus" };
288
- if (name === "tab")
289
- return { kind: "cycle", step: key.shift ? -1 : 1 };
290
- if (name === "left")
291
- return { kind: "cycle", step: -1 };
292
- if (name === "right")
293
- return { kind: "cycle", step: 1 };
294
- if (name === "up" || name === "k")
295
- return { kind: "scroll", lines: 1 };
296
- if (name === "down" || name === "j")
297
- return { kind: "scroll", lines: -1 };
298
- if (name === "pageup")
299
- return { kind: "scroll", lines: 10 };
300
- if (name === "pagedown")
301
- return { kind: "scroll", lines: -10 };
302
- if (name === "home" || character === "g")
303
- return { kind: "top" };
304
- if (name === "end" || character === "G")
305
- return { kind: "bottom" };
306
- const digit = character && /^[1-9]$/.test(character) ? Number(character) : undefined;
307
- if (digit !== undefined)
308
- return { kind: "select", index: digit - 1 };
309
- return { kind: "none" };
310
- }
311
- function applyAction(snapshot, action) {
312
- const count = snapshot.panes.length;
313
- if (count === 0 || action.kind === "none" || action.kind === "exit" || action.kind === "focus")
314
- return snapshot;
315
- if (action.kind === "select") {
316
- if (action.index < 0 || action.index >= count)
317
- return snapshot;
318
- return { ...snapshot, selected: action.index, scroll: 0 };
319
- }
320
- if (action.kind === "cycle") {
321
- const next = ((snapshot.selected + action.step) % count + count) % count;
322
- return { ...snapshot, selected: next, scroll: 0 };
323
- }
324
- const pane = selectedPane(snapshot);
325
- const height = paneHeight(snapshot.rows);
326
- const furthest = Math.max(0, (pane?.lines.length ?? 0) - height);
327
- if (action.kind === "bottom")
328
- return { ...snapshot, scroll: 0 };
329
- if (action.kind === "top")
330
- return { ...snapshot, scroll: furthest };
331
- return { ...snapshot, scroll: Math.max(0, Math.min(snapshot.scroll + action.lines, furthest)) };
332
- }
333
- function paneTabs(snapshot) {
334
- const selected = Math.max(0, Math.min(snapshot.selected, snapshot.panes.length - 1));
335
- return snapshot.panes.map((pane, index) => ({
336
- label: `${index + 1} ${pane.title}`,
337
- active: index === selected,
338
- status: pane.status
339
- }));
340
- }
341
-
342
- class PaneActivity {
343
- capacity;
344
- series = new Map;
345
- seen = new Map;
346
- constructor(capacity = 48) {
347
- this.capacity = capacity;
348
- }
349
- sample(panes) {
350
- const live = new Set(panes.map((pane) => pane.key));
351
- for (const key of [...this.series.keys()]) {
352
- if (!live.has(key)) {
353
- this.series.delete(key);
354
- this.seen.delete(key);
355
- }
356
- }
357
- const result = new Map;
358
- for (const pane of panes) {
359
- const produced = pane.lines.length + pane.dropped;
360
- const previous = this.seen.get(pane.key);
361
- this.seen.set(pane.key, produced);
362
- const stream = this.series.get(pane.key) ?? new StreamSeries(this.capacity);
363
- this.series.set(pane.key, stream);
364
- if (previous !== undefined)
365
- stream.push(Math.max(0, produced - previous));
366
- result.set(pane.key, stream.values());
367
- }
368
- return result;
369
- }
370
- }
371
- function tabPanes(views, linesFor) {
372
- return views.map((view) => {
373
- const record = linesFor(view.id);
374
- const where = view.backend && view.backend !== "local" ? ` · ${view.backend}` : "";
375
- return {
376
- kind: "tab",
377
- key: `${view.id}`,
378
- title: view.title,
379
- subtitle: `${view.model ?? "model unknown"}${where}${view.cost ? ` · ${view.cost}` : ""}`,
380
- status: view.status,
381
- lines: record.lines,
382
- dropped: record.dropped
383
- };
384
- });
385
- }
386
- var STATUS_MARK = {
387
- running: "●",
388
- failed: "✕",
389
- done: "✓",
390
- idle: " "
391
- };
392
- var WORKSPACE_LEGEND = "1-9 pane · ←→ move · ↑↓ scroll · g/G ends · Esc back";
393
- function paneBar(snapshot, columns) {
394
- const tabs = paneTabs(snapshot);
395
- const cells = tabs.map((tab) => ` ${STATUS_MARK[tab.status] ?? " "}${tab.label} `);
396
- const full = cells.join("");
397
- if (full.length <= columns)
398
- return full || " no panes ";
399
- const selected = Math.max(0, Math.min(snapshot.selected, tabs.length - 1));
400
- return `${selected > 0 ? "‹ " : ""}${cells[selected] ?? " no panes "}${selected < tabs.length - 1 ? " ›" : ""}`;
401
- }
402
- function composeFrame(snapshot) {
403
- const rows = [];
404
- const columns = Math.max(1, Math.floor(snapshot.columns));
405
- const rowCount = Math.max(1, Math.floor(snapshot.rows));
406
- const pane = selectedPane(snapshot);
407
- const height = paneHeight(rowCount);
408
- const theme = snapshot.palette.tokens;
409
- const bar = paneBar(snapshot, columns);
410
- rows.push({ text: bar || " no panes ", bold: true, color: theme.primary });
411
- const scrolled = pane && !atLiveEdge(pane, height, snapshot.scroll);
412
- const position = (() => {
413
- if (!pane || !scrolled)
414
- return "";
415
- const maxOffset = Math.max(0, pane.lines.length - height);
416
- const offsetFromTop = maxOffset - Math.min(snapshot.scroll, maxOffset);
417
- return scrollIndicator(scrollPercent(offsetFromTop, pane.lines.length, height));
418
- })();
419
- const dropped = pane && pane.dropped > 0 ? ` (${pane.dropped} earlier lines dropped)` : "";
420
- const header = pane ? `${pane.title} ${pane.subtitle}${dropped}` : "nothing open";
421
- const activityWidth = Math.min(24, Math.max(0, columns - visibleWidth(header) - 3));
422
- const activity = pane?.activity && pane.activity.length > 1 && activityWidth >= 8 ? ` ${waveLine(pane.activity, { width: activityWidth })}` : "";
423
- rows.push({
424
- text: pane ? `${header}${activity}` : header,
425
- bold: true,
426
- color: pane?.status === "failed" ? theme.error : pane?.status === "running" ? theme.success : theme.accent
427
- });
428
- const body = pane ? visibleLines(pane, height, snapshot.scroll) : [];
429
- for (let index = 0;index < height; index += 1)
430
- rows.push({ text: body[index] ?? "" });
431
- while (rows.length < rowCount - 1)
432
- rows.push({ text: "" });
433
- rows.length = Math.max(0, rowCount - 1);
434
- rows.push({
435
- text: `${WORKSPACE_LEGEND}${scrolled ? ` ▲ scrolled back ${position}` : ""}`,
436
- dim: true,
437
- color: theme.textMuted
438
- });
439
- return rows.map((row) => ({ ...row, text: clip(row.text, columns) }));
440
- }
441
- function buildWorkspaceSnapshot(input) {
442
- const panes = [
443
- ...tabPanes(input.views, input.linesFor),
444
- ...input.jobs.map((job) => ({
445
- kind: "job",
446
- key: job.id,
447
- title: `job ${job.id.slice(-6)}`,
448
- subtitle: job.objective,
449
- status: job.done ? "done" : "running",
450
- lines: job.lines,
451
- dropped: job.dropped
452
- }))
453
- ];
454
- const activity = input.activity.sample(panes);
455
- return {
456
- panes: panes.map((pane) => ({ ...pane, activity: activity.get(pane.key) })),
457
- selected: Math.max(0, input.views.findIndex((view) => view.active)),
458
- scroll: 0,
459
- palette: input.palette,
460
- columns: input.columns,
461
- rows: input.rows
462
- };
463
- }
464
-
465
- export { barChart, lineChart, plotSeries, scatterChart, waveLine, heatStrip, keyToAction, applyAction, PaneActivity, composeFrame, buildWorkspaceSnapshot };
@@ -1,144 +0,0 @@
1
- import {
2
- BOLD,
3
- DIM,
4
- UNICODE_GLYPHS,
5
- borderGlyphsFor,
6
- paint,
7
- paintAll,
8
- roleCode,
9
- visibleWidth
10
- } from "./archymedes-s4qcy0q4.mjs";
11
-
12
- // packages/archymedes-cli/src/render/sections.ts
13
- var TONE_ROLE = {
14
- good: "success",
15
- bad: "error",
16
- warn: "warning",
17
- accent: "primary"
18
- };
19
- function toneCode(tone, style) {
20
- if (style.depth === "none")
21
- return "";
22
- return tone === "neutral" ? DIM : roleCode(TONE_ROLE[tone], style.palette, style.depth);
23
- }
24
- var GUTTER = " ";
25
- function setOf(style) {
26
- return style.glyphs ?? UNICODE_GLYPHS;
27
- }
28
- function rule(style, options = {}) {
29
- const glyphs = setOf(style);
30
- const tone = options.tone ?? "neutral";
31
- const width = Math.max(4, style.width - GUTTER.length);
32
- const trailing = options.trailing ? clip(` ${options.trailing}`, Math.max(4, Math.floor(width / 2)), glyphs) : "";
33
- const trailingWidth = visibleWidth(trailing);
34
- if (!options.label) {
35
- const bare = Math.max(0, width - trailingWidth);
36
- return `${GUTTER}${paint(glyphs.boxHorizontal.repeat(bare), DIM, style.depth)}${trailing ? paint(trailing, DIM, style.depth) : ""}`;
37
- }
38
- const lead = glyphs.boxHorizontal.repeat(2);
39
- const label = clip(` ${options.label} `, Math.max(0, width - visibleWidth(lead) - trailingWidth), glyphs);
40
- const used = visibleWidth(lead) + visibleWidth(label) + trailingWidth;
41
- const fill = Math.max(0, width - used);
42
- return [
43
- GUTTER,
44
- paint(lead, DIM, style.depth),
45
- label ? paintAll(label, [toneCode(tone, style), BOLD], style.depth) : "",
46
- paint(glyphs.boxHorizontal.repeat(fill), DIM, style.depth),
47
- trailing ? paint(trailing, DIM, style.depth) : ""
48
- ].join("");
49
- }
50
- function heading(text, level, style, tone = "accent") {
51
- if (level === 1) {
52
- return [
53
- `${GUTTER}${paintAll(text, [toneCode(tone, style), BOLD], style.depth)}`,
54
- rule(style, { tone })
55
- ].join(`
56
- `);
57
- }
58
- if (level === 2)
59
- return `${GUTTER}${paintAll(text, [toneCode(tone, style), BOLD], style.depth)}`;
60
- return `${GUTTER}${paint(text, DIM, style.depth)}`;
61
- }
62
- function note(text, style, tone = "neutral") {
63
- return `${GUTTER}${GUTTER}${paint(text, toneCode(tone, style), style.depth)}`;
64
- }
65
- function panel(lines, style, options = {}) {
66
- const glyphs = setOf(style);
67
- const depth = style.depth;
68
- const tone = options.tone ?? "neutral";
69
- const available = Math.max(8, style.width - GUTTER.length - 4);
70
- const border = borderGlyphsFor(style.palette?.borderStyle ?? "round", glyphs);
71
- if (options.gutterOnly) {
72
- const edge2 = paint(border.vertical, toneCode(tone, style), depth);
73
- const head = options.title ? [`${GUTTER}${paintAll(options.title, [toneCode(tone, style), BOLD], depth)}${options.badge ? paint(` ${options.badge}`, DIM, depth) : ""}`] : [];
74
- return [...head, ...lines.map((line) => `${GUTTER}${edge2} ${clip(line, available, glyphs)}`)].join(`
75
- `);
76
- }
77
- const horizontal = border.horizontal;
78
- const titleCell = options.title ? ` ${options.title} ` : "";
79
- const badgeCell = options.badge ? ` ${options.badge} ` : "";
80
- const contentWidth = Math.min(Math.max(visibleWidth(titleCell) + visibleWidth(badgeCell) - 1, ...lines.map((line) => visibleWidth(line)), 8), available);
81
- const titleShown = titleCell ? clip(titleCell, Math.max(0, contentWidth + 1 - visibleWidth(badgeCell)), glyphs) : "";
82
- const fill = Math.max(0, contentWidth + 1 - visibleWidth(titleShown) - visibleWidth(badgeCell));
83
- const top = [
84
- GUTTER,
85
- paint(border.topLeft + horizontal, DIM, depth),
86
- titleShown ? paintAll(titleShown, [toneCode(tone, style), BOLD], depth) : "",
87
- paint(horizontal.repeat(fill), DIM, depth),
88
- badgeCell ? paint(badgeCell, DIM, depth) : "",
89
- paint(border.topRight, DIM, depth)
90
- ].join("");
91
- const bottom = `${GUTTER}${paint(border.bottomLeft + horizontal.repeat(contentWidth + 2) + border.bottomRight, DIM, depth)}`;
92
- const edge = paint(border.vertical, DIM, depth);
93
- const body = lines.map((line) => {
94
- const clipped = clip(line, contentWidth, glyphs);
95
- return `${GUTTER}${edge} ${clipped}${" ".repeat(Math.max(0, contentWidth - visibleWidth(clipped)))} ${edge}`;
96
- });
97
- return [top, ...body, bottom].join(`
98
- `);
99
- }
100
- function clip(text, width, glyphs = UNICODE_GLYPHS) {
101
- if (visibleWidth(text) <= width)
102
- return text;
103
- const marker = glyphs.ellipsis;
104
- const budget = Math.max(0, width - visibleWidth(marker));
105
- let taken = "";
106
- let used = 0;
107
- const parts = text.split(/(\x1b\[[0-9;]*m)/);
108
- let styled = false;
109
- for (const part of parts) {
110
- if (part.startsWith("\x1B[")) {
111
- taken += part;
112
- styled = styled || part !== "\x1B[0m";
113
- continue;
114
- }
115
- for (const character of part) {
116
- const next = visibleWidth(character);
117
- if (used + next > budget)
118
- return `${taken}${marker}${styled ? "\x1B[0m" : ""}`;
119
- taken += character;
120
- used += next;
121
- }
122
- }
123
- return `${taken}${marker}${styled ? "\x1B[0m" : ""}`;
124
- }
125
- function outcomeMark(outcome, style) {
126
- const glyphs = setOf(style);
127
- switch (outcome) {
128
- case "pass":
129
- return paint(glyphs.check, toneCode("good", style), style.depth);
130
- case "fail":
131
- return paint(glyphs.cross, toneCode("bad", style), style.depth);
132
- case "skip":
133
- return paint(glyphs.circleEmpty, DIM, style.depth);
134
- default:
135
- return paint(glyphs.pending, toneCode("accent", style), style.depth);
136
- }
137
- }
138
- function keyValues(rows, style) {
139
- const width = Math.max(0, ...rows.map(([key]) => visibleWidth(key)));
140
- return rows.map(([key, value]) => `${GUTTER}${paint(key.padEnd(width + 2), DIM, style.depth)}${value}`).join(`
141
- `);
142
- }
143
-
144
- export { toneCode, GUTTER, rule, heading, note, panel, clip, outcomeMark, keyValues };