diff --git a/text/string.go b/text/string.go index ab5e293..fa3e4f1 100644 --- a/text/string.go +++ b/text/string.go @@ -76,17 +76,20 @@ func LongestLineLen(str string) int { return maxLength } -// OverrideRuneWidthEastAsianWidth can *probably* help with alignment, and -// length calculation issues when dealing with Unicode character-set and a -// non-English language set in the LANG variable. +// OverrideRuneWidthEastAsianWidth overrides the East Asian width detection in +// the runewidth library. This is primarily for advanced use cases. // -// Set this to 'false' to force the "runewidth" library to pretend to deal with -// English character-set. Be warned that if the text/content you are dealing -// with contains East Asian character-set, this may result in unexpected -// behavior. +// Box drawing (U+2500-U+257F) and block element (U+2580-U+259F) characters +// are automatically handled and always reported as width 1, regardless of +// this setting, fixing alignment issues that previously required setting this +// to false. // -// References: -// * https://github.com/mattn/go-runewidth/issues/64#issuecomment-1221642154 +// Setting this to false forces runewidth to treat all characters as if in an +// English locale. Warning: this may cause East Asian characters (Chinese, +// Japanese, Korean) to be incorrectly reported as width 1 instead of 2. +// +// See: +// * https://github.com/mattn/go-runewidth/issues/64 // * https://github.com/jedib0t/go-pretty/issues/220 // * https://github.com/jedib0t/go-pretty/issues/204 func OverrideRuneWidthEastAsianWidth(val bool) { @@ -184,16 +187,28 @@ func RuneCount(str string) int { return StringWidthWithoutEscSequences(str) } -// RuneWidth returns the mostly accurate character-width of the rune. This is -// not 100% accurate as the character width is usually dependent on the -// typeface (font) used in the console/terminal. For ex.: +// RuneWidth returns the display width of a rune. Width accuracy depends on +// the terminal font, as character width is font-dependent. Examples: // // RuneWidth('A') == 1 // RuneWidth('ツ') == 2 // RuneWidth('⊙') == 1 // RuneWidth('︿') == 2 // RuneWidth(0x27) == 0 +// +// Box drawing (U+2500-U+257F) and block element (U+2580-U+259F) characters +// are always treated as width 1, regardless of locale, to ensure proper +// alignment in tables and progress indicators. This fixes incorrect width 2 +// reporting in East Asian locales (e.g., LANG=zh_CN.UTF-8). +// +// See: +// * https://github.com/mattn/go-runewidth/issues/64 +// * https://github.com/jedib0t/go-pretty/issues/220 +// * https://github.com/jedib0t/go-pretty/issues/204 func RuneWidth(r rune) int { + if (r >= 0x2500 && r <= 0x257F) || (r >= 0x2580 && r <= 0x259F) { + return 1 + } return rwCondition.RuneWidth(r) } diff --git a/text/string_test.go b/text/string_test.go index 2c45f4d..7a3a212 100644 --- a/text/string_test.go +++ b/text/string_test.go @@ -94,19 +94,33 @@ func TestOverrideRuneWidthEastAsianWidth(t *testing.T) { rwCondition.EastAsianWidth = originalValue }() + // Box drawing characters (U+2500-U+257F) are now always reported as width 1, + // regardless of the EastAsianWidth setting. This fixes alignment issues + // that previously occurred when LANG was set to something like 'zh_CN.UTF-8'. + // Previously, '╋' would be reported as width 2 when EastAsianWidth was true. OverrideRuneWidthEastAsianWidth(true) - assert.Equal(t, 2, StringWidthWithoutEscSequences("╋")) + assert.Equal(t, 1, StringWidthWithoutEscSequences("╋"), "Box drawing character should always be width 1, even when EastAsianWidth is true") OverrideRuneWidthEastAsianWidth(false) - assert.Equal(t, 1, StringWidthWithoutEscSequences("╋")) - - // Note for posterity. We want the length of the box drawing character to - // be reported as 1. However, with an environment where LANG is set to - // something like 'zh_CN.UTF-8', the value being returned is 2, which breaks - // text alignment/padding logic in this library. - // - // If a future version of runewidth is able to address this internally and - // return 1 for the above, the function being tested can be marked for - // deprecation. + assert.Equal(t, 1, StringWidthWithoutEscSequences("╋"), "Box drawing character should always be width 1, even when EastAsianWidth is false") + + // Verify that block elements (U+2580-U+259F) are also handled correctly. + // These are used in progress indicators and should always be width 1. + OverrideRuneWidthEastAsianWidth(true) + assert.Equal(t, 1, StringWidthWithoutEscSequences("█"), "Block element should always be width 1, even when EastAsianWidth is true") + assert.Equal(t, 1, StringWidthWithoutEscSequences("▒"), "Block element should always be width 1, even when EastAsianWidth is true") + assert.Equal(t, 1, StringWidthWithoutEscSequences("▓"), "Block element should always be width 1, even when EastAsianWidth is true") + OverrideRuneWidthEastAsianWidth(false) + assert.Equal(t, 1, StringWidthWithoutEscSequences("█"), "Block element should always be width 1, even when EastAsianWidth is false") + assert.Equal(t, 1, StringWidthWithoutEscSequences("▒"), "Block element should always be width 1, even when EastAsianWidth is false") + assert.Equal(t, 1, StringWidthWithoutEscSequences("▓"), "Block element should always be width 1, even when EastAsianWidth is false") + + // Verify that actual East Asian characters are still handled correctly. + // Note: The runewidth library reports 'ツ' as width 2 regardless of the + // EastAsianWidth setting, as it's inherently a full-width character. + OverrideRuneWidthEastAsianWidth(true) + assert.Equal(t, 2, StringWidthWithoutEscSequences("ツ"), "East Asian character should be width 2") + OverrideRuneWidthEastAsianWidth(false) + assert.Equal(t, 2, StringWidthWithoutEscSequences("ツ"), "East Asian character should be width 2") } func ExamplePad() {