@vincemakes/kiso-tui-cells 0.16.2 → 0.16.4
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/dist/approval-panel.d.ts +4 -3
- package/dist/approval-panel.js +136 -39
- package/dist/components.d.ts +21 -8
- package/dist/components.js +144 -48
- package/dist/ground.d.ts +46 -0
- package/dist/ground.js +76 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/md.d.ts +1 -1
- package/dist/md.js +73 -31
- package/dist/render.d.ts +47 -11
- package/dist/render.js +175 -20
- package/dist/strings.js +72 -28
- package/package.json +2 -2
package/dist/components.js
CHANGED
|
@@ -171,12 +171,17 @@ export function cellComponent(cell) {
|
|
|
171
171
|
* the ▍ rail and the indent are retired — the rail's stated pipe
|
|
172
172
|
* fallback was theoretical redundancy: the CLI's pipe path is the
|
|
173
173
|
* line-mode "you>" form and never renders UserMessage). The chip folds
|
|
174
|
-
* the text at W−2 (the side pads)
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
174
|
+
* the text at W−2 (the side pads) and pads every row to the FULL width.
|
|
175
|
+
*
|
|
176
|
+
* R2 — law 1.6's recorded reversal. This used to size the block to its
|
|
177
|
+
* longest row, on the argument that "a short message like /think would
|
|
178
|
+
* paint a bar across the terminal". That optimises the degenerate case
|
|
179
|
+
* at the cost of every real message, which is a paragraph and reads as
|
|
180
|
+
* a block only when the block has an edge. The `/think` case is the
|
|
181
|
+
* accepted price, and it is recorded as such in design.md §1.6.
|
|
182
|
+
*
|
|
183
|
+
* The padding is by cells (charWidth is the width authority), so a CJK
|
|
184
|
+
* row pads by width, never by chars, and the chip never overruns.
|
|
180
185
|
* SGR 7 closed with SGR 27 — never SGR 0, the chip composes with a
|
|
181
186
|
* surrounding span — and NEVER dim: reverse video inverts the CURRENT
|
|
182
187
|
* colours, so dimmed text would invert into a dimmed block with no
|
|
@@ -206,22 +211,39 @@ class UserMessage {
|
|
|
206
211
|
// was a 3000-line turn writing 260,298 bytes in one frame.
|
|
207
212
|
const paras = this.cell.text.split("\n");
|
|
208
213
|
let truncated = false;
|
|
214
|
+
// DC-6: the folded CONTENT first, then ONE width over all of it.
|
|
215
|
+
// The pad used to be computed per source paragraph, so a message
|
|
216
|
+
// with two lines drew as two bars of two different lengths — a
|
|
217
|
+
// ragged right edge on a block that is one block. A single
|
|
218
|
+
// paragraph was always correct, which is why it survived: the
|
|
219
|
+
// shape only appears once a message has a second line.
|
|
220
|
+
const content = [];
|
|
209
221
|
for (const para of paras) {
|
|
210
|
-
if (
|
|
222
|
+
if (content.length >= USER_CHIP_ROWS) {
|
|
211
223
|
truncated = true;
|
|
212
224
|
break;
|
|
213
225
|
}
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
for (const row of folded) {
|
|
217
|
-
if (rows.length >= USER_CHIP_ROWS) {
|
|
226
|
+
for (const row of foldLine(escapeTerminal(para), chipW)) {
|
|
227
|
+
if (content.length >= USER_CHIP_ROWS) {
|
|
218
228
|
truncated = true;
|
|
219
229
|
break;
|
|
220
230
|
}
|
|
221
|
-
|
|
222
|
-
rows.push(`${p.rv} ${row}${" ".repeat(pad)} ${p.rvEnd}`);
|
|
231
|
+
content.push(row);
|
|
223
232
|
}
|
|
224
233
|
}
|
|
234
|
+
// R2 (law 1.6's recorded reversal): the band is FULL WIDTH. It was
|
|
235
|
+
// sized to its longest row, on the argument that a one-word turn
|
|
236
|
+
// like `/think` would otherwise paint a bar across the terminal —
|
|
237
|
+
// which optimises the degenerate case at the cost of every real
|
|
238
|
+
// message. The human's words are the one surface that gets the
|
|
239
|
+
// whole row.
|
|
240
|
+
//
|
|
241
|
+
// displayWidth stays the padding authority (never `length`): a CJK
|
|
242
|
+
// row is two cells per character and pads by cells.
|
|
243
|
+
const inner = chipW;
|
|
244
|
+
for (const row of content) {
|
|
245
|
+
rows.push(`${p.rv} ${row}${" ".repeat(Math.max(0, inner - displayWidth(row)))} ${p.rvEnd}`);
|
|
246
|
+
}
|
|
225
247
|
if (!truncated)
|
|
226
248
|
return rows;
|
|
227
249
|
// The notice is OUTSIDE the chip's reverse video, in the cut-row
|
|
@@ -265,19 +287,57 @@ class ThinkingFold {
|
|
|
265
287
|
this.cell = cell;
|
|
266
288
|
}
|
|
267
289
|
render(W, _ctx) {
|
|
290
|
+
const p = palette();
|
|
268
291
|
const block = this.cell.text;
|
|
269
292
|
const trimmed = escapeTerminal(block.trim());
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
293
|
+
// R2 (owner, 2026-08-27) — three changes, each independent.
|
|
294
|
+
//
|
|
295
|
+
// ITALIC marks the row as not-the-answer without spending a colour,
|
|
296
|
+
// on the same argument that admitted italic to the alphabet.
|
|
297
|
+
//
|
|
298
|
+
// The cut lands on a WORD. It used to cut on a byte, so the fold
|
|
299
|
+
// read as `…the user's h` and the reader's eye had to reassemble a
|
|
300
|
+
// word it already knew.
|
|
301
|
+
//
|
|
302
|
+
// The affordance moves to the RIGHT EDGE, so the left edge of every
|
|
303
|
+
// row on screen is content. The char count goes with the move: it
|
|
304
|
+
// told the reader nothing they could act on, and the row it was
|
|
305
|
+
// crowding is the one thing this cell says.
|
|
306
|
+
//
|
|
307
|
+
// The ≤100 short-circuit stays width-aware: a short block at a
|
|
308
|
+
// narrow width returned the line UNFOLDED and tripped invariant ①.
|
|
309
|
+
// DC-15: the affordance is DROPPED, not squeezed. `room` floored at
|
|
310
|
+
// 1 while `pad` floored at 1 independently, so a narrow terminal
|
|
311
|
+
// produced 2 + cut + pad + 6 = 11 cells no matter what W was — and
|
|
312
|
+
// invariant ① does not truncate, it THROWS. Measured 11 cells at
|
|
313
|
+
// every W ≤ 10. Below the width where the tail and one word of
|
|
314
|
+
// content can both stand, the row is the CONTENT: a cell that
|
|
315
|
+
// cannot hold the key's name has nothing to say about the key.
|
|
316
|
+
const tail = "/think";
|
|
317
|
+
const room = W - 2 - tail.length - 1;
|
|
318
|
+
// the belt: cutLine is SGR-aware and single-row, so the invariant
|
|
319
|
+
// holds by CONSTRUCTION at every width rather than by arithmetic
|
|
320
|
+
// that has to be re-proved every time a span moves.
|
|
321
|
+
if (room < 2)
|
|
322
|
+
return [cutLine(`${p.dim}⋯ ${p.italic}${wordCut(trimmed, Math.max(1, W - 2))}${p.italicEnd}${p.reset}`, W)];
|
|
323
|
+
const cut = wordCut(trimmed, room);
|
|
324
|
+
const body = `⋯ ${p.italic}${cut}${p.italicEnd}`;
|
|
325
|
+
const pad = Math.max(1, W - 2 - visibleWidth(cut) - tail.length);
|
|
326
|
+
return [cutLine(`${p.dim}${body}${" ".repeat(pad)}${tail}${p.reset}`, W)];
|
|
279
327
|
}
|
|
280
328
|
}
|
|
329
|
+
/** R2 — cut at a word boundary, with the honest ellipsis. widthCut cuts
|
|
330
|
+
* at a cell, which is right for verbatim output and wrong for prose:
|
|
331
|
+
* the reader has to reassemble "h" into "home". Falls back to the cell
|
|
332
|
+
* cut when the first word alone overruns, because a row that cannot
|
|
333
|
+
* hold one word has no boundary to find. */
|
|
334
|
+
function wordCut(text, room) {
|
|
335
|
+
if (visibleWidth(text) <= room)
|
|
336
|
+
return text;
|
|
337
|
+
const hard = widthCut(text, Math.max(1, room - 1));
|
|
338
|
+
const at = hard.lastIndexOf(" ");
|
|
339
|
+
return `${at > room / 3 ? hard.slice(0, at) : hard}\u2026`;
|
|
340
|
+
}
|
|
281
341
|
/** Fold a line's CONTENT at W−2 and prefix EVERY row with the gutter
|
|
282
342
|
* (W2: a wrapped tool row keeps its state mark — the left edge alone
|
|
283
343
|
* distinguishes the states at --plain; the UserMessage rail precedent,
|
|
@@ -498,7 +558,7 @@ class ToolExecution {
|
|
|
498
558
|
// list of calls, not a body of output.
|
|
499
559
|
const r = c.rolled;
|
|
500
560
|
const counts = exploreCounts(parts);
|
|
501
|
-
const head =
|
|
561
|
+
const head = ` explored ${p.bold}${counts}${p.reset}`; // R2: no tick
|
|
502
562
|
const tail = ` (${r.elapsed}s)`;
|
|
503
563
|
const room = W - visibleWidth(head) - tail.length;
|
|
504
564
|
const affordance = " · ctrl+r lists them";
|
|
@@ -513,7 +573,7 @@ class ToolExecution {
|
|
|
513
573
|
// W15 expand history — the head's commit captures it).
|
|
514
574
|
const r = c.rolled;
|
|
515
575
|
const noun = ROLLUP_NOUN[c.name] ?? "calls";
|
|
516
|
-
const out = gutterCut(
|
|
576
|
+
const out = gutterCut(" ", `${verbCol} ${r.count} ${noun} (${kUnit(r.lines)} lines, ${r.elapsed}s)`, W); // R2: no tick
|
|
517
577
|
const shown = r.targets.slice(0, 3);
|
|
518
578
|
if (shown.length > 0)
|
|
519
579
|
out.push(` ${p.dim}${CUT_ROW}${escapeTerminal(shown.join(" · "))}${p.reset}`);
|
|
@@ -531,7 +591,7 @@ class ToolExecution {
|
|
|
531
591
|
// names the decider; a human denial (no decidedBy) has no tail.
|
|
532
592
|
if (c.reason !== null) {
|
|
533
593
|
const by = attribution(c);
|
|
534
|
-
const out = gutterCut(
|
|
594
|
+
const out = gutterCut(" ", `${p.red}${escapeTerminal(`${c.name} ${toolTargetOf(c)}`)} (${escapeTerminal(c.reason)}${by})${p.reset}`, W);
|
|
535
595
|
out.push(...toolBlockBody(c, W));
|
|
536
596
|
return out;
|
|
537
597
|
}
|
|
@@ -568,7 +628,14 @@ class ToolExecution {
|
|
|
568
628
|
// the semantics. TUI2-R1.5 pin 4: and the parts give way in a
|
|
569
629
|
// PINNED ORDER, rather than whichever happened to be last.
|
|
570
630
|
const text = settledHeadText(verbCol, escapeTerminal(toolTargetOf(c)), meta, approvedBy, elapsed, W - 2 - (hidden === null ? 0 : SUFFIX_MIN));
|
|
571
|
-
|
|
631
|
+
// R2 (owner, 2026-08-27): no tick, no cross. A symbol earns its
|
|
632
|
+
// cell by carrying a fact the words do not, and a row that
|
|
633
|
+
// already says `exit 0` does not need one more thing saying it
|
|
634
|
+
// went fine. The gutter is two spaces; the OUTCOME lives in the
|
|
635
|
+
// metadata, in words, which is also the only form that survives
|
|
636
|
+
// a pipe with the colour stripped. A failure keeps its colour
|
|
637
|
+
// AND its words — see settledMeta.
|
|
638
|
+
const out = c.isError ? [` ${p.red}${text}${p.reset}`] : [` ${text}`];
|
|
572
639
|
out[0] = appendSuffix(out[0], expandSuffix(hidden, W - visibleWidth(out[0])));
|
|
573
640
|
out.push(...toolBlockBody(c, W));
|
|
574
641
|
return out;
|
|
@@ -758,7 +825,7 @@ function appendSuffix(row, suffix) {
|
|
|
758
825
|
* would put the invariant "exactly one bright token" in as many hands as
|
|
759
826
|
* there are emitters. Here it has exactly one.
|
|
760
827
|
*
|
|
761
|
-
* NO_COLOR: p.
|
|
828
|
+
* NO_COLOR: p.dim is empty, so the row's bytes are untouched.
|
|
762
829
|
*/
|
|
763
830
|
export function focusToken(row, W) {
|
|
764
831
|
const p = palette();
|
|
@@ -766,9 +833,17 @@ export function focusToken(row, W) {
|
|
|
766
833
|
if (at !== -1) {
|
|
767
834
|
// the row already names the key — brighten the token in place, and
|
|
768
835
|
// leave every other span exactly as it was
|
|
769
|
-
if (p.
|
|
836
|
+
if (p.dim === "")
|
|
770
837
|
return row;
|
|
771
|
-
|
|
838
|
+
// DC-3: the marker takes the WASH. It used to take the inline-code
|
|
839
|
+
// tint and inherited its 1.54:1 — the cue naming the key that
|
|
840
|
+
// reveals a cell was itself the least readable thing on a white
|
|
841
|
+
// terminal. The wash is right for a second reason: this token has
|
|
842
|
+
// to be UNIQUE on the frame ("exactly one bright token"), and an
|
|
843
|
+
// attribute like bold is spent everywhere. It closes with washEnd
|
|
844
|
+
// rather than a reset, so the surrounding dim survives instead of
|
|
845
|
+
// having to be re-applied.
|
|
846
|
+
return `${row.slice(0, at)}${p.wash}${CTRL_R}${p.washEnd}${row.slice(at + CTRL_R.length)}`;
|
|
772
847
|
}
|
|
773
848
|
// A LIVE row does not carry the affordance today, and the live cell is
|
|
774
849
|
// the one ctrl+r takes FIRST (expandNext scans the live tail before
|
|
@@ -780,7 +855,7 @@ export function focusToken(row, W) {
|
|
|
780
855
|
const room = W - visibleWidth(row);
|
|
781
856
|
if (room < SUFFIX_MIN)
|
|
782
857
|
return row; // never at the cost of invariant ①
|
|
783
|
-
return `${row}${p.dim} · ${p.reset}${p.
|
|
858
|
+
return `${row}${p.dim} · ${p.reset}${p.wash}${CTRL_R}${p.washEnd}`;
|
|
784
859
|
}
|
|
785
860
|
const CTRL_R = "ctrl+r";
|
|
786
861
|
/** TUI2-R1 (A) — the expanded block's last row: the way back. The
|
|
@@ -894,26 +969,26 @@ export function turnFold(t, W) {
|
|
|
894
969
|
const meta = parts.join(" · ");
|
|
895
970
|
const words = escapeTerminal(t.words);
|
|
896
971
|
if (words === "") {
|
|
897
|
-
const row = `${p.bold}
|
|
898
|
-
return visibleWidth(row) <= W ? [row] : [`${p.bold}
|
|
972
|
+
const row = `${p.bold}✦${p.reset} ${meta}`;
|
|
973
|
+
return visibleWidth(row) <= W ? [row] : [`${p.bold}✦${p.reset} ${widthCut(meta, Math.max(1, W - 3))}…`]; // a wordless turn folds to the W14 shape
|
|
899
974
|
}
|
|
900
975
|
// A9 (ruling R2, mock A): the user chip rides the fold — the human's
|
|
901
976
|
// words LEAD the one line, the same SGR-7 bracket as the live user
|
|
902
977
|
// row (#16f, side pads included). The words take the fold's width
|
|
903
|
-
// budget: W − the gutter ("
|
|
978
|
+
// budget: W − the gutter ("✦ " = 2) − the join (" · " = 3) − the
|
|
904
979
|
// chip's side pads (2) − the cut-tail reserve (1, the "…") − the
|
|
905
980
|
// metadata's own width — the metadata survives, the words width-cut
|
|
906
981
|
// at the end with the honest "…" (the "…" alone is the honest floor:
|
|
907
982
|
// the words were there, cut).
|
|
908
|
-
const budget = Math.max(0, W - visibleWidth(
|
|
983
|
+
const budget = Math.max(0, W - visibleWidth(`✦ ${meta}`) - 6);
|
|
909
984
|
const cut = visibleWidth(words) > budget ? `${widthCut(words, budget)}…` : words;
|
|
910
|
-
const row = `${p.bold}
|
|
985
|
+
const row = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${meta}`;
|
|
911
986
|
if (visibleWidth(row) <= W)
|
|
912
987
|
return [row];
|
|
913
988
|
// the last resort: the METADATA gives way — the words hold their
|
|
914
989
|
// budget, the meta cuts with the honest "…"; invariant ① never trips
|
|
915
990
|
// at ANY width (a degenerate W's fold is a cut, never a crash).
|
|
916
|
-
return [`${p.bold}
|
|
991
|
+
return [`${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(meta, Math.max(1, W - 8 - visibleWidth(cut)))}…`];
|
|
917
992
|
}
|
|
918
993
|
// ---- the bounded-block flow contract (W7, W8, W10) ----
|
|
919
994
|
/** The caps — screen rows counted AFTER the fold, at the current width
|
|
@@ -1270,8 +1345,11 @@ class Banner {
|
|
|
1270
1345
|
}
|
|
1271
1346
|
render(W, ctx) {
|
|
1272
1347
|
const p = palette();
|
|
1273
|
-
|
|
1274
|
-
|
|
1348
|
+
// R2: NO blanket dim. bannerLines styles itself — the labels are
|
|
1349
|
+
// dim, the values are ink — and wrapping the whole thing in dim
|
|
1350
|
+
// made the answers as faint as the questions.
|
|
1351
|
+
void p;
|
|
1352
|
+
return bannerLines(W, ctx.height, this.cell.version, this.cell.extensionsText, this.cell.resume, ctx.now, this.cell.meta);
|
|
1275
1353
|
}
|
|
1276
1354
|
}
|
|
1277
1355
|
/** W20 — the task block's fixed-window height: the whole live block
|
|
@@ -1355,7 +1433,7 @@ class Checklist {
|
|
|
1355
1433
|
const fixed = done
|
|
1356
1434
|
? `task done · ${plural(items.length, "item")} · ${formatDuration(durationSeconds)}`
|
|
1357
1435
|
: `task · ${plural(items.length, "item")} · ${active.length} active · ${doneCount} done`;
|
|
1358
|
-
const header = `${p.bold}
|
|
1436
|
+
const header = `${p.bold}✦${p.reset} ${escapeTerminal(fixed + tail)}`;
|
|
1359
1437
|
// the FULL-list forms: SETTLED — the durable record (the fold is
|
|
1360
1438
|
// fine — committed content wraps naturally) — and the LIVE ctrl+r
|
|
1361
1439
|
// toggle (the header CUTS — the block stays one window high; the
|
|
@@ -1457,20 +1535,38 @@ export function widthCut(text, max) {
|
|
|
1457
1535
|
*/
|
|
1458
1536
|
export function selectionBar(styled, visible, W) {
|
|
1459
1537
|
const p = palette();
|
|
1460
|
-
|
|
1538
|
+
// R2 (design §2.1 — nothing dim ever sits on the wash): the bar IS a
|
|
1539
|
+
// wash. A dim span inside it renders grey-on-grey — 3.91:1 on the
|
|
1540
|
+
// light ground, under the 4.5 floor — and the dim spans are exactly
|
|
1541
|
+
// the descriptions and the metadata, i.e. the half of the row the
|
|
1542
|
+
// selection was supposed to help you read. Dim is dropped INSIDE the
|
|
1543
|
+
// bar and nowhere else; the same row unselected keeps it.
|
|
1544
|
+
const inner = (p.dim === "" ? styled : styled.replaceAll(p.dim, "")).replaceAll(p.reset, `${p.reset}${p.rv}`);
|
|
1461
1545
|
return `${p.rv} ${inner}${" ".repeat(Math.max(0, W - visible - 2))} ${p.rvEnd}`;
|
|
1462
1546
|
}
|
|
1463
|
-
/**
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1547
|
+
/**
|
|
1548
|
+
* R2 — the composer's rails, and the ONE edge vocabulary.
|
|
1549
|
+
*
|
|
1550
|
+
* W6 turned two \u254c dotted rows into a rounded box, reasoning that
|
|
1551
|
+
* "the box already says input lives here". That is reversed here, and
|
|
1552
|
+
* the reason is not taste: a rule is a DELIMITER and a box is a
|
|
1553
|
+
* CONTAINER, and the screen was carrying six edge vocabularies at once
|
|
1554
|
+
* (this box, the panel's \u2502 gutter and \u2514\u2500\u2500 tail, the
|
|
1555
|
+
* diff gutter, the quote's \u258f, the table's rails, the markdown
|
|
1556
|
+
* rule). One dashed rule replaces the ones that SEPARATE; the \u2502
|
|
1557
|
+
* gutter survives where it SCOPES.
|
|
1558
|
+
*
|
|
1559
|
+
* Row-neutral by construction: CHROME_ROWS is still 4, so every gate
|
|
1560
|
+
* keyed on H \u2212 4 is untouched, and the input row gains the two
|
|
1561
|
+
* columns the walls were taking.
|
|
1562
|
+
*/
|
|
1468
1563
|
export function boxTop(W) {
|
|
1469
|
-
return `\x1b[2m
|
|
1564
|
+
return `\x1b[2m${"\u254c".repeat(Math.max(0, W))}\x1b[0m`;
|
|
1470
1565
|
}
|
|
1471
|
-
/**
|
|
1566
|
+
/** R2 — the same rule below. Named for its POSITION, not its shape, so
|
|
1567
|
+
* the compositor's two call sites did not have to move. */
|
|
1472
1568
|
export function boxBottom(W) {
|
|
1473
|
-
return `\x1b[2m
|
|
1569
|
+
return `\x1b[2m${"\u254c".repeat(Math.max(0, W))}\x1b[0m`;
|
|
1474
1570
|
}
|
|
1475
1571
|
/** The terminal label + rhythm gap (the pipe path's v2c bytes — the
|
|
1476
1572
|
* exact render the passthrough needs). */
|
package/dist/ground.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DC-3 §1 — the GROUND: is the terminal light or dark.
|
|
3
|
+
*
|
|
4
|
+
* Every colour in the palette is chosen against a background, and until
|
|
5
|
+
* now kiso had no way to know what that background was: `palette()`
|
|
6
|
+
* returned one constant, its greys were picked on a dark terminal, and
|
|
7
|
+
* on a white one the inline-code token measured 1.54:1 against a 4.5:1
|
|
8
|
+
* floor (finding DC-3).
|
|
9
|
+
*
|
|
10
|
+
* This module is the answer, and it is deliberately PURE. The terminal's
|
|
11
|
+
* reply arrives as a string and the environment arrives as fields, so
|
|
12
|
+
* the whole decision is testable without a terminal and the terminal
|
|
13
|
+
* work is reduced to plumbing.
|
|
14
|
+
*
|
|
15
|
+
* `unknown` is a real answer, not a failure. It means the caller must
|
|
16
|
+
* use the mark that is correct on ANY ground — reverse video rather than
|
|
17
|
+
* a wash, the terminal's own dim rather than a chosen grey. A design
|
|
18
|
+
* that degrades is the price of never painting light-mode paint onto a
|
|
19
|
+
* dark screen.
|
|
20
|
+
*/
|
|
21
|
+
export type Ground = "light" | "dark" | "unknown";
|
|
22
|
+
export interface Rgb {
|
|
23
|
+
readonly r: number;
|
|
24
|
+
readonly g: number;
|
|
25
|
+
readonly b: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function parseOscColor(body: string): Rgb | null;
|
|
28
|
+
/** The WCAG relative luminance — the same formula the contrast ratios in
|
|
29
|
+
* `packages/tui/design.md` §2 are computed with, so the ground and the
|
|
30
|
+
* floor cannot drift apart. */
|
|
31
|
+
export declare function relativeLuminance({ r, g, b }: Rgb): number;
|
|
32
|
+
export declare function groundFrom(rgb: Rgb): Exclude<Ground, "unknown">;
|
|
33
|
+
export interface GroundInputs {
|
|
34
|
+
/** KISO_THEME — an explicit answer from the human. */
|
|
35
|
+
readonly theme?: string | undefined;
|
|
36
|
+
/** The body of the terminal's OSC 11 answer, if one arrived. */
|
|
37
|
+
readonly osc?: string | undefined;
|
|
38
|
+
/** The COLORFGBG environment variable, if it is set. */
|
|
39
|
+
readonly colorfgbg?: string | undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The ladder, first hit wins. Every rung that cannot answer falls
|
|
43
|
+
* through rather than guessing, and the bottom of the ladder is
|
|
44
|
+
* `unknown` — see the module comment for why that is a result.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveGround({ theme, osc, colorfgbg }: GroundInputs): Ground;
|
package/dist/ground.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DC-3 §1 — the GROUND: is the terminal light or dark.
|
|
3
|
+
*
|
|
4
|
+
* Every colour in the palette is chosen against a background, and until
|
|
5
|
+
* now kiso had no way to know what that background was: `palette()`
|
|
6
|
+
* returned one constant, its greys were picked on a dark terminal, and
|
|
7
|
+
* on a white one the inline-code token measured 1.54:1 against a 4.5:1
|
|
8
|
+
* floor (finding DC-3).
|
|
9
|
+
*
|
|
10
|
+
* This module is the answer, and it is deliberately PURE. The terminal's
|
|
11
|
+
* reply arrives as a string and the environment arrives as fields, so
|
|
12
|
+
* the whole decision is testable without a terminal and the terminal
|
|
13
|
+
* work is reduced to plumbing.
|
|
14
|
+
*
|
|
15
|
+
* `unknown` is a real answer, not a failure. It means the caller must
|
|
16
|
+
* use the mark that is correct on ANY ground — reverse video rather than
|
|
17
|
+
* a wash, the terminal's own dim rather than a chosen grey. A design
|
|
18
|
+
* that degrades is the price of never painting light-mode paint onto a
|
|
19
|
+
* dark screen.
|
|
20
|
+
*/
|
|
21
|
+
/** `<n>;rgb:R/G/B` with 1–4 hex digits per component — the body of an
|
|
22
|
+
* OSC 10/11 answer, already stripped of its introducer and terminator
|
|
23
|
+
* by the editor (DC-7). Anything else is null: a guess about the
|
|
24
|
+
* ground is worse than admitting there isn't one. */
|
|
25
|
+
const OSC_RGB = /^\d+;rgb:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})$/i;
|
|
26
|
+
export function parseOscColor(body) {
|
|
27
|
+
const m = OSC_RGB.exec(body.trim());
|
|
28
|
+
if (m === null)
|
|
29
|
+
return null;
|
|
30
|
+
// a component is n hex digits of a full-scale value, so it scales by
|
|
31
|
+
// its own maximum — `f` is full white exactly as `ffff` is.
|
|
32
|
+
const comp = (h) => Math.round((Number.parseInt(h, 16) / (16 ** h.length - 1)) * 255);
|
|
33
|
+
return { r: comp(m[1]), g: comp(m[2]), b: comp(m[3]) };
|
|
34
|
+
}
|
|
35
|
+
/** The WCAG relative luminance — the same formula the contrast ratios in
|
|
36
|
+
* `packages/tui/design.md` §2 are computed with, so the ground and the
|
|
37
|
+
* floor cannot drift apart. */
|
|
38
|
+
export function relativeLuminance({ r, g, b }) {
|
|
39
|
+
const lin = (v) => {
|
|
40
|
+
const c = v / 255;
|
|
41
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
42
|
+
};
|
|
43
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
44
|
+
}
|
|
45
|
+
export function groundFrom(rgb) {
|
|
46
|
+
return relativeLuminance(rgb) > 0.5 ? "light" : "dark";
|
|
47
|
+
}
|
|
48
|
+
/** COLORFGBG is `fg;bg` or `fg;default;bg` — the BACKGROUND is the last
|
|
49
|
+
* field, as an ANSI index. 0–6 and 8 are the dark half of the sixteen;
|
|
50
|
+
* 7 and 9–15 are the light half. Absent on most terminals, which is why
|
|
51
|
+
* it sits below the question kiso can actually ask. */
|
|
52
|
+
function fromColorFgBg(value) {
|
|
53
|
+
const fields = value.split(";");
|
|
54
|
+
const bg = Number.parseInt(fields[fields.length - 1] ?? "", 10);
|
|
55
|
+
if (!Number.isInteger(bg) || bg < 0 || bg > 15)
|
|
56
|
+
return "unknown";
|
|
57
|
+
return bg === 7 || bg >= 9 ? "light" : "dark";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The ladder, first hit wins. Every rung that cannot answer falls
|
|
61
|
+
* through rather than guessing, and the bottom of the ladder is
|
|
62
|
+
* `unknown` — see the module comment for why that is a result.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveGround({ theme, osc, colorfgbg }) {
|
|
65
|
+
const explicit = theme?.trim().toLowerCase();
|
|
66
|
+
if (explicit === "light" || explicit === "dark")
|
|
67
|
+
return explicit;
|
|
68
|
+
if (osc !== undefined && osc !== "") {
|
|
69
|
+
const rgb = parseOscColor(osc);
|
|
70
|
+
if (rgb !== null)
|
|
71
|
+
return groundFrom(rgb);
|
|
72
|
+
}
|
|
73
|
+
if (colorfgbg !== undefined && colorfgbg !== "")
|
|
74
|
+
return fromColorFgBg(colorfgbg);
|
|
75
|
+
return "unknown";
|
|
76
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,4 +14,7 @@ export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWi
|
|
|
14
14
|
export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, type TrustArtifact, } from "./strings.js";
|
|
15
15
|
export { extensionsBannerText, helpRows, unansweredAskView, type BannerExtension } from "./strings.js";
|
|
16
16
|
export { displayVerb } from "./strings.js";
|
|
17
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type ResumeMeta, } from "./render.js";
|
|
17
|
+
export { bannerLines, COLOR_OFF, COLOR_DARK, COLOR_LIGHT, COLOR_NEUTRAL, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type ResumeMeta, } from "./render.js";
|
|
18
|
+
/** DC-3 — the ground: is the terminal light or dark. Pure; see the
|
|
19
|
+
* module comment for why `unknown` is a result and not a failure. */
|
|
20
|
+
export { groundFrom, parseOscColor, relativeLuminance, resolveGround, type Ground, type GroundInputs, type Rgb } from "./ground.js";
|
package/dist/index.js
CHANGED
|
@@ -28,4 +28,7 @@ export { extensionsBannerText, helpRows, unansweredAskView } from "./strings.js"
|
|
|
28
28
|
// TUI2-R2pre ④: the ONE display-verb table — the screen names the act,
|
|
29
29
|
// the tool table names the call.
|
|
30
30
|
export { displayVerb } from "./strings.js";
|
|
31
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
|
|
31
|
+
export { bannerLines, COLOR_OFF, COLOR_DARK, COLOR_LIGHT, COLOR_NEUTRAL, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
|
|
32
|
+
/** DC-3 — the ground: is the terminal light or dark. Pure; see the
|
|
33
|
+
* module comment for why `unknown` is a result and not a failure. */
|
|
34
|
+
export { groundFrom, parseOscColor, relativeLuminance, resolveGround } from "./ground.js";
|
package/dist/md.d.ts
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
/** The block kinds. `fence-open`/`fence-line` are separate kinds on
|
|
40
40
|
* purpose: a fence's rows must be able to freeze ONE AT A TIME. */
|
|
41
|
-
export type MdKind = "para" | "heading" | "list" | "table" | "quote" | "rule" | "fence-open" | "fence-line";
|
|
41
|
+
export type MdKind = "para" | "heading" | "list" | "table" | "quote" | "rule" | "fence-open" | "fence-line" | "fence-close";
|
|
42
42
|
/** One block: its SOURCE lines, never a rendered form. The render is a
|
|
43
43
|
* pure function of (block, width), which is what makes the freeze
|
|
44
44
|
* property a property of the scanner alone. */
|