aboutsummaryrefslogtreecommitdiff
path: root/cmd/krino/render.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/krino/render.go')
-rw-r--r--cmd/krino/render.go71
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
}
}