1use ratatui::prelude::*;
6use std::sync::RwLock;
7
8#[derive(Debug, Clone, Copy)]
11pub struct ColorScheme {
12 pub added: Color,
14 pub removed: Color,
15 pub modified: Color,
16 pub unchanged: Color,
17
18 pub critical: Color,
20 pub high: Color,
21 pub medium: Color,
22 pub low: Color,
23 pub info: Color,
24
25 pub permissive: Color,
27 pub copyleft: Color,
28 pub weak_copyleft: Color,
29 pub proprietary: Color,
30 pub unknown_license: Color,
31
32 pub primary: Color,
34 pub secondary: Color,
35 pub accent: Color,
36 pub muted: Color,
37 pub border: Color,
38 pub border_focused: Color,
39 pub background: Color,
40 pub background_alt: Color,
41 pub text: Color,
42 pub text_muted: Color,
43 pub selection: Color,
44 pub highlight: Color,
45
46 pub success: Color,
48 pub warning: Color,
49 pub error: Color,
50
51 pub badge_fg_dark: Color, pub badge_fg_light: Color, pub selection_bg: Color, pub search_highlight_bg: Color, pub error_bg: Color, pub success_bg: Color, pub scope_bg: Color, }
64
65impl Default for ColorScheme {
66 fn default() -> Self {
67 Self::dark()
68 }
69}
70
71impl ColorScheme {
72 const fn dark_const() -> Self {
74 Self {
75 added: Color::Green,
77 removed: Color::Red,
78 modified: Color::Yellow,
79 unchanged: Color::Gray,
80
81 critical: Color::Magenta,
83 high: Color::Red,
84 medium: Color::Yellow,
85 low: Color::Cyan,
86 info: Color::Blue,
87
88 permissive: Color::Green,
90 copyleft: Color::Yellow,
91 weak_copyleft: Color::Cyan,
92 proprietary: Color::Red,
93 unknown_license: Color::DarkGray,
94
95 primary: Color::Cyan,
97 secondary: Color::Blue,
98 accent: Color::Yellow,
99 muted: Color::DarkGray,
100 border: Color::DarkGray,
101 border_focused: Color::Cyan,
102 background: Color::Reset,
103 background_alt: Color::Rgb(30, 30, 40),
104 text: Color::White,
105 text_muted: Color::Gray,
106 selection: Color::Rgb(50, 50, 70),
107 highlight: Color::Yellow,
108
109 success: Color::Green,
111 warning: Color::Yellow,
112 error: Color::Red,
113
114 badge_fg_dark: Color::Black,
116 badge_fg_light: Color::White,
117
118 selection_bg: Color::Rgb(60, 60, 80),
120 search_highlight_bg: Color::Rgb(100, 80, 0),
121 error_bg: Color::Rgb(80, 30, 30),
122 success_bg: Color::Rgb(30, 80, 30),
123
124 scope_bg: Color::Rgb(35, 35, 50),
126 }
127 }
128
129 #[must_use]
131 pub const fn dark() -> Self {
132 Self {
133 added: Color::Green,
135 removed: Color::Red,
136 modified: Color::Yellow,
137 unchanged: Color::Gray,
138
139 critical: Color::Magenta,
141 high: Color::Red,
142 medium: Color::Yellow,
143 low: Color::Cyan,
144 info: Color::Blue,
145
146 permissive: Color::Green,
148 copyleft: Color::Yellow,
149 weak_copyleft: Color::Cyan,
150 proprietary: Color::Red,
151 unknown_license: Color::DarkGray,
152
153 primary: Color::Cyan,
155 secondary: Color::Blue,
156 accent: Color::Yellow,
157 muted: Color::DarkGray,
158 border: Color::DarkGray,
159 border_focused: Color::Cyan,
160 background: Color::Reset,
161 background_alt: Color::Rgb(30, 30, 40),
162 text: Color::White,
163 text_muted: Color::Gray,
164 selection: Color::Rgb(50, 50, 70),
165 highlight: Color::Yellow,
166
167 success: Color::Green,
169 warning: Color::Yellow,
170 error: Color::Red,
171
172 badge_fg_dark: Color::Black,
174 badge_fg_light: Color::White,
175
176 selection_bg: Color::Rgb(60, 60, 80),
178 search_highlight_bg: Color::Rgb(100, 80, 0),
179 error_bg: Color::Rgb(80, 30, 30),
180 success_bg: Color::Rgb(30, 80, 30),
181
182 scope_bg: Color::Rgb(35, 35, 50),
184 }
185 }
186
187 #[must_use]
189 pub const fn light() -> Self {
190 Self {
191 added: Color::Rgb(0, 128, 0),
193 removed: Color::Rgb(200, 0, 0),
194 modified: Color::Rgb(180, 140, 0),
195 unchanged: Color::Rgb(100, 100, 100),
196
197 critical: Color::Rgb(128, 0, 128),
199 high: Color::Rgb(200, 0, 0),
200 medium: Color::Rgb(180, 140, 0),
201 low: Color::Rgb(0, 128, 128),
202 info: Color::Rgb(0, 0, 200),
203
204 permissive: Color::Rgb(0, 128, 0),
206 copyleft: Color::Rgb(180, 140, 0),
207 weak_copyleft: Color::Rgb(0, 128, 128),
208 proprietary: Color::Rgb(200, 0, 0),
209 unknown_license: Color::Rgb(100, 100, 100),
210
211 primary: Color::Rgb(0, 100, 150),
213 secondary: Color::Rgb(0, 0, 150),
214 accent: Color::Rgb(180, 140, 0),
215 muted: Color::Rgb(150, 150, 150),
216 border: Color::Rgb(180, 180, 180),
217 border_focused: Color::Rgb(0, 100, 150),
218 background: Color::Rgb(255, 255, 255),
219 background_alt: Color::Rgb(240, 240, 245),
220 text: Color::Rgb(30, 30, 30),
221 text_muted: Color::Rgb(100, 100, 100),
222 selection: Color::Rgb(200, 220, 240),
223 highlight: Color::Rgb(180, 140, 0),
224
225 success: Color::Rgb(0, 128, 0),
227 warning: Color::Rgb(180, 140, 0),
228 error: Color::Rgb(200, 0, 0),
229
230 badge_fg_dark: Color::Rgb(30, 30, 30),
232 badge_fg_light: Color::White,
233
234 selection_bg: Color::Rgb(200, 220, 240),
236 search_highlight_bg: Color::Rgb(255, 230, 150),
237 error_bg: Color::Rgb(255, 200, 200),
238 success_bg: Color::Rgb(200, 255, 200),
239
240 scope_bg: Color::Rgb(235, 240, 250),
242 }
243 }
244
245 #[must_use]
247 pub const fn high_contrast() -> Self {
248 Self {
249 added: Color::Green,
251 removed: Color::LightRed,
252 modified: Color::LightYellow,
253 unchanged: Color::White,
254
255 critical: Color::LightMagenta,
257 high: Color::LightRed,
258 medium: Color::LightYellow,
259 low: Color::LightCyan,
260 info: Color::LightBlue,
261
262 permissive: Color::LightGreen,
264 copyleft: Color::LightYellow,
265 weak_copyleft: Color::LightCyan,
266 proprietary: Color::LightRed,
267 unknown_license: Color::Gray,
268
269 primary: Color::LightCyan,
271 secondary: Color::LightBlue,
272 accent: Color::LightYellow,
273 muted: Color::Gray,
274 border: Color::White,
275 border_focused: Color::LightCyan,
276 background: Color::Black,
277 background_alt: Color::Rgb(20, 20, 20),
278 text: Color::White,
279 text_muted: Color::Gray,
280 selection: Color::White,
281 highlight: Color::LightYellow,
282
283 success: Color::LightGreen,
285 warning: Color::LightYellow,
286 error: Color::LightRed,
287
288 badge_fg_dark: Color::Black,
290 badge_fg_light: Color::White,
291
292 selection_bg: Color::Rgb(50, 50, 80),
294 search_highlight_bg: Color::Rgb(120, 100, 0),
295 error_bg: Color::Rgb(100, 30, 30),
296 success_bg: Color::Rgb(30, 100, 30),
297
298 scope_bg: Color::Rgb(25, 25, 40),
300 }
301 }
302
303 #[must_use]
305 pub fn severity_color(&self, severity: &str) -> Color {
306 match severity.to_lowercase().as_str() {
307 "critical" => self.critical,
308 "high" => self.high,
309 "medium" | "moderate" => self.medium,
310 "low" => self.low,
311 "info" | "informational" | "none" => self.info,
312 _ => self.text_muted,
313 }
314 }
315
316 #[must_use]
318 pub fn severity_bg_tint(&self, severity: &str) -> Color {
319 match severity.to_lowercase().as_str() {
320 "critical" => Color::Rgb(50, 15, 50),
321 "high" => Color::Rgb(50, 15, 15),
322 "medium" => Color::Rgb(45, 40, 10),
323 "low" => Color::Rgb(15, 35, 40),
324 _ => Color::Reset,
325 }
326 }
327
328 #[must_use]
330 pub fn change_color(&self, status: &str) -> Color {
331 match status.to_lowercase().as_str() {
332 "added" | "new" | "introduced" => self.added,
333 "removed" | "deleted" | "resolved" => self.removed,
334 "modified" | "changed" | "updated" => self.modified,
335 _ => self.unchanged,
336 }
337 }
338
339 #[must_use]
341 pub fn license_color(&self, category: &str) -> Color {
342 match category.to_lowercase().as_str() {
343 "permissive" => self.permissive,
344 "copyleft" | "strong copyleft" => self.copyleft,
345 "weak copyleft" => self.weak_copyleft,
346 "proprietary" | "commercial" => self.proprietary,
347 _ => self.unknown_license,
348 }
349 }
350
351 #[must_use]
354 pub fn severity_badge_fg(&self, severity: &str) -> Color {
355 match severity.to_lowercase().as_str() {
356 "critical" | "high" | "info" | "informational" => self.badge_fg_light,
357 _ => self.badge_fg_dark,
358 }
359 }
360
361 #[must_use]
364 pub const fn kev(&self) -> Color {
365 Color::Rgb(255, 100, 50) }
367
368 #[must_use]
370 pub const fn kev_badge_fg(&self) -> Color {
371 self.badge_fg_dark
372 }
373
374 #[must_use]
376 pub const fn direct_dep(&self) -> Color {
377 Color::Rgb(46, 160, 67) }
379
380 #[must_use]
382 pub const fn transitive_dep(&self) -> Color {
383 Color::Rgb(110, 118, 129) }
385
386 #[must_use]
389 pub const fn change_badge_fg(&self) -> Color {
390 self.badge_fg_dark
391 }
392
393 #[must_use]
395 pub fn license_badge_fg(&self, category: &str) -> Color {
396 match category.to_lowercase().as_str() {
397 "proprietary" | "commercial" => self.badge_fg_light,
398 _ => self.badge_fg_dark,
399 }
400 }
401
402 #[must_use]
404 pub const fn chart_palette(&self) -> [Color; 5] {
405 [
406 self.primary,
407 self.success,
408 self.warning,
409 self.critical,
410 self.secondary,
411 ]
412 }
413}
414
415static THEME: RwLock<Theme> = RwLock::new(Theme::dark_const());
417
418#[derive(Debug, Clone)]
420pub struct Theme {
421 pub colors: ColorScheme,
422 pub name: &'static str,
423}
424
425impl Default for Theme {
426 fn default() -> Self {
427 Self::dark()
428 }
429}
430
431impl Theme {
432 const fn dark_const() -> Self {
434 Self {
435 colors: ColorScheme::dark_const(),
436 name: "dark",
437 }
438 }
439
440 #[must_use]
441 pub const fn dark() -> Self {
442 Self {
443 colors: ColorScheme::dark(),
444 name: "dark",
445 }
446 }
447
448 #[must_use]
449 pub const fn light() -> Self {
450 Self {
451 colors: ColorScheme::light(),
452 name: "light",
453 }
454 }
455
456 #[must_use]
457 pub const fn high_contrast() -> Self {
458 Self {
459 colors: ColorScheme::high_contrast(),
460 name: "high-contrast",
461 }
462 }
463
464 #[must_use]
465 pub fn from_name(name: &str) -> Self {
466 match name.to_lowercase().as_str() {
467 "light" => Self::light(),
468 "high-contrast" | "highcontrast" | "hc" => Self::high_contrast(),
469 _ => Self::dark(),
470 }
471 }
472
473 #[must_use]
475 pub fn next(&self) -> Self {
476 match self.name {
477 "dark" => Self::light(),
478 "light" => Self::high_contrast(),
479 _ => Self::dark(),
480 }
481 }
482}
483
484pub fn current_theme_name() -> &'static str {
486 THEME.read().expect("THEME lock not poisoned").name
487}
488
489pub fn set_theme(theme: Theme) {
491 *THEME.write().expect("THEME lock not poisoned") = theme;
492}
493
494pub fn toggle_theme() -> &'static str {
496 let mut theme = THEME.write().expect("THEME lock not poisoned");
497 *theme = theme.next();
498 theme.name
499}
500
501pub fn colors() -> ColorScheme {
503 THEME.read().expect("THEME lock not poisoned").colors
504}
505
506pub struct Styles;
512
513impl Styles {
514 #[must_use]
516 pub fn header_title() -> Style {
517 Style::default().fg(colors().primary).bold()
518 }
519
520 #[must_use]
522 pub fn section_title() -> Style {
523 Style::default().fg(colors().primary).bold()
524 }
525
526 #[must_use]
528 pub fn subsection_title() -> Style {
529 Style::default().fg(colors().primary)
530 }
531
532 #[must_use]
534 pub fn text() -> Style {
535 Style::default().fg(colors().text)
536 }
537
538 #[must_use]
540 pub fn text_muted() -> Style {
541 Style::default().fg(colors().text_muted)
542 }
543
544 #[must_use]
546 pub fn label() -> Style {
547 Style::default().fg(colors().muted)
548 }
549
550 #[must_use]
552 pub fn value() -> Style {
553 Style::default().fg(colors().text).bold()
554 }
555
556 #[must_use]
558 pub fn highlight() -> Style {
559 Style::default().fg(colors().highlight).bold()
560 }
561
562 #[must_use]
564 pub fn selected() -> Style {
565 Style::default()
566 .bg(colors().selection)
567 .fg(colors().text)
568 .bold()
569 }
570
571 #[must_use]
573 pub fn border() -> Style {
574 Style::default().fg(colors().border)
575 }
576
577 #[must_use]
579 pub fn border_focused() -> Style {
580 Style::default().fg(colors().border_focused)
581 }
582
583 #[must_use]
585 pub fn status_bar() -> Style {
586 Style::default().bg(colors().background_alt)
587 }
588
589 #[must_use]
591 pub fn shortcut_key() -> Style {
592 Style::default().fg(colors().accent)
593 }
594
595 #[must_use]
597 pub fn shortcut_desc() -> Style {
598 Style::default().fg(colors().text_muted)
599 }
600
601 #[must_use]
603 pub fn success() -> Style {
604 Style::default().fg(colors().success)
605 }
606
607 #[must_use]
609 pub fn warning() -> Style {
610 Style::default().fg(colors().warning)
611 }
612
613 #[must_use]
615 pub fn error() -> Style {
616 Style::default().fg(colors().error)
617 }
618
619 #[must_use]
621 pub fn added() -> Style {
622 Style::default().fg(colors().added)
623 }
624
625 #[must_use]
627 pub fn removed() -> Style {
628 Style::default().fg(colors().removed)
629 }
630
631 #[must_use]
633 pub fn modified() -> Style {
634 Style::default().fg(colors().modified)
635 }
636
637 #[must_use]
639 pub fn critical() -> Style {
640 Style::default().fg(colors().critical).bold()
641 }
642
643 #[must_use]
645 pub fn high() -> Style {
646 Style::default().fg(colors().high).bold()
647 }
648
649 #[must_use]
651 pub fn medium() -> Style {
652 Style::default().fg(colors().medium)
653 }
654
655 #[must_use]
657 pub fn low() -> Style {
658 Style::default().fg(colors().low)
659 }
660}
661
662#[must_use]
668pub fn status_badge(status: &str) -> Span<'static> {
669 let scheme = colors();
670 let (label, color, symbol) = match status.to_lowercase().as_str() {
671 "added" | "new" | "introduced" => ("ADDED", scheme.added, "+"),
672 "removed" | "deleted" | "resolved" => ("REMOVED", scheme.removed, "-"),
673 "modified" | "changed" | "updated" => ("MODIFIED", scheme.modified, "~"),
674 _ => ("UNCHANGED", scheme.unchanged, "="),
675 };
676
677 Span::styled(
678 format!(" {symbol} {label} "),
679 Style::default()
680 .fg(scheme.change_badge_fg())
681 .bg(color)
682 .bold(),
683 )
684}
685
686#[must_use]
688pub fn severity_badge(severity: &str) -> Span<'static> {
689 let scheme = colors();
690 let (label, bg_color, is_unknown) = match severity.to_lowercase().as_str() {
691 "critical" => ("CRITICAL", scheme.critical, false),
692 "high" => ("HIGH", scheme.high, false),
693 "medium" | "moderate" => ("MEDIUM", scheme.medium, false),
694 "low" => ("LOW", scheme.low, false),
695 "info" | "informational" => ("INFO", scheme.info, false),
696 "none" => ("NONE", scheme.muted, false),
697 _ => ("UNKNOWN", scheme.muted, true),
698 };
699 let fg_color = scheme.severity_badge_fg(severity);
700
701 let style = if is_unknown {
702 Style::default().fg(fg_color).bg(bg_color).dim()
703 } else {
704 Style::default().fg(fg_color).bg(bg_color).bold()
705 };
706
707 Span::styled(format!(" {label} "), style)
708}
709
710#[must_use]
712pub fn severity_indicator(severity: &str) -> Span<'static> {
713 let scheme = colors();
714 let (symbol, bg_color, is_unknown) = match severity.to_lowercase().as_str() {
715 "critical" => ("C", scheme.critical, false),
716 "high" => ("H", scheme.high, false),
717 "medium" | "moderate" => ("M", scheme.medium, false),
718 "low" => ("L", scheme.low, false),
719 "info" | "informational" => ("I", scheme.info, false),
720 "none" => ("-", scheme.muted, false),
721 _ => ("U", scheme.muted, true),
722 };
723 let fg_color = scheme.severity_badge_fg(severity);
724
725 let style = if is_unknown {
726 Style::default().fg(fg_color).bg(bg_color).dim()
727 } else {
728 Style::default().fg(fg_color).bg(bg_color).bold()
729 };
730
731 Span::styled(format!(" {symbol} "), style)
732}
733
734#[must_use]
736pub fn count_badge(count: usize, bg_color: Color) -> Span<'static> {
737 let scheme = colors();
738 Span::styled(
739 format!(" {count} "),
740 Style::default()
741 .fg(scheme.badge_fg_dark)
742 .bg(bg_color)
743 .bold(),
744 )
745}
746
747#[must_use]
749pub fn filter_badge(label: &str, value: &str) -> Vec<Span<'static>> {
750 let scheme = colors();
751 vec![
752 Span::styled(format!("{label}: "), Style::default().fg(scheme.text_muted)),
753 Span::styled(
754 format!(" {value} "),
755 Style::default()
756 .fg(scheme.badge_fg_dark)
757 .bg(scheme.accent)
758 .bold(),
759 ),
760 ]
761}
762
763#[must_use]
769pub fn mode_badge(mode: &str) -> Span<'static> {
770 let scheme = colors();
771 let color = match mode.to_lowercase().as_str() {
772 "diff" => scheme.modified,
773 "view" => scheme.primary,
774 "multi-diff" | "multidiff" => scheme.added,
775 "timeline" => scheme.secondary,
776 "matrix" => scheme.high,
777 _ => scheme.muted,
778 };
779
780 Span::styled(
781 format!(" {} ", mode.to_uppercase()),
782 Style::default().fg(scheme.badge_fg_dark).bg(color).bold(),
783 )
784}
785
786pub struct FooterHints;
792
793impl FooterHints {
794 #[must_use]
796 pub fn for_diff_tab(tab: &str) -> Vec<(&'static str, &'static str)> {
797 let mut hints = Self::global();
798
799 match tab.to_lowercase().as_str() {
800 "components" => {
801 hints.insert(0, ("f", "filter"));
802 hints.insert(1, ("s", "sort"));
803 }
804 "dependencies" => {
805 hints.insert(0, ("f", "filter"));
806 hints.insert(1, ("t", "transitive"));
807 hints.insert(2, ("h", "highlight"));
808 hints.insert(3, ("Enter", "expand"));
809 hints.insert(4, ("c", "component"));
810 }
811 "licenses" => {
812 hints.insert(0, ("g", "group"));
813 hints.insert(1, ("s", "sort"));
814 hints.insert(2, ("r", "risk"));
815 hints.insert(3, ("c", "compat"));
816 hints.insert(4, ("Tab", "panel"));
817 }
818 "vulnerabilities" | "vulns" => {
819 hints.insert(0, ("f", "filter"));
820 hints.insert(1, ("s", "sort"));
821 hints.insert(2, ("g", "group"));
822 }
823 "sidebyside" | "side-by-side" | "diff" => {
824 hints.insert(0, ("←→/p", "panel"));
825 hints.insert(1, ("J/K", "scroll both"));
826 }
827 "quality" => {
828 hints.insert(0, ("v", "view"));
829 }
830 "compliance" => {
831 hints.insert(0, ("←→", "standard"));
832 hints.insert(1, ("v", "view"));
833 hints.insert(2, ("g", "group"));
834 hints.insert(3, ("↑↓", "select"));
835 }
836 "source" => {
837 hints.insert(0, ("w", "panel"));
838 hints.insert(1, ("v", "tree/raw"));
839 hints.insert(2, ("↑↓", "scroll"));
840 }
841 "graphchanges" | "graph" => {
842 hints.insert(0, ("↑↓", "select"));
843 hints.insert(1, ("PgUp/Dn", "page"));
844 }
845 _ => {}
846 }
847
848 hints
849 }
850
851 #[must_use]
853 pub fn for_view_tab(tab: &str) -> Vec<(&'static str, &'static str)> {
854 let mut hints = Self::global();
855
856 match tab.to_lowercase().as_str() {
857 "tree" | "components" => {
858 hints.insert(0, ("p", "panel"));
860 hints.insert(1, ("Enter", "select"));
861 hints.insert(2, ("1-4", "detail tabs"));
862 }
863 "vulnerabilities" | "vulns" => {
864 hints.insert(0, ("f", "filter"));
865 hints.insert(1, ("s", "sort"));
866 hints.insert(2, ("g", "group"));
867 hints.insert(3, ("d", "dedup"));
868 hints.insert(4, ("Enter", "component"));
869 }
870 "licenses" => {
871 hints.insert(0, ("g", "group"));
872 hints.insert(1, ("Enter", "inspect"));
873 hints.insert(2, ("K/J", "scroll"));
874 }
875 "dependencies" => {
876 hints.insert(0, ("Enter", "expand/inspect"));
877 hints.insert(1, ("←", "collapse"));
878 hints.insert(2, ("p", "panel"));
879 hints.insert(3, ("J/K", "scroll"));
880 }
881 "quality" => {
882 hints.insert(0, ("v", "view"));
883 }
884 "compliance" => {
885 hints.insert(0, ("f", "filter"));
886 hints.insert(1, ("←→", "standard"));
887 hints.insert(2, ("↑↓", "select"));
888 }
889 "source" => {
890 hints.insert(0, ("v", "tree/raw"));
891 hints.insert(1, ("p", "panel"));
892 hints.insert(2, ("H/L", "fold all"));
893 hints.insert(3, ("Enter", "select"));
894 }
895 "algorithms" => {
896 hints.insert(0, ("s", "sort"));
897 hints.insert(1, ("↑↓", "select"));
898 hints.insert(2, ("Enter", "detail"));
899 }
900 "certificates" => {
901 hints.insert(0, ("↑↓", "select"));
902 hints.insert(1, ("Enter", "detail"));
903 }
904 "keys" => {
905 hints.insert(0, ("↑↓", "select"));
906 hints.insert(1, ("Enter", "detail"));
907 }
908 "protocols" => {
909 hints.insert(0, ("↑↓", "select"));
910 hints.insert(1, ("Enter", "detail"));
911 }
912 "pqc-compliance" => {
913 hints.insert(0, ("↑↓", "scroll"));
914 }
915 "models" | "datasets" => {
916 hints.insert(0, ("↑↓", "select"));
917 }
918 "ai-readiness" => {
919 hints.insert(0, ("↑↓", "scroll"));
920 }
921 _ => {}
922 }
923
924 hints
925 }
926
927 #[must_use]
929 pub fn global() -> Vec<(&'static str, &'static str)> {
930 vec![
931 ("Tab", "switch"),
932 ("/", "search"),
933 ("e", "export"),
934 ("?", "help"),
935 ("q", "quit"),
936 ]
937 }
938
939 pub const GLOBAL_COUNT: usize = 5;
941}
942
943#[must_use]
948pub fn render_footer_hints(hints: &[(&str, &str)]) -> Vec<Span<'static>> {
949 let scheme = colors();
950 let mut spans = Vec::new();
951 let tab_count = hints.len().saturating_sub(FooterHints::GLOBAL_COUNT);
952
953 for (i, (key, desc)) in hints.iter().enumerate() {
954 if i > 0 {
955 spans.push(Span::raw(" "));
956 }
957 if i == tab_count && tab_count > 0 {
959 spans.push(Span::styled("│ ", Style::default().fg(scheme.muted)));
960 }
961 spans.push(Span::styled(
962 format!(" {key} "),
963 Style::default()
964 .fg(scheme.badge_fg_dark)
965 .bg(scheme.accent)
966 .bold(),
967 ));
968 spans.push(Span::styled(
969 desc.to_string(),
970 Style::default().fg(scheme.text_muted),
971 ));
972 }
973
974 spans
975}