neovim configuration using rocks.nvim plugin manager
1---@class bt.util.highlights
2local M = {}
3
4-- TODO: fuck. rewrite this sometime
5-- dealing with types is way hard then I thought
6
7---@alias HLAttr {from: string, attr: "fg" | "bg", alter: integer}
8---@alias float number
9
10---@class HLData
11---@field fg? string foreground
12---@field bg? string background
13---@field sp? string special
14---@field blend? integer between 0 and 100
15---@field bold? boolean
16---@field standout? boolean
17---@field underline? boolean
18---@field undercurl? boolean
19---@field underdouble? boolean
20---@field underdotted? boolean
21---@field underdashed? boolean
22---@field strikethrough? boolean
23---@field italic? boolean
24---@field reverse? boolean
25---@field nocombine? boolean
26---@field link? string
27---@field default? boolean
28
29---@alias HLAttrName
30---| '"fg"'
31---| '"bg"'
32---| '"sp"'
33---| '"blend"'
34---| '"bold"'
35---| '"standout"'
36---| '"underline"'
37---| '"undercurl"'
38---| '"underdouble"'
39---| '"underdotted"'
40---| '"underdashed"'
41---| '"strikethrough"'
42---| '"italic"'
43---| '"reverse"'
44---| '"nocombine"'
45---| '"link"'
46---| '"default"'
47
48---@class HLArgs: HLData
49---@field fg? string | HLAttr
50---@field bg? string | HLAttr
51---@field sp? string | HLAttr
52---@field clear? boolean clear existing highlight
53---@field inherit? string inherit other highlight
54
55local function num_to_hex(color)
56 return string.format("#%06X", color)
57end
58
59---@param opts? {name?: string, link?: boolean}
60---@param ns? integer
61---@return vim.api.keyset.hl_info|nil
62local function get_hl_as_hex(opts, ns)
63 opts = opts or {}
64 ns = ns or 0
65 opts.link = opts.link ~= nil and opts.link or false
66 local hl = vim.api.nvim_get_hl(ns, opts)
67 if vim.tbl_isempty(hl) then
68 return nil
69 end
70 hl.fg = hl.fg and num_to_hex(hl.fg)
71 hl.bg = hl.bg and num_to_hex(hl.bg)
72 return hl
73end
74
75---Change the brightness of a color, negative numbers darken and positive ones brighten
76---see:
77---1. https://stackoverflow.com/q/5560248
78---2. https://stackoverflow.com/a/37797380
79---@param color string A hex color
80---@param percent float a negative number darkens and a positive one brightens
81---@return string
82function M.tint(color, percent)
83 assert(color and percent, "cannot alter a color without specifying a color and percentage")
84 local r = tonumber(color:sub(2, 3), 16)
85 local g = tonumber(color:sub(4, 5), 16)
86 local b = tonumber(color:sub(6), 16)
87 if not r or not g or not b then
88 return "NONE"
89 end
90 local blend = function(component)
91 component = math.floor(component * (1 + percent))
92 return math.min(math.max(component, 0), 255)
93 end
94 return string.format("#%02x%02x%02x", blend(r), blend(g), blend(b))
95end
96
97---Get the value a highlight group whilst handling errors and fallbacks as well as returning a gui value
98---If no attribute is specified return the entire highlight table
99---in the right format
100---@param group string
101---@param attribute HLAttrName
102---@param fallback string?
103---@return string
104function M.get(group, attribute, fallback)
105 local data = get_hl_as_hex({ name = group })
106 local color = (data and data[attribute]) or fallback or "NONE"
107 if not color then
108 local error_msg =
109 string.format("failed to get highlight %s for attribute %s\n%s", group, attribute, debug.traceback())
110 local error_title = string.format("Highlight - get(%s)", group)
111 vim.schedule(function()
112 vim.notify(error_msg, vim.log.levels.ERROR, { title = error_title })
113 end)
114 return "NONE"
115 end
116 return color
117end
118
119---resolve fg/bg/sp attribute type
120---@param hl string | HLAttr
121---@param attr string
122---@return string
123local function resolve_from_attr(hl, attr)
124 if type(hl) ~= "table" then
125 return hl
126 end
127 local color = M.get(hl.from, hl.attr or attr)
128 color = color == "NONE" and M.get("Normal", hl.attr or attr) or color
129 -- TODO: tint color
130 return color
131end
132
133--- Sets a neovim highlight with some syntactic sugar. It takes a highlight table and converts
134--- any highlights specified as `GroupName = {fg = { from = 'group'}}` into the underlying colour
135--- by querying the highlight property of the from group so it can be used when specifying highlights
136--- as a shorthand to derive the right colour.
137--- For example:
138--- ```lua
139--- M.set({ MatchParen = {fg = {from = 'ErrorMsg'}}})
140--- ```
141--- This will take the foreground colour from ErrorMsg and set it to the foreground of MatchParen.
142--- NOTE: this function must NOT mutate the options table as these are re-used when the colorscheme is updated
143---
144---@param ns integer
145---@param name string
146---@param opts HLArgs
147---@overload fun(name: string, opts: HLArgs)
148function M.set(ns, name, opts)
149 if type(ns) == "string" and type(name) == "table" then
150 opts, name, ns = name, ns, 0
151 end
152
153 local hl = opts.clear and {} or get_hl_as_hex({ name = opts.inherit or name }) or {}
154 -- clear cterm
155 if not opts.cterm then
156 hl.cterm = nil
157 end
158 for attribute, data in pairs(opts) do
159 if attribute ~= "clear" and attribute ~= "inherit" then
160 local new_data = resolve_from_attr(data, attribute)
161 hl[attribute] = new_data
162 end
163 end
164
165 -- FIXME: this part
166 vim.api.nvim_set_hl(ns, name, hl --[[@as vim.api.keyset.highlight]])
167end
168
169---Apply a list of highlights
170---@param hls table<string, HLArgs>
171---@param namespace integer?
172function M.all(hls, namespace)
173 for name, args in pairs(hls) do
174 M.set(namespace or 0, name, args)
175 end
176end
177
178---Set window local highlights
179---@param name string
180---@param win_id number
181---@param hls table<string, HLArgs>
182function M.set_winhl(name, win_id, hls)
183 local namespace = vim.api.nvim_create_namespace(name)
184 M.all(hls, namespace)
185 vim.api.nvim_win_set_hl_ns(win_id, namespace)
186end
187
188---Run `cb()` on `ColorScheme` event.
189---This is useful when *color override* code is quite complicate
190---@param name string
191---@param cb function
192function M.plugin_wrap(name, cb)
193 cb()
194 local augroup_name = name:gsub("^%l", string.upper) .. "HighlightOverrides"
195 vim.api.nvim_create_autocmd({ "ColorScheme", "UIEnter" }, {
196 group = vim.api.nvim_create_augroup(augroup_name, { clear = true }),
197 callback = function()
198 -- Defer resetting these highlights to ensure they apply *after* other overrides
199 vim.defer_fn(function()
200 cb()
201 end, 1)
202 end,
203 })
204end
205
206---Apply highlights for a plugin and refresh on colorscheme change
207---@param name string plugin name
208---@param hls table<string, HLArgs>
209function M.plugin(name, hls)
210 M.plugin_wrap(name, function()
211 M.all(hls)
212 end)
213end
214
215---Apply highlight to given text
216---@param content any
217---@param hlgroup string
218---@return string
219function M.hl_text(content, hlgroup)
220 return string.format("%%#%s#%s%%*", hlgroup, content)
221end
222
223return M