diff --git a/lua/cp/ui/ansi.lua b/lua/cp/ui/ansi.lua index facfd1f..8bc8581 100644 --- a/lua/cp/ui/ansi.lua +++ b/lua/cp/ui/ansi.lua @@ -12,13 +12,81 @@ local M = {} local logger = require('cp.log') ----@param raw_output string|table +local dyn_hl_cache = {} + +---@param s string|table ---@return string -function M.bytes_to_string(raw_output) - if type(raw_output) == 'string' then - return raw_output +function M.bytes_to_string(s) + if type(s) == 'string' then + return s end - return table.concat(vim.tbl_map(string.char, raw_output)) + return table.concat(vim.tbl_map(string.char, s)) +end + +---@param fg table|nil +---@param bold boolean +---@param italic boolean +---@return string|nil +local function ensure_hl_for(fg, bold, italic) + if not fg and not bold and not italic then + return nil + end + + local base = 'CpAnsi' + local suffix + local opts = {} + + if fg and fg.kind == 'named' then + suffix = fg.name + elseif fg and fg.kind == 'xterm' then + suffix = ('X%03d'):format(fg.idx) + local function xterm_to_hex(n) + if n >= 0 and n <= 15 then + local key = 'terminal_color_' .. n + return vim.g[key] + end + if n >= 16 and n <= 231 then + local c = n - 16 + local r = math.floor(c / 36) % 6 + local g = math.floor(c / 6) % 6 + local b = c % 6 + local function level(x) + return x == 0 and 0 or 55 + 40 * x + end + return ('#%02x%02x%02x'):format(level(r), level(g), level(b)) + end + local l = 8 + 10 * (n - 232) + return ('#%02x%02x%02x'):format(l, l, l) + end + opts.fg = xterm_to_hex(fg.idx) or 'NONE' + elseif fg and fg.kind == 'rgb' then + suffix = ('Rgb%02x%02x%02x'):format(fg.r, fg.g, fg.b) + opts.fg = ('#%02x%02x%02x'):format(fg.r, fg.g, fg.b) + end + + local parts = { base } + if bold then + table.insert(parts, 'Bold') + end + if italic then + table.insert(parts, 'Italic') + end + if suffix then + table.insert(parts, suffix) + end + local name = table.concat(parts) + + if not dyn_hl_cache[name] then + if bold then + opts.bold = true + end + if italic then + opts.italic = true + end + vim.api.nvim_set_hl(0, name, opts) + dyn_hl_cache[name] = true + end + return name end ---@param text string @@ -38,22 +106,7 @@ function M.parse_ansi_text(text) } local function get_highlight_group() - if not ansi_state.bold and not ansi_state.italic and not ansi_state.foreground then - return nil - end - - local parts = { 'CpAnsi' } - if ansi_state.bold then - table.insert(parts, 'Bold') - end - if ansi_state.italic then - table.insert(parts, 'Italic') - end - if ansi_state.foreground then - table.insert(parts, ansi_state.foreground) - end - - return table.concat(parts) + return ensure_hl_for(ansi_state.foreground, ansi_state.bold, ansi_state.italic) end local function apply_highlight(start_line, start_col, end_col) @@ -137,6 +190,7 @@ end ---@param ansi_state table ---@param code_string string +---@return nil function M.update_ansi_state(ansi_state, code_string) if code_string == '' or code_string == '0' then ansi_state.bold = false @@ -146,40 +200,60 @@ function M.update_ansi_state(ansi_state, code_string) end local codes = vim.split(code_string, ';', { plain = true }) + local idx = 1 + while idx <= #codes do + local num = tonumber(codes[idx]) - for _, code in ipairs(codes) do - local num = tonumber(code) - if num then - if num == 1 then - ansi_state.bold = true - elseif num == 3 then - ansi_state.italic = true - elseif num == 22 then - ansi_state.bold = false - elseif num == 23 then - ansi_state.italic = false - elseif num >= 30 and num <= 37 then - local colors = { 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White' } - ansi_state.foreground = colors[num - 29] - elseif num >= 90 and num <= 97 then - local colors = { - 'BrightBlack', - 'BrightRed', - 'BrightGreen', - 'BrightYellow', - 'BrightBlue', - 'BrightMagenta', - 'BrightCyan', - 'BrightWhite', - } - ansi_state.foreground = colors[num - 89] - elseif num == 39 then - ansi_state.foreground = nil + if num == 1 then + ansi_state.bold = true + elseif num == 3 then + ansi_state.italic = true + elseif num == 22 then + ansi_state.bold = false + elseif num == 23 then + ansi_state.italic = false + elseif num and num >= 30 and num <= 37 then + local colors = { 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White' } + ansi_state.foreground = { kind = 'named', name = colors[num - 29] } + elseif num and num >= 90 and num <= 97 then + local colors = { + 'BrightBlack', + 'BrightRed', + 'BrightGreen', + 'BrightYellow', + 'BrightBlue', + 'BrightMagenta', + 'BrightCyan', + 'BrightWhite', + } + ansi_state.foreground = { kind = 'named', name = colors[num - 89] } + elseif num == 39 then + ansi_state.foreground = nil + elseif num == 38 or num == 48 then + local is_fg = (num == 38) + local mode = tonumber(codes[idx + 1] or '') + if mode == 5 and codes[idx + 2] then + local pal = tonumber(codes[idx + 2]) or 0 + if is_fg then + ansi_state.foreground = { kind = 'xterm', idx = pal } + end + idx = idx + 2 + elseif mode == 2 and codes[idx + 2] and codes[idx + 3] and codes[idx + 4] then + local r = tonumber(codes[idx + 2]) or 0 + local g = tonumber(codes[idx + 3]) or 0 + local b = tonumber(codes[idx + 4]) or 0 + if is_fg then + ansi_state.foreground = { kind = 'rgb', r = r, g = g, b = b } + end + idx = idx + 4 end end + + idx = idx + 1 end end +---@return nil function M.setup_highlight_groups() local color_map = { Black = vim.g.terminal_color_0, @@ -202,7 +276,7 @@ function M.setup_highlight_groups() if vim.tbl_count(color_map) < 16 then logger.log( - 'ansi terminal colors (vim.g.terminal_color_*) not configured. ANSI colors will not display properly. ', + 'ansi terminal colors (vim.g.terminal_color_*) not configured. ANSI colors will not display properly.', vim.log.levels.WARN ) end @@ -218,7 +292,6 @@ function M.setup_highlight_groups() for color_name, terminal_color in pairs(color_map) do local parts = { 'CpAnsi' } local opts = { fg = terminal_color or 'NONE' } - if combo.bold then table.insert(parts, 'Bold') opts.bold = true @@ -228,7 +301,6 @@ function M.setup_highlight_groups() opts.italic = true end table.insert(parts, color_name) - local hl_name = table.concat(parts) vim.api.nvim_set_hl(0, hl_name, opts) end @@ -239,4 +311,30 @@ function M.setup_highlight_groups() vim.api.nvim_set_hl(0, 'CpAnsiBoldItalic', { bold = true, italic = true }) end +---@param text string +---@return string[] +function M.debug_ansi_tokens(text) + local out = {} + local i = 1 + while true do + local s, e, codes, cmd = text:find('\027%[([%d;]*)([a-zA-Z])', i) + if not s then + break + end + table.insert(out, ('ESC[%s%s'):format(codes, cmd)) + i = e + 1 + end + return out +end + +---@param s string +---@return string +function M.hex_dump(s) + local t = {} + for i = 1, #s do + t[#t + 1] = ('%02X'):format(s:byte(i)) + end + return table.concat(t, ' ') +end + return M