Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions text/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}

Expand Down
36 changes: 25 additions & 11 deletions text/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down