use crate::tui::license_utils::{LicenseCategory, LicenseInfo, RiskLevel};
use crate::tui::theme::colors;
use ratatui::prelude::*;
pub fn category_color(category: LicenseCategory) -> Color {
let scheme = colors();
match category {
LicenseCategory::Permissive | LicenseCategory::PublicDomain => scheme.success,
LicenseCategory::WeakCopyleft => scheme.info,
LicenseCategory::StrongCopyleft => scheme.warning,
LicenseCategory::NetworkCopyleft | LicenseCategory::Proprietary => scheme.error,
LicenseCategory::Unknown => scheme.text_muted,
}
}
pub fn category_glyph(category: LicenseCategory) -> &'static str {
match category {
LicenseCategory::Permissive | LicenseCategory::PublicDomain => "✓",
LicenseCategory::StrongCopyleft | LicenseCategory::NetworkCopyleft => "©",
LicenseCategory::WeakCopyleft => "◐",
LicenseCategory::Proprietary => "⊘",
LicenseCategory::Unknown => "?",
}
}
pub fn category_glyph_str(category: &str) -> &'static str {
match category.to_lowercase().as_str() {
"permissive" | "public domain" => "✓",
"copyleft" | "network copyleft" | "strong copyleft" => "©",
"weak copyleft" => "◐",
"proprietary" | "commercial" => "⊘",
_ => "?",
}
}
pub fn risk_level_color(risk: RiskLevel) -> Color {
let scheme = colors();
match risk {
RiskLevel::Low => scheme.success,
RiskLevel::Medium => scheme.info,
RiskLevel::High => scheme.warning,
RiskLevel::Critical => scheme.error,
}
}
pub fn render_license_metadata_lines(
license: &str,
category: LicenseCategory,
risk_level: RiskLevel,
family: &str,
component_count: usize,
is_dual_licensed: bool,
) -> Vec<Line<'static>> {
let scheme = colors();
let mut lines = vec![];
let title = if is_dual_licensed {
format!("{license} (Dual/Multi License)")
} else {
license.to_string()
};
lines.push(Line::from(vec![Span::styled(
title,
Style::default().fg(scheme.text).bold(),
)]));
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled("Category: ", Style::default().fg(scheme.text_muted)),
Span::styled(
category.as_str(),
Style::default().fg(category_color(category)).bold(),
),
]));
lines.push(Line::from(vec![
Span::styled("Risk: ", Style::default().fg(scheme.text_muted)),
Span::styled(
risk_level.as_str(),
Style::default().fg(risk_level_color(risk_level)).bold(),
),
]));
lines.push(Line::from(vec![
Span::styled("Family: ", Style::default().fg(scheme.text_muted)),
Span::styled(family.to_string(), Style::default().fg(scheme.accent)),
]));
lines.push(Line::from(vec![
Span::styled("Components: ", Style::default().fg(scheme.text_muted)),
Span::styled(
component_count.to_string(),
Style::default().fg(scheme.primary),
),
]));
lines
}
pub fn get_license_characteristics(license: &str) -> Vec<(&'static str, bool)> {
let info = LicenseInfo::from_spdx(license);
match info.category {
LicenseCategory::Permissive | LicenseCategory::PublicDomain => vec![
("Commercial use allowed", true),
("Modification allowed", true),
("Distribution allowed", true),
("Patent grant", info.patent_grant),
("Copyleft/Share-alike", false),
],
LicenseCategory::StrongCopyleft | LicenseCategory::NetworkCopyleft => vec![
("Commercial use allowed", true),
("Modification allowed", true),
("Distribution allowed", true),
("Copyleft/Share-alike", true),
("Derivative work must be open", true),
],
LicenseCategory::WeakCopyleft => vec![
("Commercial use allowed", true),
("Modification allowed", true),
("Distribution allowed", true),
("Copyleft/Share-alike", true),
("Linking allowed", true),
],
LicenseCategory::Proprietary => vec![
("Commercial use allowed", false),
("Modification allowed", false),
("Distribution allowed", false),
("Source access", false),
],
LicenseCategory::Unknown => vec![("License terms unknown", false)],
}
}
pub fn render_license_characteristics_lines(license: &str) -> Vec<Line<'static>> {
let scheme = colors();
let mut lines = vec![];
lines.push(Line::styled(
"Characteristics:",
Style::default().fg(scheme.primary).bold(),
));
let characteristics = get_license_characteristics(license);
for (desc, allowed) in characteristics {
let (icon, color) = if allowed {
("✓", scheme.success)
} else {
("✗", scheme.error)
};
lines.push(Line::from(vec![
Span::styled(format!(" {icon} "), Style::default().fg(color)),
Span::raw(desc),
]));
}
let info = LicenseInfo::from_spdx(license);
if info.network_copyleft {
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled("⚠ ", Style::default().fg(scheme.warning)),
Span::styled(
"Network copyleft (AGPL-style)",
Style::default().fg(scheme.warning),
),
]));
}
lines
}
#[cfg(test)]
mod glyph_tests {
use super::{category_glyph, category_glyph_str};
use crate::tui::license_utils::LicenseCategory;
#[test]
fn category_glyph_covers_all_categories() {
let all = [
LicenseCategory::Permissive,
LicenseCategory::WeakCopyleft,
LicenseCategory::StrongCopyleft,
LicenseCategory::NetworkCopyleft,
LicenseCategory::Proprietary,
LicenseCategory::PublicDomain,
LicenseCategory::Unknown,
];
for cat in all {
let glyph = category_glyph(cat);
assert!(!glyph.is_empty());
assert_eq!(
category_glyph_str(cat.as_str()),
glyph,
"string path disagrees for {:?} (as_str: {})",
cat,
cat.as_str()
);
}
}
#[test]
fn category_color_uses_risk_slots_not_data_gap_gray() {
crate::tui::test_support::pin_theme();
let s = crate::tui::theme::colors();
assert_eq!(
super::category_color(LicenseCategory::NetworkCopyleft),
s.error,
"AGPL-class network copyleft must use the error slot"
);
assert_eq!(
super::category_color(LicenseCategory::Proprietary),
s.error,
"proprietary must use the error slot"
);
assert_eq!(
super::category_color(LicenseCategory::PublicDomain),
s.success,
"public domain must use the success slot"
);
assert_eq!(
super::category_color(LicenseCategory::Permissive),
s.success,
"permissive must use the success slot"
);
assert_eq!(
super::category_color(LicenseCategory::WeakCopyleft),
s.info,
"weak copyleft must use the info slot"
);
assert_eq!(
super::category_color(LicenseCategory::StrongCopyleft),
s.warning,
"strong copyleft must use the warning slot"
);
assert_eq!(
super::category_color(LicenseCategory::Unknown),
s.text_muted,
"only Unknown is the data-gap gray"
);
assert_ne!(
s.error, s.text_muted,
"dark theme keeps the error slot distinct from the data-gap gray"
);
assert_ne!(
s.success, s.text_muted,
"dark theme keeps the success slot distinct from the data-gap gray"
);
}
}