fix(cli): make stream width handling rune-safe
Some checks are pending
Security Scan / security (push) Waiting to run

This commit is contained in:
Virgil 2026-04-02 05:13:07 +00:00
parent cf9c068650
commit e29b6e4889
2 changed files with 27 additions and 6 deletions

View file

@ -6,7 +6,8 @@ import (
"os" "os"
"strings" "strings"
"sync" "sync"
"unicode/utf8"
"github.com/mattn/go-runewidth"
) )
// StreamOption configures a Stream. // StreamOption configures a Stream.
@ -60,11 +61,11 @@ func (s *Stream) Write(text string) {
if s.wrap <= 0 { if s.wrap <= 0 {
fmt.Fprint(s.out, text) 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 { if idx := strings.LastIndex(text, "\n"); idx >= 0 {
s.col = utf8.RuneCountInString(text[idx+1:]) s.col = runewidth.StringWidth(text[idx+1:])
} else { } else {
s.col += utf8.RuneCountInString(text) s.col += runewidth.StringWidth(text)
} }
return return
} }
@ -76,13 +77,14 @@ func (s *Stream) Write(text string) {
continue 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) fmt.Fprintln(s.out)
s.col = 0 s.col = 0
} }
fmt.Fprint(s.out, string(r)) fmt.Fprint(s.out, string(r))
s.col++ s.col += rw
} }
} }

View file

@ -99,6 +99,14 @@ func TestStream_Good(t *testing.T) {
assert.Equal(t, 11, s.Column()) 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) { t.Run("WriteFrom io.Reader", func(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
s := NewStream(WithStreamOutput(&buf)) s := NewStream(WithStreamOutput(&buf))
@ -144,6 +152,17 @@ func TestStream_Good(t *testing.T) {
assert.Equal(t, "text\n", buf.String()) // no double newline 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) { func TestStream_Bad(t *testing.T) {