aboutsummaryrefslogtreecommitdiff
path: root/cmd/krino/colour.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/krino/colour.go')
-rw-r--r--cmd/krino/colour.go109
1 files changed, 109 insertions, 0 deletions
diff --git a/cmd/krino/colour.go b/cmd/krino/colour.go
new file mode 100644
index 0000000..ee9055c
--- /dev/null
+++ b/cmd/krino/colour.go
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "unicode/utf8"
+
+ "krino/internal/plan"
+ "krino/internal/tui"
+)
+
+// palette styles text for a terminal (spec §8.2) with SGR codes from the
+// 16-colour ANSI palette plus bold and faint only, so the terminal's own
+// theme decides what they look like. The zero value is plain: every method
+// returns its text unchanged, which keeps output with colour off
+// byte-identical to what krino printed before colour existed.
+type palette struct{ on bool }
+
+func (p palette) style(code, s string) string {
+ if !p.on || s == "" {
+ return s
+ }
+ return "\x1b[" + code + "m" + s + "\x1b[0m"
+}
+
+func (p palette) bold(s string) string { return p.style("1", s) }
+func (p palette) faint(s string) string { return p.style("2", s) }
+func (p palette) bad(s string) string { return p.style("31", s) }
+func (p palette) good(s string) string { return p.style("32", s) }
+func (p palette) warn(s string) string { return p.style("33", s) }
+func (p palette) rule(s string) string { return p.style("34", s) }
+func (p palette) alarm(s string) string { return p.style("1;31", s) }
+
+// keys bolds every one-character [x] key in a prompt line.
+func (p palette) keys(menu string) string {
+ if !p.on {
+ return menu
+ }
+ var b strings.Builder
+ for i := 0; i < len(menu); {
+ if menu[i] == '[' && i+2 < len(menu) && menu[i+2] == ']' {
+ b.WriteString(p.bold(menu[i : i+3]))
+ i += 3
+ continue
+ }
+ b.WriteByte(menu[i])
+ i++
+ }
+ return b.String()
+}
+
+// padStyled styles s and pads it to w runes measured on s itself, so the
+// escape bytes never count toward the column's width.
+func padStyled(s string, w int, style func(string) string) string {
+ pad := w - utf8.RuneCountInString(s)
+ if pad < 0 {
+ pad = 0
+ }
+ return style(s) + strings.Repeat(" ", pad)
+}
+
+// styleAction styles an action cell actionCell rendered for s: a skipped
+// step faint throughout; otherwise only its leading kind word, green for
+// copy, move and rename, yellow for trash, bold red for DELETE permanently.
+func styleAction(p palette, s plan.Step, cell string) string {
+ if s.Skip != "" {
+ return p.faint(cell)
+ }
+ word := s.Kind.String()
+ if !strings.HasPrefix(cell, word) {
+ return cell
+ }
+ var styled string
+ switch s.Kind {
+ case plan.Trash:
+ styled = p.warn(word)
+ case plan.DeletePermanent:
+ styled = p.alarm(word)
+ default:
+ styled = p.good(word)
+ }
+ return styled + cell[len(word):]
+}
+
+// outcome renders the "N applied · N failed · N declined" line: the applied
+// count green and the failed count red, each only when above 0.
+func outcome(p palette, applied, failed, declined int) string {
+ a, f := fmt.Sprint(applied), fmt.Sprint(failed)
+ if applied > 0 {
+ a = p.good(a)
+ }
+ if failed > 0 {
+ f = p.bad(f)
+ }
+ return fmt.Sprintf("%s applied · %s failed · %d declined", a, f, declined)
+}
+
+// colourPolicy is tui.Colour (terminal, and NO_COLOR unset); tests replace
+// it, since no test runs on a terminal.
+var colourPolicy = tui.Colour
+
+// colourOn reports whether output to w is coloured: never with --no-color,
+// otherwise as colourPolicy decides.
+func colourOn(g *globals, w io.Writer) bool {
+ return !g.noColor && colourPolicy(w)
+}