aboutsummaryrefslogtreecommitdiff
path: root/cmd/krino/colour.go
blob: ee9055cebe6b83cd9ce8da326f73d4e23f163672 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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)
}