use serde_json::Value;
use std::{
env,
io::Write,
error::Error,
ffi::OsString,
num::ParseIntError,
collections::{
HashMap,
HashSet
},
fs::{
File,
DirEntry,
read_dir,
read_to_string,
remove_file,
create_dir_all
},
path::{
Path,
PathBuf
}
};
use serde::{
Deserialize,
Serialize
};
#[derive(Debug, Deserialize, Serialize)]
struct Logo {
r#type: String,
ascii: Vec<String>
}
#[derive(Debug, Deserialize, Serialize)]
struct Distro {
logos: Vec<Logo>,
colors: Vec<Value>
}
#[derive(Debug, Clone)]
enum ColorType {
Ansi,
Css,
Xterm
}
#[derive(Debug, Clone)]
struct Color {
color_type: ColorType,
value: String
}
fn get_distro_template() -> String {
[
"// SPDX-FileCopyrightText: 2023 CELESTIFYX Team",
"// SPDX-License-Identifier: GPL-3.0-or-later",
"",
"{imports}",
"",
"pub(super) fn large_logo() -> &'static [&'static str; {large_logo_length}] {",
" {large_logo_str}",
"}",
"",
"pub(super) fn small_logo() -> &'static [&'static str; {small_logo_length}] {",
" {small_logo_str}",
"}",
"",
"pub(super) fn colors() -> (AnyColor, AnyColor, &'static [AnyColor; {colors_len}]) {",
" (",
" {primary},",
" {secondary},",
"",
" {colors_array}",
" )",
"}",
""
].join("\n")
}
fn get_mod_template() -> String {
[
"// SPDX-FileCopyrightText: 2023 CELESTIFYX Team",
"// SPDX-License-Identifier: GPL-3.0-or-later",
"",
"use super::register_logos;",
"",
"register_logos!({files});",
""
].join("\n")
}
impl Color {
fn new(value: &Value) -> Result<Self, String> {
match value {
Value::String(s) => Ok(Color {
color_type: ColorType::Ansi,
value: s.clone()
}),
Value::Object(map) => {
let (color_name, type_value): (&String, &Value) = map.iter().next().ok_or_else(|| "Empty color object".to_string())?;
let type_str: String = type_value.as_str().ok_or_else(|| "Color type must be string".to_string())?.to_lowercase();
let color_type: ColorType = match type_str.as_str() {
"ansi" => ColorType::Ansi,
"css" => ColorType::Css,
"xterm" => ColorType::Xterm,
_ => return Err(format!("Unknown color type: {}", type_str))
};
Ok(Color {
color_type,
value: color_name.clone()
})
},
_ => Err("Invalid color format".to_string())
}
}
fn to_rust_code(&self) -> String {
match self.color_type {
ColorType::Ansi => format!("AnyColor::Ansi(AnsiColor::{})", self.value),
ColorType::Css => format!("AnyColor::Css(CssColor::{})", self.value),
ColorType::Xterm => format!("AnyColor::Xterm(XtermColor::{})", self.value)
}
}
}
struct DistroGenerator {
valid_logo_types: HashSet<String>
}
impl DistroGenerator {
fn new() -> Self {
let mut valid_logo_types: HashSet<String> = HashSet::new();
(&mut valid_logo_types).insert("small".to_string());
(&mut valid_logo_types).insert("large".to_string());
DistroGenerator {
valid_logo_types
}
}
fn validate_distro(&self, name: &str, distro: &Distro) -> Option<String> {
if name.contains(' ') || name.contains('-') {
return Some(format!("Distro '{}': Invalid name format - must not contain spaces or hyphens", name));
}
if distro.logos.is_empty() {
return Some(format!("Distro '{}': Missing or invalid 'logos' field", name));
}
if distro.colors.is_empty() {
return Some(format!("Distro '{}': Missing or invalid 'colors' field", name));
}
for logo in &distro.logos {
if !self.valid_logo_types.contains(&logo.r#type) {
return Some(format!("Distro '{}': Invalid logo type '{}' - must be 'small' or 'large'", name, logo.r#type));
}
}
None
}
fn get_logo_by_type<'a>(&self, logos: &'a [Logo], logo_type: &str) -> Option<&'a Logo> {
logos.iter().find(|logo: &&Logo| (logo.r#type == logo_type) && !logo.ascii.is_empty())
}
fn escape_rust_string(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
fn format_logo(&self, logo: Option<&Logo>) -> (String, usize) {
match logo {
None => ("&[]".to_string(), 0),
Some(logo) if logo.ascii.is_empty() => ("&[]".to_string(), 0),
Some(logo) => {
let formatted_lines: Vec<String> = logo.ascii.iter().map(|line: &String| format!("\"{}\"", Self::escape_rust_string(line))).collect();
let length: usize = formatted_lines.len();
let result: String = format!("&[\n {}\n ]", formatted_lines.join(",\n "));
(result, length)
}
}
}
fn parse_colors(&self, name: &str, colors: &[Value]) -> Result<(Value, Value, Vec<Value>), String> {
if colors.len() < 3 {
return Err(format!("Distro '{}': Colors array must have at least 3 elements: [first, two, [ascii]]", name));
}
let ascii_colors: Vec<Value> = colors[2].as_array().ok_or_else(|| {
format!("Distro '{}': Third element of colors must be an array for ASCII colors", name)
})?.clone();
if ascii_colors.is_empty() {
return Err(format!("Distro '{}': ASCII colors array cannot be empty", name));
}
Ok((colors[0].clone(), colors[1].clone(), ascii_colors))
}
fn resolve_color_index(&self, name: &str, value: &Value, ascii_colors: &[Value]) -> Result<Value, String> {
match value {
Value::String(s) if s.starts_with('%') => {
let index_str: &str = &s[1..];
let index: usize = index_str.parse().map_err(|_: ParseIntError| {
format!("Distro '{}': Invalid index format '{}' - must be %NUMBER", name, s)
})?;
if index >= ascii_colors.len() {
return Err(format!("Distro '{}': Index {} out of range - ascii_colors has {} elements (valid: 0-{})", name, index, ascii_colors.len(), ascii_colors.len() - 1));
}
Ok(ascii_colors[index].clone())
},
_ => Ok(value.clone())
}
}
fn get_required_imports(&self, colors: &[Color]) -> String {
let mut used_types: HashSet<&str> = HashSet::new();
for color in colors {
match color.color_type {
ColorType::Ansi => {
(&mut used_types).insert("ansi");
},
ColorType::Css => {
(&mut used_types).insert("css");
},
ColorType::Xterm => {
(&mut used_types).insert("xterm");
}
}
}
let mut imports: Vec<String> = vec!["AnyColor".to_string()];
if used_types.contains("ansi") {
(&mut imports).push("AnsiColor".to_string());
}
if used_types.contains("css") {
(&mut imports).push("CssColor".to_string());
}
if used_types.contains("xterm") {
(&mut imports).push("XtermColor".to_string());
}
(&mut imports).sort();
format!("use chromakitx::{{\n {}\n}};", imports.join(",\n "))
}
fn generate_colors_array(&self, ascii_colors: &[Color]) -> String {
if ascii_colors.len() == 1 {
return format!("&[{}]", ascii_colors[0].to_rust_code());
}
let colors_str: Vec<String> = ascii_colors.iter().map(|c: &Color| c.to_rust_code()).collect();
format!("&[\n {},\n ]", colors_str.join(",\n "))
}
fn has_valid_logos(&self, distro: &Distro) -> bool {
self.valid_logo_types.iter().any(|t: &String| self.get_logo_by_type(&distro.logos, t).is_some())
}
fn generate_distro_file(&self, name: &str, distro: &Distro) -> Result<String, String> {
if let Some(error) = self.validate_distro(name, distro) {
return Err(error);
}
if distro.logos.is_empty() {
return Err(format!("Distro '{}': No logos defined", name));
}
let large_logo: Option<&Logo> = self.get_logo_by_type(&distro.logos, "large");
let small_logo: Option<&Logo> = self.get_logo_by_type(&distro.logos, "small");
if large_logo.is_none() && small_logo.is_none() {
return Err(format!("Distro '{}': No valid logos found", name));
}
let (large_logo_str, large_logo_length): (String, usize) = self.format_logo(large_logo);
let (small_logo_str, small_logo_length): (String, usize) = self.format_logo(small_logo);
let (first, two, ascii_colors): (Value, Value, Vec<Value>) = self.parse_colors(name, &distro.colors)?;
let first: Value = self.resolve_color_index(name, &first, &ascii_colors)?;
let two: Value = self.resolve_color_index(name, &two, &ascii_colors)?;
let mut all_colors_values: Vec<Value> = vec![first.clone(), two.clone()];
(&mut all_colors_values).extend(ascii_colors.clone());
let all_colors: Result<Vec<Color>, String> = all_colors_values.iter().map(Color::new).collect();
let all_colors: Vec<Color> = all_colors?;
let primary: String = Color::new(&first)?.to_rust_code();
let secondary: String = Color::new(&two)?.to_rust_code();
let ascii_colors_parsed: Result<Vec<Color>, String> = ascii_colors.iter().map(Color::new).collect();
let ascii_colors_parsed: Vec<Color> = ascii_colors_parsed?;
let colors_array: String = self.generate_colors_array(&ascii_colors_parsed);
let colors_len: usize = ascii_colors_parsed.len();
let imports: String = self.get_required_imports(&all_colors);
let template: String = get_distro_template().replace("{imports}", &imports).replace("{large_logo_length}", &large_logo_length.to_string()).replace("{large_logo_str}", &large_logo_str).replace("{small_logo_length}", &small_logo_length.to_string()).replace("{small_logo_str}", &small_logo_str).replace("{colors_len}", &colors_len.to_string()).replace("{primary}", &primary).replace("{secondary}", &secondary).replace("{colors_array}", &colors_array);
Ok(format!("{}", template))
}
fn generate_mod_file(&self, distros: &HashMap<String, Distro>) -> String {
let mut valid_distros: Vec<String> = distros.iter().filter(|(name, distro): &(&String, &Distro)| {
self.validate_distro(name, distro).is_none() && !distro.logos.is_empty() && self.has_valid_logos(distro)
}).map(|(name, _): (&String, &Distro)| name.clone()).collect();
(&mut valid_distros).sort();
get_mod_template().replace("{files}", &valid_distros.join(", "))
}
}
fn read_distros() -> Result<HashMap<String, Distro>, Box<dyn Error>> {
let distros_path: &Path = Path::new("resources/distros.json");
let content: String = read_to_string(distros_path)?;
let distros: HashMap<String, Distro> = serde_json::from_str(&content)?;
Ok(distros)
}
fn main() -> Result<(), Box<dyn Error>> {
println!("cargo:rerun-if-changed=resources/distros.json");
println!("cargo:rerun-if-changed=templates/distro.rs.template");
println!("cargo:rerun-if-changed=templates/mod.rs.template");
let out_dir: String = env::var("OUT_DIR")?;
let target_dir: PathBuf = Path::new(&out_dir).join("distros");
create_dir_all(&target_dir)?;
let all_distros: HashMap<String, Distro> = read_distros()?;
let generator: DistroGenerator = DistroGenerator::new();
let mut valid_files: HashSet<String> = HashSet::new();
(&mut valid_files).insert("mod.rs".to_string());
let mut errors: Vec<String> = Vec::new();
for (name, distro) in &all_distros {
match generator.generate_distro_file(name, distro) {
Ok(content) => {
let file_path: PathBuf = target_dir.join(format!("{}.rs", name));
let mut file: File = File::create(&file_path)?;
(&mut file).write_all(content.as_bytes())?;
(&mut valid_files).insert(format!("{}.rs", name));
},
Err(e) => {
(&mut errors).push(format!("{}", e));
}
}
}
if !errors.is_empty() {
for err in errors {
eprintln!("❌ {}", err);
}
panic!("Build failed due to invalid distros");
}
let mod_content: String = generator.generate_mod_file(&all_distros);
let mod_path: PathBuf = target_dir.join("mod.rs");
let mut mod_file: File = File::create(&mod_path)?;
(&mut mod_file).write_all(mod_content.as_bytes())?;
if target_dir.exists() {
for entry in read_dir(target_dir)? {
let entry: DirEntry = entry?;
let file_name: OsString = entry.file_name();
let file_name_str: String = file_name.to_string_lossy().to_string();
if !valid_files.contains(&file_name_str) {
remove_file(entry.path())?;
}
}
}
Ok(())
}