diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 11:05:13 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 11:05:13 +0200 |
| commit | adc3410395771609d2db5ee5ae2b9da71115c5ca (patch) | |
| tree | 453def41e18f607332be1088920f9d0c563603a3 /cmd/krino/colour.go | |
| parent | 0468ce38470aa3ae8092b92d4f77e72d25dfa108 (diff) | |
| download | krino-adc3410395771609d2db5ee5ae2b9da71115c5ca.tar.gz krino-adc3410395771609d2db5ee5ae2b9da71115c5ca.zip | |
krino: coloured output, and --no-color
One palette type styles the plan table, warnings, headers, outcome counts,
prompt keys, undo's refused steps and krino log's (undone), from the
16-colour ANSI palette plus bold and faint only. Widths are measured on the
plain text, so columns line up; with colour off the output is unchanged.
--no-color works before or after any subcommand, as NO_COLOR does. The two
search-and-replace colourings are gone.
Diffstat (limited to 'cmd/krino/colour.go')
| -rw-r--r-- | cmd/krino/colour.go | 109 |
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) +} |
