--- Compare cterm highlight snapshot against the original hex baseline. --- Verifies that every ctermfg/ctermbg index matches the expected nugu color. --- Usage: nvim --headless /tmp/nugu_compare.txt -c "luafile tests/manual/compare_highlights.lua" -c "qa!" -- The original hex baseline (captured before the cterm change) local baseline_path = '/tmp/nugu_highlights_baseline.txt' -- nugu color mapping: cterm index -> { dark_hex, semantic_name } local slot_info = { [0] = { '#121212', 'content_backdrop' }, [1] = { '#e46479', 'error' }, [2] = { '#7ba1aa', 'content_focus' }, [3] = { '#9e9a69', 'warning' }, [4] = { '#b59ae4', 'content_important_local' }, [5] = { '#d151d1', 'content_accent' }, [6] = { '#5d5d5d', 'content_unfocus' }, [7] = { '#ededed', 'content_normal' }, [8] = { '#8b8b8b', 'content_minor' }, [9] = { '#3ec5dd', 'info' }, [10] = { '#6f6f6f', 'ui_unfocus' }, [11] = { '#242424', 'ui_backdrop' }, [12] = { '#dfd3f3', 'content_important_global' }, [13] = { '#d86dd8', 'ui_accent' }, [14] = { '#c9b6eb', 'ui_important_local' }, [15] = { '#f3effb', 'ui_important_global' }, [16] = { '#ed5eed', 'debug' }, [17] = { '#dbdbdb', 'ui_normal' }, [18] = { '#9d9d9d', 'ui_minor' }, [19] = { '#9aabae', 'ui_focus' }, } -- Build hex -> slot reverse map (lowercase for comparison) local hex_to_slot = {} for idx, info in pairs(slot_info) do hex_to_slot[info[1]:lower()] = idx end -- Parse baseline file: group_name -> { fg=hex, bg=hex, sp=hex, bold=bool, ... } local function parse_file(path) local f = io.open(path, 'r') if not f then return {} end local groups = {} for line in f:lines() do if not line:match '^%-%-' and line ~= '' then local name, rest = line:match '^(%S+) (.+)$' if name then local props = {} for token in rest:gmatch '%S+' do local k, v = token:match '^(%a+)=(.+)$' if k then props[k] = v else props[token] = true end end groups[name] = props end end end f:close() return groups end local baseline = parse_file(baseline_path) local cterm = parse_file('/tmp/nugu_highlights_cterm.txt') local outpath = vim.fn.argv()[1] or '/tmp/nugu_compare.txt' local lines = { '-- Highlight comparison: hex baseline vs cterm', '-- ' .. os.date '!%Y-%m-%dT%H:%M:%SZ', '', } local match_count = 0 local mismatch_count = 0 local missing_count = 0 local skipped_count = 0 -- Check all groups that exist in the baseline (nugu-defined groups) for name, base_props in pairs(baseline) do local ct = cterm[name] if not ct then missing_count = missing_count + 1 lines[#lines + 1] = 'MISSING: ' .. name else -- Compare fg if base_props.fg and ct.ctermfg then local base_hex = base_props.fg:lower() local slot = tonumber(ct.ctermfg) local expected_hex = slot_info[slot] and slot_info[slot][1]:lower() if base_hex == expected_hex then match_count = match_count + 1 elseif hex_to_slot[base_hex] then mismatch_count = mismatch_count + 1 lines[#lines + 1] = string.format( 'MISMATCH FG: %s baseline=%s (slot %d) cterm=%s (slot %d, %s)', name, base_hex, hex_to_slot[base_hex], ct.ctermfg, slot, expected_hex or '?' ) else -- baseline has a hex that isn't in our palette (built-in default) skipped_count = skipped_count + 1 end end -- Compare bg if base_props.bg and ct.ctermbg then local base_hex = base_props.bg:lower() local slot = tonumber(ct.ctermbg) local expected_hex = slot_info[slot] and slot_info[slot][1]:lower() if base_hex == expected_hex then match_count = match_count + 1 elseif hex_to_slot[base_hex] then mismatch_count = mismatch_count + 1 lines[#lines + 1] = string.format( 'MISMATCH BG: %s baseline=%s (slot %d) cterm=%s (slot %d, %s)', name, base_hex, hex_to_slot[base_hex], ct.ctermbg, slot, expected_hex or '?' ) else skipped_count = skipped_count + 1 end end -- Check for missing ctermfg when baseline has fg if base_props.fg and not ct.ctermfg and not ct.link then local base_hex = base_props.fg:lower() if hex_to_slot[base_hex] then missing_count = missing_count + 1 lines[#lines + 1] = 'NO CTERMFG: ' .. name .. ' (baseline fg=' .. base_props.fg .. ')' end end if base_props.bg and not ct.ctermbg and not ct.link then local base_hex = base_props.bg:lower() if hex_to_slot[base_hex] then missing_count = missing_count + 1 lines[#lines + 1] = 'NO CTERMBG: ' .. name .. ' (baseline bg=' .. base_props.bg .. ')' end end -- Compare boolean attributes for _, attr in ipairs({ 'bold', 'italic', 'underline', 'undercurl', 'underdouble', 'underdotted', 'underdashed', 'strikethrough', 'reverse' }) do local base_has = base_props[attr] and true or false local ct_has = ct[attr] and true or false if base_has ~= ct_has then lines[#lines + 1] = string.format('ATTR DIFF: %s %s baseline=%s cterm=%s', name, attr, tostring(base_has), tostring(ct_has)) end end end end -- Summary lines[#lines + 1] = '' lines[#lines + 1] = string.format('Matches: %d', match_count) lines[#lines + 1] = string.format('Mismatches: %d', mismatch_count) lines[#lines + 1] = string.format('Missing: %d', missing_count) lines[#lines + 1] = string.format('Skipped: %d (non-nugu colors)', skipped_count) local f = assert(io.open(outpath, 'w')) f:write(table.concat(lines, '\n') .. '\n') f:close() print(table.concat(lines, '\n'))