@vincemakes/kiso-tui-cells 0.8.0 → 0.10.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/dist/diff.js CHANGED
@@ -91,23 +91,37 @@ function stats(diff) {
91
91
  }
92
92
  return { added, removed };
93
93
  }
94
- /** edit_file: the search→replace windows replace in place — the changed
95
- * region is KNOWN, so the diff is the old window vs the new window,
96
- * context from the surrounding file. */
97
- export function editFileDiff(oldContent, search, replace) {
98
- const oldLines = oldContent.split("\n");
99
- const searchLines = search.split("\n");
100
- const replaceLines = replace.split("\n");
101
- // Locate the search window (the first occurrence — the edit tool's own
102
- // semantics); no occurrence → the whole file is the old side.
103
- let at = -1;
104
- for (let i = 0; i + searchLines.length <= oldLines.length; i += 1) {
105
- if (oldLines.slice(i, i + searchLines.length).join("\n") === search) {
106
- at = i;
107
- break;
108
- }
94
+ /** edit_file: the preview of a CHARACTER splice.
95
+ *
96
+ * TUI2-R1.5 ② (VD-2): the locator is the tool's own, verbatim — the
97
+ * workspace edit_file does `i = text.indexOf(search)` and writes
98
+ * `text.slice(0, i) + replace + text.slice(i + search.length)`. This
99
+ * function mirrors those two lines and diffs the result against the
100
+ * original; it does not model the edit, it reproduces it.
101
+ *
102
+ * The retired locator required the search to align to FULL LINES. A
103
+ * mid-line search ("// OLD" inside " // OLD") therefore missed, and
104
+ * the miss branch rendered the WHOLE FILE as the old side: a one-line
105
+ * edit was drawn as a catastrophic rewrite, on the approval panel, at
106
+ * the moment a human was deciding whether to allow it. A preview that
107
+ * can be that wrong is worse than no preview.
108
+ *
109
+ * A genuine miss is now reported as a miss: the tool will return
110
+ * `pattern not found in <path>` and change nothing, so the panel says
111
+ * exactly that instead of inventing a diff for an edit that will not
112
+ * happen. `path` names the file in that note. */
113
+ export function editFileDiff(oldContent, search, replace, path) {
114
+ const at = oldContent.indexOf(search);
115
+ if (at < 0) {
116
+ return {
117
+ lines: [{ kind: " ", text: `pattern not found in ${path ?? "the file"}` }],
118
+ added: 0,
119
+ removed: 0,
120
+ notFound: true,
121
+ };
109
122
  }
110
- const lines = at < 0 ? withContext(lcsDiff(oldLines, replaceLines)) : withContext(lcsDiff(oldLines, [...oldLines.slice(0, at), ...replaceLines, ...oldLines.slice(at + searchLines.length)]));
123
+ const result = oldContent.slice(0, at) + replace + oldContent.slice(at + search.length);
124
+ const lines = withContext(lcsDiff(oldContent.split("\n"), result.split("\n")));
111
125
  return { lines, ...stats(lines) };
112
126
  }
113
127
  /** write_file: a new file is all +; an existing file diffs row-level
package/dist/index.d.ts CHANGED
@@ -6,11 +6,12 @@
6
6
  * package); the cli never imports it directly. Experimental — no
7
7
  * API-stability promise yet.
8
8
  */
9
- export { SPINNER, foldLine, visibleWidth, bodySpacing, Container, cellComponent, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, type FrameCtx, type RenderLine, type Component, type BodyCell, } from "./components.js";
9
+ export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, focusToken, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, type FrameCtx, type RenderLine, type Component, type BodyCell, } from "./components.js";
10
10
  export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
11
11
  export { pendingQueueRows } from "./components.js";
12
12
  export { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
13
13
  export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, type PanelArgs, type PanelFlavor, type PanelPhase, type PanelSel, type PanelState, type PanelVerdict, type PanelView, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, } from "./approval-panel.js";
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
+ export { displayVerb } from "./strings.js";
16
17
  export { bannerLines, COLOR_OFF, COLOR_ON, colorInlineCode, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type ResumeMeta, } from "./render.js";
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * package); the cli never imports it directly. Experimental — no
7
7
  * API-stability promise yet.
