paline 0.5.0

一个用 Rust 编写的代码行数统计命令行工具。递归扫描目录,按编程语言统计行数,并以彩色横向条形图在终端中可视化展示。
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 {
    /// 将颜色转换为 owo_colors::Rgb
    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),
                ))
            }
        }
    }
}