use crate::ui::theme::Theme;
use ratatui::style::Color as TuiColor;
use ratatui::text::{Line, Span};
use std::collections::{HashMap, VecDeque};
use std::hash::{Hash, Hasher};
use std::sync::Mutex;
fn hash_code(lang: &str, code: &str, theme_sig: &str) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
lang.hash(&mut hasher);
code.hash(&mut hasher);
theme_sig.hash(&mut hasher);
hasher.finish()
}
struct SimpleCache {
map: HashMap<(String, u64), Vec<Line<'static>>>,
order: VecDeque<(String, u64)>,
cap: usize,
}
impl SimpleCache {
fn new(cap: usize) -> Self {
Self {
map: HashMap::new(),
order: VecDeque::new(),
cap,
}
}
fn get(&mut self, k: &(String, u64)) -> Option<Vec<Line<'static>>> {
self.map.get(k).cloned()
}
fn put(&mut self, k: (String, u64), v: Vec<Line<'static>>) {
if !self.map.contains_key(&k) {
self.order.push_back(k.clone());
}
self.map.insert(k.clone(), v);
while self.map.len() > self.cap {
if let Some(old) = self.order.pop_front() {
self.map.remove(&old);
} else {
break;
}
}
}
}
static SYNTAX_CACHE: Mutex<Option<SimpleCache>> = Mutex::new(None);
fn get_cache() -> std::sync::MutexGuard<'static, Option<SimpleCache>> {
SYNTAX_CACHE.lock().unwrap()
}
fn ensure_cache(cap: usize) {
let mut guard = get_cache();
if guard.is_none() {
*guard = Some(SimpleCache::new(cap));
}
}
fn is_dark_background(c: &TuiColor) -> bool {
match c {
TuiColor::Rgb(r, g, b) => {
let br = 0.2126 * (*r as f32) + 0.7152 * (*g as f32) + 0.0722 * (*b as f32);
br < 128.0
}
TuiColor::Black => true,
TuiColor::White => false,
TuiColor::Gray | TuiColor::DarkGray => true,
_ => true,
}
}
fn normalize_lang_hint(s: &str) -> String {
let t = s.trim().to_ascii_lowercase();
match t.as_str() {
"py" | "python" => "python".into(),
"bash" | "sh" | "zsh" | "shell" => "bash".into(),
"js" | "javascript" | "jsx" => "javascript".into(),
"ts" | "tsx" | "typescript" => "typescript".into(),
"json" => "json".into(),
"toml" => "toml".into(),
"yaml" | "yml" => "yaml".into(),
"rust" | "rs" => "rust".into(),
"go" => "go".into(),
"c" | "h" => "c".into(),
"cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp".into(),
"java" => "java".into(),
"kotlin" | "kt" => "kotlin".into(),
"swift" => "swift".into(),
"html" => "html".into(),
"css" => "css".into(),
"sql" => "sql".into(),
other => other.into(),
}
}
fn parse_tui_color_from_syntect(c: syntect::highlighting::Color) -> TuiColor {
let rgb = TuiColor::Rgb(c.r, c.g, c.b);
let depth = crate::utils::color::detect_color_depth();
crate::utils::color::quantize_color(rgb, depth)
}
pub(crate) fn pick_syntect_theme_name_for_theme(theme: &Theme) -> &'static str {
if is_dark_background(&theme.background_color) {
"base16-ocean.dark"
} else {
"InspiredGitHub"
}
}
pub(crate) fn build_theme_signature(theme: &Theme, chosen_syntect: &str) -> String {
fn color_sig_opt(c: Option<TuiColor>) -> String {
match c {
Some(TuiColor::Rgb(r, g, b)) => format!("#{:02x}{:02x}{:02x}", r, g, b),
Some(other) => format!("{:?}", other),
None => "none".to_string(),
}
}
format!(
"{}|{}|{:?}",
chosen_syntect,
color_sig_opt(theme.md_codeblock_bg_color()),
theme.background_color
)
}
pub fn highlight_code_block(
lang_hint: &str,
code: &str,
theme: &Theme,
) -> Option<Vec<Line<'static>>> {
ensure_cache(64);
let lang_norm = normalize_lang_hint(lang_hint);
use std::sync::LazyLock;
static SYNTAX_SET: LazyLock<syntect::parsing::SyntaxSet> =
LazyLock::new(syntect::parsing::SyntaxSet::load_defaults_newlines);
static THEME_SET: LazyLock<syntect::highlighting::ThemeSet> =
LazyLock::new(syntect::highlighting::ThemeSet::load_defaults);
let ps = &*SYNTAX_SET;
let ts = &*THEME_SET;
let theme_name = pick_syntect_theme_name_for_theme(theme);
let fallback_names = [
"base16-ocean.light",
"Solarized (light)",
"base16-ocean.dark",
];
let mut syn_theme = ts.themes.get(theme_name);
if syn_theme.is_none() {
for name in &fallback_names {
if let Some(th) = ts.themes.get(*name) {
syn_theme = Some(th);
break;
}
}
}
let syn_theme = syn_theme?;
let theme_sig = build_theme_signature(theme, theme_name);
let key = (lang_norm.clone(), hash_code(&lang_norm, code, &theme_sig));
if let Some(lines) = get_cache().as_mut().and_then(|c| c.get(&key)) {
return Some(lines);
}
let syntax = ps
.find_syntax_by_token(&lang_norm)
.unwrap_or_else(|| ps.find_syntax_plain_text());
let mut h = syntect::easy::HighlightLines::new(syntax, syn_theme);
let bg = theme.md_codeblock_bg_color();
let mut out: Vec<Line<'static>> = Vec::new();
for line in syntect::util::LinesWithEndings::from(code) {
let ranges = h.highlight_line(line, ps).ok()?;
let mut spans: Vec<Span<'static>> = Vec::new();
for (style, text) in ranges {
let mut frag = text;
if let Some(stripped) = frag.strip_suffix('\n') {
frag = stripped;
}
let mut st =
ratatui::style::Style::default().fg(parse_tui_color_from_syntect(style.foreground));
if let Some(bgcol) = bg {
st = st.bg(bgcol);
}
spans.push(Span::styled(frag.to_string(), st));
}
if spans.is_empty() {
out.push(Line::from(""));
} else {
out.push(Line::from(spans));
}
}
{
let mut guard = get_cache();
if let Some(cache) = guard.as_mut() {
cache.put(key, out.clone());
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::style::Color;
#[test]
fn normalize_lang_hint_maps_common_aliases() {
assert_eq!(normalize_lang_hint("py"), "python");
assert_eq!(normalize_lang_hint("JS"), "javascript");
assert_eq!(normalize_lang_hint("TsX"), "typescript");
assert_eq!(normalize_lang_hint("yml"), "yaml");
assert_eq!(normalize_lang_hint("hpp"), "cpp");
assert_eq!(normalize_lang_hint("rs"), "rust");
}
#[test]
fn dark_background_heuristic_basic() {
assert!(is_dark_background(&Color::Black));
assert!(!is_dark_background(&Color::White));
assert!(is_dark_background(&Color::Rgb(10, 10, 10)));
assert!(!is_dark_background(&Color::Rgb(240, 240, 240)));
}
#[test]
fn theme_selection_matches_brightness() {
let mut dark = crate::ui::theme::Theme::dark_default();
dark.background_color = Color::Rgb(10, 10, 10);
let mut light = crate::ui::theme::Theme::light();
light.background_color = Color::Rgb(245, 245, 245);
assert_eq!(
pick_syntect_theme_name_for_theme(&dark),
"base16-ocean.dark"
);
assert_eq!(pick_syntect_theme_name_for_theme(&light), "InspiredGitHub");
}
#[test]
fn theme_signature_changes_with_theme() {
let mut dark = crate::ui::theme::Theme::dark_default();
dark.background_color = Color::Rgb(10, 10, 10);
let mut light = crate::ui::theme::Theme::light();
light.background_color = Color::Rgb(245, 245, 245);
dark.md_codeblock_bg = Some(Color::Rgb(30, 30, 30));
light.md_codeblock_bg = Some(Color::Rgb(230, 230, 230));
let s1 = build_theme_signature(&dark, pick_syntect_theme_name_for_theme(&dark));
let s2 = build_theme_signature(&light, pick_syntect_theme_name_for_theme(&light));
assert_ne!(s1, s2);
}
}