use {
ratatui::text::{Line, Span},
std::sync::OnceLock,
syntect::{
easy::HighlightLines,
highlighting::{Theme, ThemeSet},
parsing::{syntax_definition::SyntaxDefinition, SyntaxReference, SyntaxSet},
},
terminal_colorsaurus::{theme_mode, QueryOptions, ThemeMode},
};
const SBPF_SYNTAX_YAML: &str = include_str!("sbpf.sublime-syntax");
static DETECTED_MODE: OnceLock<ThemeMode> = OnceLock::new();
pub fn detect_theme_mode_once() {
let _ = DETECTED_MODE.set(theme_mode(QueryOptions::default()).unwrap_or(ThemeMode::Dark));
}
struct Ctx {
syntaxes: SyntaxSet,
theme: Theme,
rust_syntax: SyntaxReference,
asm_syntax: SyntaxReference,
}
fn ctx() -> &'static Ctx {
static C: OnceLock<Ctx> = OnceLock::new();
C.get_or_init(|| {
let mut builder = two_face::syntax::extra_newlines().into_builder();
let sbpf_def =
SyntaxDefinition::load_from_str(SBPF_SYNTAX_YAML, true, Some("SBPF Assembly"))
.expect("compile bundled sbpf.sublime-syntax");
builder.add(sbpf_def);
let syntaxes = builder.build();
let themes = ThemeSet::load_defaults();
let (dark_name, light_name) = ("base16-eighties.dark", "Solarized (light)");
let theme_name = match DETECTED_MODE.get() {
Some(ThemeMode::Light) => light_name,
_ => dark_name,
};
let theme = themes
.themes
.get(theme_name)
.cloned()
.or_else(|| themes.themes.get(dark_name).cloned())
.unwrap_or_else(|| {
themes
.themes
.values()
.next()
.cloned()
.expect("default theme")
});
let rust_syntax = syntaxes
.find_syntax_by_token("rs")
.or_else(|| syntaxes.find_syntax_by_name("Rust"))
.unwrap_or_else(|| syntaxes.find_syntax_plain_text())
.clone();
let asm_syntax = syntaxes
.find_syntax_by_name("SBPF Assembly")
.or_else(|| syntaxes.find_syntax_by_name("Assembly x86 (NASM)"))
.or_else(|| syntaxes.find_syntax_by_token("asm"))
.unwrap_or_else(|| syntaxes.find_syntax_plain_text())
.clone();
Ctx {
syntaxes,
theme,
rust_syntax,
asm_syntax,
}
})
}
pub fn highlight_rust(line: &str) -> Line<'static> {
highlight_with(&ctx().rust_syntax, line)
}
pub fn highlight_asm(line: &str) -> Line<'static> {
highlight_with(&ctx().asm_syntax, line)
}
fn syntect_style_to_ratatui(style: syntect::highlighting::Style) -> ratatui::style::Style {
fn translate_colour(
syntect_color: syntect::highlighting::Color,
) -> Option<ratatui::style::Color> {
match syntect_color {
syntect::highlighting::Color { r, g, b, a } if a > 0 => {
Some(ratatui::style::Color::Rgb(r, g, b))
}
_ => None,
}
}
fn translate_font_style(
syntect_font_style: syntect::highlighting::FontStyle,
) -> ratatui::style::Modifier {
use {ratatui::style::Modifier, syntect::highlighting::FontStyle};
match syntect_font_style {
x if x == FontStyle::empty() => Modifier::empty(),
x if x == FontStyle::BOLD => Modifier::BOLD,
x if x == FontStyle::ITALIC => Modifier::ITALIC,
x if x == FontStyle::UNDERLINE => Modifier::UNDERLINED,
x if x == FontStyle::BOLD | FontStyle::ITALIC => Modifier::BOLD | Modifier::ITALIC,
x if x == FontStyle::BOLD | FontStyle::UNDERLINE => {
Modifier::BOLD | Modifier::UNDERLINED
}
x if x == FontStyle::ITALIC | FontStyle::UNDERLINE => {
Modifier::ITALIC | Modifier::UNDERLINED
}
x if x == FontStyle::BOLD | FontStyle::ITALIC | FontStyle::UNDERLINE => {
Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED
}
_ => Modifier::empty(),
}
}
ratatui::style::Style {
fg: translate_colour(style.foreground),
bg: None,
underline_color: translate_colour(style.foreground),
add_modifier: translate_font_style(style.font_style),
sub_modifier: ratatui::style::Modifier::empty(),
}
}
fn highlight_with(syntax: &SyntaxReference, line: &str) -> Line<'static> {
let c = ctx();
let mut hl = HighlightLines::new(syntax, &c.theme);
let owned_line = if line.ends_with('\n') {
line.to_owned()
} else {
format!("{line}\n")
};
let regions = match hl.highlight_line(&owned_line, &c.syntaxes) {
Ok(r) => r,
Err(_) => return Line::from(Span::raw(line.to_owned())),
};
let spans: Vec<Span<'static>> = regions
.into_iter()
.filter_map(|(style, text)| {
let trimmed = text.trim_end_matches('\n');
if trimmed.is_empty() {
return None;
}
let style = syntect_style_to_ratatui(style);
Some(Span::styled(trimmed.to_owned(), style))
})
.collect();
if spans.is_empty() {
Line::from(Span::raw(line.to_owned()))
} else {
Line::from(spans)
}
}