tui: strip syntax highlight backgrounds
- commit
- b5ab07f9a0fbce8f29fc936b7e98332d229e1d9d
- author
- patriceckhart · · 2026-05-04 13:54 UTC
- parent
- 17dec30
- signature
- unsigned
1 file · +61 -1
M internal/tui/highlight.go +61 -1
| @@ -48,7 +48,7 @@ func HighlightCode(src, lang string) []string { | |||
| 48 | 48 | highlightCache.store(lang, src, out) | |
| 49 | 49 | return out | |
| 50 | 50 | } | |
| 51 | - | out := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") | |
| 51 | + | out := strings.Split(strings.TrimRight(stripANSIBackgrounds(buf.String()), "\n"), "\n") | |
| 52 | 52 | highlightCache.store(lang, src, out) | |
| 53 | 53 | return out | |
| 54 | 54 | } | |
| @@ -110,6 +110,66 @@ func LanguageFromPath(p string) string { | |||
| 110 | 110 | ||
| 111 | 111 | // chooseLexer picks the best lexer for a language hint; falls back to | |
| 112 | 112 | // nil (no highlighting) if the hint is empty or unknown. | |
| 113 | + | // stripANSIBackgrounds removes SGR background-color attributes emitted by | |
| 114 | + | // terminal syntax formatters while preserving foreground colors and styles. | |
| 115 | + | // Chroma's inherited styles can assign black backgrounds to a few tokens | |
| 116 | + | // (notably punctuation/error-ish spans), which looks like random black blocks | |
| 117 | + | // inside zot's dark-gray tool boxes. Foreground color is enough for code. | |
| 118 | + | func stripANSIBackgrounds(s string) string { | |
| 119 | + | var out strings.Builder | |
| 120 | + | out.Grow(len(s)) | |
| 121 | + | for i := 0; i < len(s); { | |
| 122 | + | if s[i] != 0x1b || i+1 >= len(s) || s[i+1] != '[' { | |
| 123 | + | out.WriteByte(s[i]) | |
| 124 | + | i++ | |
| 125 | + | continue | |
| 126 | + | } | |
| 127 | + | j := i + 2 | |
| 128 | + | for j < len(s) && s[j] != 'm' { | |
| 129 | + | j++ | |
| 130 | + | } | |
| 131 | + | if j >= len(s) { | |
| 132 | + | out.WriteString(s[i:]) | |
| 133 | + | break | |
| 134 | + | } | |
| 135 | + | params := strings.Split(s[i+2:j], ";") | |
| 136 | + | kept := make([]string, 0, len(params)) | |
| 137 | + | for p := 0; p < len(params); p++ { | |
| 138 | + | param := params[p] | |
| 139 | + | switch param { | |
| 140 | + | case "48": | |
| 141 | + | // 48;5;N or 48;2;R;G;B background color. | |
| 142 | + | if p+1 < len(params) && params[p+1] == "5" { | |
| 143 | + | p += 2 | |
| 144 | + | continue | |
| 145 | + | } | |
| 146 | + | if p+1 < len(params) && params[p+1] == "2" { | |
| 147 | + | p += 4 | |
| 148 | + | continue | |
| 149 | + | } | |
| 150 | + | continue | |
| 151 | + | case "49": | |
| 152 | + | continue | |
| 153 | + | } | |
| 154 | + | // 40-47 and 100-107 are ANSI background colors. | |
| 155 | + | if len(param) == 2 && param[0] == '4' && param[1] >= '0' && param[1] <= '7' { | |
| 156 | + | continue | |
| 157 | + | } | |
| 158 | + | if len(param) == 3 && param[0] == '1' && param[1] == '0' && param[2] >= '0' && param[2] <= '7' { | |
| 159 | + | continue | |
| 160 | + | } | |
| 161 | + | kept = append(kept, param) | |
| 162 | + | } | |
| 163 | + | if len(kept) > 0 { | |
| 164 | + | out.WriteString("\x1b[") | |
| 165 | + | out.WriteString(strings.Join(kept, ";")) | |
| 166 | + | out.WriteByte('m') | |
| 167 | + | } | |
| 168 | + | i = j + 1 | |
| 169 | + | } | |
| 170 | + | return out.String() | |
| 171 | + | } | |
| 172 | + | ||
| 113 | 173 | func chooseLexer(lang string) chroma.Lexer { | |
| 114 | 174 | if lang == "" { | |
| 115 | 175 | return nil | |
| @@ -48,7 +48,7 @@ func HighlightCode(src, lang string) []string { | |||||
| 48 | highlightCache.store(lang, src, out) | 48 | highlightCache.store(lang, src, out) | ||
| 49 | return out | 49 | return out | ||
| 50 | } | 50 | } | ||
| 51 | - | out := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") | 51 | + | out := strings.Split(strings.TrimRight(stripANSIBackgrounds(buf.String()), "\n"), "\n") |
| 52 | highlightCache.store(lang, src, out) | 52 | highlightCache.store(lang, src, out) | ||
| 53 | return out | 53 | return out | ||
| 54 | } | 54 | } | ||
| @@ -110,6 +110,66 @@ func LanguageFromPath(p string) string { | |||||
| 110 | 110 | ||||
| 111 | // chooseLexer picks the best lexer for a language hint; falls back to | 111 | // chooseLexer picks the best lexer for a language hint; falls back to | ||
| 112 | // nil (no highlighting) if the hint is empty or unknown. | 112 | // nil (no highlighting) if the hint is empty or unknown. | ||
| 113 | + | // stripANSIBackgrounds removes SGR background-color attributes emitted by | |||
| 114 | + | // terminal syntax formatters while preserving foreground colors and styles. | |||
| 115 | + | // Chroma's inherited styles can assign black backgrounds to a few tokens | |||
| 116 | + | // (notably punctuation/error-ish spans), which looks like random black blocks | |||
| 117 | + | // inside zot's dark-gray tool boxes. Foreground color is enough for code. | |||
| 118 | + | func stripANSIBackgrounds(s string) string { | |||
| 119 | + | var out strings.Builder | |||
| 120 | + | out.Grow(len(s)) | |||
| 121 | + | for i := 0; i < len(s); { | |||
| 122 | + | if s[i] != 0x1b || i+1 >= len(s) || s[i+1] != '[' { | |||
| 123 | + | out.WriteByte(s[i]) | |||
| 124 | + | i++ | |||
| 125 | + | continue | |||
| 126 | + | } | |||
| 127 | + | j := i + 2 | |||
| 128 | + | for j < len(s) && s[j] != 'm' { | |||
| 129 | + | j++ | |||
| 130 | + | } | |||
| 131 | + | if j >= len(s) { | |||
| 132 | + | out.WriteString(s[i:]) | |||
| 133 | + | break | |||
| 134 | + | } | |||
| 135 | + | params := strings.Split(s[i+2:j], ";") | |||
| 136 | + | kept := make([]string, 0, len(params)) | |||
| 137 | + | for p := 0; p < len(params); p++ { | |||
| 138 | + | param := params[p] | |||
| 139 | + | switch param { | |||
| 140 | + | case "48": | |||
| 141 | + | // 48;5;N or 48;2;R;G;B background color. | |||
| 142 | + | if p+1 < len(params) && params[p+1] == "5" { | |||
| 143 | + | p += 2 | |||
| 144 | + | continue | |||
| 145 | + | } | |||
| 146 | + | if p+1 < len(params) && params[p+1] == "2" { | |||
| 147 | + | p += 4 | |||
| 148 | + | continue | |||
| 149 | + | } | |||
| 150 | + | continue | |||
| 151 | + | case "49": | |||
| 152 | + | continue | |||
| 153 | + | } | |||
| 154 | + | // 40-47 and 100-107 are ANSI background colors. | |||
| 155 | + | if len(param) == 2 && param[0] == '4' && param[1] >= '0' && param[1] <= '7' { | |||
| 156 | + | continue | |||
| 157 | + | } | |||
| 158 | + | if len(param) == 3 && param[0] == '1' && param[1] == '0' && param[2] >= '0' && param[2] <= '7' { | |||
| 159 | + | continue | |||
| 160 | + | } | |||
| 161 | + | kept = append(kept, param) | |||
| 162 | + | } | |||
| 163 | + | if len(kept) > 0 { | |||
| 164 | + | out.WriteString("\x1b[") | |||
| 165 | + | out.WriteString(strings.Join(kept, ";")) | |||
| 166 | + | out.WriteByte('m') | |||
| 167 | + | } | |||
| 168 | + | i = j + 1 | |||
| 169 | + | } | |||
| 170 | + | return out.String() | |||
| 171 | + | } | |||
| 172 | + | ||||
| 113 | func chooseLexer(lang string) chroma.Lexer { | 173 | func chooseLexer(lang string) chroma.Lexer { | ||
| 114 | if lang == "" { | 174 | if lang == "" { | ||
| 115 | return nil | 175 | return nil | ||