8
8
  */
9
- export { SPINNER, foldLine, visibleWidth, bodySpacing, Container, cellComponent, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, } from "./components.js";
9
+ export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, focusToken, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, } from "./components.js";
10
10
  export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
11
11
  // W22 (the v8 input round): the pending-queue chips — the SAME
12
12
  // UserMessage chip with the □ gutter, pre-rendered above the input
@@ -25,4 +25,7 @@ export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWi
25
25
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, } from "./strings.js";
26
26
  // KC3.5: the interrupted-ask copy and the extracted /help table.
27
27
  export { extensionsBannerText, helpRows, unansweredAskView } from "./strings.js";
28
+ // TUI2-R2pre ④: the ONE display-verb table — the screen names the act,
29
+ // the tool table names the call.
30
+ export { displayVerb } from "./strings.js";
28
31
  export { bannerLines, COLOR_OFF, COLOR_ON, colorInlineCode, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
package/dist/render.d.ts CHANGED
@@ -34,6 +34,14 @@ export interface Palette {
34
34
  readonly dim: string;
35
35
  readonly red: string;
36
36
  readonly green: string;
37
+ /** TUI2-R2 ①: the third functional exception, finally spelled. The
38
+ * mono-discipline ruling above names "green ✓, yellow warn and red
39
+ * error" as the ONLY functional colours; warn had no entry because
40
+ * nothing had needed it yet. The uncertain badge needs exactly it —
41
+ * a state that is neither success nor failure but a question
42
+ * addressed to the human. This is the ruling's own set gaining its
43
+ * missing member, not a fourth colour. */
44
+ readonly warn: string;
37
45
  readonly code: string;
38
46
  readonly rv: string;
39
47
  readonly rvEnd: string;
package/dist/render.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * dependencies (the tui-cells package has none).
7
7
  */
8
8
  import { charWidth, displayWidth } from "./width.js";
9
- export const COLOR_ON = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", code: "\x1b[38;5;252m", rv: "\x1b[7m", rvEnd: "\x1b[27m", reset: "\x1b[0m" };
10
- export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", code: "", rv: "", rvEnd: "", reset: "" };
9
+ export const COLOR_ON = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", warn: "\x1b[33m", code: "\x1b[38;5;252m", rv: "\x1b[7m", rvEnd: "\x1b[27m", reset: "\x1b[0m" };
10
+ export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", rv: "", rvEnd: "", reset: "" };
11
11
  export function palette() {
12
12
  return process.env.NO_COLOR === undefined && process.stdout.isTTY ? COLOR_ON : COLOR_OFF;
13
13
  }
package/dist/strings.d.ts CHANGED
@@ -93,6 +93,63 @@ export interface BannerExtension {
93
93
  * reads `built-in: mcp, skills, subagent`, from this one composition.
94
94
  */
95
95
  export declare function extensionsBannerText(builtIn: readonly BannerExtension[], user: readonly BannerExtension[], project: readonly BannerExtension[]): string;
96
+ /** One gesture: what you press, and what it does. */
97
+ export interface KeyBinding {
98
+ readonly keys: string;
99
+ readonly what: string;
100
+ }
101
+ /**
102
+ * TUI2-R1 (D) — THE key table. Every reader derives from it: the `?`
103
+ * sheet and /help's keys row. A sheet that has drifted from the keys is
104
+ * worse than no sheet, and the only way to make drift impossible is to
105
+ * have one table and no second copy of it.
106
+ *
107
+ * The order is the sheet's reading order, which is why it is grouped by
108
+ * WHAT A HUMAN IS DOING rather than alphabetically: the three ways to
109
+ * put something in (send, newline, files), the three ways to change
110
+ * course (stop, redirect, commands), then the walks (history, expand),
111
+ * then the completions.
112
+ */
113
+ export declare const KEY_BINDINGS: readonly KeyBinding[];
114
+ /** A tool's name as the SCREEN says it. Display-only: the raw name stays
115
+ * on the cell, and dispatch, the mode gate, the policy keys, the /last
116
+ * RAW block and every model-facing byte keep reading that. */
117
+ export declare function displayVerb(name: string): string;
118
+ /** The panel keys, which belong to a panel rather than the composer —
119
+ * one dim line rather than four table rows, because they apply only
120
+ * while a panel is up. */
121
+ /**
122
+ * TUI2-R1.5 pin 6 — this row has to be true of BOTH panel flavors, and
123
+ * "digits select" was wrong in both directions at once.
124
+ *
125
+ * On an APPROVAL a digit only moves the selection (editor's #panelSelect
126
+ * sets `sel` and renders); ENTER is what resolves it. A reader who
127
+ * pressed 1 and walked away had approved nothing — the worst kind of
128
+ * false affordance, on the surface where the stakes are a side effect.
129
+ *
130
+ * On an ASK the opposite: a digit on a SINGLE-choice question answers it
131
+ * and advances the walk (ask-panel's askKey → advance), so "select"
132
+ * undersold it. A multi-select question toggles and waits for enter,
133
+ * like the approval.
134
+ *
135
+ * "digits pick · ⏎ confirms" is the sentence both flavors satisfy. The
136
+ * single-choice fast path — where the confirm is implicit — is the one
137
+ * thing a single row cannot also carry; it is an omission, never a lie.
138
+ */
139
+ export declare const PANEL_KEYS_ROW = "panels: digits pick \u00B7 \u23CE confirms \u00B7 space toggles \u00B7 t types an answer";
140
+ /**
141
+ * TUI2-R1 (D) — the sheet, one screen, static.
142
+ *
143
+ * Rows CUT at the width rather than folding: the sheet's contract with
144
+ * the reader is "one screen", and a folded grid at 40 columns is two
145
+ * screens pretending to be one. A narrow terminal shows fewer columns
146
+ * of the same truth, which is the honest degradation.
147
+ */
148
+ export declare function keysSheetRows(W: number): string[];
149
+ /** TUI2-R1 (D) — the keys as ONE line, for /help. The same table the
150
+ * sheet renders, joined — so the two can disagree only by deleting a
151
+ * test. The sheet is the readable form; this is the greppable one. */
152
+ export declare function keysHelpRow(): string;
96
153
  /**
97
154
  * KC3.5 slice ⓪ (the extraction) — the /help command table.
98
155
  *
package/dist/strings.js CHANGED
@@ -18,6 +18,7 @@
18
18
  * looked up here.
19
19
  */
20
20
  import { escapeTerminal, palette } from "./render.js";
21
+ import { displayWidth } from "./width.js";
21
22
  /** v2a: the interactive prompt — the identity accent. readline owns the
22
23
  * echo of what the user types; we own the prompt's color. (v2c: the
23
24
  * readline prompt keeps "you> " — the brick ▌ is the dock's row only;
@@ -130,6 +131,169 @@ export function extensionsBannerText(builtIn, user, project) {
130
131
  parts.push(`project: ${project.map(label).join(", ")}`);
131
132
  return ` · [${total} extension${total === 1 ? "" : "s"}: ${parts.join(" · ")}]`;
132
133
  }
134
+ /**
135
+ * TUI2-R1 (D) — THE key table. Every reader derives from it: the `?`
136
+ * sheet and /help's keys row. A sheet that has drifted from the keys is
137
+ * worse than no sheet, and the only way to make drift impossible is to
138
+ * have one table and no second copy of it.
139
+ *
140
+ * The order is the sheet's reading order, which is why it is grouped by
141
+ * WHAT A HUMAN IS DOING rather than alphabetically: the three ways to
142
+ * put something in (send, newline, files), the three ways to change
143
+ * course (stop, redirect, commands), then the walks (history, expand),
144
+ * then the completions.
145
+ */
146
+ export const KEY_BINDINGS = [
147
+ { keys: "enter", what: "send" },
148
+ { keys: "ctrl+j / shift+⏎", what: "newline" },
149
+ { keys: "@", what: "files" },
150
+ { keys: "esc", what: "stop" },
151
+ { keys: "alt+⏎ / ctrl+⏎", what: "redirect" },
152
+ { keys: "/", what: "commands" },
153
+ { keys: "↑↓", what: "history / queue pop" },
154
+ { keys: "ctrl+r", what: "expand cells" },
155
+ { keys: "tab", what: "complete (menu / @)" },
156
+ { keys: "?", what: "this sheet" },
157
+ ];
158
+ /**
159
+ * TUI2-R2pre ④ — THE display-verb table (the integrator's ruling).
160
+ *
161
+ * The screen names the ACT; the tool table names the CALL. Two
162
+ * audiences, two vocabularies, and only the human's one lives here: the
163
+ * API names DO NOT change, because the model-request surface is frozen
164
+ * rent and every byte of it is paid for on every turn. The
165
+ * rename-the-tools path is REJECTED by ruling.
166
+ *
167
+ * One table, for the same reason KEY_BINDINGS above is one table. The
168
+ * mapping used to exist three and a half times — a `.replace("_file",
169
+ * "")` in components.ts, another in render.ts, two more in the
170
+ * compositor, and a private three-tool table for the rollup's expanded
171
+ * list — and the drift was visible on a single screen: a card head
172
+ * reading `read` directly above one reading `list_dir`.
173
+ *
174
+ * An unmapped tool (an extension's, an MCP server's) renders its own
175
+ * name. Inventing a verb for a tool this package has never heard of
176
+ * would be a worse lie than printing what the model actually calls.
177
+ */
178
+ const DISPLAY_VERB = {
179
+ read_file: "read",
180
+ list_dir: "list",
181
+ search_text: "search",
182
+ write_file: "write",
183
+ edit_file: "edit",
184
+ shell: "shell",
185
+ };
186
+ /** A tool's name as the SCREEN says it. Display-only: the raw name stays
187
+ * on the cell, and dispatch, the mode gate, the policy keys, the /last
188
+ * RAW block and every model-facing byte keep reading that. */
189
+ export function displayVerb(name) {
190
+ return DISPLAY_VERB[name] ?? name;
191
+ }
192
+ /** The panel keys, which belong to a panel rather than the composer —
193
+ * one dim line rather than four table rows, because they apply only
194
+ * while a panel is up. */
195
+ /**
196
+ * TUI2-R1.5 pin 6 — this row has to be true of BOTH panel flavors, and
197
+ * "digits select" was wrong in both directions at once.
198
+ *
199
+ * On an APPROVAL a digit only moves the selection (editor's #panelSelect
200
+ * sets `sel` and renders); ENTER is what resolves it. A reader who
201
+ * pressed 1 and walked away had approved nothing — the worst kind of
202
+ * false affordance, on the surface where the stakes are a side effect.
203
+ *
204
+ * On an ASK the opposite: a digit on a SINGLE-choice question answers it
205
+ * and advances the walk (ask-panel's askKey → advance), so "select"
206
+ * undersold it. A multi-select question toggles and waits for enter,
207
+ * like the approval.
208
+ *
209
+ * "digits pick · ⏎ confirms" is the sentence both flavors satisfy. The
210
+ * single-choice fast path — where the confirm is implicit — is the one
211
+ * thing a single row cannot also carry; it is an omission, never a lie.
212
+ */
213
+ export const PANEL_KEYS_ROW = "panels: digits pick · ⏎ confirms · space toggles · t types an answer";
214
+ /** The sheet's grid: the first six bindings in two 3-column rows, the
215
+ * last four in two 2-column rows (the wide entries get the room). The
216
+ * COLUMN STOPS are the prototype's absolute positions, floored by the
217
+ * content (a future binding widens its column rather than overrunning
218
+ * it — the grid degrades to one space, never to a collision). */
219
+ const SHEET_GRID = [
220
+ [0, 1, 2],
221
+ [3, 4, 5],
222
+ [6, 7],
223
+ [8, 9],
224
+ ];
225
+ const SHEET_STOPS = [
226
+ [16, 43],
227
+ [16, 43],
228
+ [36],
229
+ [36],
230
+ ];
231
+ /**
232
+ * TUI2-R1 (D) — the sheet, one screen, static.
233
+ *
234
+ * Rows CUT at the width rather than folding: the sheet's contract with
235
+ * the reader is "one screen", and a folded grid at 40 columns is two
236
+ * screens pretending to be one. A narrow terminal shows fewer columns
237
+ * of the same truth, which is the honest degradation.
238
+ */
239
+ export function keysSheetRows(W) {
240
+ const p = palette();
241
+ const cell = (i) => {
242
+ const b = KEY_BINDINGS[i];
243
+ return b === undefined ? "" : `${p.code}${b.keys}${p.reset} ${b.what}`;
244
+ };
245
+ const plainCell = (i) => {
246
+ const b = KEY_BINDINGS[i];
247
+ return b === undefined ? "" : `${b.keys} ${b.what}`;
248
+ };
249
+ const rows = [`${p.bold}keys${p.reset}`];
250
+ for (let r = 0; r < SHEET_GRID.length; r += 1) {
251
+ const indexes = SHEET_GRID[r];
252
+ let row = "";
253
+ let width = 0;
254
+ for (let c = 0; c < indexes.length; c += 1) {
255
+ row += cell(indexes[c]);
256
+ width += displayWidth(plainCell(indexes[c]));
257
+ const stop = SHEET_STOPS[r][c];
258
+ if (stop === undefined)
259
+ continue; // the last column pads nothing
260
+ const pad = Math.max(1, stop - width);
261
+ row += " ".repeat(pad);
262
+ width += pad;
263
+ }
264
+ rows.push(row);
265
+ }
266
+ rows.push(`${p.dim}${PANEL_KEYS_ROW}${p.reset}`);
267
+ return rows.map((row) => cutRow(row, W));
268
+ }
269
+ /** One row, cut at the width — SGR-aware, the ellipsis after the reset
270
+ * (the cutLine convention; duplicated here rather than imported so the
271
+ * strings module keeps its no-components-dependency shape). */
272
+ function cutRow(row, W) {
273
+ let out = "";
274
+ let width = 0;
275
+ for (let i = 0; i < row.length;) {
276
+ if (row[i] === "\x1b") {
277
+ const m = /^\x1b\[[0-9;]*m/.exec(row.slice(i))?.[0] ?? row[i];
278
+ out += m;
279
+ i += m.length;
280
+ continue;
281
+ }
282
+ const cw = displayWidth(row[i]);
283
+ if (width + cw > W)
284
+ return `${out}${palette().reset}`;
285
+ out += row[i];
286
+ width += cw;
287
+ i += 1;
288
+ }
289
+ return out;
290
+ }
291
+ /** TUI2-R1 (D) — the keys as ONE line, for /help. The same table the
292
+ * sheet renders, joined — so the two can disagree only by deleting a
293
+ * test. The sheet is the readable form; this is the greppable one. */
294
+ export function keysHelpRow() {
295
+ return KEY_BINDINGS.map((b) => `${b.keys} ${b.what}`).join(" · ");
296
+ }
133
297
  /**
134
298
  * KC3.5 slice ⓪ (the extraction) — the /help command table.
135
299
  *
@@ -152,6 +316,13 @@ export function helpRows() {
152
316
  cmd("/mode", "show the approval tier; /mode <name> switches (manual/default/accept-edits/plan/bypass)"),
153
317
  cmd("/model", "list model profiles; /model <name|provider/model> switches"),
154
318
  cmd("/compact", "summarize the older conversation to free context"),
319
+ // TUI2-R1 (D): DELIBERATELY UNCHANGED. Deriving this sentence from
320
+ // KEY_BINDINGS would be an improvement and it would also move an
321
+ // assertion outside the round's two declared supersession classes,
322
+ // so the sheet is the derived surface and this row keeps its bytes.
323
+ // `keysHelpRow()` exists for the round that is allowed to make the
324
+ // swap; until then the drift guard is the test that every binding
325
+ // in the table is mentioned here.
155
326
  `${cmd("exit", "leave the session")}\n${cmd("keys", "enter sends · ctrl+J newline (shift+enter where encoded) · esc stops the run · alt+⏎ stops it and sends this instead · @ files · 1-4 answers an ask")}`,
156
327
  ];
157
328
  }
package/dist/width.d.ts CHANGED
@@ -1,11 +1,21 @@
1
1
  /**
2
2
  * The display-width primitives — the SINGLE width authority (TUI v5
3
- * #16e: "charWidth is the width authority"). The eastAsianWidth table
4
- * is a ~40-line subset (CJK ideographs/kana/hangul/fullwidth/common
5
- * wide symbols = 2, everything else = 1 — the box-drawing/brick glyphs
6
- * █▀▄▞▸ are narrow). Known limitation, documented in the README: emoji
7
- * ZWJ clusters are not guaranteed perfect — each code point counts as
8
- * its width. Zero dependencies (importable from any module).
3
+ * #16e: "charWidth is the width authority"). The table covers the East
4
+ * Asian Wide/Fullwidth ranges and Emoji_Presentation=Yes; everything
5
+ * else is one column, the box-drawing/brick glyphs █▀▄▞▸ and the text-
6
+ * presentation marks ✓ ✗ ⚠ ⏸ included.
7
+ *
8
+ * This is the compositor's FLOOR, not a cosmetic detail: a glyph scored
9
+ * one column that a terminal draws in two makes a line whose measured
10
+ * width is <= W really need W+1, the terminal soft-wraps the tail onto
11
+ * the row below, and the live region silently eats a row it never
12
+ * budgeted (TUI2-R2pre ① — the composer clobber).
13
+ *
14
+ * Known limitation, documented in the README: emoji ZWJ clusters and
15
+ * variation-selector sequences are not guaranteed perfect — each code
16
+ * point counts as its own width (U+26A0 + FE0F sums to 2, which is what
17
+ * a terminal draws, but that is arithmetic luck, not a model).
18
+ * Zero dependencies (importable from any module).
9
19
  */
10
20
  /** A code point's display width: 2 for the wide ranges, 1 otherwise. */
11
21
  export declare function charWidth(cp: number): number;
package/dist/width.js CHANGED
@@ -1,12 +1,63 @@
1
1
  /**
2
2
  * The display-width primitives — the SINGLE width authority (TUI v5
3
- * #16e: "charWidth is the width authority"). The eastAsianWidth table
4
- * is a ~40-line subset (CJK ideographs/kana/hangul/fullwidth/common
5
- * wide symbols = 2, everything else = 1 — the box-drawing/brick glyphs
6
- * █▀▄▞▸ are narrow). Known limitation, documented in the README: emoji
7
- * ZWJ clusters are not guaranteed perfect — each code point counts as
8
- * its width. Zero dependencies (importable from any module).
3
+ * #16e: "charWidth is the width authority"). The table covers the East
4
+ * Asian Wide/Fullwidth ranges and Emoji_Presentation=Yes; everything
5
+ * else is one column, the box-drawing/brick glyphs █▀▄▞▸ and the text-
6
+ * presentation marks ✓ ✗ ⚠ ⏸ included.
7
+ *
8
+ * This is the compositor's FLOOR, not a cosmetic detail: a glyph scored
9
+ * one column that a terminal draws in two makes a line whose measured
10
+ * width is <= W really need W+1, the terminal soft-wraps the tail onto
11
+ * the row below, and the live region silently eats a row it never
12
+ * budgeted (TUI2-R2pre ① — the composer clobber).
13
+ *
14
+ * Known limitation, documented in the README: emoji ZWJ clusters and
15
+ * variation-selector sequences are not guaranteed perfect — each code
16
+ * point counts as its own width (U+26A0 + FE0F sums to 2, which is what
17
+ * a terminal draws, but that is arithmetic luck, not a model).
18
+ * Zero dependencies (importable from any module).
9
19
  */
20
+ /** TUI2-R2pre ① — the Emoji_Presentation=Yes code points inside
21
+ * U+2000..U+2BFF. The rest of that span is TEXT presentation and stays
22
+ * one column: ✓ ✗ ⚠ ⏸ ▞ ▸ and the box-drawing rails are all narrow, and
23
+ * widening any of them would move every card head on the screen. Listed
24
+ * as ranges because that is what the property is — the singles are
25
+ * singles in Unicode too. */
26
+ const EMOJI_PRESENTATION = [
27
+ [0x231a, 0x231b],
28
+ [0x23e9, 0x23ec],
29
+ [0x23f0, 0x23f0],
30
+ [0x23f3, 0x23f3],
31
+ [0x25fd, 0x25fe],
32
+ [0x2614, 0x2615],
33
+ [0x2648, 0x2653],
34
+ [0x267f, 0x267f],
35
+ [0x2693, 0x2693],
36
+ [0x26a1, 0x26a1],
37
+ [0x26aa, 0x26ab],
38
+ [0x26bd, 0x26be],
39
+ [0x26c4, 0x26c5],
40
+ [0x26ce, 0x26ce],
41
+ [0x26d4, 0x26d4],
42
+ [0x26ea, 0x26ea],
43
+ [0x26f2, 0x26f3],
44
+ [0x26f5, 0x26f5],
45
+ [0x26fa, 0x26fa],
46
+ [0x26fd, 0x26fd],
47
+ [0x2705, 0x2705],
48
+ [0x270a, 0x270b],
49
+ [0x2728, 0x2728],
50
+ [0x274c, 0x274c],
51
+ [0x274e, 0x274e],
52
+ [0x2753, 0x2755],
53
+ [0x2757, 0x2757],
54
+ [0x2795, 0x2797],
55
+ [0x27b0, 0x27b0],
56
+ [0x27bf, 0x27bf],
57
+ [0x2b1b, 0x2b1c],
58
+ [0x2b50, 0x2b50],
59
+ [0x2b55, 0x2b55],
60
+ ];
10
61
  /** A code point's display width: 2 for the wide ranges, 1 otherwise. */
11
62
  export function charWidth(cp) {
12
63
  if (cp >= 0x1100 && cp <= 0x115f)
@@ -35,12 +86,33 @@ export function charWidth(cp) {
35
86
  return 2; // fullwidth forms
36
87
  if (cp >= 0xffe0 && cp <= 0xffe6)
37
88
  return 2; // fullwidth signs
89
+ // TUI2-R1.5 shipped only two of the pictographic ranges; the holes
90
+ // (transport, mahjong/cards, enclosed, colored shapes, the extended
91
+ // block) were scored ONE column while every terminal draws them in
92
+ // two — the composer clobber of the owner's field report (①).
93
+ if (cp === 0x1f004 || cp === 0x1f0cf)
94
+ return 2; // mahjong red dragon, joker
95
+ if (cp >= 0x1f18e && cp <= 0x1f19a)
96
+ return 2; // enclosed alphanumerics
97
+ if (cp >= 0x1f200 && cp <= 0x1f251)
98
+ return 2; // enclosed ideographic
38
99
  if (cp >= 0x1f300 && cp <= 0x1f64f)
39
100
  return 2; // emoji (misc + emoticons)
101
+ if (cp >= 0x1f680 && cp <= 0x1f6ff)
102
+ return 2; // transport + map
103
+ if (cp >= 0x1f7e0 && cp <= 0x1f7eb)
104
+ return 2; // colored circles + squares
40
105
  if (cp >= 0x1f900 && cp <= 0x1f9ff)
41
106
  return 2; // supplemental emoji
107
+ if (cp >= 0x1fa70 && cp <= 0x1faff)
108
+ return 2; // symbols + pictographs ext-A
42
109
  if (cp >= 0x20000 && cp <= 0x3fffd)
43
110
  return 2; // CJK ext B..G
111
+ if (cp >= 0x231a && cp <= 0x2b55) {
112
+ for (const [lo, hi] of EMOJI_PRESENTATION)
113
+ if (cp >= lo && cp <= hi)
114
+ return 2;
115
+ }
44
116
  return 1;
45
117
  }
46
118
  /** Display width of a code-point array (cursor math, scrolling). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "kiso tui-cells — the components cell renderer (components, diff, width, the render slice). Zero runtime dependencies: input is data, output is bytes.",
5
5
  "type": "module",
6
6
  "license": "MIT",