Repository navigation
text: apply the box drawing width override in StringWidth - #429
yu2971512385-ui wants to merge 1 commit into
Conversation
RuneWidth treats box drawing (U+2500-U+257F) and block element
(U+2580-U+259F) characters as one cell wide regardless of locale, and
StringWidthWithoutEscSequences inherits that because it sums RuneWidth
over the string. StringWidth calls runewidth directly, so in East Asian
locales (where go-runewidth sets EastAsianWidth from LANG/LC_ALL) it
reports those characters as two cells: StringWidth("┌──┐") returns 8
while StringWidthWithoutEscSequences("┌──┐") returns 4.
Apply the same correction in StringWidth, keeping the grapheme cluster
aware count from runewidth for everything else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Coverage Report for CI Build 36663913458Coverage remained the same at 100.0%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
|
Hey @yu2971512385-ui, thanks for the PR, the fix and tests look good. I found one small edge case, if you'd like to tighten it up. runewidth's StringWidth counts width per grapheme cluster, using only the first visible character in each cluster. The correction loop here subtracts 1 for every box-drawing character in the string, including ones runewidth never counted. That happens when a "prepend" character such as U+0600 (Arabic number sign) comes right before a box-drawing character, because they form one cluster: text.OverrideRuneWidthEastAsianWidth(true)
text.StringWidth("aツ\u0600─") // 3, expected 4It's rare, but a cleaner fix is to count per grapheme cluster with RuneWidth directly. That matches runewidth's own loop, plus the box-drawing rule. uniseg is already imported in string.go: func StringWidth(str string) (width int) {
g := uniseg.NewGraphemes(str)
for g.Next() {
for _, r := range g.Runes() {
if w := RuneWidth(r); w > 0 {
width += w
break
}
}
}
return
}This also removes the need for the Also, you'd have to rebase your branch to the latest main. |



Problem
RuneWidthreports box drawing (U+2500-U+257F) and block element (U+2580-U+259F) characters as one cell wide regardless of locale, and its doc comment says so explicitly ("always treated as width 1, regardless of locale").StringWidthWithoutEscSequencesinherits that, because it sumsRuneWidthover the string.StringWidthdoes not: it forwards straight torwCondition.StringWidth, which has no such override. go-runewidth initialisesEastAsianWidthfromLANG/LC_ALL/RUNEWIDTH_EASTASIAN, so on a machine with e.g.LANG=zh_CN.UTF-8the two functions disagree:This looks like an oversight when #220 / #204 were fixed: the override was added to
RuneWidth(and so toStringWidthWithoutEscSequences) but not to the siblingStringWidth, which is public API and documented as "similar to RuneWidth, except it works on a string". The renderers all useStringWidthWithoutEscSequences, so table/progress output is unaffected — callers ofStringWidthare.Fix
Apply the same correction in
StringWidth, and only whenEastAsianWidthis set, so the common path is unchanged. The count still comes fromrwCondition.StringWidth, which is grapheme cluster aware, so emoji and combining marks are unaffected. The range check is factored intoisSingleCellRuneand shared withRuneWidth.StringWidth("┌──┐"), EastAsianWidth=trueStringWidth("┌──┐"), EastAsianWidth=falseStringWidth("│ツ│"), EastAsianWidth=trueStringWidth("Ghost 生命")Tests
TestOverrideRuneWidthEastAsianWidthnow assertsStringWidthagrees withStringWidthWithoutEscSequencesfor both settings, plus a case inTestStringWidth. Verified the new assertions fail onmain(expected: 1, actual: 2) and pass with the change;go test ./...is green.AI assistance was used to help find and fix this; I have reviewed and tested every line, and the failing/passing runs above were done locally.
🤖 Generated with Claude Code