Repository navigation
fix(text): truncate by grapheme cluster, like DisplayWidth measures - #314
Closed
yu2971512385-ui wants to merge 1 commit into
Closed
yu2971512385-ui wants to merge 1 commit into
yu2971512385-ui wants to merge 1 commit into
Conversation
Truncate asks DisplayWidth whether a string is too wide, then hands the cut to reflow's truncate, which measures rune by rune. The two disagree on anything built from more than one rune: lipgloss.Width reads an emoji ZWJ sequence as the two columns a terminal actually draws, while a rune-wise walk reads it as two columns per person in it. The result is that Truncate stops short of the budget it was given. A table cell holding "👨👩👧👦 family names in the author field" gives back six fewer columns than asked for, so that row's later columns sit six places right of every other row's. A lone regional indicator pair goes the other way and overshoots: Truncate(1, "🇯🇵") returned a half flag two columns wide. cli#159 moved DisplayWidth onto lipgloss for exactly this reason; this moves the matching cut onto the same width model. ansi.Truncate comes from charmbracelet/x/ansi, which is what lipgloss.Width already calls, so the two can no longer drift apart. It was already in the module graph as an indirect dependency. Every existing case in TestTruncate passes unchanged, including the Japanese, Chinese, Korean, accented and ANSI-coloured ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
yu2971512385-ui
requested review from
sergiou87
and removed request for
a team
October 10, 2026 16:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Truncatedecides whether a string needs cutting withDisplayWidth(
lipgloss.Width, which measures grapheme clusters), then performs the cut withreflow/truncate, which measures rune by rune. The two disagree about anythingassembled from more than one rune, so the result misses the budget it was given.
The symptom
A table at a 46-column terminal width, rendered with
pkg/tableprinter:A terminal draws
👨👩👧👦in two columns, andlipgloss.Widthagrees.A rune-wise walk sees four wide emoji joined by zero-width joiners and charges
eight. So
Truncatehands back six fewer columns than it was asked for, and theopencolumn on that row lands six places right of where it does on every otherrow.
The error runs the other way for regional indicator pairs, which are two runes
of width 1 each but one two-column glyph:
The change
ansi.Truncatecomes fromcharmbracelet/x/ansi, which is whatlipgloss.Widthalready calls to produce the number
DisplayWidthreturns, so the measurementand the cut can no longer drift apart. It was already in the module graph as an
indirect dependency, and it handles the ANSI-escape preservation
reflowwasdoing (
reflowstays in the graph viaglamour).This finishes what #159 started: that PR moved
DisplayWidthontolipglosstofix cli/cli#9018, but left the cut itself on the old width model.
Tests
Every existing case in
TestTruncatepasses unchanged — including the Japanese,Chinese, Korean, accented-character and ANSI-coloured ones — plus two new cases
for a ZWJ sequence and a regional indicator pair.
TestTruncateFillsTheBudgetis the invariant underneath all of them: across acorpus of ASCII, CJK, emoji, combining-mark and coloured strings at every width
from 1 to 24,
DisplayWidth(Truncate(n, s))is never aboven, and is exactlynwhenever the input was genuinely too wide. It records 97 assertion failureson trunk and none after the change.
🤖 Generated with Claude Code