diff --git a/pkg/cli/stream.go b/pkg/cli/stream.go index e12aa4b..42c5aed 100644 --- a/pkg/cli/stream.go +++ b/pkg/cli/stream.go @@ -6,7 +6,8 @@ import ( "os" "strings" "sync" - "unicode/utf8" + + "github.com/mattn/go-runewidth" ) // StreamOption configures a Stream. @@ -60,11 +61,11 @@ func (s *Stream) Write(text string) { if s.wrap <= 0 { fmt.Fprint(s.out, text) - // Track column across newlines for Done() trailing-newline logic. + // Track visible width across newlines for Done() trailing-newline logic. if idx := strings.LastIndex(text, "\n"); idx >= 0 { - s.col = utf8.RuneCountInString(text[idx+1:]) + s.col = runewidth.StringWidth(text[idx+1:]) } else { - s.col += utf8.RuneCountInString(text) + s.col += runewidth.StringWidth(text) } return } @@ -76,13 +77,14 @@ func (s *Stream) Write(text string) { continue } - if s.col >= s.wrap { + rw := runewidth.RuneWidth(r) + if rw > 0 && s.col > 0 && s.col+rw > s.wrap { fmt.Fprintln(s.out) s.col = 0 } fmt.Fprint(s.out, string(r)) - s.col++ + s.col += rw } } diff --git a/pkg/cli/stream_test.go b/pkg/cli/stream_test.go index 822a13c..6937547 100644 --- a/pkg/cli/stream_test.go +++ b/pkg/cli/stream_test.go @@ -99,6 +99,14 @@ func TestStream_Good(t *testing.T) { assert.Equal(t, 11, s.Column()) }) + t.Run("column tracking uses visible width", func(t *testing.T) { + var buf bytes.Buffer + s := NewStream(WithStreamOutput(&buf)) + + s.Write("東京") + assert.Equal(t, 4, s.Column()) + }) + t.Run("WriteFrom io.Reader", func(t *testing.T) { var buf bytes.Buffer s := NewStream(WithStreamOutput(&buf)) @@ -144,6 +152,17 @@ func TestStream_Good(t *testing.T) { assert.Equal(t, "text\n", buf.String()) // no double newline }) + + t.Run("word wrap uses visible width", func(t *testing.T) { + var buf bytes.Buffer + s := NewStream(WithWordWrap(4), WithStreamOutput(&buf)) + + s.Write("東京A") + s.Done() + s.Wait() + + assert.Equal(t, "東京\nA\n", buf.String()) + }) } func TestStream_Bad(t *testing.T) {