-- lua/theme.lua -- Reads Noctalia's generated Alacritty theme (~/.config/alacritty/themes/noctalia.toml) -- and applies matching highlight groups in Neovim, including Treesitter captures. -- Monotone palette: only keywords, functions, parameters, strings, and comments -- carry a distinct color. Everything else (variables, types, constants, -- punctuation, operators, tags, numbers, booleans) sits in foreground or gray. local M = {} local THEME_PATH = vim.fn.expand("~/.config/alacritty/themes/noctalia.toml") local function parse_alacritty_colors(path) local f = io.open(path, "r") if not f then return nil end local colors = { primary = {}, normal = {}, bright = {} } local section for line in f:lines() do local sec = line:match("^%[colors%.(%a+)%]") if sec then -- only track top-level sections we care about (primary, normal, bright); -- nested ones like [colors.search.matches] won't match %a+ and are skipped section = colors[sec] and sec or nil elseif section then local key, val = line:match("^(%a+)%s*=%s*['\"](#%x+)['\"]") if key and val then colors[section][key] = val end end end f:close() return colors end function M.load() local c = parse_alacritty_colors(THEME_PATH) if not c or not c.primary.background or not c.primary.foreground then vim.notify("noctalia theme not found or unparsable: " .. THEME_PATH, vim.log.levels.WARN) return end local bg, fg = c.primary.background, c.primary.foreground local n, b = c.normal, c.bright local hl = vim.api.nvim_set_hl local gray_dim = "#5a5a5a" -- comments, punctuation, gutter — de-emphasized local gray_mid = "#7a7a7a" -- line numbers -- The five accent colors — everything else stays monotone local c_keyword = n.magenta local c_function = n.blue local c_parameter = b.white -- subtly brighter than fg, not a hue, keeps "args" scannable local c_string = n.green local c_comment = "#968466" -- Base UI hl(0, "Normal", { fg = fg, bg = bg }) hl(0, "NormalFloat", { fg = fg, bg = bg }) hl(0, "CursorLine", { bg = n.black }) hl(0, "Visual", { bg = n.black, fg = fg }) hl(0, "Pmenu", { fg = fg, bg = n.black }) hl(0, "PmenuSel", { fg = bg, bg = b.white }) hl(0, "LineNr", { fg = gray_mid }) hl(0, "CursorLineNr", { fg = b.white, bold = true }) hl(0, "SignColumn", { bg = bg }) hl(0, "VertSplit", { fg = n.black }) hl(0, "WinSeparator", { fg = n.black }) hl(0, "StatusLine", { fg = fg, bg = n.black }) hl(0, "StatusLineNC", { fg = gray_mid, bg = n.black }) hl(0, "Search", { fg = "#161616", bg = gray_mid }) hl(0, "IncSearch", { fg = "#161616", bg = b.white }) -- Treesitter capture groups — the five accented categories hl(0, "@comment", { fg = c_comment, italic = true }) hl(0, "@keyword", { fg = c_keyword }) hl(0, "@keyword.function", { fg = c_keyword }) hl(0, "@keyword.return", { fg = c_keyword }) hl(0, "@conditional", { fg = c_keyword }) hl(0, "@repeat", { fg = c_keyword }) hl(0, "@function", { fg = c_function }) hl(0, "@function.builtin", { fg = c_function }) hl(0, "@function.call", { fg = c_function }) hl(0, "@method", { fg = c_function }) hl(0, "@method.call", { fg = c_function }) hl(0, "@parameter", { fg = c_parameter, italic = true }) hl(0, "@string", { fg = c_string }) hl(0, "@string.escape", { fg = c_string, bold = true }) -- Everything else — monotone, falls back to fg or gray hl(0, "@variable", { fg = fg }) hl(0, "@variable.builtin", { fg = fg }) hl(0, "@type", { fg = fg }) hl(0, "@type.builtin", { fg = fg }) hl(0, "@constructor", { fg = fg }) hl(0, "@constant", { fg = fg }) hl(0, "@constant.builtin", { fg = fg }) hl(0, "@property", { fg = fg }) hl(0, "@field", { fg = fg }) hl(0, "@number", { fg = fg }) hl(0, "@boolean", { fg = fg }) hl(0, "@tag", { fg = fg }) hl(0, "@tag.attribute", { fg = fg }) hl(0, "@operator", { fg = gray_mid }) hl(0, "@punctuation.delimiter", { fg = gray_dim }) hl(0, "@punctuation.bracket", { fg = gray_dim }) -- Legacy / non-Treesitter syntax groups (classic :syntax highlighting — -- used by filetypes with no Treesitter parser, or plugins that still -- rely on regex-based syntax files). Mirrors the same monotone scheme. hl(0, "Comment", { fg = c_comment, italic = true }) hl(0, "Statement", { fg = c_keyword }) hl(0, "Keyword", { fg = c_keyword }) hl(0, "Conditional", { fg = c_keyword }) hl(0, "Repeat", { fg = c_keyword }) hl(0, "Label", { fg = c_keyword }) hl(0, "Exception", { fg = c_keyword }) hl(0, "Function", { fg = c_function }) hl(0, "String", { fg = c_string }) hl(0, "Character", { fg = c_string }) -- Everything else — monotone, no separate hue hl(0, "Identifier", { fg = fg }) hl(0, "Constant", { fg = fg }) hl(0, "Number", { fg = fg }) hl(0, "Boolean", { fg = fg }) hl(0, "Float", { fg = fg }) hl(0, "Type", { fg = fg }) hl(0, "StorageClass", { fg = fg }) hl(0, "Structure", { fg = fg }) hl(0, "Typedef", { fg = fg }) hl(0, "PreProc", { fg = fg }) hl(0, "Include", { fg = fg }) hl(0, "Define", { fg = fg }) hl(0, "Macro", { fg = fg }) hl(0, "PreCondit", { fg = fg }) hl(0, "Special", { fg = fg }) hl(0, "SpecialChar", { fg = fg }) hl(0, "SpecialComment", { fg = c_comment, italic = true }) hl(0, "Tag", { fg = fg }) hl(0, "Debug", { fg = fg }) hl(0, "Underlined", { fg = fg, underline = true }) hl(0, "Ignore", { fg = gray_dim }) hl(0, "Operator", { fg = gray_mid }) hl(0, "Delimiter", { fg = gray_dim }) -- Kept distinct: these are signals, not syntax decoration hl(0, "Todo", { fg = n.yellow, bold = true }) hl(0, "Error", { fg = n.red, bold = true }) -- Diagnostics stay colored — functional signal, not syntax decoration hl(0, "DiagnosticError", { fg = n.red }) hl(0, "DiagnosticWarn", { fg = n.yellow }) hl(0, "DiagnosticInfo", { fg = n.blue }) hl(0, "DiagnosticHint", { fg = n.cyan }) end -- Reapply automatically whenever Noctalia regenerates the Alacritty theme -- (e.g. on a theme switch), without needing to restart Neovim. function M.watch() local w = vim.uv.new_fs_event() if not w then return end w:start(THEME_PATH, {}, vim.schedule_wrap(function() M.load() end)) end return M