Skip to content

Commit 2a21d52

Browse files
authored
feat: adds support for xterm highlighting (#155)
1 parent 8a9b722 commit 2a21d52

File tree

5 files changed

+172
-1
lines changed

5 files changed

+172
-1
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [Hooks](#hooks)
1616
- [Setup Examples](#setup-examples)
1717
- [Updating color even when buffer is not focused](#updating-color-even-when-buffer-is-not-focused)
18+
- [Xterm 256-color support](#xterm-256-color-support)
1819
- [Lazyload Colorizer with Lazy.nvim](#lazyload-colorizer-with-lazynvim)
1920
- [Tailwind](#tailwind)
2021
- [Testing](#testing)
@@ -155,6 +156,7 @@ library to do custom highlighting themselves.
155156
},
156157
-- parsers can contain values used in `user_default_options`
157158
sass = { enable = false, parsers = { "css" } }, -- Enable sass colors
159+
xterm = false, -- Enable xterm 256-color codes (#xNN, \e[38;5;NNNm)
158160
-- Highlighting mode. 'background'|'foreground'|'virtualtext'
159161
mode = "background", -- Set the display mode
160162
-- Virtualtext character to use
@@ -270,13 +272,20 @@ require("colorizer").setup({
270272
},
271273
},
272274
})
275+
276+
-- Enable support for xterm 256-color codes (`#xNN`, `\e[38;5;NNNm`) by setting the `xterm` option:
277+
require("colorizer").setup({
278+
user_default_options = {
279+
xterm = true,
280+
},
281+
})
273282
```
274283

275284
In `user_default_options`, there are 2 types of options
276285

277286
1. Individual options - `names`, `names_opts`, `names_custom`, `RGB`, `RGBA`,
278287
`RRGGBB`, `RRGGBBAA`, `hsl_fn`, `rgb_fn`, `RRGGBBAA`, `AARRGGBB`, `tailwind`,
279-
`sass`
288+
`sass`, `xterm`
280289

281290
1. Alias options - `css`, `css_fn`
282291

lua/colorizer/config.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ local plugin_user_default_options = {
2828
update_names = false,
2929
},
3030
sass = { enable = false, parsers = { css = true } },
31+
xterm = false,
3132
mode = "background",
3233
virtualtext = "",
3334
virtualtext_inline = false,
@@ -76,6 +77,7 @@ If both `css` and `css_fn` are true, `css_fn` has more priority over `css`.
7677
-- @field always_update boolean: Always update color values, even if buffer is not focused.
7778
-- @field hooks table: Table of hook functions
7879
-- @field hooks.disable_line_highlight function: Returns boolean which controls if line should be parsed for highlights
80+
-- @field xterm boolean: Enables xterm 256-color codes (#xNN, \e[38;5;NNNm)
7981

8082
--- Options for colorizer that were passed in to setup function
8183
--@field filetypes

lua/colorizer/matcher.lua

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ local parsers = {
1717
rgba_hex = require("colorizer.parser.rgba_hex").parser,
1818
-- TODO: 2024-12-21 - Should this be moved into parsers module?
1919
sass_name = require("colorizer.sass").parser,
20+
xterm = require("colorizer.parser.xterm").parser,
2021
}
2122

2223
parsers.prefix = {
@@ -48,6 +49,12 @@ local function compile(matchers, matchers_trie, hooks)
4849
-- prefix #
4950
if matchers.rgba_hex_parser then
5051
if line:byte(i) == ("#"):byte() then
52+
if matchers.xterm_enabled then
53+
local len, rgb_hex = parsers.xterm(line, i)
54+
if len and rgb_hex then
55+
return len, rgb_hex
56+
end
57+
end
5158
return parsers.rgba_hex(line, i, matchers.rgba_hex_parser)
5259
end
5360
end
@@ -58,6 +65,13 @@ local function compile(matchers, matchers_trie, hooks)
5865
return parsers.sass_name(line, i, bufnr)
5966
end
6067
end
68+
-- xterm ANSI escape: \e[38;5;NNNm
69+
if matchers.xterm_enabled then
70+
local len, rgb_hex = parsers.xterm(line, i)
71+
if len and rgb_hex then
72+
return len, rgb_hex
73+
end
74+
end
6175

6276
-- Prefix 0x, rgba, rgb, hsla, hsl
6377
local prefix = trie:longest_prefix(line, i)
@@ -111,6 +125,7 @@ function M.make(ud_opts)
111125
local enable_AARRGGBB = ud_opts.AARRGGBB
112126
local enable_rgb = ud_opts.rgb_fn
113127
local enable_hsl = ud_opts.hsl_fn
128+
local enable_xterm = ud_opts.xterm
114129

115130
-- Rather than use bit.lshift or calculate 2^x, use precalculated values to
116131
-- create unique bitmask
@@ -132,6 +147,7 @@ function M.make(ud_opts)
132147
+ (enable_tailwind == "lsp" and 16384 or 0)
133148
+ (enable_tailwind == "both" and 32768 or 0)
134149
+ (enable_sass and 65536 or 0)
150+
+ (enable_xterm and 131072 or 0)
135151

136152
if matcher_mask == 0 then
137153
return false
@@ -148,6 +164,7 @@ function M.make(ud_opts)
148164

149165
local matchers = {}
150166
local matchers_prefix = {}
167+
matchers.xterm_enabled = enable_xterm
151168

152169
local enable_tailwind_names = enable_tailwind == "normal" or enable_tailwind == "both"
153170
if enable_names or enable_names_custom or enable_tailwind_names then

lua/colorizer/parser/xterm.lua

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
local M = {}
2+
3+
-- Xterm 256-color palette (0-255) as RGB hex strings
4+
local xterm_palette = {
5+
"000000", "800000", "008000", "808000", "000080", "800080", "008080", "c0c0c0",
6+
"808080", "ff0000", "00ff00", "ffff00", "0000ff", "ff00ff", "00ffff", "ffffff",
7+
-- 16-231: 6x6x6 color cube
8+
}
9+
-- Fill in the 6x6x6 color cube
10+
for r = 0, 5 do
11+
for g = 0, 5 do
12+
for b = 0, 5 do
13+
local idx = 16 + 36 * r + 6 * g + b
14+
local function scale(x) return x == 0 and 0 or 95 + 40 * (x - 1) end
15+
xterm_palette[idx + 1] = string.format("%02x%02x%02x", scale(r), scale(g), scale(b))
16+
end
17+
end
18+
end
19+
-- 232-255: grayscale ramp
20+
for i = 0, 23 do
21+
local level = 8 + i * 10
22+
xterm_palette[233 + i] = string.format("%02x%02x%02x", level, level, level)
23+
end
24+
25+
--- Parses #xNN and \e[38;5;NNNm xterm color codes and returns (length, rgb_hex)
26+
---@param line string: The line of text
27+
---@param i number: The index to start parsing from
28+
---@return number|nil, string|nil: Length of match and RGB hex string
29+
function M.parser(line, i)
30+
-- #xNN (decimal, 0-255)
31+
local hash_x = line:sub(i, i + 1)
32+
if hash_x == "#x" then
33+
local num = line:sub(i + 2):match("^(%d?%d?%d)")
34+
if num then
35+
local idx = tonumber(num)
36+
if idx and idx >= 0 and idx <= 255 then
37+
local next_char = line:sub(i + 2 + #num, i + 2 + #num)
38+
if next_char == "" or not next_char:match("%w") then
39+
return 2 + #num, xterm_palette[idx + 1]
40+
end
41+
end
42+
end
43+
end
44+
-- \e[38;5;NNNm (decimal, 0-255), support both literal '\e' and actual escape char
45+
local ansi_patterns = {
46+
"^\\e%[38;5;(%d?%d?%d)m", -- literal '\e'
47+
"^\27%[38;5;(%d?%d?%d)m", -- ASCII 27
48+
"^\x1b%[38;5;(%d?%d?%d)m", -- hex escape
49+
}
50+
for _, esc_pat in ipairs(ansi_patterns) do
51+
local esc_match = line:sub(i):match(esc_pat)
52+
if esc_match then
53+
local idx = tonumber(esc_match)
54+
if idx and idx >= 0 and idx <= 255 then
55+
-- Use string.find to get the end index of the match
56+
local _, end_idx = line:sub(i):find(esc_pat)
57+
if end_idx then
58+
return end_idx, xterm_palette[idx + 1]
59+
else
60+
return 7 + #esc_match, xterm_palette[idx + 1]
61+
end
62+
end
63+
end
64+
end
65+
return nil
66+
end
67+
68+
return M

test/expect.lua

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ local opts = {
6464
hsl_fn = true,
6565
css = true,
6666
css_fn = true,
67+
xterm = true,
6768
mode = "background",
6869
tailwind = true,
6970
sass = { enable = true, parsers = { css = true } },
@@ -82,6 +83,80 @@ return opts
8283
0xFFFFFFF1 -- why does this highlight?
8384
8485
SUCCESS CASES:
86+
-- Xterm 256-color codes:
87+
#x0 -- black #000000
88+
#x1 -- maroon #800000
89+
#x2 -- green #008000
90+
#x3 -- olive #808000
91+
#x4 -- navy #000080
92+
#x5 -- purple #800080
93+
#x6 -- teal #008080
94+
#x7 -- silver #c0c0c0
95+
#x8 -- grey #808080
96+
#x9 -- red #ff0000
97+
#x10 -- lime #00ff00
98+
#x11 -- yellow #ffff00
99+
#x12 -- blue #0000ff
100+
#x13 -- fuchsia #ff00ff
101+
#x14 -- aqua #00ffff
102+
#x15 -- white #ffffff
103+
#x16 -- start of color cube #000000
104+
#x17 -- color cube #00005f
105+
#x21 -- color cube #0000ff
106+
#x51 -- color cube #00ff00
107+
#x88 -- color cube #870000
108+
#x160 -- color cube #d70000
109+
#x231 -- color cube #ffffff
110+
#x232 -- grayscale ramp #080808
111+
#x243 -- grayscale ramp #767676
112+
#x254 -- grayscale ramp #e4e4e4
113+
#x255 -- last grayscale #eeeeee
114+
#x000 #000000
115+
#x099 #5fafd7
116+
#x42 #00d75f #x43 #00d787
117+
#x42, #00d75f
118+
#x42 #00d75f #x43 #00d787
119+
120+
-- Xterm ANSI escape codes:
121+
\e[38;5;0m #000000
122+
\e[38;5;1m #800000
123+
\e[38;5;2m #008000
124+
\e[38;5;3m #808000
125+
\e[38;5;4m #000080
126+
\e[38;5;5m #800080
127+
\e[38;5;6m #008080
128+
\e[38;5;7m #c0c0c0
129+
\e[38;5;15m #ffffff
130+
\e[38;5;16m #000000
131+
\e[38;5;21m #0000ff
132+
\e[38;5;51m #00ff00
133+
\e[38;5;42m #00d75f
134+
\e[38;5;43m #00d787
135+
\e[38;5;88m #870000
136+
\e[38;5;160m #d70000
137+
\e[38;5;231m #ffffff
138+
\e[38;5;232m #080808
139+
\e[38;5;243m #767676
140+
\e[38;5;254m #e4e4e4
141+
\e[38;5;255m #eeeeee
142+
\e[38;5;99m #af5f87
143+
\e[38;5;200m #ff00af
144+
\e[38;5;201m #ff00d7
145+
\e[38;5;202m #ff005f
146+
\e[38;5;220m #ffff00
147+
\e[38;5;226m #ffff5f
148+
\e[38;5;250m #c6c6c6
149+
\e[38;5;251m #d0d0d0
150+
\e[38;5;252m #dadada
151+
\e[38;5;253m #e4e4e4
152+
\e[38;5;000m #000000
153+
\e[38;5;099m #5fafd7
154+
\e[38;5;42m #00d75f \e[38;5;43m #00d787
155+
\e[38;5;42m, #00d75f
156+
\e[38;5;42m #00d75f \e[38;5;43m #00d787
157+
158+
 #00d75f
159+
85160
CSS Named Colors:
86161
olive -- do not remove
87162
cyan magenta gold chartreuse lightgreen pink violet orange

0 commit comments

Comments
 (0)