local M = {} ---@class rc.Highlight.Operation ---@field reverse? true ---@field extend? string | rc.Highlight ---@field mix? { hl: string | rc.Highlight, ratio: integer } ---@field clear? 'fg' | 'bg' | 'sp' ---@field bold? boolean ---@class rc.Highlight ---@field name string ---@field package operations rc.Highlight.Operation[] local Highlight = {} ---@private Highlight.__index = Highlight ---@param hl_name string ---@return rc.Highlight function M.get(hl_name) return setmetatable({ name = hl_name, operations = {}, }, Highlight) end ---@param hl_name string ---@param hl fun(): rc.Highlight ---@return rc.Highlight function M.ensure(hl_name, hl) local self = M.get(hl_name) if self:exists() then return self else return hl():set(hl_name) end end ---@param new_hl_name string? ---@return self function Highlight:set(new_hl_name) local name = new_hl_name or self.name local _self = vim.deepcopy(self) local function set_hl() vim.api.nvim_set_hl(0, name, _self:get_hl_info()) end set_hl() vim.api.nvim_create_autocmd("ColorScheme", { callback = set_hl }) self.name = name return self end ---@param color integer ---@return string local function int2hex(color) return ("#%06x"):format(color) end ---@param color integer ---@return integer, integer, integer local function int2rgb(color) return bit.rshift(color, 2 ^ 4), bit.band(bit.rshift(color, 2 ^ 3), 0xff), bit.band(color, 0xff) end ---@param r integer ---@param g integer ---@param b integer ---@return integer local function rgb2int(r, g, b) return bit.lshift(r, 2 ^ 4) + bit.lshift(g, 2 ^ 3) + b end ---@param color string ---@return integer local function hex2int(color) return rgb2int( tonumber(color:sub(2, 3), 16), tonumber(color:sub(4, 5), 16), tonumber(color:sub(6, 7), 16) ) end ---@param min integer ---@param n integer ---@param max integer ---@return integer local function clamp(min, n, max) return math.min(math.max(n, min), max) end ---@param base integer ---@param other integer ---@param ratio number 0.0 ~ 1.0 ---@return integer local function mix_colors(base, other, ratio) ratio = clamp(0, ratio, 1) local base_r, base_g, base_b = int2rgb(base) local other_r, other_g, other_b = int2rgb(other) return rgb2int( clamp(0, base_r * (1 - ratio) + other_r * ratio, 0xff), clamp(0, base_g * (1 - ratio) + other_g * ratio, 0xff), clamp(0, base_b * (1 - ratio) + other_b * ratio, 0xff) ) end ---@return vim.api.keyset.highlight function Highlight:get_hl_info() local info = vim.api.nvim_get_hl(0, { name = self.name, link = false }) --[[@as table]] ---@type vim.api.keyset.highlight info = vim.tbl_isempty(info) and {} or info for _, op in ipairs(self.operations) do local reverse = op.reverse if reverse ~= nil then info.fg, info.bg = info.bg, info.fg info.ctermfg, info.ctermbg = info.ctermbg, info.ctermfg end local extend = op.extend if extend ~= nil then if type(extend) == "string" then info = vim.tbl_extend("keep", info, M.get(extend):get_hl_info()) else info = vim.tbl_extend("keep", info, extend:get_hl_info()) end end local mix = op.mix if mix ~= nil then local hl = mix.hl if type(hl) == "string" then hl = M.get(hl) end local other = hl:copy():extend("Normal"):get_hl_info() for _, key in ipairs({ "fg", "bg", "sp" }) do local base_color, other_color = info[key], other[key] if base_color and other_color then info[key] = int2hex( mix_colors( type(base_color) == "number" and base_color or hex2int(base_color), type(other_color) == "number" and other_color or hex2int(other_color), mix.ratio / 100 ) ) end end end local clear = op.clear if clear ~= nil then info[clear] = nil end local bold = op.bold if bold ~= nil then info.bold = bold end end return info end ---@return rc.Highlight function Highlight:copy() return vim.deepcopy(self) end ---@return boolean function Highlight:exists() return not vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = self.name })) end ---@private ---@param op rc.Highlight.Operation function Highlight:push_operation(op) table.insert(self.operations, op) end ---@param other rc.Highlight | string ---@return self function Highlight:extend(other) self:push_operation({ extend = other }) return self end ---@return self function Highlight:reverse() self:push_operation({ reverse = true }) return self end ---@return self function Highlight:clear_fg() self:push_operation({ clear = "fg" }) return self end ---@return self function Highlight:clear_bg() self:push_operation({ clear = "bg" }) return self end ---@param hl string | rc.Highlight ---@param ratio integer 0 ~ 100 ---@return self function Highlight:mix(hl, ratio) self:push_operation({ mix = { hl = hl, ratio = ratio } }) return self end ---@param bold boolean ---@return self function Highlight:bold(bold) self:push_operation({ bold = bold }) return self end ---@param name string ---@param val vim.api.keyset.highlight | rc.Highlight local function set(name, val) if getmetatable(val) == Highlight then ---@cast val rc.Highlight val:set(name) else ---@cast val vim.api.keyset.highlight local function set_hl() vim.api.nvim_set_hl(0, name, val) end set_hl() vim.api.nvim_create_autocmd("ColorScheme", { callback = set_hl }) end end ---@param highlights { [string]: string | rc.Highlight } function M.set(highlights) for name, val in pairs(highlights) do if type(val) == "string" then set(name, { link = val }) else set(name, val) end end end return M