local fmt = string.format local fn = vim.fn local api = vim.api local P = as.style.palette local L = as.style.lsp.colors local levels = vim.log.levels local M = {} ---Convert a hex color to RGB ---@param color string ---@return number ---@return number ---@return number local function hex_to_rgb(color) local hex = color:gsub('#', '') return tonumber(hex:sub(1, 2), 16), tonumber(hex:sub(3, 4), 16), tonumber(hex:sub(5), 16) end local function alter(attr, percent) return math.floor(attr * (100 + percent) / 100) end ---@source https://stackoverflow.com/q/5560248 ---@see: https://stackoverflow.com/a/37797380 ---Darken a specified hex color ---@param color string ---@param percent number ---@return string function M.alter_color(color, percent) local r, g, b = hex_to_rgb(color) if not r or not g or not b then return 'NONE' end r, g, b = alter(r, percent), alter(g, percent), alter(b, percent) r, g, b = math.min(r, 255), math.min(g, 255), math.min(b, 255) return fmt('#%02x%02x%02x', r, g, b) end --- Check if the current window has a winhighlight --- which includes the specific target highlight --- @param win_id integer --- @vararg string --- @return boolean, string function M.winhighlight_exists(win_id, ...) local win_hl = vim.wo[win_id].winhighlight for _, target in ipairs { ... } do if win_hl:match(target) ~= nil then return true, win_hl end end return false, win_hl end ---@param group_name string A highlight group name local function get_hl(group_name) local ok, hl = pcall(api.nvim_get_hl_by_name, group_name, true) if ok then hl.foreground = hl.foreground and '#' .. bit.tohex(hl.foreground, 6) hl.background = hl.background and '#' .. bit.tohex(hl.background, 6) hl[true] = nil -- BUG: API returns a true key which errors during the merge return hl end return {} end ---A mechanism to allow inheritance of the winhighlight of a specific ---group in a window ---@param win_id number ---@param target string ---@param name string ---@param fallback string function M.adopt_winhighlight(win_id, target, name, fallback) local win_hl_name = name .. win_id local _, win_hl = M.winhighlight_exists(win_id, target) local hl_exists = fn.hlexists(win_hl_name) > 0 if hl_exists then return win_hl_name end local parts = vim.split(win_hl, ',') local found = as.find(parts, function(part) return part:match(target) end) if not found then return fallback end local hl_group = vim.split(found, ':')[2] local bg = M.get_hl(hl_group, 'bg') M.set_hl(win_hl_name, { background = bg, inherit = fallback }) return win_hl_name end ---@param name string ---@param opts table function M.set_hl(name, opts) assert(name and opts, "Both 'name' and 'opts' must be specified") local hl = get_hl(opts.inherit or name) opts.inherit = nil local ok, msg = pcall(api.nvim_set_hl, 0, name, vim.tbl_deep_extend('force', hl, opts)) if not ok then vim.notify(fmt('Failed to set %s because: %s', name, msg)) end end ---Get the value a highlight group whilst handling errors, fallbacks as well as returning a gui value ---in the right format ---@param group string ---@param attribute string ---@param fallback string? ---@return string function M.get_hl(group, attribute, fallback) if not group then vim.notify('Cannot get a highlight without specifying a group', levels.ERROR) return 'NONE' end local hl = get_hl(group) attribute = ({ fg = 'foreground', bg = 'background' })[attribute] or attribute local color = hl[attribute] or fallback if not color then vim.schedule(function() vim.notify(fmt('%s %s does not exist', group, attribute), levels.INFO) end) return 'NONE' end -- convert the decimal RGBA value from the hl by name to a 6 character hex + padding if needed return color end function M.clear_hl(name) assert(name, 'name is required to clear a highlight') api.nvim_set_hl(0, name, {}) end ---Apply a list of highlights ---@param hls table> function M.all(hls) for name, hl in pairs(hls) do M.set_hl(name, hl) end end --------------------------------------------------------------------------------- -- Plugin highlights --------------------------------------------------------------------------------- ---Apply highlights for a plugin and refresh on colorscheme change ---@param name string plugin name ---@vararg table list of highlights function M.plugin(name, hls) name = name:gsub('^%l', string.upper) -- capitalise the name for autocommand convention sake M.all(hls) as.augroup(fmt('%sHighlightOverrides', name), { { event = 'ColorScheme', command = function() M.all(hls) end, }, }) end local function general_overrides() local comment_fg = M.get_hl('Comment', 'fg') local keyword_fg = M.get_hl('Keyword', 'fg') local normal_bg = M.get_hl('Normal', 'bg') local code_block = M.alter_color(normal_bg, 30) local msg_area_bg = M.alter_color(normal_bg, -10) local hint_line = M.alter_color(L.hint, -80) local error_line = M.alter_color(L.error, -80) local warn_line = M.alter_color(L.warn, -80) M.all { VertSplit = { background = 'NONE', foreground = M.get_hl('NonText', 'fg') }, WinSeparator = { background = 'NONE', foreground = M.get_hl('NonText', 'fg') }, mkdLineBreak = { link = 'NONE' }, Directory = { inherit = 'Keyword', bold = true }, URL = { inherit = 'Keyword', underline = true }, -----------------------------------------------------------------------------// -- Commandline -----------------------------------------------------------------------------// MsgArea = { background = msg_area_bg }, MsgSeparator = { foreground = comment_fg, background = msg_area_bg }, -----------------------------------------------------------------------------// -- Floats -----------------------------------------------------------------------------// NormalFloat = { inherit = 'Pmenu' }, FloatBorder = { inherit = 'NormalFloat', foreground = M.get_hl('NonText', 'fg') }, CodeBlock = { background = code_block }, markdownCode = { background = code_block }, markdownCodeBlock = { background = code_block }, -----------------------------------------------------------------------------// CursorLineNr = { bold = true }, FoldColumn = { background = 'background' }, Folded = { inherit = 'Comment', italic = true, bold = true }, TermCursor = { ctermfg = 'green', foreground = 'royalblue' }, -- Add undercurl to existing spellbad highlight SpellBad = { undercurl = true, background = 'NONE', foreground = 'NONE', sp = 'green' }, SpellRare = { undercurl = true }, PmenuSbar = { background = P.grey }, -----------------------------------------------------------------------------// -- Diff -----------------------------------------------------------------------------// DiffAdd = { background = '#26332c', foreground = 'NONE' }, DiffDelete = { background = '#572E33', foreground = '#5c6370' }, DiffChange = { background = '#273842', foreground = 'NONE' }, DiffText = { background = '#314753', foreground = 'NONE' }, diffAdded = { link = 'DiffAdd' }, diffChanged = { link = 'DiffChange' }, diffRemoved = { link = 'DiffDelete' }, diffBDiffer = { link = 'WarningMsg' }, diffCommon = { link = 'WarningMsg' }, diffDiffer = { link = 'WarningMsg' }, diffFile = { link = 'Directory' }, diffIdentical = { link = 'WarningMsg' }, diffIndexLine = { link = 'Number' }, diffIsA = { link = 'WarningMsg' }, diffNoEOL = { link = 'WarningMsg' }, diffOnly = { link = 'WarningMsg' }, -----------------------------------------------------------------------------// -- colorscheme overrides -----------------------------------------------------------------------------// Comment = { italic = true }, Type = { italic = true, bold = true }, Include = { italic = true, bold = false }, QuickFixLine = { inherit = 'PmenuSbar', foreground = 'NONE', italic = true }, -- Neither the sign column or end of buffer highlights require an explicit background -- they should both just use the background that is in the window they are in. -- if either are specified this can lead to issues when a winhighlight is set SignColumn = { background = 'NONE' }, EndOfBuffer = { background = 'NONE' }, -----------------------------------------------------------------------------// -- Treesitter -----------------------------------------------------------------------------// TSNamespace = { link = 'TypeBuiltin' }, TSKeywordReturn = { italic = true, foreground = keyword_fg }, TSParameter = { italic = true, bold = true, foreground = 'NONE' }, TSError = { undercurl = true, sp = error_line, foreground = 'NONE' }, -- highlight FIXME comments commentTSWarning = { background = P.light_red, foreground = 'fg', bold = true }, commentTSDanger = { background = L.hint, foreground = '#1B2229', bold = true }, commentTSNote = { background = L.info, foreground = '#1B2229', bold = true }, -----------------------------------------------------------------------------// -- LSP -----------------------------------------------------------------------------// LspCodeLens = { link = 'NonText' }, LspReferenceText = { underline = true, background = 'NONE' }, LspReferenceRead = { underline = true, background = 'NONE' }, -- This represents when a reference is assigned which is more interesting than regular -- occurrences so should be highlighted more distinctly LspReferenceWrite = { underline = true, bold = true, italic = true, background = 'NONE' }, DiagnosticHint = { foreground = L.hint }, DiagnosticError = { foreground = L.error }, DiagnosticWarning = { foreground = L.warn }, DiagnosticInfo = { foreground = L.info }, DiagnosticUnderlineError = { undercurl = true, sp = L.error, foreground = 'none' }, DiagnosticUnderlineHint = { undercurl = true, sp = L.hint, foreground = 'none' }, DiagnosticUnderlineWarn = { undercurl = true, sp = L.warn, foreground = 'none' }, DiagnosticUnderlineInfo = { undercurl = true, sp = L.info, foreground = 'none' }, DiagnosticSignHintLine = { background = hint_line }, DiagnosticSignErrorLine = { background = error_line }, DiagnosticSignWarnLine = { background = warn_line }, DiagnosticSignWarn = { link = 'DiagnosticWarn' }, DiagnosticSignInfo = { link = 'DiagnosticInfo' }, DiagnosticSignHint = { link = 'DiagnosticHint' }, DiagnosticSignError = { link = 'DiagnosticError' }, DiagnosticFloatingWarn = { link = 'DiagnosticWarn' }, DiagnosticFloatingInfo = { link = 'DiagnosticInfo' }, DiagnosticFloatingHint = { link = 'DiagnosticHint' }, DiagnosticFloatingError = { link = 'DiagnosticError' }, } end local function set_sidebar_highlight() local normal_bg = M.get_hl('Normal', 'bg') local split_color = M.get_hl('VertSplit', 'fg') local bg_color = M.alter_color(normal_bg, -8) local st_color = M.alter_color(M.get_hl('Visual', 'bg'), -20) M.all { PanelBackground = { background = bg_color }, PanelHeading = { background = bg_color, bold = true }, PanelVertSplit = { foreground = split_color, background = bg_color }, PanelWinSeparator = { foreground = split_color, background = bg_color }, PanelStNC = { background = bg_color, foreground = split_color }, PanelSt = { background = st_color }, } end local sidebar_fts = { 'packer', 'flutterToolsOutline', 'undotree', 'neo-tree', 'Outline', } local function on_sidebar_enter() vim.wo.winhighlight = table.concat({ 'Normal:PanelBackground', 'EndOfBuffer:PanelBackground', 'StatusLine:PanelSt', 'StatusLineNC:PanelStNC', 'SignColumn:PanelBackground', 'VertSplit:PanelVertSplit', 'WinSeparator:PanelWinSeparator', }, ',') end local function colorscheme_overrides() if vim.g.colors_name == 'doom-one' then local keyword_fg = M.get_hl('Keyword', 'fg') M.all { CursorLineNr = { foreground = keyword_fg } } end end local function user_highlights() general_overrides() colorscheme_overrides() set_sidebar_highlight() end as.augroup('UserHighlights', { { event = 'ColorScheme', command = function() user_highlights() end, }, { event = 'FileType', pattern = sidebar_fts, command = function() on_sidebar_enter() end, }, }) -----------------------------------------------------------------------------// -- Color Scheme {{{1 -----------------------------------------------------------------------------// if as.plugin_installed 'doom-one.nvim' then vim.cmd 'colorscheme doom-one' end return M