diff --git a/internal/textutil/text.go b/internal/textutil/text.go new file mode 100644 index 0000000..42f467f --- /dev/null +++ b/internal/textutil/text.go @@ -0,0 +1,72 @@ +// Package textutil wraps and clips plain text for a fixed terminal width. +package textutil + +import "strings" + +// Clip shortens s to at most n runes, replacing tabs and newlines with spaces. +func Clip(s string, n int) string { + if n <= 0 { + return "" + } + s = strings.ReplaceAll(strings.ReplaceAll(s, "\t", " "), "\n", " ") + r := []rune(s) + if len(r) <= n { + return string(r) + } + if n == 1 { + return "…" + } + return string(r[:n-1]) + "…" +} + +// Wrap breaks s into lines no wider than width runes, preserving blank lines. +func Wrap(s string, width int) []string { + if width <= 1 { + if s == "" { + return []string{""} + } + return []string{s} + } + var out []string + for _, para := range strings.Split(s, "\n") { + if para == "" { + out = append(out, "") + continue + } + line := "" + for _, word := range strings.Split(para, " ") { + for len([]rune(word)) > width { + if line != "" { + out = append(out, line) + line = "" + } + r := []rune(word) + out = append(out, string(r[:width])) + word = string(r[width:]) + } + switch { + case line == "": + line = word + case len([]rune(line))+1+len([]rune(word)) <= width: + line += " " + word + default: + out = append(out, line) + line = word + } + } + out = append(out, line) + } + if len(out) == 0 { + return []string{""} + } + return out +} + +// Pad pads s with spaces on the right until it is n runes wide. +func Pad(s string, n int) string { + r := []rune(s) + if len(r) >= n { + return string(r) + } + return string(r) + strings.Repeat(" ", n-len(r)) +} diff --git a/internal/textutil/text_test.go b/internal/textutil/text_test.go new file mode 100644 index 0000000..491ca4b --- /dev/null +++ b/internal/textutil/text_test.go @@ -0,0 +1,32 @@ +package textutil + +import "testing" + +func TestClip(t *testing.T) { + if got := Clip("abcdef", 4); got != "abc…" { + t.Fatalf("clip: %q", got) + } + if got := Clip("ab", 4); got != "ab" { + t.Fatalf("short: %q", got) + } + if got := Clip("a\nb", 5); got != "a b" { + t.Fatalf("newline: %q", got) + } +} + +func TestWrap(t *testing.T) { + got := Wrap("one two three", 7) + want := []string{"one two", "three"} + if len(got) != len(want) { + t.Fatalf("wrap %#v", got) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("line %d: %q", i, got[i]) + } + } + blank := Wrap("a\n\nb", 10) + if len(blank) != 3 || blank[1] != "" { + t.Fatalf("blank lines: %#v", blank) + } +}