1use core::fmt;
16use core::fmt::Display;
17
18use nvim_oxi::Dictionary;
19use nvim_oxi::api::SuperIterator;
20use nvim_oxi::api::opts::GetHighlightOpts;
21use nvim_oxi::api::opts::GetHighlightOptsBuilder;
22use nvim_oxi::api::types::GetHlInfos;
23use nvim_oxi::api::types::HighlightInfos;
24use rootcause::report;
25
26const GLOBAL_BG: &str = "#000000";
27const GLOBAL_FG: &str = "#c9c9c9";
28
29const CURSOR_BG: &str = "white";
30const CURSOR_FG: &str = "black";
31const NON_TEXT_FG: &str = "#777777";
32const COMMENTS_FG: &str = "#777777";
33const NONE: &str = "none";
34
35const DIAG_ERROR_FG: &str = "#ec635c";
36const DIAG_OK_FG: &str = "#8ce479";
37const DIAG_WARN_FG: &str = "#ffaa33";
38const DIAG_HINT_FG: &str = "NvimLightGrey3";
39const DIAG_INFO_FG: &str = "white";
40
41const GITSIGNS_ADDED: &str = DIAG_OK_FG;
42const GITSIGNS_CHANGED: &str = "#6a6adf";
43const GITSIGNS_REMOVED: &str = DIAG_ERROR_FG;
44
45const TREESITTER_CONTEXT_BG: &str = "NvimDarkGrey3";
46
47const DIAGNOSTICS_FG: [(&str, &str); 5] = [
48 ("Error", DIAG_ERROR_FG),
49 ("Warn", DIAG_WARN_FG),
50 ("Ok", DIAG_OK_FG),
51 ("Hint", DIAG_HINT_FG),
52 ("Info", DIAG_INFO_FG),
53];
54
55const GITSIGNS_FG: [(&str, &str); 3] = [
56 ("Added", GITSIGNS_ADDED),
57 ("Changed", GITSIGNS_CHANGED),
58 ("Removed", GITSIGNS_REMOVED),
59];
60
61pub fn dict() -> Dictionary {
63 dict! {
64 "set": fn_from!(set),
65 }
66}
67
68#[allow(clippy::needless_pass_by_value)]
70pub fn set(colorscheme: Option<String>) {
71 if let Some(cs) = colorscheme {
72 let _ = ytil_noxi::common::exec_vim_cmd("colorscheme", Some(&[cs]));
73 }
74
75 let opts = crate::vim_opts::global_scope();
76 crate::vim_opts::set("background", "dark", &opts);
77 crate::vim_opts::set("termguicolors", true, &opts);
78
79 let non_text_hl = LuaHlOpts::new().fg(NON_TEXT_FG).bg(NONE);
80 let statusline_hl = non_text_hl.clone().reverse(false);
81
82 for (hl_name, hl_opts) in [
83 ("Cursor", LuaHlOpts::new().fg(CURSOR_FG).bg(CURSOR_BG)),
84 ("CursorLine", LuaHlOpts::new().fg(NONE)),
85 ("ErrorMsg", LuaHlOpts::new().fg(DIAG_ERROR_FG)),
86 ("MsgArea", LuaHlOpts::new().fg(COMMENTS_FG).bg(NONE)),
87 ("LineNr", non_text_hl),
88 ("Normal", LuaHlOpts::new().bg(GLOBAL_BG)),
89 ("NormalFloat", LuaHlOpts::new().bg(GLOBAL_BG)),
90 ("StatusLine", statusline_hl.clone()),
91 ("StatusLineNC", statusline_hl),
92 ("TreesitterContext", LuaHlOpts::new().bg(TREESITTER_CONTEXT_BG)),
93 ("WinSeparator", LuaHlOpts::new().fg(TREESITTER_CONTEXT_BG)),
94 ("@variable", LuaHlOpts::new().fg(GLOBAL_FG)),
96 ("Comment", LuaHlOpts::new().fg(COMMENTS_FG)),
97 ("Constant", LuaHlOpts::new().fg(GLOBAL_FG)),
98 ("Delimiter", LuaHlOpts::new().fg(GLOBAL_FG)),
99 ("PreProc", LuaHlOpts::new().fg(GLOBAL_FG)),
101 ("Operator", LuaHlOpts::new().fg(GLOBAL_FG)),
102 ("Statement", LuaHlOpts::new().fg(GLOBAL_FG).bold(true)),
103 ("Type", LuaHlOpts::new().fg(GLOBAL_FG)),
104 ] {
105 set_hl(0, hl_name, &hl_opts);
106 }
107
108 for (lvl, fg) in DIAGNOSTICS_FG {
109 let _ = get_overridden_hl_opts(
111 &format!("Diagnostic{lvl}"),
112 |hl_opts| hl_opts.fg(fg).bg(NONE).bold(true),
113 None,
114 )
115 .map(|hl_opts| {
116 set_hl(0, &format!("Diagnostic{lvl}"), &hl_opts);
117 set_hl(0, &format!("DiagnosticStatusLine{lvl}"), &hl_opts);
118 });
119
120 let diag_underline_hl_name = format!("DiagnosticUnderline{lvl}");
121 let _ = get_overridden_hl_opts(&diag_underline_hl_name, |hl_opts| hl_opts.special(fg).bg(NONE), None)
123 .map(|hl_opts| set_hl(0, &diag_underline_hl_name, &hl_opts));
124 }
125
126 for (hl_name, fg) in GITSIGNS_FG {
127 set_hl(0, hl_name, &LuaHlOpts::new().fg(fg));
128 }
129}
130
131fn get_overridden_hl_opts(
140 hl_name: &str,
141 override_hl_opts: impl FnOnce(LuaHlOpts) -> LuaHlOpts,
142 opts_builder: Option<GetHighlightOptsBuilder>,
143) -> rootcause::Result<LuaHlOpts> {
144 let mut get_hl_opts = opts_builder.unwrap_or_default();
145 let hl_infos = get_hl_single(0, &get_hl_opts.name(hl_name).build())?;
146 Ok(override_hl_opts(LuaHlOpts::from(&hl_infos)))
147}
148
149fn set_hl(ns_id: u32, hl_name: &str, hl_opts: &LuaHlOpts) {
155 let lua_cmd = format!("lua vim.api.nvim_set_hl({ns_id}, '{hl_name}', {hl_opts})",);
156
157 if let Err(err) = nvim_oxi::api::command(&lua_cmd) {
158 ytil_noxi::notify::error(format!(
159 "error setting highlight opts | lua_cmd={lua_cmd:?} error={err:#?}"
160 ));
161 }
162}
163
164fn get_hl_single(ns_id: u32, hl_opts: &GetHighlightOpts) -> rootcause::Result<HighlightInfos> {
170 get_hl(ns_id, hl_opts).and_then(|hl| match hl {
171 GetHlInfos::Single(highlight_infos) => Ok(highlight_infos),
172 GetHlInfos::Map(hl_infos) => Err(report!(
173 "multiple highlight infos returned | hl_infos={:#?} hl_opts={hl_opts:#?}",
174 hl_infos.collect::<Vec<_>>()
175 )),
176 })
177}
178
179#[allow(dead_code)]
185fn get_hl_multiple(
186 ns_id: u32,
187 hl_opts: &GetHighlightOpts,
188) -> rootcause::Result<Vec<(nvim_oxi::String, HighlightInfos)>> {
189 get_hl(ns_id, hl_opts).and_then(|hl| match hl {
190 GetHlInfos::Single(hl_info) => Err(report!(
191 "single highlight info returned | hl_info={hl_info:#?} hl_opts={hl_opts:#?}",
192 )),
193 GetHlInfos::Map(hl_infos) => Ok(hl_infos.into_iter().collect()),
194 })
195}
196
197fn get_hl(
202 ns_id: u32,
203 hl_opts: &GetHighlightOpts,
204) -> rootcause::Result<GetHlInfos<impl SuperIterator<(nvim_oxi::String, HighlightInfos)>>> {
205 nvim_oxi::api::get_hl(ns_id, hl_opts)
206 .inspect_err(|err| {
207 ytil_noxi::notify::error(format!(
208 "cannot get highlight infos | hl_opts={hl_opts:#?} error={err:#?}"
209 ));
210 })
211 .map_err(From::from)
212}
213
214#[derive(Clone, Debug, Default)]
220struct LuaHlOpts {
221 foreground: Option<String>,
222 background: Option<String>,
223 special_color: Option<String>,
224 bold: Option<bool>,
225 italic: Option<bool>,
226 reverse: Option<bool>,
227 standout: Option<bool>,
228 strikethrough: Option<bool>,
229 underline: Option<bool>,
230 undercurl: Option<bool>,
231 underdouble: Option<bool>,
232 underdotted: Option<bool>,
233 underdashed: Option<bool>,
234 altfont: Option<bool>,
235 nocombine: Option<bool>,
236 fallback: Option<bool>,
237 fg_indexed: Option<bool>,
238 bg_indexed: Option<bool>,
239 force: Option<bool>,
240 blend: Option<u32>,
241}
242
243impl LuaHlOpts {
244 fn new() -> Self {
245 Self::default()
246 }
247
248 fn fg(mut self, color: &str) -> Self {
249 self.foreground = Some(color.to_owned());
250 self
251 }
252
253 fn bg(mut self, color: &str) -> Self {
254 self.background = Some(color.to_owned());
255 self
256 }
257
258 fn special(mut self, color: &str) -> Self {
259 self.special_color = Some(color.to_owned());
260 self
261 }
262
263 const fn bold(mut self, value: bool) -> Self {
264 self.bold = Some(value);
265 self
266 }
267
268 const fn reverse(mut self, value: bool) -> Self {
269 self.reverse = Some(value);
270 self
271 }
272}
273
274impl From<&HighlightInfos> for LuaHlOpts {
275 fn from(infos: &HighlightInfos) -> Self {
276 let mut opts = Self::new();
277 if let Some(v) = infos.foreground {
278 opts.foreground = Some(decimal_to_hex_color(v));
279 }
280 if let Some(v) = infos.background {
281 opts.background = Some(decimal_to_hex_color(v));
282 }
283 if let Some(v) = infos.special {
284 opts.special_color = Some(decimal_to_hex_color(v));
285 }
286 opts.bold = infos.bold;
287 opts.italic = infos.italic;
288 opts.reverse = infos.reverse;
289 opts.standout = infos.standout;
290 opts.strikethrough = infos.strikethrough;
291 opts.underline = infos.underline;
292 opts.undercurl = infos.undercurl;
293 opts.underdouble = infos.underlineline;
294 opts.underdotted = infos.underdot;
295 opts.underdashed = infos.underdash;
296 opts.altfont = infos.altfont;
297 opts.fallback = infos.fallback;
298 opts.fg_indexed = infos.fg_indexed;
299 opts.bg_indexed = infos.bg_indexed;
300 opts.force = infos.force;
301 opts.blend = infos.blend;
302 opts
303 }
304}
305
306impl Display for LuaHlOpts {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 let mut entries: Vec<String> = Vec::new();
310
311 for (key, val) in [
312 ("fg", &self.foreground),
313 ("bg", &self.background),
314 ("sp", &self.special_color),
315 ] {
316 if let Some(v) = val {
317 entries.push(format!("{key} = '{v}'"));
318 }
319 }
320
321 for (key, val) in [
322 ("bold", self.bold),
323 ("italic", self.italic),
324 ("reverse", self.reverse),
325 ("standout", self.standout),
326 ("strikethrough", self.strikethrough),
327 ("underline", self.underline),
328 ("undercurl", self.undercurl),
329 ("underdouble", self.underdouble),
330 ("underdotted", self.underdotted),
331 ("underdashed", self.underdashed),
332 ("altfont", self.altfont),
333 ("nocombine", self.nocombine),
334 ("fallback", self.fallback),
335 ("fg_indexed", self.fg_indexed),
336 ("bg_indexed", self.bg_indexed),
337 ("force", self.force),
338 ] {
339 if let Some(v) = val {
340 entries.push(format!("{key} = {v}"));
341 }
342 }
343
344 if let Some(v) = self.blend {
345 entries.push(format!("blend = {v}"));
346 }
347
348 write!(f, "{{ {} }}", entries.join(", "))
349 }
350}
351
352fn decimal_to_hex_color(decimal: u32) -> String {
354 format!("#{decimal:06X}")
355}
356
357#[cfg(test)]
358mod tests {
359 use rstest::rstest;
360
361 use super::*;
362
363 #[test]
364 fn from_default_highlight_infos_produces_default_lua_hl_opts() {
365 let infos = HighlightInfos::default();
366 let opts = LuaHlOpts::from(&infos);
367 pretty_assertions::assert_eq!(opts.foreground, None);
368 pretty_assertions::assert_eq!(opts.background, None);
369 pretty_assertions::assert_eq!(opts.special_color, None);
370 pretty_assertions::assert_eq!(opts.bold, None);
371 pretty_assertions::assert_eq!(opts.blend, None);
372 }
373
374 #[rstest]
375 #[case(0x00_00_00, "#000000")]
376 #[case(0xFF_FF_FF, "#FFFFFF")]
377 #[case(0xFF_00_00, "#FF0000")]
378 #[case(0x00_20_20, "#002020")]
379 fn from_highlight_infos_converts_foreground_to_hex(#[case] rgb: u32, #[case] expected: &str) {
380 let mut infos = HighlightInfos::default();
381 infos.foreground = Some(rgb);
382 pretty_assertions::assert_eq!(LuaHlOpts::from(&infos).foreground.as_deref(), Some(expected));
383 }
384
385 #[rstest]
386 #[case(0xFF_FF_FF, "#FFFFFF")]
387 #[case(0x00_20_20, "#002020")]
388 fn from_highlight_infos_converts_background_to_hex(#[case] rgb: u32, #[case] expected: &str) {
389 let mut infos = HighlightInfos::default();
390 infos.background = Some(rgb);
391 pretty_assertions::assert_eq!(LuaHlOpts::from(&infos).background.as_deref(), Some(expected));
392 }
393
394 #[test]
395 fn from_highlight_infos_converts_special_to_hex() {
396 let mut infos = HighlightInfos::default();
397 infos.special = Some(0xFF_00_00);
398 pretty_assertions::assert_eq!(LuaHlOpts::from(&infos).special_color.as_deref(), Some("#FF0000"));
399 }
400
401 #[test]
402 fn from_highlight_infos_maps_boolean_fields() {
403 let mut infos = HighlightInfos::default();
404 infos.bold = Some(true);
405 infos.italic = Some(false);
406 infos.underline = Some(true);
407 infos.underdot = Some(true);
408 infos.underdash = Some(true);
409 infos.underlineline = Some(true);
410
411 let opts = LuaHlOpts::from(&infos);
412 pretty_assertions::assert_eq!(opts.bold, Some(true));
413 pretty_assertions::assert_eq!(opts.italic, Some(false));
414 pretty_assertions::assert_eq!(opts.underline, Some(true));
415 pretty_assertions::assert_eq!(opts.underdotted, Some(true));
416 pretty_assertions::assert_eq!(opts.underdashed, Some(true));
417 pretty_assertions::assert_eq!(opts.underdouble, Some(true));
418 }
419
420 #[test]
421 fn from_highlight_infos_maps_blend() {
422 let mut infos = HighlightInfos::default();
423 infos.blend = Some(50);
424 pretty_assertions::assert_eq!(LuaHlOpts::from(&infos).blend, Some(50));
425 }
426
427 #[rstest]
428 #[case(LuaHlOpts::new(), "{ }")]
429 #[case(LuaHlOpts::new().fg("black"), "{ fg = 'black' }")]
430 #[case(LuaHlOpts::new().bg("#000000"), "{ bg = '#000000' }")]
431 #[case(LuaHlOpts::new().fg("#000000").bg("white"), "{ fg = '#000000', bg = 'white' }")]
432 #[case(LuaHlOpts::new().bold(true), "{ bold = true }")]
433 #[case(LuaHlOpts::new().special("red"), "{ sp = 'red' }")]
434 #[case(LuaHlOpts { blend: Some(30), ..Default::default() }, "{ blend = 30 }")]
435 #[case(
436 LuaHlOpts::new().fg("black").bg("white").special("#FF0000").bold(true),
437 "{ fg = 'black', bg = 'white', sp = '#FF0000', bold = true }",
438 )]
439 fn display_renders_lua_table(#[case] opts: LuaHlOpts, #[case] expected: &str) {
440 pretty_assertions::assert_eq!(opts.to_string(), expected);
441 }
442}