1use nvim_oxi::Dictionary;
6use nvim_oxi::api::SuperIterator;
7use nvim_oxi::api::opts::GetHighlightOpts;
8use nvim_oxi::api::opts::GetHighlightOptsBuilder;
9use nvim_oxi::api::opts::SetHighlightOpts;
10use nvim_oxi::api::types::GetHlInfos;
11use nvim_oxi::api::types::HighlightInfos;
12use rootcause::report;
13
14const GLOBAL_BG: &str = "#001300";
15const GLOBAL_FG: &str = "#c9c9c9";
16
17const CURSOR_BG: &str = "white";
18const CURSOR_FG: &str = "black";
19const NON_TEXT_FG: &str = "#777777";
20const COMMENTS_FG: &str = "#777777";
21const NORMAL_FG: &str = "fg";
22const NONE: &str = "none";
23
24const DIAG_ERROR_FG: &str = "#ec635c";
25const DIAG_OK_FG: &str = "#8ce479";
26const DIAG_WARN_FG: &str = "#ffaa33";
27const DIAG_HINT_FG: &str = "#00ffff";
28const DIAG_INFO_FG: &str = "#00ffff";
29
30const GITSIGNS_ADDED: &str = DIAG_OK_FG;
31const GITSIGNS_CHANGED: &str = "#6a6adf";
32const GITSIGNS_REMOVED: &str = DIAG_ERROR_FG;
33
34const TREESITTER_CONTEXT_BG: &str = "NvimDarkGrey3";
35
36const DIAGNOSTICS_FG: [(&str, &str); 5] = [
37 ("Error", DIAG_ERROR_FG),
38 ("Warn", DIAG_WARN_FG),
39 ("Ok", DIAG_OK_FG),
40 ("Hint", DIAG_HINT_FG),
41 ("Info", DIAG_INFO_FG),
42];
43
44const GITSIGNS_FG: [(&str, &str); 3] = [
45 ("Added", GITSIGNS_ADDED),
46 ("Changed", GITSIGNS_CHANGED),
47 ("Removed", GITSIGNS_REMOVED),
48];
49
50pub fn dict() -> Dictionary {
52 dict! {
53 "set": fn_from!(set),
54 }
55}
56
57pub fn set(colorscheme: Option<String>) {
59 if let Some(cs) = colorscheme {
60 drop(ytil_noxi::common::exec_vim_cmd("colorscheme", Some(&[cs])));
61 }
62
63 let opts = crate::vim_opts::global_scope();
64 crate::vim_opts::set("background", "dark", &opts);
65 crate::vim_opts::set("termguicolors", true, &opts);
66
67 let non_text_hl = HighlightOpts::new().fg(NON_TEXT_FG).bg(NONE);
68 let statusline_hl = non_text_hl.clone().reverse(false);
69
70 for (hl_name, hl_opts) in [
71 ("Cursor", HighlightOpts::new().fg(CURSOR_FG).bg(CURSOR_BG)),
72 ("CursorLine", HighlightOpts::new().fg(NONE)),
73 (
74 "DiagnosticUnnecessary",
75 HighlightOpts::new()
76 .fg(NORMAL_FG)
77 .bg(NONE)
78 .underline(false)
79 .undercurl(false),
80 ),
81 ("ErrorMsg", HighlightOpts::new().fg(DIAG_ERROR_FG)),
82 ("MsgArea", HighlightOpts::new().fg(COMMENTS_FG).bg(NONE)),
83 ("LineNr", non_text_hl),
84 ("Normal", HighlightOpts::new().bg(GLOBAL_BG)),
85 ("NormalFloat", HighlightOpts::new().bg(GLOBAL_BG)),
86 ("StatusLine", statusline_hl.clone()),
87 ("StatusLineNC", statusline_hl),
88 ("TreesitterContext", HighlightOpts::new().bg(TREESITTER_CONTEXT_BG)),
89 ("WinSeparator", HighlightOpts::new().fg(TREESITTER_CONTEXT_BG)),
90 ("@variable", HighlightOpts::new().fg(GLOBAL_FG)),
92 ("Comment", HighlightOpts::new().fg(COMMENTS_FG)),
93 ("Constant", HighlightOpts::new().fg(GLOBAL_FG)),
94 ("Delimiter", HighlightOpts::new().fg(GLOBAL_FG)),
95 ("PreProc", HighlightOpts::new().fg(GLOBAL_FG)),
97 ("Operator", HighlightOpts::new().fg(GLOBAL_FG)),
98 ("Statement", HighlightOpts::new().fg(GLOBAL_FG).bold(true)),
99 ("Type", HighlightOpts::new().fg(GLOBAL_FG)),
100 ] {
101 set_hl(0, hl_name, &hl_opts);
102 }
103
104 for (lvl, fg) in DIAGNOSTICS_FG {
105 if let Ok(hl_opts) = get_overridden_hl_opts(
107 &format!("Diagnostic{lvl}"),
108 |hl_opts| hl_opts.fg(fg).bg(NONE).bold(true),
109 None,
110 ) {
111 set_hl(0, &format!("Diagnostic{lvl}"), &hl_opts);
112 set_hl(0, &format!("DiagnosticStatusLine{lvl}"), &hl_opts);
113 }
114
115 let diag_underline_hl_name = format!("DiagnosticUnderline{lvl}");
116 if let Ok(hl_opts) = get_overridden_hl_opts(
118 &diag_underline_hl_name,
119 |hl_opts| hl_opts.special(fg).bg(NONE).underline(true).undercurl(false),
120 None,
121 ) {
122 set_hl(0, &diag_underline_hl_name, &hl_opts);
123 }
124 }
125
126 for (hl_name, fg) in GITSIGNS_FG {
127 set_hl(0, hl_name, &HighlightOpts::new().fg(fg));
128 }
129}
130
131fn get_overridden_hl_opts(
140 hl_name: &str,
141 override_hl_opts: impl FnOnce(HighlightOpts) -> HighlightOpts,
142 opts_builder: Option<GetHighlightOptsBuilder>,
143) -> rootcause::Result<HighlightOpts> {
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(HighlightOpts::from(&hl_infos)))
147}
148
149fn set_hl(ns_id: u32, hl_name: &str, hl_opts: &HighlightOpts) {
151 if let Err(err) = nvim_oxi::api::set_hl(ns_id, hl_name, &hl_opts.to_set_highlight_opts()) {
152 ytil_noxi::notify::error(format!(
153 "error setting highlight opts | hl_name={hl_name:?} hl_opts={hl_opts:#?} error={err:#?}"
154 ));
155 }
156}
157
158fn get_hl_single(ns_id: u32, hl_opts: &GetHighlightOpts) -> rootcause::Result<HighlightInfos> {
164 get_hl(ns_id, hl_opts).and_then(|hl| match hl {
165 GetHlInfos::Single(highlight_infos) => Ok(highlight_infos),
166 GetHlInfos::Map(hl_infos) => Err(report!(
167 "multiple highlight infos returned | hl_infos={:#?} hl_opts={hl_opts:#?}",
168 hl_infos.collect::<Vec<_>>()
169 )),
170 })
171}
172
173#[expect(dead_code, reason = "kept for debugging highlight maps")]
179fn get_hl_multiple(
180 ns_id: u32,
181 hl_opts: &GetHighlightOpts,
182) -> rootcause::Result<Vec<(nvim_oxi::String, HighlightInfos)>> {
183 get_hl(ns_id, hl_opts).and_then(|hl| match hl {
184 GetHlInfos::Single(hl_info) => Err(report!(
185 "single highlight info returned | hl_info={hl_info:#?} hl_opts={hl_opts:#?}",
186 )),
187 GetHlInfos::Map(hl_infos) => Ok(hl_infos.into_iter().collect()),
188 })
189}
190
191fn get_hl(
196 ns_id: u32,
197 hl_opts: &GetHighlightOpts,
198) -> rootcause::Result<GetHlInfos<impl SuperIterator<(nvim_oxi::String, HighlightInfos)>>> {
199 nvim_oxi::api::get_hl(ns_id, hl_opts)
200 .inspect_err(|err| {
201 ytil_noxi::notify::error(format!(
202 "cannot get highlight infos | hl_opts={hl_opts:#?} error={err:#?}"
203 ));
204 })
205 .map_err(From::from)
206}
207
208#[derive(Clone, Debug, Default)]
210struct HighlightOpts {
211 foreground: Option<String>,
212 background: Option<String>,
213 special_color: Option<String>,
214 bold: Option<bool>,
215 italic: Option<bool>,
216 reverse: Option<bool>,
217 standout: Option<bool>,
218 strikethrough: Option<bool>,
219 underline: Option<bool>,
220 undercurl: Option<bool>,
221 underdouble: Option<bool>,
222 underdotted: Option<bool>,
223 underdashed: Option<bool>,
224 altfont: Option<bool>,
225 nocombine: Option<bool>,
226 fallback: Option<bool>,
227 fg_indexed: Option<bool>,
228 bg_indexed: Option<bool>,
229 force: Option<bool>,
230 blend: Option<u32>,
231}
232
233impl HighlightOpts {
234 fn new() -> Self {
235 Self::default()
236 }
237
238 fn fg(mut self, color: &str) -> Self {
239 self.foreground = Some(color.to_owned());
240 self
241 }
242
243 fn bg(mut self, color: &str) -> Self {
244 self.background = Some(color.to_owned());
245 self
246 }
247
248 fn special(mut self, color: &str) -> Self {
249 self.special_color = Some(color.to_owned());
250 self
251 }
252
253 const fn bold(mut self, value: bool) -> Self {
254 self.bold = Some(value);
255 self
256 }
257
258 const fn reverse(mut self, value: bool) -> Self {
259 self.reverse = Some(value);
260 self
261 }
262
263 const fn underline(mut self, value: bool) -> Self {
264 self.underline = Some(value);
265 self
266 }
267
268 const fn undercurl(mut self, value: bool) -> Self {
269 self.undercurl = Some(value);
270 self
271 }
272
273 fn to_set_highlight_opts(&self) -> SetHighlightOpts {
274 let mut opts = SetHighlightOpts::builder();
275
276 if let Some(v) = self.foreground.as_deref() {
277 let _ = opts.foreground(v);
278 }
279 if let Some(v) = self.background.as_deref() {
280 let _ = opts.background(v);
281 }
282 if let Some(v) = self.special_color.as_deref() {
283 let _ = opts.special(v);
284 }
285 if let Some(v) = self.bold {
286 let _ = opts.bold(v);
287 }
288 if let Some(v) = self.italic {
289 let _ = opts.italic(v);
290 }
291 if let Some(v) = self.reverse {
292 let _ = opts.reverse(v);
293 }
294 if let Some(v) = self.standout {
295 let _ = opts.standout(v);
296 }
297 if let Some(v) = self.strikethrough {
298 let _ = opts.strikethrough(v);
299 }
300 if let Some(v) = self.underline {
301 let _ = opts.underline(v);
302 }
303 if let Some(v) = self.undercurl {
304 let _ = opts.undercurl(v);
305 }
306 if let Some(v) = self.underdouble {
307 let _ = opts.underdouble(v);
308 }
309 if let Some(v) = self.underdotted {
310 let _ = opts.underdotted(v);
311 }
312 if let Some(v) = self.underdashed {
313 let _ = opts.underdashed(v);
314 }
315 if let Some(v) = self.altfont {
316 let _ = opts.altfont(v);
317 }
318 if let Some(v) = self.nocombine {
319 let _ = opts.nocombine(v);
320 }
321 if let Some(v) = self.fallback {
322 let _ = opts.fallback(v);
323 }
324 if let Some(v) = self.fg_indexed {
325 let _ = opts.fg_indexed(v);
326 }
327 if let Some(v) = self.bg_indexed {
328 let _ = opts.bg_indexed(v);
329 }
330 if let Some(v) = self.force {
331 let _ = opts.force(v);
332 }
333 if let Some(v) = self.blend {
334 let Ok(v) = v.try_into() else {
335 return opts.build();
336 };
337 let _ = opts.blend(v);
338 }
339
340 opts.build()
341 }
342}
343
344impl From<&HighlightInfos> for HighlightOpts {
345 fn from(infos: &HighlightInfos) -> Self {
346 let mut opts = Self::new();
347 if let Some(v) = infos.foreground {
348 opts.foreground = Some(decimal_to_hex_color(v));
349 }
350 if let Some(v) = infos.background {
351 opts.background = Some(decimal_to_hex_color(v));
352 }
353 if let Some(v) = infos.special {
354 opts.special_color = Some(decimal_to_hex_color(v));
355 }
356 opts.bold = infos.bold;
357 opts.italic = infos.italic;
358 opts.reverse = infos.reverse;
359 opts.standout = infos.standout;
360 opts.strikethrough = infos.strikethrough;
361 opts.underline = infos.underline;
362 opts.undercurl = infos.undercurl;
363 opts.underdouble = infos.underlineline;
364 opts.underdotted = infos.underdot;
365 opts.underdashed = infos.underdash;
366 opts.altfont = infos.altfont;
367 opts.fallback = infos.fallback;
368 opts.fg_indexed = infos.fg_indexed;
369 opts.bg_indexed = infos.bg_indexed;
370 opts.force = infos.force;
371 opts.blend = infos.blend;
372 opts
373 }
374}
375
376fn decimal_to_hex_color(decimal: u32) -> String {
378 format!("#{decimal:06X}")
379}
380
381#[cfg(test)]
382mod tests {
383 use rstest::rstest;
384
385 use super::*;
386
387 #[test]
388 fn test_from_default_highlight_infos_produces_default_highlight_opts() {
389 let infos = HighlightInfos::default();
390 let opts = HighlightOpts::from(&infos);
391 pretty_assertions::assert_eq!(opts.foreground, None);
392 pretty_assertions::assert_eq!(opts.background, None);
393 pretty_assertions::assert_eq!(opts.special_color, None);
394 pretty_assertions::assert_eq!(opts.bold, None);
395 pretty_assertions::assert_eq!(opts.blend, None);
396 }
397
398 #[rstest]
399 #[case(0x00_00_00, "#000000")]
400 #[case(0xFF_FF_FF, "#FFFFFF")]
401 #[case(0xFF_00_00, "#FF0000")]
402 #[case(0x00_20_20, "#002020")]
403 fn test_from_highlight_infos_converts_foreground_to_hex(#[case] rgb: u32, #[case] expected: &str) {
404 let mut infos = HighlightInfos::default();
405 infos.foreground = Some(rgb);
406 pretty_assertions::assert_eq!(HighlightOpts::from(&infos).foreground.as_deref(), Some(expected));
407 }
408
409 #[rstest]
410 #[case(0xFF_FF_FF, "#FFFFFF")]
411 #[case(0x00_20_20, "#002020")]
412 fn test_from_highlight_infos_converts_background_to_hex(#[case] rgb: u32, #[case] expected: &str) {
413 let mut infos = HighlightInfos::default();
414 infos.background = Some(rgb);
415 pretty_assertions::assert_eq!(HighlightOpts::from(&infos).background.as_deref(), Some(expected));
416 }
417
418 #[test]
419 fn test_from_highlight_infos_converts_special_to_hex() {
420 let mut infos = HighlightInfos::default();
421 infos.special = Some(0xFF_00_00);
422 pretty_assertions::assert_eq!(HighlightOpts::from(&infos).special_color.as_deref(), Some("#FF0000"));
423 }
424
425 #[test]
426 fn test_from_highlight_infos_maps_boolean_fields() {
427 let mut infos = HighlightInfos::default();
428 infos.bold = Some(true);
429 infos.italic = Some(false);
430 infos.underline = Some(true);
431 infos.underdot = Some(true);
432 infos.underdash = Some(true);
433 infos.underlineline = Some(true);
434
435 let opts = HighlightOpts::from(&infos);
436 pretty_assertions::assert_eq!(opts.bold, Some(true));
437 pretty_assertions::assert_eq!(opts.italic, Some(false));
438 pretty_assertions::assert_eq!(opts.underline, Some(true));
439 pretty_assertions::assert_eq!(opts.underdotted, Some(true));
440 pretty_assertions::assert_eq!(opts.underdashed, Some(true));
441 pretty_assertions::assert_eq!(opts.underdouble, Some(true));
442 }
443
444 #[test]
445 fn test_from_highlight_infos_maps_blend() {
446 let mut infos = HighlightInfos::default();
447 infos.blend = Some(50);
448 pretty_assertions::assert_eq!(HighlightOpts::from(&infos).blend, Some(50));
449 }
450
451 #[test]
452 fn test_to_set_highlight_opts_maps_present_fields() {
453 let opts = HighlightOpts::new()
454 .fg("#000000")
455 .bg("white")
456 .special("#FF0000")
457 .bold(true)
458 .reverse(false)
459 .underline(true)
460 .undercurl(false);
461
462 let mut expected = SetHighlightOpts::builder();
463 let _ = expected.foreground("#000000");
464 let _ = expected.background("white");
465 let _ = expected.special("#FF0000");
466 let _ = expected.bold(true);
467 let _ = expected.reverse(false);
468 let _ = expected.underline(true);
469 let _ = expected.undercurl(false);
470
471 pretty_assertions::assert_eq!(opts.to_set_highlight_opts(), expected.build(),);
472 }
473
474 #[test]
475 fn test_to_set_highlight_opts_maps_blend() {
476 let opts = HighlightOpts {
477 blend: Some(30),
478 ..Default::default()
479 };
480
481 let mut expected = SetHighlightOpts::builder();
482 let _ = expected.blend(30);
483
484 pretty_assertions::assert_eq!(opts.to_set_highlight_opts(), expected.build());
485 }
486
487 #[test]
488 fn test_to_set_highlight_opts_ignores_out_of_range_blend() {
489 let opts = HighlightOpts {
490 blend: Some(u32::from(u8::MAX) + 1),
491 ..Default::default()
492 };
493
494 pretty_assertions::assert_eq!(opts.to_set_highlight_opts(), SetHighlightOpts::default());
495 }
496
497 #[test]
498 fn test_to_set_highlight_opts_preserves_false_attrs() {
499 let opts = HighlightOpts::new().reverse(false).underline(false);
500
501 let mut expected = SetHighlightOpts::builder();
502 let _ = expected.reverse(false);
503 let _ = expected.underline(false);
504
505 pretty_assertions::assert_eq!(opts.to_set_highlight_opts(), expected.build());
506 }
507}