Skip to content

fix(table): correct display width for spacing marks, unskewing list columns - #25

Merged
colevels merged 1 commit into
mainfrom
fix/display-width-spacing-marks
Jul 19, 2026
Merged

fix(table): correct display width for spacing marks, unskewing list columns#25
colevels merged 1 commit into
mainfrom
fix/display-width-spacing-marks

Conversation

@colevels

Copy link
Copy Markdown
Owner

The bug

A Thai tab title pushed its URL / WIN / FLAGS cells two columns right of every other row in tabbrew tabs list:

1300952698  Everything Was Fine Until I Touched It... -…  youtube.com/watch?v=IaRY_tdl1s4  1300952653
1300952701  เมื่อพี่สาวแฟน ชอบมาคุยกับผม... - YouTube          youtube.com/watch?v=HUPj6Hm3ryg  1300952653
                                                          ^^ two columns of drift

This is not a pad-on-.length bug — table.ts already measured with Bun.stringWidth. Bun.stringWidth is itself wrong, and in both directions:

Characters Bun Terminal Effect
Thai / Lao — SARA AA/AM, category Lo 0 1 over-pads → row shifts right
Indic spacing matras \p{Mc} (e.g. ि, 77 codepoints) 0 1 over-pads → shifts right
Arabic/Hebrew harakat \p{Mn} (104 codepoints) 1 0 under-pads → shifts left

The reported title carries two U+0E32 (), hence exactly two columns of drift — confirmed by measuring the screenshot's pixel offset against the character cell width.

The fix

width() now walks grapheme clusters and applies two rules:

  • a combining mark (Mn/Me/Cf/Cc) is always 0 — fixes the Arabic over-count;
  • a spacing character is never 0 (Math.max(1, Bun.stringWidth(ch))) — fixes Thai/Lao/Indic.

Bun.stringWidth still supplies the 2 for everything wide, and whole emoji clusters are delegated to it untouched, since it gets ZWJ sequences, regional-indicator flags and skin-tone modifiers right today. ASCII takes an s.length fast path (~every cell), and the existing ANSI-stripping behavior is preserved.

width() is the single chokepoint for colWidth / padEnd / padEndLink / truncate, so this fixes both tabs list and docs list without touching either command file.

Also: grapheme-safe truncation in the preview renderer

fmtTab (src/tabbrew-script/render.ts) truncated with title.slice(0, 33) on code units, which splits an emoji's surrogate pair and orphans Thai vowels from their consonants:

old (slice):    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ud83d…"  → renders  aaaa…�
new (truncate): "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…"        → renders  aaaa…

Replaced with the existing grapheme-aware truncate() from table.ts — reuse, not a new helper.

Tests

Introduces bun test — built into Bun, so this holds the repo's zero-runtime-deps line. src/table.test.ts covers Thai/Lao, Arabic, Devanagari, CJK, ZWJ/flag/skin-tone emoji, NFC-vs-NFD, ANSI, and grapheme-safe truncation. Wired into package.json and ci.yml between typecheck and build. CLAUDE.md updated — it claimed "no test runner configured" and that width was measured "via Bun.stringWidth".

Verification

bun run typecheck   ✓
bun run test        ✓  14 pass, 0 fail
bun run build       ✓

Against the real ~/.config/tabbrew/tabs.json from the report, all 14 rows now start the WIN column at display column 93 (the Thai row previously sat at 95). docs list, which has its own Thai titles, is uniform at column 71.

Confirm in a real terminal, not through a pipe — the bug is only visible when a terminal does the rendering.

🤖 Generated with Claude Code

…olumns

A Thai tab title pushed its URL/WIN/FLAGS cells two columns right of every
other row in `tabbrew tabs list`. Not a pad-on-.length bug — table.ts already
measured with Bun.stringWidth. Bun's mark table is itself wrong, in both
directions:

  า ำ  Thai/Lao SARA AA / AM (Lo)  Bun 0  terminal 1  -> over-pads, shifts right
  ि    Indic spacing matras (Mc)   Bun 0  terminal 1  -> over-pads
  harakat  Arabic/Hebrew (Mn)      Bun 1  terminal 0  -> under-pads, shifts left

The reported title carries two U+0E32, hence exactly two columns of drift.

width() now walks grapheme clusters and applies two rules: a combining mark
(Mn/Me/Cf/Cc) is always 0, and a spacing character is never 0. Emoji clusters
are still delegated to Bun, which gets ZWJ/flag/skin-tone sequences right, and
ASCII takes an s.length fast path. Fixing this one function fixes colWidth,
padEnd, padEndLink and truncate — so both `tabs list` and `docs list` align
without touching either command.

Also replace fmtTab's `title.slice(0, 33)` in the preview renderer with the
existing grapheme-aware truncate(): a code-unit cut split an emoji's surrogate
pair into a replacement char and orphaned Thai vowels from their consonants.

Adds bun test (built into Bun — still zero deps) with src/table.test.ts, wires
`bun run test` into CI, and updates the two CLAUDE.md claims this invalidates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@colevels
colevels merged commit 09ec18d into main Jul 19, 2026
1 check passed
@colevels
colevels deleted the fix/display-width-spacing-marks branch July 19, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant