From 2725e2927a1595c7b100d942d1f14146252adeb7 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 25 Jun 2026 04:46:40 -0400 Subject: feat: who, global chat, removed redundant YAML IDs --- internal/color/color.go | 42 +++++++++++++++++++++++++++++++++ internal/color/color_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 internal/color/color_test.go (limited to 'internal/color') diff --git a/internal/color/color.go b/internal/color/color.go index dcbb280..22fa143 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -180,6 +180,48 @@ func VisibleLen(s string) int { return utf8.RuneCountInString(ansiRe.ReplaceAllString(s, "")) } +// WrapANSI wraps text to the given visible width, ignoring ANSI color codes +// when measuring. Each input line (split on "\n") is handled on its own: a line +// that already fits is returned untouched, so indentation and aligned UI are +// preserved. Only over-long lines are word-wrapped. The result is joined with +// "\r\n". A width <= 0 disables wrapping. +func WrapANSI(s string, width int) string { + if width <= 0 { + return s + } + var out []string + for _, line := range strings.Split(s, "\n") { + line = strings.TrimRight(line, "\r") + out = append(out, wrapLine(line, width)...) + } + return strings.Join(out, "\r\n") +} + +func wrapLine(line string, width int) []string { + if VisibleLen(line) <= width { + return []string{line} + } + words := strings.Fields(line) + if len(words) == 0 { + return []string{line} + } + cur := words[0] + curWidth := VisibleLen(words[0]) + var lines []string + for _, w := range words[1:] { + ww := VisibleLen(w) + if curWidth+1+ww <= width { + cur += " " + w + curWidth += 1 + ww + } else { + lines = append(lines, cur) + cur = w + curWidth = ww + } + } + return append(lines, cur) +} + func ContrastFg(mode string, bgIndex int) string { r, g, b := Xterm256ToRGB(bgIndex) if r+g+b > 384 { diff --git a/internal/color/color_test.go b/internal/color/color_test.go new file mode 100644 index 0000000..c1f5e5b --- /dev/null +++ b/internal/color/color_test.go @@ -0,0 +1,55 @@ +package color + +import ( + "strings" + "testing" +) + +func TestWrapANSIDisabled(t *testing.T) { + s := "this is a very long line that would normally be wrapped if enabled" + if got := WrapANSI(s, 0); got != s { + t.Errorf("WrapANSI(_, 0) = %q, want unchanged", got) + } +} + +func TestWrapANSIShortLineUntouched(t *testing.T) { + s := " indented menu item" + if got := WrapANSI(s, 80); got != s { + t.Errorf("short line was modified: got %q, want %q", got, s) + } +} + +func TestWrapANSIWrapsLongLine(t *testing.T) { + s := "alpha bravo charlie delta echo foxtrot golf hotel india juliet" + got := WrapANSI(s, 20) + lines := strings.Split(got, "\r\n") + if len(lines) < 2 { + t.Fatalf("expected multiple lines, got %d: %q", len(lines), got) + } + for _, l := range lines { + if VisibleLen(l) > 20 { + t.Errorf("line exceeds width: %q (%d)", l, VisibleLen(l)) + } + } + if strings.Join(strings.Fields(got), " ") != s { + t.Errorf("words not preserved: %q", got) + } +} + +func TestWrapANSIIgnoresColorCodes(t *testing.T) { + colored := FgCode("xterm256", 182) + "hello world" + Reset + if VisibleLen(colored) > 80 { + t.Fatalf("test setup wrong, visible len = %d", VisibleLen(colored)) + } + if got := WrapANSI(colored, 80); got != colored { + t.Errorf("colored line within width was wrapped: %q", got) + } +} + +func TestWrapANSISplitsOnNewlines(t *testing.T) { + got := WrapANSI("first line\nsecond line", 80) + want := "first line\r\nsecond line" + if got != want { + t.Errorf("WrapANSI newline handling = %q, want %q", got, want) + } +} -- cgit v1.2.3