diff options
Diffstat (limited to 'cmd/krino/render.go')
| -rw-r--r-- | cmd/krino/render.go | 71 |
1 files changed, 57 insertions, 14 deletions
diff --git a/cmd/krino/render.go b/cmd/krino/render.go index 50e52b0..7205488 100644 --- a/cmd/krino/render.go +++ b/cmd/krino/render.go @@ -8,7 +8,9 @@ import ( "path/filepath" "strconv" "strings" - "unicode/utf8" + "unicode" + + "golang.org/x/text/width" "krino/internal/engine" "krino/internal/plan" @@ -86,6 +88,14 @@ func printPlan(w io.Writer, dp *engine.DirPlan, verbose bool, p palette, width i fmt.Fprintln(w, "skipped") printSkipped(w, r.Skipped) } + if len(r.Unscanned) > 0 { + fmt.Fprintln(w) + fmt.Fprintln(w, "not scanned (a rule's destination)") + for _, dir := range r.Unscanned { + rel, _ := relToRoot(dp.Dir.Root, dir) + fmt.Fprintf(w, " %s/\n", display(rel)) + } + } } } @@ -175,7 +185,9 @@ func printBlocks(w io.Writer, chains []plan.Chain, root string, p palette, width i++ fmt.Fprintln(w) head := " " + padLeft(strconv.Itoa(i), numW) + " " - for _, l := range wrapped(head, display(c.File.Rel), indent, width, plainText) { + // A long name continues at the value column, never at the label + // column, so its text cannot pass for a step line (triage 28l). + for _, l := range wrapped(head, display(c.File.Rel), indent+labelWidth+1, width, plainText) { fmt.Fprintln(w, l) } for _, l := range stepLines(c, indent, root, p, width) { @@ -253,12 +265,12 @@ func field(indent int, label, value string, width int, styleLabel, styleValue fu if value == "" { return []string{lead + styleLabel(label)} } - pad := labelWidth - utf8.RuneCountInString(label) + pad := labelWidth - cols(label) if pad < 0 { pad = 0 } head := lead + styleLabel(label) + strings.Repeat(" ", pad) + " " - valueCol := indent + utf8.RuneCountInString(label) + pad + 1 + valueCol := indent + cols(label) + pad + 1 return wrapped(head, value, valueCol, width, styleValue) } @@ -281,21 +293,30 @@ func wrapped(head, text string, col, width int, style func(string) string) []str return out } -// wrapText splits s into pieces of at most max runes. Each break falls just -// after the last space, "/", "_" or "-" in the second half of the piece, -// or exactly at max when there is none, so a long word is cut rather than -// overflowing. Every rune of s is in exactly one piece, in order: joining -// the pieces gives s back. +// wrapText splits s into pieces of at most max terminal columns (cols). +// Each break falls just after the last space, "/", "_" or "-" in the second +// half of the piece, or at the last rune that fits when there is none, so a +// long word is cut rather than overflowing. Every rune of s is in exactly +// one piece, in order: joining the pieces gives s back. func wrapText(s string, max int) []string { r := []rune(s) var out []string - for len(r) > max { - cut := max - for i := max; i > max/2; i-- { + for cols(string(r)) > max { + fit, used := 0, 0 // runes that fit in max columns + for fit < len(r) && used+runeCols(r[fit]) <= max { + used += runeCols(r[fit]) + fit++ + } + if fit == 0 { + fit = 1 // a rune wider than max still goes somewhere + } + cut, at := fit, used + for i := fit; i > 0 && at > max/2; i-- { if c := r[i-1]; c == ' ' || c == '/' || c == '_' || c == '-' { cut = i break } + at -= runeCols(r[i-1]) } out = append(out, string(r[:cut])) r = r[cut:] @@ -303,6 +324,28 @@ func wrapText(s string, max int) []string { return append(out, string(r)) } +// cols is how many terminal columns s takes: two for a wide or full-width +// character (CJK), none for a combining mark or format character, one for +// the rest (triage 28j). +func cols(s string) int { + n := 0 + for _, r := range s { + n += runeCols(r) + } + return n +} + +func runeCols(r rune) int { + if unicode.In(r, unicode.Mn, unicode.Me, unicode.Cf) { + return 0 + } + switch width.LookupRune(r).Kind() { + case width.EastAsianWide, width.EastAsianFullwidth: + return 2 + } + return 1 +} + // destText renders a copy/move/rename step's destination, per spec ยง8.2: // for rename, just the new base name. For copy and move, a directory with // a trailing "/" so it reads as one - root-relative when it lies inside @@ -350,7 +393,7 @@ func relToRoot(root, dir string) (rel string, ok bool) { // block and table row numbers - every other column reads left-aligned, per // padCell. func padLeft(s string, w int) string { - n := utf8.RuneCountInString(s) + n := cols(s) if n >= w { return s } @@ -363,7 +406,7 @@ func padLeft(s string, w int) string { func colWidth(ss []string, max int) int { w := 0 for _, s := range ss { - if n := utf8.RuneCountInString(s); n > w { + if n := cols(s); n > w { w = n } } |
