Files
dotfiles/config/nvim/lua/theme/init.lua
T
2026-08-04 08:10:32 +02:00

177 lines
6.0 KiB
Lua

-- 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