// 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) }