diff --git a/progress/indicator.go b/progress/indicator.go index 7bc4179..7046d89 100644 --- a/progress/indicator.go +++ b/progress/indicator.go @@ -94,7 +94,7 @@ func indeterminateIndicatorMovingBackAndForth(indicator string) IndeterminateInd if currentPosition == 0 { direction = 1 - } else if currentPosition+text.RuneCount(indicator) == maxLen { + } else if currentPosition+text.RuneWidthWithoutEscSequences(indicator) == maxLen { direction = -1 } nextPosition += direction @@ -113,7 +113,7 @@ func indeterminateIndicatorMovingLeftToRight(indicator string) IndeterminateIndi currentPosition := nextPosition nextPosition++ - if nextPosition+text.RuneCount(indicator) > maxLen { + if nextPosition+text.RuneWidthWithoutEscSequences(indicator) > maxLen { nextPosition = 0 } @@ -129,7 +129,7 @@ func indeterminateIndicatorMovingRightToLeft(indicator string) IndeterminateIndi return func(maxLen int) IndeterminateIndicator { if nextPosition == -1 { - nextPosition = maxLen - text.RuneCount(indicator) + nextPosition = maxLen - text.RuneWidthWithoutEscSequences(indicator) } currentPosition := nextPosition nextPosition-- @@ -165,7 +165,7 @@ func indeterminateIndicatorPacMan() IndeterminateIndicatorGenerator { if currentPosition == 0 { direction = 1 indicator = pacManMovingRight - } else if currentPosition+text.RuneCount(indicator) == maxLen { + } else if currentPosition+text.RuneWidthWithoutEscSequences(indicator) == maxLen { direction = -1 indicator = pacManMovingLeft } diff --git a/progress/progress.go b/progress/progress.go index f3a9260..0e5c0a4 100644 --- a/progress/progress.go +++ b/progress/progress.go @@ -282,10 +282,10 @@ func (p *Progress) initForRender() { utf8.RuneCountInString(p.style.Chars.BoxLeft) - utf8.RuneCountInString(p.style.Chars.BoxRight) p.lengthProgressOverall = p.messageWidth + - text.RuneCount(p.style.Options.Separator) + + text.RuneWidthWithoutEscSequences(p.style.Options.Separator) + p.lengthProgress + 1 if p.style.Visibility.Percentage { - p.lengthProgressOverall += text.RuneCount(fmt.Sprintf(p.style.Options.PercentFormat, 0.0)) + p.lengthProgressOverall += text.RuneWidthWithoutEscSequences(fmt.Sprintf(p.style.Options.PercentFormat, 0.0)) } // if not output write has been set, output to STDOUT diff --git a/progress/render.go b/progress/render.go index bc1e135..6b22ea2 100644 --- a/progress/render.go +++ b/progress/render.go @@ -124,7 +124,7 @@ func (p *Progress) generateTrackerStrDeterminate(value int64, total int64, maxLe } else if pFinishedDotsFraction == 0 { pInProgress = "" } - pFinishedStrLen := text.RuneCount(pFinished + pInProgress) + pFinishedStrLen := text.RuneWidthWithoutEscSequences(pFinished + pInProgress) if pFinishedStrLen < maxLen { pUnfinished = strings.Repeat(p.style.Chars.Unfinished, maxLen-pFinishedStrLen) } @@ -144,8 +144,8 @@ func (p *Progress) generateTrackerStrIndeterminate(maxLen int) string { pUnfinished += strings.Repeat(p.style.Chars.Unfinished, indicator.Position) } pUnfinished += indicator.Text - if text.RuneCount(pUnfinished) < maxLen { - pUnfinished += strings.Repeat(p.style.Chars.Unfinished, maxLen-text.RuneCount(pUnfinished)) + if text.RuneWidthWithoutEscSequences(pUnfinished) < maxLen { + pUnfinished += strings.Repeat(p.style.Chars.Unfinished, maxLen-text.RuneWidthWithoutEscSequences(pUnfinished)) } return p.style.Colors.Tracker.Sprintf("%s%s%s", @@ -172,7 +172,7 @@ func (p *Progress) renderTracker(out *strings.Builder, t *Tracker, hint renderHi message = strings.Replace(message, "\r", "", -1) } if p.messageWidth > 0 { - messageLen := text.RuneCount(message) + messageLen := text.RuneWidthWithoutEscSequences(message) if messageLen < p.messageWidth { message = text.Pad(message, p.messageWidth, ' ') } else { diff --git a/table/render.go b/table/render.go index 379969e..796b657 100644 --- a/table/render.go +++ b/table/render.go @@ -221,7 +221,7 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) { func (t *Table) renderLineMergeOutputs(out *strings.Builder, outLine *strings.Builder) { outLineStr := outLine.String() - if text.RuneCount(outLineStr) > t.allowedRowLength { + if text.RuneWidthWithoutEscSequences(outLineStr) > t.allowedRowLength { trimLength := t.allowedRowLength - utf8.RuneCountInString(t.style.Box.UnfinishedRow) if trimLength > 0 { out.WriteString(text.Trim(outLineStr, trimLength)) @@ -358,15 +358,15 @@ func (t *Table) renderTitle(out *strings.Builder) { rowLength = t.allowedRowLength } if t.style.Options.DrawBorder { - lenBorder := rowLength - text.RuneCount(t.style.Box.TopLeft+t.style.Box.TopRight) + lenBorder := rowLength - text.RuneWidthWithoutEscSequences(t.style.Box.TopLeft+t.style.Box.TopRight) out.WriteString(t.style.Box.TopLeft) out.WriteString(text.RepeatAndTrim(t.style.Box.MiddleHorizontal, lenBorder)) out.WriteString(t.style.Box.TopRight) } - lenText := rowLength - text.RuneCount(t.style.Box.PaddingLeft+t.style.Box.PaddingRight) + lenText := rowLength - text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft+t.style.Box.PaddingRight) if t.style.Options.DrawBorder { - lenText -= text.RuneCount(t.style.Box.Left + t.style.Box.Right) + lenText -= text.RuneWidthWithoutEscSequences(t.style.Box.Left + t.style.Box.Right) } titleText := text.WrapText(t.title, lenText) for _, titleLine := range strings.Split(titleText, "\n") { diff --git a/table/style.go b/table/style.go index d6e2ed3..f440672 100644 --- a/table/style.go +++ b/table/style.go @@ -358,7 +358,7 @@ var ( BottomLeft: "+", BottomRight: "+", BottomSeparator: "+", - EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("+")), + EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("+")), Left: "|", LeftSeparator: "+", MiddleHorizontal: "-", @@ -389,7 +389,7 @@ var ( BottomLeft: "┗", BottomRight: "┛", BottomSeparator: "┻", - EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("╋")), + EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("╋")), Left: "┃", LeftSeparator: "┣", MiddleHorizontal: "━", @@ -420,7 +420,7 @@ var ( BottomLeft: "╚", BottomRight: "╝", BottomSeparator: "╩", - EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("╬")), + EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("╬")), Left: "║", LeftSeparator: "╠", MiddleHorizontal: "═", @@ -451,7 +451,7 @@ var ( BottomLeft: "└", BottomRight: "┘", BottomSeparator: "┴", - EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("┼")), + EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("┼")), Left: "│", LeftSeparator: "├", MiddleHorizontal: "─", @@ -482,7 +482,7 @@ var ( BottomLeft: "╰", BottomRight: "╯", BottomSeparator: "┴", - EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("┼")), + EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("┼")), Left: "│", LeftSeparator: "├", MiddleHorizontal: "─", @@ -513,7 +513,7 @@ var ( BottomLeft: "\\", BottomRight: "/", BottomSeparator: "v", - EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("+")), + EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("+")), Left: "[", LeftSeparator: "{", MiddleHorizontal: "--", diff --git a/table/table.go b/table/table.go index eadf9b7..03967d4 100644 --- a/table/table.go +++ b/table/table.go @@ -550,9 +550,9 @@ func (t *Table) getFormat(hint renderHint) text.Format { func (t *Table) getMaxColumnLengthForMerging(colIdx int) int { maxColumnLength := t.maxColumnLengths[colIdx] - maxColumnLength += text.RuneCount(t.style.Box.PaddingRight + t.style.Box.PaddingLeft) + maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingRight + t.style.Box.PaddingLeft) if t.style.Options.SeparateColumns { - maxColumnLength += text.RuneCount(t.style.Box.EmptySeparator) + maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.EmptySeparator) } return maxColumnLength } @@ -781,24 +781,24 @@ func (t *Table) initForRenderRowsStringify(rows []Row, hint renderHint) []rowStr func (t *Table) initForRenderRowSeparator() { t.maxRowLength = 0 if t.autoIndex { - t.maxRowLength += text.RuneCount(t.style.Box.PaddingLeft) + t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft) t.maxRowLength += len(fmt.Sprint(len(t.rows))) - t.maxRowLength += text.RuneCount(t.style.Box.PaddingRight) + t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingRight) if t.style.Options.SeparateColumns { - t.maxRowLength += text.RuneCount(t.style.Box.MiddleSeparator) + t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator) } } if t.style.Options.SeparateColumns { - t.maxRowLength += text.RuneCount(t.style.Box.MiddleSeparator) * (t.numColumns - 1) + t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator) * (t.numColumns - 1) } t.rowSeparator = make(rowStr, t.numColumns) for colIdx, maxColumnLength := range t.maxColumnLengths { - maxColumnLength += text.RuneCount(t.style.Box.PaddingLeft + t.style.Box.PaddingRight) + maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight) t.maxRowLength += maxColumnLength t.rowSeparator[colIdx] = text.RepeatAndTrim(t.style.Box.MiddleHorizontal, maxColumnLength) } if t.style.Options.DrawBorder { - t.maxRowLength += text.RuneCount(t.style.Box.Left + t.style.Box.Right) + t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.Left + t.style.Box.Right) } } diff --git a/text/align.go b/text/align.go index dd4134d..ade9ded 100644 --- a/text/align.go +++ b/text/align.go @@ -28,7 +28,7 @@ const ( func (a Align) Apply(text string, maxLength int) string { text = a.trimString(text) sLen := utf8.RuneCountInString(text) - sLenWoE := RuneCount(text) + sLenWoE := RuneWidthWithoutEscSequences(text) numEscChars := sLen - sLenWoE // now, align the text diff --git a/text/ansi.go b/text/ansi.go index 1ea5cdf..aaf5b30 100644 --- a/text/ansi.go +++ b/text/ansi.go @@ -37,7 +37,7 @@ func Escape(str string, escapeSeq string) string { // StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m") == "Nymeria Ghost Lady" func StripEscape(str string) string { var out strings.Builder - out.Grow(RuneCount(str)) + out.Grow(RuneWidthWithoutEscSequences(str)) isEscSeq := false for _, sChr := range str { diff --git a/text/string.go b/text/string.go index 877f04a..2de9e40 100644 --- a/text/string.go +++ b/text/string.go @@ -27,7 +27,7 @@ func InsertEveryN(str string, runeToInsert rune, n int) string { return str } - sLen := RuneCount(str) + sLen := RuneWidthWithoutEscSequences(str) var out strings.Builder out.Grow(sLen + (sLen / n)) outLen, isEscSeq := 0, false @@ -88,7 +88,7 @@ func LongestLineLen(str string) int { // Pad("Ghost", 7, ' ') == "Ghost " // Pad("Ghost", 10, '.') == "Ghost....." func Pad(str string, maxLen int, paddingChar rune) string { - strLen := RuneCount(str) + strLen := RuneWidthWithoutEscSequences(str) if strLen < maxLen { str += strings.Repeat(string(paddingChar), maxLen-strLen) } @@ -118,7 +118,30 @@ func RepeatAndTrim(str string, maxRunes int) string { // RuneCount("Ghost") == 5 // RuneCount("\x1b[33mGhost\x1b[0m") == 5 // RuneCount("\x1b[33mGhost\x1b[0") == 5 +// Deprecated: in favor of RuneWidthWithoutEscSequences func RuneCount(str string) int { + return RuneWidthWithoutEscSequences(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('A') == 1 +// RuneWidth('ツ') == 2 +// RuneWidth('⊙') == 1 +// RuneWidth('︿') == 2 +// RuneWidth(0x27) == 0 +func RuneWidth(r rune) int { + return runewidth.RuneWidth(r) +} + +// RuneWidthWithoutEscSequences is similar to RuneWidth, except for the fact +// that it ignores escape sequences while counting. For ex.: +// RuneWidthWithoutEscSequences("") == 0 +// RuneWidthWithoutEscSequences("Ghost") == 5 +// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m") == 5 +// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0") == 5 +func RuneWidthWithoutEscSequences(str string) int { count, isEscSeq := 0, false for _, c := range str { if c == EscapeStartRune { @@ -134,18 +157,6 @@ func RuneCount(str string) int { return count } -// RuneWidth returns the mostly accurate character-width of the rune. This is -// not 100% accurate as the character width is usually dependant on the -// typeface (font) used in the console/terminal. For ex.: -// RuneWidth('A') == 1 -// RuneWidth('ツ') == 2 -// RuneWidth('⊙') == 1 -// RuneWidth('︿') == 2 -// RuneWidth(0x27) == 0 -func RuneWidth(r rune) int { - return runewidth.RuneWidth(r) -} - // Snip returns the given string with a fixed length. For ex.: // Snip("Ghost", 0, "~") == "Ghost" // Snip("Ghost", 1, "~") == "~" @@ -155,9 +166,9 @@ func RuneWidth(r rune) int { // Snip("\x1b[33mGhost\x1b[0m", 7, "~") == "\x1b[33mGhost\x1b[0m " func Snip(str string, length int, snipIndicator string) string { if length > 0 { - lenStr := RuneCount(str) + lenStr := RuneWidthWithoutEscSequences(str) if lenStr > length { - lenStrFinal := length - RuneCount(snipIndicator) + lenStrFinal := length - RuneWidthWithoutEscSequences(snipIndicator) return Trim(str, lenStrFinal) + snipIndicator } } diff --git a/text/string_test.go b/text/string_test.go index dad5452..b412f90 100644 --- a/text/string_test.go +++ b/text/string_test.go @@ -174,6 +174,28 @@ func TestRuneWidth(t *testing.T) { assert.Equal(t, 0, RuneWidth(rune(27))) // ANSI escape sequence } +func ExampleRuneWidthWithoutEscSequences() { + fmt.Printf("RuneWidthWithoutEscSequences(\"\"): %d\n", RuneWidthWithoutEscSequences("")) + fmt.Printf("RuneWidthWithoutEscSequences(\"Ghost\"): %d\n", RuneWidthWithoutEscSequences("Ghost")) + fmt.Printf("RuneWidthWithoutEscSequences(\"Ghostツ\"): %d\n", RuneWidthWithoutEscSequences("Ghostツ")) + fmt.Printf("RuneWidthWithoutEscSequences(\"\\x1b[33mGhost\\x1b[0m\"): %d\n", RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m")) + fmt.Printf("RuneWidthWithoutEscSequences(\"\\x1b[33mGhost\\x1b[0\"): %d\n", RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0")) + + // Output: RuneWidthWithoutEscSequences(""): 0 + // RuneWidthWithoutEscSequences("Ghost"): 5 + // RuneWidthWithoutEscSequences("Ghostツ"): 7 + // RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m"): 5 + // RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0"): 5 +} + +func TestRuneWidthWithoutEscSequences(t *testing.T) { + assert.Equal(t, 0, RuneWidthWithoutEscSequences("")) + assert.Equal(t, 5, RuneWidthWithoutEscSequences("Ghost")) + assert.Equal(t, 7, RuneWidthWithoutEscSequences("Ghostツ")) + assert.Equal(t, 5, RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m")) + assert.Equal(t, 5, RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0")) +} + func ExampleSnip() { fmt.Printf("Snip(\"Ghost\", 0, \"~\"): %#v\n", Snip("Ghost", 0, "~")) fmt.Printf("Snip(\"Ghost\", 1, \"~\"): %#v\n", Snip("Ghost", 1, "~")) diff --git a/text/wrap.go b/text/wrap.go index ca15276..a55cb51 100644 --- a/text/wrap.go +++ b/text/wrap.go @@ -201,7 +201,7 @@ func wrapHard(paragraph string, wrapLen int, out *strings.Builder) { lineLen++ } - wordLen := RuneCount(word) + wordLen := RuneWidthWithoutEscSequences(word) if lineLen+wordLen <= wrapLen { // word fits within the line out.WriteString(word) lineLen += wordLen @@ -227,7 +227,7 @@ func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) { } spacing, spacingLen := wrapSoftSpacing(lineLen) - wordLen := RuneCount(word) + wordLen := RuneWidthWithoutEscSequences(word) if lineLen+spacingLen+wordLen <= wrapLen { // word fits within the line out.WriteString(spacing) out.WriteString(word)