use ratatui::prelude::*;
use std::sync::RwLock;
#[derive(Debug, Clone, Copy)]
pub struct ColorScheme {
pub added: Color,
pub removed: Color,
pub modified: Color,
pub unchanged: Color,
pub critical: Color,
pub high: Color,
pub medium: Color,
pub low: Color,
pub info: Color,
pub permissive: Color,
pub copyleft: Color,
pub weak_copyleft: Color,
pub proprietary: Color,
pub unknown_license: Color,
pub primary: Color,
pub secondary: Color,
pub accent: Color,
pub muted: Color,
pub border: Color,
pub border_focused: Color,
pub background: Color,
pub background_alt: Color,
pub text: Color,
pub text_muted: Color,
pub selection: Color,
pub highlight: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
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, }
impl Default for ColorScheme {
fn default() -> Self {
Self::dark()
}
}
impl ColorScheme {
const fn dark_const() -> Self {
Self {
added: Color::Green,
removed: Color::Red,
modified: Color::Yellow,
unchanged: Color::Gray,
critical: Color::Magenta,
high: Color::Red,
medium: Color::Yellow,
low: Color::Cyan,
info: Color::Blue,
permissive: Color::Green,
copyleft: Color::Yellow,
weak_copyleft: Color::Cyan,
proprietary: Color::Red,
unknown_license: Color::DarkGray,
primary: Color::Cyan,
secondary: Color::Blue,
accent: Color::Yellow,
muted: Color::DarkGray,
border: Color::DarkGray,
border_focused: Color::Cyan,
background: Color::Reset,
background_alt: Color::Rgb(30, 30, 40),
text: Color::White,
text_muted: Color::Gray,
selection: Color::Rgb(50, 50, 70),
highlight: Color::Yellow,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
badge_fg_dark: Color::Black,
badge_fg_light: Color::White,
selection_bg: Color::Rgb(60, 60, 80),
search_highlight_bg: Color::Rgb(100, 80, 0),
error_bg: Color::Rgb(80, 30, 30),
success_bg: Color::Rgb(30, 80, 30),
scope_bg: Color::Rgb(35, 35, 50),
}
}
#[must_use]
pub const fn dark() -> Self {
Self {
added: Color::Green,
removed: Color::Red,
modified: Color::Yellow,
unchanged: Color::Gray,
critical: Color::Magenta,
high: Color::Red,
medium: Color::Yellow,
low: Color::Cyan,
info: Color::Blue,
permissive: Color::Green,
copyleft: Color::Yellow,
weak_copyleft: Color::Cyan,
proprietary: Color::Red,
unknown_license: Color::DarkGray,
primary: Color::Cyan,
secondary: Color::Blue,
accent: Color::Yellow,
muted: Color::DarkGray,
border: Color::DarkGray,
border_focused: Color::Cyan,
background: Color::Reset,
background_alt: Color::Rgb(30, 30, 40),
text: Color::White,
text_muted: Color::Gray,
selection: Color::Rgb(50, 50, 70),
highlight: Color::Yellow,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
badge_fg_dark: Color::Black,
badge_fg_light: Color::White,
selection_bg: Color::Rgb(60, 60, 80),
search_highlight_bg: Color::Rgb(100, 80, 0),
error_bg: Color::Rgb(80, 30, 30),
success_bg: Color::Rgb(30, 80, 30),
scope_bg: Color::Rgb(35, 35, 50),
}
}
#[must_use]
pub const fn light() -> Self {
Self {
added: Color::Rgb(0, 128, 0),
removed: Color::Rgb(200, 0, 0),
modified: Color::Rgb(180, 140, 0),
unchanged: Color::Rgb(100, 100, 100),
critical: Color::Rgb(128, 0, 128),
high: Color::Rgb(200, 0, 0),
medium: Color::Rgb(180, 140, 0),
low: Color::Rgb(0, 128, 128),
info: Color::Rgb(0, 0, 200),
permissive: Color::Rgb(0, 128, 0),
copyleft: Color::Rgb(180, 140, 0),
weak_copyleft: Color::Rgb(0, 128, 128),
proprietary: Color::Rgb(200, 0, 0),
unknown_license: Color::Rgb(100, 100, 100),
primary: Color::Rgb(0, 100, 150),
secondary: Color::Rgb(0, 0, 150),
accent: Color::Rgb(180, 140, 0),
muted: Color::Rgb(150, 150, 150),
border: Color::Rgb(180, 180, 180),
border_focused: Color::Rgb(0, 100, 150),
background: Color::Rgb(255, 255, 255),
background_alt: Color::Rgb(240, 240, 245),
text: Color::Rgb(30, 30, 30),
text_muted: Color::Rgb(100, 100, 100),
selection: Color::Rgb(200, 220, 240),
highlight: Color::Rgb(180, 140, 0),
success: Color::Rgb(0, 128, 0),
warning: Color::Rgb(180, 140, 0),
error: Color::Rgb(200, 0, 0),
badge_fg_dark: Color::Rgb(30, 30, 30),
badge_fg_light: Color::White,
selection_bg: Color::Rgb(200, 220, 240),
search_highlight_bg: Color::Rgb(255, 230, 150),
error_bg: Color::Rgb(255, 200, 200),
success_bg: Color::Rgb(200, 255, 200),
scope_bg: Color::Rgb(235, 240, 250),
}
}
#[must_use]
pub const fn high_contrast() -> Self {
Self {
added: Color::Green,
removed: Color::LightRed,
modified: Color::LightYellow,
unchanged: Color::White,
critical: Color::LightMagenta,
high: Color::LightRed,
medium: Color::LightYellow,
low: Color::LightCyan,
info: Color::LightBlue,
permissive: Color::LightGreen,
copyleft: Color::LightYellow,
weak_copyleft: Color::LightCyan,
proprietary: Color::LightRed,
unknown_license: Color::Gray,
primary: Color::LightCyan,
secondary: Color::LightBlue,
accent: Color::LightYellow,
muted: Color::Gray,
border: Color::White,
border_focused: Color::LightCyan,
background: Color::Black,
background_alt: Color::Rgb(20, 20, 20),
text: Color::White,
text_muted: Color::Gray,
selection: Color::White,
highlight: Color::LightYellow,
success: Color::LightGreen,
warning: Color::LightYellow,
error: Color::LightRed,
badge_fg_dark: Color::Black,
badge_fg_light: Color::White,
selection_bg: Color::Rgb(50, 50, 80),
search_highlight_bg: Color::Rgb(120, 100, 0),
error_bg: Color::Rgb(100, 30, 30),
success_bg: Color::Rgb(30, 100, 30),
scope_bg: Color::Rgb(25, 25, 40),
}
}
#[must_use]
pub fn severity_color(&self, severity: &str) -> Color {
match severity.to_lowercase().as_str() {
"critical" => self.critical,
"high" => self.high,
"medium" | "moderate" => self.medium,
"low" => self.low,
"info" | "informational" | "none" => self.info,
_ => self.text_muted,
}
}
#[must_use]
pub fn severity_bg_tint(&self, severity: &str) -> Color {
match severity.to_lowercase().as_str() {
"critical" => Color::Rgb(50, 15, 50),
"high" => Color::Rgb(50, 15, 15),
"medium" => Color::Rgb(45, 40, 10),
"low" => Color::Rgb(15, 35, 40),
_ => Color::Reset,
}
}
#[must_use]
pub fn change_color(&self, status: &str) -> Color {
match status.to_lowercase().as_str() {
"added" | "new" | "introduced" => self.added,
"removed" | "deleted" | "resolved" => self.removed,
"modified" | "changed" | "updated" => self.modified,
_ => self.unchanged,
}
}
#[must_use]
pub fn license_color(&self, category: &str) -> Color {
match category.to_lowercase().as_str() {
"permissive" => self.permissive,
"copyleft" | "strong copyleft" => self.copyleft,
"weak copyleft" => self.weak_copyleft,
"proprietary" | "commercial" => self.proprietary,
_ => self.unknown_license,
}
}
#[must_use]
pub fn severity_badge_fg(&self, severity: &str) -> Color {
match severity.to_lowercase().as_str() {
"critical" | "high" | "info" | "informational" => self.badge_fg_light,
_ => self.badge_fg_dark,
}
}
#[must_use]
pub const fn kev(&self) -> Color {
Color::Rgb(255, 100, 50) }
#[must_use]
pub const fn kev_badge_fg(&self) -> Color {
self.badge_fg_dark
}
#[must_use]
pub const fn direct_dep(&self) -> Color {
Color::Rgb(46, 160, 67) }
#[must_use]
pub const fn transitive_dep(&self) -> Color {
Color::Rgb(110, 118, 129) }
#[must_use]
pub const fn change_badge_fg(&self) -> Color {
self.badge_fg_dark
}
#[must_use]
pub fn license_badge_fg(&self, category: &str) -> Color {
match category.to_lowercase().as_str() {
"proprietary" | "commercial" => self.badge_fg_light,
_ => self.badge_fg_dark,
}
}
#[must_use]
pub const fn chart_palette(&self) -> [Color; 5] {
[
self.primary,
self.success,
self.warning,
self.critical,
self.secondary,
]
}
}
static THEME: RwLock<Theme> = RwLock::new(Theme::dark_const());
#[derive(Debug, Clone)]
pub struct Theme {
pub colors: ColorScheme,
pub name: &'static str,
}
impl Default for Theme {
fn default() -> Self {
Self::dark()
}
}
impl Theme {
const fn dark_const() -> Self {
Self {
colors: ColorScheme::dark_const(),
name: "dark",
}
}
#[must_use]
pub const fn dark() -> Self {
Self {
colors: ColorScheme::dark(),
name: "dark",
}
}
#[must_use]
pub const fn light() -> Self {
Self {
colors: ColorScheme::light(),
name: "light",
}
}
#[must_use]
pub const fn high_contrast() -> Self {
Self {
colors: ColorScheme::high_contrast(),
name: "high-contrast",
}
}
#[must_use]
pub fn from_name(name: &str) -> Self {
match name.to_lowercase().as_str() {
"light" => Self::light(),
"high-contrast" | "highcontrast" | "hc" => Self::high_contrast(),
_ => Self::dark(),
}
}
#[must_use]
pub fn next(&self) -> Self {
match self.name {
"dark" => Self::light(),
"light" => Self::high_contrast(),
_ => Self::dark(),
}
}
}
pub fn current_theme_name() -> &'static str {
THEME.read().expect("THEME lock not poisoned").name
}
pub fn set_theme(theme: Theme) {
*THEME.write().expect("THEME lock not poisoned") = theme;
}
pub fn toggle_theme() -> &'static str {
let mut theme = THEME.write().expect("THEME lock not poisoned");
*theme = theme.next();
theme.name
}
pub fn colors() -> ColorScheme {
THEME.read().expect("THEME lock not poisoned").colors
}
pub struct Styles;
impl Styles {
#[must_use]
pub fn header_title() -> Style {
Style::default().fg(colors().primary).bold()
}
#[must_use]
pub fn section_title() -> Style {
Style::default().fg(colors().primary).bold()
}
#[must_use]
pub fn subsection_title() -> Style {
Style::default().fg(colors().primary)
}
#[must_use]
pub fn text() -> Style {
Style::default().fg(colors().text)
}
#[must_use]
pub fn text_muted() -> Style {
Style::default().fg(colors().text_muted)
}
#[must_use]
pub fn label() -> Style {
Style::default().fg(colors().muted)
}
#[must_use]
pub fn value() -> Style {
Style::default().fg(colors().text).bold()
}
#[must_use]
pub fn highlight() -> Style {
Style::default().fg(colors().highlight).bold()
}
#[must_use]
pub fn selected() -> Style {
Style::default()
.bg(colors().selection)
.fg(colors().text)
.bold()
}
#[must_use]
pub fn border() -> Style {
Style::default().fg(colors().border)
}
#[must_use]
pub fn border_focused() -> Style {
Style::default().fg(colors().border_focused)
}
#[must_use]
pub fn status_bar() -> Style {
Style::default().bg(colors().background_alt)
}
#[must_use]
pub fn shortcut_key() -> Style {
Style::default().fg(colors().accent)
}
#[must_use]
pub fn shortcut_desc() -> Style {
Style::default().fg(colors().text_muted)
}
#[must_use]
pub fn success() -> Style {
Style::default().fg(colors().success)
}
#[must_use]
pub fn warning() -> Style {
Style::default().fg(colors().warning)
}
#[must_use]
pub fn error() -> Style {
Style::default().fg(colors().error)
}
#[must_use]
pub fn added() -> Style {
Style::default().fg(colors().added)
}
#[must_use]
pub fn removed() -> Style {
Style::default().fg(colors().removed)
}
#[must_use]
pub fn modified() -> Style {
Style::default().fg(colors().modified)
}
#[must_use]
pub fn critical() -> Style {
Style::default().fg(colors().critical).bold()
}
#[must_use]
pub fn high() -> Style {
Style::default().fg(colors().high).bold()
}
#[must_use]
pub fn medium() -> Style {
Style::default().fg(colors().medium)
}
#[must_use]
pub fn low() -> Style {
Style::default().fg(colors().low)
}
}
#[must_use]
pub fn status_badge(status: &str) -> Span<'static> {
let scheme = colors();
let (label, color, symbol) = match status.to_lowercase().as_str() {
"added" | "new" | "introduced" => ("ADDED", scheme.added, "+"),
"removed" | "deleted" | "resolved" => ("REMOVED", scheme.removed, "-"),
"modified" | "changed" | "updated" => ("MODIFIED", scheme.modified, "~"),
_ => ("UNCHANGED", scheme.unchanged, "="),
};
Span::styled(
format!(" {symbol} {label} "),
Style::default()
.fg(scheme.change_badge_fg())
.bg(color)
.bold(),
)
}
#[must_use]
pub fn severity_badge(severity: &str) -> Span<'static> {
let scheme = colors();
let (label, bg_color, is_unknown) = match severity.to_lowercase().as_str() {
"critical" => ("CRITICAL", scheme.critical, false),
"high" => ("HIGH", scheme.high, false),
"medium" | "moderate" => ("MEDIUM", scheme.medium, false),
"low" => ("LOW", scheme.low, false),
"info" | "informational" => ("INFO", scheme.info, false),
"none" => ("NONE", scheme.muted, false),
_ => ("UNKNOWN", scheme.muted, true),
};
let fg_color = scheme.severity_badge_fg(severity);
let style = if is_unknown {
Style::default().fg(fg_color).bg(bg_color).dim()
} else {
Style::default().fg(fg_color).bg(bg_color).bold()
};
Span::styled(format!(" {label} "), style)
}
#[must_use]
pub fn severity_indicator(severity: &str) -> Span<'static> {
let scheme = colors();
let (symbol, bg_color, is_unknown) = match severity.to_lowercase().as_str() {
"critical" => ("C", scheme.critical, false),
"high" => ("H", scheme.high, false),
"medium" | "moderate" => ("M", scheme.medium, false),
"low" => ("L", scheme.low, false),
"info" | "informational" => ("I", scheme.info, false),
"none" => ("-", scheme.muted, false),
_ => ("U", scheme.muted, true),
};
let fg_color = scheme.severity_badge_fg(severity);
let style = if is_unknown {
Style::default().fg(fg_color).bg(bg_color).dim()
} else {
Style::default().fg(fg_color).bg(bg_color).bold()
};
Span::styled(format!(" {symbol} "), style)
}
#[must_use]
pub fn count_badge(count: usize, bg_color: Color) -> Span<'static> {
let scheme = colors();
Span::styled(
format!(" {count} "),
Style::default()
.fg(scheme.badge_fg_dark)
.bg(bg_color)
.bold(),
)
}
#[must_use]
pub fn filter_badge(label: &str, value: &str) -> Vec<Span<'static>> {
let scheme = colors();
vec![
Span::styled(format!("{label}: "), Style::default().fg(scheme.text_muted)),
Span::styled(
format!(" {value} "),
Style::default()
.fg(scheme.badge_fg_dark)
.bg(scheme.accent)
.bold(),
),
]
}
#[must_use]
pub fn mode_badge(mode: &str) -> Span<'static> {
let scheme = colors();
let color = match mode.to_lowercase().as_str() {
"diff" => scheme.modified,
"view" => scheme.primary,
"multi-diff" | "multidiff" => scheme.added,
"timeline" => scheme.secondary,
"matrix" => scheme.high,
_ => scheme.muted,
};
Span::styled(
format!(" {} ", mode.to_uppercase()),
Style::default().fg(scheme.badge_fg_dark).bg(color).bold(),
)
}
pub struct FooterHints;
impl FooterHints {
#[must_use]
pub fn for_diff_tab(tab: &str) -> Vec<(&'static str, &'static str)> {
let mut hints = Self::global();
match tab.to_lowercase().as_str() {
"components" => {
hints.insert(0, ("f", "filter"));
hints.insert(1, ("s", "sort"));
}
"dependencies" => {
hints.insert(0, ("f", "filter"));
hints.insert(1, ("t", "transitive"));
hints.insert(2, ("h", "highlight"));
hints.insert(3, ("Enter", "expand"));
hints.insert(4, ("c", "component"));
}
"licenses" => {
hints.insert(0, ("g", "group"));
hints.insert(1, ("s", "sort"));
hints.insert(2, ("r", "risk"));
hints.insert(3, ("c", "compat"));
hints.insert(4, ("Tab", "panel"));
}
"vulnerabilities" | "vulns" => {
hints.insert(0, ("f", "filter"));
hints.insert(1, ("s", "sort"));
hints.insert(2, ("g", "group"));
}
"sidebyside" | "side-by-side" | "diff" => {
hints.insert(0, ("←→/p", "panel"));
hints.insert(1, ("J/K", "scroll both"));
}
"quality" => {
hints.insert(0, ("v", "view"));
}
"compliance" => {
hints.insert(0, ("←→", "standard"));
hints.insert(1, ("v", "view"));
hints.insert(2, ("g", "group"));
hints.insert(3, ("↑↓", "select"));
}
"source" => {
hints.insert(0, ("w", "panel"));
hints.insert(1, ("v", "tree/raw"));
hints.insert(2, ("↑↓", "scroll"));
}
"graphchanges" | "graph" => {
hints.insert(0, ("↑↓", "select"));
hints.insert(1, ("PgUp/Dn", "page"));
}
_ => {}
}
hints
}
#[must_use]
pub fn for_view_tab(tab: &str) -> Vec<(&'static str, &'static str)> {
let mut hints = Self::global();
match tab.to_lowercase().as_str() {
"tree" | "components" => {
hints.insert(0, ("p", "panel"));
hints.insert(1, ("Enter", "select"));
hints.insert(2, ("1-4", "detail tabs"));
}
"vulnerabilities" | "vulns" => {
hints.insert(0, ("f", "filter"));
hints.insert(1, ("s", "sort"));
hints.insert(2, ("g", "group"));
hints.insert(3, ("d", "dedup"));
hints.insert(4, ("Enter", "component"));
}
"licenses" => {
hints.insert(0, ("g", "group"));
hints.insert(1, ("Enter", "inspect"));
hints.insert(2, ("K/J", "scroll"));
}
"dependencies" => {
hints.insert(0, ("Enter", "expand/inspect"));
hints.insert(1, ("←", "collapse"));
hints.insert(2, ("p", "panel"));
hints.insert(3, ("J/K", "scroll"));
}
"quality" => {
hints.insert(0, ("v", "view"));
}
"compliance" => {
hints.insert(0, ("f", "filter"));
hints.insert(1, ("←→", "standard"));
hints.insert(2, ("↑↓", "select"));
}
"source" => {
hints.insert(0, ("v", "tree/raw"));
hints.insert(1, ("p", "panel"));
hints.insert(2, ("H/L", "fold all"));
hints.insert(3, ("Enter", "select"));
}
"algorithms" => {
hints.insert(0, ("s", "sort"));
hints.insert(1, ("↑↓", "select"));
hints.insert(2, ("Enter", "detail"));
}
"certificates" => {
hints.insert(0, ("↑↓", "select"));
hints.insert(1, ("Enter", "detail"));
}
"keys" => {
hints.insert(0, ("↑↓", "select"));
hints.insert(1, ("Enter", "detail"));
}
"protocols" => {
hints.insert(0, ("↑↓", "select"));
hints.insert(1, ("Enter", "detail"));
}
"pqc-compliance" => {
hints.insert(0, ("↑↓", "scroll"));
}
_ => {}
}
hints
}
#[must_use]
pub fn global() -> Vec<(&'static str, &'static str)> {
vec![
("Tab", "switch"),
("/", "search"),
("e", "export"),
("?", "help"),
("q", "quit"),
]
}
pub const GLOBAL_COUNT: usize = 5;
}
#[must_use]
pub fn render_footer_hints(hints: &[(&str, &str)]) -> Vec<Span<'static>> {
let scheme = colors();
let mut spans = Vec::new();
let tab_count = hints.len().saturating_sub(FooterHints::GLOBAL_COUNT);
for (i, (key, desc)) in hints.iter().enumerate() {
if i > 0 {
spans.push(Span::raw(" "));
}
if i == tab_count && tab_count > 0 {
spans.push(Span::styled("│ ", Style::default().fg(scheme.muted)));
}
spans.push(Span::styled(
format!(" {key} "),
Style::default()
.fg(scheme.badge_fg_dark)
.bg(scheme.accent)
.bold(),
));
spans.push(Span::styled(
desc.to_string(),
Style::default().fg(scheme.text_muted),
));
}
spans
}