use colored::*;
use crate::utils::cache::{Cache, CacheManager};
use crate::utils::pattern::filter_by_wildcard;
use crate::utils::progress;
use crate::utils::remote::Fetcher;
use super::{SPDX_LICENSE_LIST_URL, ensure_github_api_license_cache, ensure_spdx_license_cache};
#[derive(clap::Args, Clone, Debug)]
pub struct ListArgs {
#[arg(long, short)]
pub popular: bool,
#[arg(long, short)]
pub non_software: bool,
#[arg(long, short)]
pub search: Option<String>,
#[arg(long)]
pub include_deprecated: bool,
#[arg(long)]
pub update_cache: bool,
#[arg(allow_hyphen_values = true)]
pub args: Vec<String>,
#[arg(long)]
pub osi_approved: bool,
#[arg(long)]
pub fsf_libre: bool,
}
impl super::Runnable for ListArgs {
fn run(&self) -> anyhow::Result<()> {
if !self.args.is_empty() {
for arg in &self.args {
return Err(anyhow::anyhow!("Unknown argument: {}", arg));
}
}
let license_args = LicenseArgs {
update_cache: self.update_cache,
search: self.search.clone(),
include_deprecated: self.include_deprecated,
osi_approved: self.osi_approved,
fsf_libre: self.fsf_libre,
};
if self.popular {
return list_popular_licenses(license_args);
} else if self.non_software {
return list_non_software_licenses(self.update_cache);
} else {
list_all_licenses(license_args)
.map_err(|e| anyhow::anyhow!("Failed to list licenses: {}", e))
}
}
}
struct LicenseArgs {
update_cache: bool,
search: Option<String>,
include_deprecated: bool,
osi_approved: bool,
fsf_libre: bool,
}
fn list_popular_licenses(args: LicenseArgs) -> anyhow::Result<()> {
let mut cache_manager = CacheManager::new()?;
let cache: Cache<serde_json::Value> =
ensure_github_api_license_cache(&mut cache_manager, args.update_cache)?;
if let Some(search) = &args.search {
let mut matches = Vec::new();
let mut all_items = Vec::new();
for (id, entry) in &cache.entries {
all_items.push(id.clone());
if let Some(name) = entry.data.get("name").and_then(|n| n.as_str()) {
all_items.push(name.to_string());
}
}
let filtered = filter_by_wildcard(search, &all_items);
for (id, entry) in &cache.entries {
let name = entry
.data
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("");
if filtered.contains(id) || filtered.contains(&name.to_string()) {
matches.push((id, entry));
}
}
if matches.is_empty() {
println!("No popular licenses found matching '{}'", search);
return Ok(());
}
println!(
"{} Popular licenses matching '{}' ({} found):",
"✓".green(),
search,
matches.len()
);
println!();
for (id, entry) in matches {
if let Some(name) = entry.data.get("name").and_then(|n| n.as_str()) {
println!(" {} {:<20} {}", ">".green(), id, name);
} else {
println!("{}", id);
}
}
return Ok(());
}
for (id, entry) in &cache.entries {
if let Some(name) = entry.data.get("name").and_then(|n| n.as_str()) {
println!(" {} {:<20} {}", ">".green(), id, name);
} else {
println!(" {} {:<20} {:?}", ">".green(), id, entry.data);
println!(" {} {}", ">".green(), id);
}
}
Ok(())
}
fn list_all_licenses(args: LicenseArgs) -> anyhow::Result<()> {
let fetcher = Fetcher::new();
let pb = progress::spinner("Fetching SPDX license list...");
let licenses_data = fetcher.fetch_json(SPDX_LICENSE_LIST_URL)?;
pb.set_message("Parsing license list...");
let licenses = licenses_data
.get("licenses")
.and_then(|l| l.as_array())
.ok_or_else(|| anyhow::anyhow!("Failed to parse SPDX licenses list"))?;
pb.set_message("Successfully fetched licenses");
pb.finish_and_clear();
let mut filtered_licenses = Vec::new();
for license in licenses {
let license_id = license
.get("licenseId")
.and_then(|id| id.as_str())
.unwrap_or("unknown");
let license_name = license
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("Unknown License");
let is_deprecated = license
.get("isDeprecatedLicenseId")
.and_then(|d| d.as_bool())
.unwrap_or(false);
if is_deprecated && !args.include_deprecated {
continue;
}
if let Some(search) = &args.search {
let mut search_lower = search.to_lowercase();
if !search_lower.ends_with('*') && !search_lower.ends_with('?') {
search_lower.push('*');
}
let candidates = vec![license_id.to_lowercase(), license_name.to_lowercase()];
let filtered = filter_by_wildcard(&search_lower, &candidates);
if !filtered.contains(&license_id.to_lowercase())
&& !filtered.contains(&license_name.to_lowercase())
{
continue;
}
}
if args.osi_approved {
let osi_approved = license
.get("isOsiApproved")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !osi_approved {
continue;
}
}
if args.fsf_libre {
let fsf_libre = license
.get("isFsfLibre")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !fsf_libre {
continue;
}
}
filtered_licenses.push((license_id, license_name, is_deprecated, license));
}
filtered_licenses.sort_by(|a, b| a.0.cmp(b.0));
if filtered_licenses.is_empty() {
if let Some(search) = &args.search {
println!("No licenses found matching '{}'", search);
} else {
println!("No licenses found");
}
return Ok(());
}
let header = if let Some(search) = &args.search {
format!("Licenses matching '{}'", search)
} else {
"Available SPDX licenses".to_string()
};
println!(
"{} {} ({} found):",
"✓".green(),
header,
filtered_licenses.len()
);
println!();
display_simple_licenses(&filtered_licenses);
if !args.include_deprecated {
println!("\nNote: deprecated licenses are hidden. Use --include-deprecated to show them.");
}
Ok(())
}
fn display_simple_licenses(licenses: &[(&str, &str, bool, &serde_json::Value)]) {
for (id, name, is_deprecated, _) in licenses {
let deprecated_marker = if *is_deprecated { " (deprecated)" } else { "" };
println!(
" {} {:<20} {}{}",
">".green(),
id,
name,
deprecated_marker.yellow()
);
}
}
fn list_non_software_licenses(update_cache: bool) -> anyhow::Result<()> {
let mut cache_manager = CacheManager::new()?;
let cache: Cache<serde_json::Value> =
ensure_spdx_license_cache(&mut cache_manager, update_cache)?;
println!("{}", "✓ Non-Software Licenses:".green());
println!();
println!("{}", "Data, media, etc.".bold());
for id in &["CC0-1.0", "CC-BY-4.0", "CC-BY-SA-4.0"] {
if let Some(entry) = cache.entries.get(*id) {
let name = entry
.data
.get("name")
.and_then(|n| n.as_str())
.unwrap_or(id);
println!(" {} {:<20} {}", ">".green(), id, name);
} else {
println!(
" {} {:<20} {}",
"!".red(),
id,
"(not found in SPDX cache)".yellow()
);
}
}
println!(
" Creative Commons licenses are for non-software material. Not recommended for software/hardware.\n"
);
println!("{}", "Fonts".bold());
if let Some(entry) = cache.entries.get("OFL-1.1") {
let name = entry
.data
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("OFL-1.1");
println!(" {} {:<20} {}\n", ">".green(), "OFL-1.1", name);
} else {
println!(
" {} {:<20} {}",
"!".red(),
"OFL-1.1",
"(not found in SPDX cache)".yellow()
);
}
println!("{}", "Hardware".bold());
for id in &["CERN-OHL-P-2.0", "CERN-OHL-W-2.0", "CERN-OHL-S-2.0"] {
if let Some(entry) = cache.entries.get(*id) {
let name = entry
.data
.get("name")
.and_then(|n| n.as_str())
.unwrap_or(id);
println!(" {} {:<20} {}", ">".green(), id, name);
} else {
println!(
" {} {:<20} {}",
"!".red(),
id,
"(not found in SPDX cache)".yellow()
);
}
}
println!();
println!("For more information, see: https://choosealicense.com/non-software/");
Ok(())
}