use once_cell::sync::Lazy; use ratatui::style::{Color as TuiColor, Modifier, Style as TuiStyle}; use std::path::{Path, PathBuf}; use syntect::easy::HighlightLines; use syntect::highlighting::{FontStyle, Style as SynStyle, Theme, ThemeSet}; use syntect::parsing::{SyntaxReference, SyntaxSet}; static SYNTAX_SET: Lazy = Lazy::new(SyntaxSet::load_defaults_newlines); static THEME_SET: Lazy = Lazy::new(ThemeSet::load_defaults); static THEME: Lazy = Lazy::new(|| { THEME_SET .themes .get("base16-ocean.dark") .cloned() .or_else(|| THEME_SET.themes.values().next().cloned()) .unwrap_or_default() }); fn select_syntax(path_hint: Option<&Path>) -> &'static SyntaxReference { if let Some(path) = path_hint { if let Ok(Some(syntax)) = SYNTAX_SET.find_syntax_for_file(path) { return syntax; } if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) { if let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(ext) { return syntax; } } if let Some(name) = path.file_name().and_then(|name| name.to_str()) { if let Some(syntax) = SYNTAX_SET.find_syntax_by_token(name) { return syntax; } } } SYNTAX_SET.find_syntax_plain_text() } fn select_syntax_for_language(language: Option<&str>) -> &'static SyntaxReference { let token = language .map(|lang| lang.trim().to_ascii_lowercase()) .filter(|lang| !lang.is_empty()); if let Some(token) = token { let mut attempts: Vec<&str> = vec![token.as_str()]; match token.as_str() { "c++" => attempts.extend(["cpp", "c"]), "c#" | "cs" => attempts.extend(["csharp", "cs"]), "shell" => attempts.extend(["bash", "sh"]), "typescript" | "ts" => attempts.extend(["typescript", "ts", "tsx"]), "javascript" | "js" => attempts.extend(["javascript", "js", "jsx"]), "py" => attempts.push("python"), "rs" => attempts.push("rust"), "yml" => attempts.push("yaml"), other => { if let Some(stripped) = other.strip_prefix('.') { attempts.push(stripped); } } } for candidate in attempts { if let Some(syntax) = SYNTAX_SET.find_syntax_by_token(candidate) { return syntax; } if let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(candidate) { return syntax; } } } SYNTAX_SET.find_syntax_plain_text() } fn path_hint_from_components(absolute: Option<&Path>, display: Option<&str>) -> Option { if let Some(abs) = absolute { return Some(abs.to_path_buf()); } display.map(PathBuf::from) } fn style_from_syntect(style: SynStyle) -> TuiStyle { let mut tui_style = TuiStyle::default().fg(TuiColor::Rgb( style.foreground.r, style.foreground.g, style.foreground.b, )); let mut modifiers = Modifier::empty(); if style.font_style.contains(FontStyle::BOLD) { modifiers |= Modifier::BOLD; } if style.font_style.contains(FontStyle::ITALIC) { modifiers |= Modifier::ITALIC; } if style.font_style.contains(FontStyle::UNDERLINE) { modifiers |= Modifier::UNDERLINED; } if !modifiers.is_empty() { tui_style = tui_style.add_modifier(modifiers); } tui_style } pub fn build_highlighter( absolute: Option<&Path>, display: Option<&str>, ) -> HighlightLines<'static> { let hint_path = path_hint_from_components(absolute, display); let syntax = select_syntax(hint_path.as_deref()); HighlightLines::new(syntax, &THEME) } pub fn highlight_line( highlighter: &mut HighlightLines<'static>, line: &str, ) -> Vec<(TuiStyle, String)> { let mut segments = Vec::new(); match highlighter.highlight_line(line, &SYNTAX_SET) { Ok(result) => { for (style, piece) in result { let tui_style = style_from_syntect(style); segments.push((tui_style, piece.to_string())); } } Err(_) => { segments.push((TuiStyle::default(), line.to_string())); } } if segments.is_empty() { segments.push((TuiStyle::default(), String::new())); } segments } pub fn build_highlighter_for_language(language: Option<&str>) -> HighlightLines<'static> { let syntax = select_syntax_for_language(language); HighlightLines::new(syntax, &THEME) } #[cfg(test)] mod tests { use super::*; #[test] fn rust_highlighting_produces_colored_segment() { let mut highlighter = build_highlighter_for_language(Some("rust")); let segments = highlight_line(&mut highlighter, "fn main() {}"); assert!( segments .iter() .any(|(style, text)| style.fg.is_some() && !text.trim().is_empty()), "Expected at least one colored segment" ); } }