@@ -0,0 +1,128 @@
8+ "github.com/charmbracelet/x/ansi"
11+// remapANSI16 replaces basic ANSI 16-color SGR codes with 24-bit
12+// truecolor from palette. Programs emit \x1b[31m etc. and trust the
13+// terminal to pick the color; inside Crush's TUI those defaults are
14+// often illegible on our dark background. Rewriting them to explicit
15+// RGB keeps output readable regardless of terminal configuration.
17+// Uses [ansi.DecodeSequence] for parsing (same approach as
18+// [colorprofile.Writer]) since there is no upstream palette-remap API.
19+func remapANSI16(s string, palette [16]color.Color) string {
20+ if !strings.ContainsRune(s, 0x1b) {
24+ var buf strings.Builder
27+ parser := ansi.GetParser()
28+ defer ansi.PutParser(parser)
33+ seq, _, n, newState := ansi.DecodeSequence(s, state, parser)
35+ if ansi.HasCsiPrefix(seq) && parser.Command() == 'm' {
36+ remapSGR(parser.Params(), palette, &buf)
48+// remapSGR rewrites one SGR sequence, replacing 16-color params with
49+// truecolor from palette. Extended colors (38/48/58 with ;5;n or
50+// ;2;r;g;b sub-params) pass through unchanged. Non-color attributes
51+// (bold, italic, etc.) and default color resets (39/49/59) also pass
53+func remapSGR(params ansi.Params, palette [16]color.Color, buf *strings.Builder) {
54+ buf.WriteString("\x1b[")
57+ for i := 0; i < len(params); i++ {
58+ p := params[i].Param(0)
66+ // Extended color introducers consume subsequent params as
67+ // arguments. Skip them whole so they aren't misread.
68+ case p == 38 || p == 48 || p == 58:
69+ buf.WriteString(strconv.Itoa(p))
70+ if i+1 < len(params) {
71+ sub := params[i+1].Param(0)
73+ case 5: // 256-color: 38;5;n
75+ buf.WriteString(strconv.Itoa(sub))
76+ if i+2 < len(params) {
78+ buf.WriteString(strconv.Itoa(params[i+2].Param(0)))
83+ case 2: // truecolor: 38;2;r;g;b
85+ buf.WriteString(strconv.Itoa(sub))
86+ for j := 2; j <= 4 && i+j < len(params); j++ {
88+ buf.WriteString(strconv.Itoa(params[i+j].Param(0)))
90+ i += min(4, len(params)-i-1)
96+ case p >= 30 && p <= 37:
97+ writeTruecolor(buf, 38, palette[p-30])
98+ case p >= 90 && p <= 97:
99+ writeTruecolor(buf, 38, palette[8+p-90])
100+ case p >= 40 && p <= 47:
101+ writeTruecolor(buf, 48, palette[p-40])
102+ case p >= 100 && p <= 107:
103+ writeTruecolor(buf, 48, palette[8+p-100])
106+ buf.WriteString(strconv.Itoa(p))
113+// writeTruecolor appends "introducer;2;r;g;b" to buf. Nil color emits
114+// the bare introducer so the terminal default applies.
115+func writeTruecolor(buf *strings.Builder, introducer int, c color.Color) {
117+ buf.WriteString(strconv.Itoa(introducer))
120+ r, g, b, _ := c.RGBA()
121+ buf.WriteString(strconv.Itoa(introducer))
122+ buf.WriteString(";2;")
123+ buf.WriteString(strconv.Itoa(int(r >> 8)))
125+ buf.WriteString(strconv.Itoa(int(g >> 8)))
127+ buf.WriteString(strconv.Itoa(int(b >> 8)))