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