horus-cli 0.6.11

The terminal client for a Horus gateway
Documentation
use std::path::Path;
use std::sync::OnceLock;

use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::text::Span;
use syntect::easy::HighlightLines;
use syntect::highlighting::FontStyle;
use syntect::highlighting::Theme;
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
use two_face::theme::EmbeddedThemeName;

const MAX_LINE_BYTES: usize = 4 * 1024;

fn syntaxes() -> &'static SyntaxSet {
    static SYNTAXES: OnceLock<SyntaxSet> = OnceLock::new();
    SYNTAXES.get_or_init(two_face::syntax::extra_newlines)
}

fn theme() -> &'static Theme {
    static THEME: OnceLock<Theme> = OnceLock::new();
    THEME.get_or_init(|| {
        two_face::theme::extra()
            .get(EmbeddedThemeName::CatppuccinMocha)
            .clone()
    })
}

pub(super) fn lines(code: &str, path: &str) -> Option<Vec<Vec<Span<'static>>>> {
    if code.lines().any(|line| line.len() > MAX_LINE_BYTES) {
        return None;
    }
    let extension = Path::new(path).extension()?.to_str()?;
    let syntax = syntaxes().find_syntax_by_extension(extension)?;
    let mut highlighter = HighlightLines::new(syntax, theme());
    LinesWithEndings::from(code)
        .map(|line| {
            highlighter
                .highlight_line(line, syntaxes())
                .ok()
                .map(|ranges| {
                    let mut spans = ranges
                        .into_iter()
                        .filter_map(|(style, text)| {
                            let text = text.trim_end_matches(['\n', '\r']);
                            (!text.is_empty()).then(|| {
                                let mut rendered = Style::default().fg(Color::Rgb(
                                    style.foreground.r,
                                    style.foreground.g,
                                    style.foreground.b,
                                ));
                                if style.font_style.contains(FontStyle::BOLD) {
                                    rendered = rendered.add_modifier(Modifier::BOLD);
                                }
                                Span::styled(text.to_string(), rendered)
                            })
                        })
                        .collect::<Vec<_>>();
                    if spans.is_empty() {
                        spans.push(Span::raw(String::new()));
                    }
                    spans
                })
        })
        .collect()
}