diff --git a/rust/crates/rusty-claude-cli/Cargo.toml b/rust/crates/rusty-claude-cli/Cargo.toml index d5b9aeae..1c95cad3 100644 --- a/rust/crates/rusty-claude-cli/Cargo.toml +++ b/rust/crates/rusty-claude-cli/Cargo.toml @@ -14,6 +14,7 @@ api = { path = "../api" } commands = { path = "../commands" } crossterm = "0.28" pulldown-cmark = "0.13" +once_cell = "1" ratatui = "0.29" rustyline = "15" tui-textarea = "0.7" diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index ff7605dd..0dbe8edd 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -18,6 +18,7 @@ mod init; mod input; mod render; mod setup_wizard; +mod markdown; mod tui; mod tui_error; mod tui_update; diff --git a/rust/crates/rusty-claude-cli/src/markdown.rs b/rust/crates/rusty-claude-cli/src/markdown.rs new file mode 100644 index 00000000..74d4b8b5 --- /dev/null +++ b/rust/crates/rusty-claude-cli/src/markdown.rs @@ -0,0 +1,441 @@ +//! Markdown → ratatui `Line` renderer with syntect syntax highlighting. +//! +//! Syntect assets (`SyntaxSet`, `ThemeSet`) are loaded once via `once_cell::Lazy`. + +use once_cell::sync::Lazy; +use pulldown_cmark::{Event, Parser, Tag, TagEnd}; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::text::{Line, Span}; +use syntect::highlighting::ThemeSet; +use syntect::parsing::SyntaxSet; + +/// Loaded once on first access (~10-50ms), cached for process lifetime. +pub static SYNTAX_SET: Lazy = Lazy::new(SyntaxSet::load_defaults_newlines); +pub static THEME_SET: Lazy = Lazy::new(ThemeSet::load_defaults); + +#[derive(Clone)] +pub struct MarkdownRenderer { + code_theme_name: String, +} + +impl MarkdownRenderer { + pub fn new() -> Self { + Self { + code_theme_name: "base16-ocean.dark".to_string(), + } + } + + pub fn set_code_theme(&mut self, name: &str) { + self.code_theme_name = name.to_string(); + } + + /// Render a markdown string into ratatui Lines at the given terminal width. + pub fn render(&self, markdown: &str, width: u16) -> Vec> { + let parser = Parser::new(markdown); + let events: Vec = parser.collect(); + self.render_events(&events, width) + } + + fn render_events(&self, events: &[Event], width: u16) -> Vec> { + let mut lines: Vec> = Vec::new(); + let mut current_spans: Vec> = Vec::new(); + let mut style_stack: Vec