feat(textutil): add width-aware text wrapping and clipping
Wrap, Clip, and Pad plain text to a fixed rune width for the terminal panes.
This commit is contained in:
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user