use std::collections::HashMap;
use std::fs::File;
use std::path::Path;
use owo_colors::Rgb;
use serde::{Deserialize, Serialize};
use crate::logger::{Level, Log, Verbosity};
const CONFIG_HEADER: &str = "\
# Paline 语言配置文件
# 每个 [[lang]] 项定义一种编程语言
# 字段说明:
# id - 语言唯一标识(内部使用)
# name - 显示名称
# extensions - 扩展名列表(不含点)
# exact_filenames - 精确文件名列表(如 Makefile)
# color - RGB 颜色分量 (0-255)
";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanguageEntry {
pub id: String,
pub name: String,
pub extensions: Vec<String>,
pub exact_filenames: Vec<String>,
pub color: (u8, u8, u8),
}
impl LanguageEntry {
pub fn to_rgb(&self) -> Rgb {
Rgb(self.color.0, self.color.1, self.color.2)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanguagesConfig {
pub lang: Vec<LanguageEntry>,
}
impl LanguagesConfig {
pub fn from_file(path: &Path) -> Result<Self, Log> {
let content = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
return Err(Log::new(
Level::Error,
format!(
"Failed to initialize the language configuration file: {}",
e
),
Some(Verbosity::Normal),
))
}
};
let config: Self = match toml::from_str(&content) {
Ok(lang_cfg) => lang_cfg,
Err(e) => {
return Err(Log::new(
Level::Error,
format!(
"Failed to initialize the language configuration file: {}",
e
),
Some(Verbosity::Normal),
))
}
};
Ok(config)
}
pub fn build_index(&self) -> LanguageIndex {
let mut by_ext = HashMap::new();
let mut by_exact = HashMap::new();
let mut by_id = HashMap::new();
for entry in &self.lang {
by_id.insert(entry.id.clone(), entry.clone());
for ext in &entry.extensions {
by_ext.insert(ext.clone(), entry.id.clone());
}
for name in &entry.exact_filenames {
by_exact.insert(name.clone(), entry.id.clone());
}
}
LanguageIndex {
by_ext,
by_exact,
by_id,
}
}
}
pub struct LanguageIndex {
by_ext: HashMap<String, String>,
by_exact: HashMap<String, String>,
by_id: HashMap<String, LanguageEntry>,
}
impl LanguageIndex {
pub fn detect(&self, path: &Path) -> Option<&LanguageEntry> {
let file_name = path.file_name()?.to_str()?;
if let Some(id) = self.by_exact.get(file_name) {
return self.by_id.get(id);
}
let ext = path.extension()?.to_str()?;
if let Some(id) = self.by_ext.get(ext) {
return self.by_id.get(id);
}
None
}
pub fn get(&self, id: &str) -> Option<&LanguageEntry> {
self.by_id.get(id)
}
pub fn all_entries(&self) -> impl Iterator<Item = &LanguageEntry> {
self.by_id.values()
}
}
impl LanguagesConfig {
pub fn default_config() -> Self {
let lang = vec![
LanguageEntry {
id: "cpp".into(),
name: "C++".into(),
extensions: vec![
"cpp".into(),
"hpp".into(),
"cxx".into(),
"cc".into(),
"hxx".into(),
"hh".into(),
"inl".into(),
],
exact_filenames: vec![],
color: (243, 75, 125),
},
LanguageEntry {
id: "c".into(),
name: "C".into(),
extensions: vec!["c".into(), "h".into()],
exact_filenames: vec![],
color: (85, 85, 85),
},
LanguageEntry {
id: "cmake".into(),
name: "CMake".into(),
extensions: vec!["cmake".into()],
exact_filenames: vec!["CMakeLists.txt".into()],
color: (218, 52, 52),
},
LanguageEntry {
id: "make".into(),
name: "Make".into(),
extensions: vec!["mk".into()],
exact_filenames: vec!["Makefile".into()],
color: (66, 120, 25),
},
LanguageEntry {
id: "ninja".into(),
name: "Ninja".into(),
extensions: vec!["ninja".into()],
exact_filenames: vec![],
color: (128, 0, 255),
},
LanguageEntry {
id: "gradle".into(),
name: "Gradle".into(),
extensions: vec!["gradle".into()],
exact_filenames: vec!["build.gradle".into()],
color: (2, 48, 58),
},
LanguageEntry {
id: "maven".into(),
name: "Maven".into(),
extensions: vec![],
exact_filenames: vec!["pom.xml".into()],
color: (199, 26, 54),
},
LanguageEntry {
id: "python".into(),
name: "Python".into(),
extensions: vec!["py".into(), "pyw".into(), "pyi".into()],
exact_filenames: vec![],
color: (53, 114, 165),
},
LanguageEntry {
id: "java".into(),
name: "Java".into(),
extensions: vec!["java".into()],
exact_filenames: vec![],
color: (176, 114, 25),
},
LanguageEntry {
id: "javascript".into(),
name: "JavaScript".into(),
extensions: vec!["js".into(), "mjs".into(), "cjs".into()],
exact_filenames: vec![],
color: (241, 224, 90),
},
LanguageEntry {
id: "typescript".into(),
name: "TypeScript".into(),
extensions: vec!["ts".into(), "tsx".into()],
exact_filenames: vec![],
color: (49, 120, 198),
},
LanguageEntry {
id: "go".into(),
name: "Go".into(),
extensions: vec!["go".into()],
exact_filenames: vec![],
color: (0, 173, 216),
},
LanguageEntry {
id: "rust".into(),
name: "Rust".into(),
extensions: vec!["rs".into()],
exact_filenames: vec![],
color: (222, 165, 132),
},
LanguageEntry {
id: "ruby".into(),
name: "Ruby".into(),
extensions: vec!["rb".into(), "rake".into(), "gemspec".into()],
exact_filenames: vec![],
color: (112, 21, 22),
},
LanguageEntry {
id: "php".into(),
name: "PHP".into(),
extensions: vec!["php".into(), "phtml".into()],
exact_filenames: vec![],
color: (79, 93, 149),
},
LanguageEntry {
id: "shell".into(),
name: "Shell".into(),
extensions: vec!["sh".into(), "bash".into(), "zsh".into()],
exact_filenames: vec![],
color: (137, 224, 81),
},
LanguageEntry {
id: "lua".into(),
name: "Lua".into(),
extensions: vec!["lua".into()],
exact_filenames: vec![],
color: (0, 0, 128),
},
LanguageEntry {
id: "r".into(),
name: "R".into(),
extensions: vec!["R".into(), "r".into()],
exact_filenames: vec![],
color: (25, 140, 231),
},
LanguageEntry {
id: "swift".into(),
name: "Swift".into(),
extensions: vec!["swift".into()],
exact_filenames: vec![],
color: (240, 81, 56),
},
LanguageEntry {
id: "kotlin".into(),
name: "Kotlin".into(),
extensions: vec!["kt".into(), "kts".into()],
exact_filenames: vec![],
color: (169, 123, 255),
},
LanguageEntry {
id: "fsharp".into(),
name: "F#".into(),
extensions: vec!["fs".into(), "fsi".into()],
exact_filenames: vec![],
color: (184, 69, 252),
},
LanguageEntry {
id: "fsharpscript".into(),
name: "F# Script".into(),
extensions: vec!["fsx".into()],
exact_filenames: vec![],
color: (184, 69, 252),
},
LanguageEntry {
id: "csharp".into(),
name: "C#".into(),
extensions: vec!["cs".into()],
exact_filenames: vec![],
color: (23, 134, 0),
},
LanguageEntry {
id: "vb".into(),
name: "Visual Basic".into(),
extensions: vec!["vb".into()],
exact_filenames: vec![],
color: (148, 93, 183),
},
LanguageEntry {
id: "assembly".into(),
name: "Assembly".into(),
extensions: vec!["s".into(), "asm".into()],
exact_filenames: vec![],
color: (110, 76, 19),
},
LanguageEntry {
id: "json".into(),
name: "JSON".into(),
extensions: vec!["json".into()],
exact_filenames: vec![],
color: (139, 148, 158),
},
LanguageEntry {
id: "yaml".into(),
name: "YAML".into(),
extensions: vec!["yaml".into(), "yml".into()],
exact_filenames: vec![],
color: (203, 23, 30),
},
LanguageEntry {
id: "toml".into(),
name: "TOML".into(),
extensions: vec!["toml".into()],
exact_filenames: vec![],
color: (156, 66, 33),
},
LanguageEntry {
id: "xml".into(),
name: "XML".into(),
extensions: vec!["xml".into()],
exact_filenames: vec![],
color: (0, 96, 172),
},
];
Self { lang }
}
pub fn init_default_file(path: &Path) -> Result<(), Log> {
if let Some(parent) = path.parent() {
match std::fs::create_dir_all(parent) {
Ok(_) => {}
Err(e) => {
return Err(Log::new(
Level::Error,
format!(
"Failed to initialize the language configuration file: {}",
e
),
Some(Verbosity::Normal),
))
}
}
}
let config = Self::default_config();
match File::create(path) {
Ok(_) => {}
Err(e) => {
return Err(Log::new(
Level::Error,
format!(
"Failed to initialize the language configuration file: {}",
e
),
Some(Verbosity::Normal),
))
}
}
let toml_str = match toml::to_string(&config) {
Ok(s) => s,
Err(e) => {
return Err(Log::new(
Level::Error,
format!(
"Failed to initialize the language configuration file: {}",
e
),
Some(Verbosity::Normal),
))
}
};
match std::fs::write(path, format!("{}{}", CONFIG_HEADER, toml_str)) {
Ok(_) => return Ok(()),
Err(e) => {
return Err(Log::new(
Level::Error,
format!(
"Failed to initialize the language configuration file: {}",
e
),
Some(Verbosity::Normal),
))
}
}
}
}