1use std::sync::Arc;
7use syntect::easy::HighlightLines;
8use syntect::highlighting::ThemeSet;
9use syntect::parsing::SyntaxSet;
10use syntect::util::as_24_bit_terminal_escaped;
11use termimad::crossterm::style::{Attribute, Color};
12use termimad::{CompoundStyle, LineStyle, MadSkin};
13
14pub mod brand {
16 pub const PURPLE: &str = "\x1b[38;5;141m";
18 pub const MAGENTA: &str = "\x1b[38;5;207m";
20 pub const LIGHT_PURPLE: &str = "\x1b[38;5;183m";
22 pub const CYAN: &str = "\x1b[38;5;51m";
24 pub const TEXT: &str = "\x1b[38;5;252m";
26 pub const DIM: &str = "\x1b[38;5;245m";
28 pub const SUCCESS: &str = "\x1b[38;5;114m";
30 pub const YELLOW: &str = "\x1b[38;5;221m";
32 pub const PEACH: &str = "\x1b[38;5;216m";
34 pub const LIGHT_PEACH: &str = "\x1b[38;5;223m";
36 pub const CORAL: &str = "\x1b[38;5;209m";
38 pub const RESET: &str = "\x1b[0m";
40 pub const BOLD: &str = "\x1b[1m";
42 pub const ITALIC: &str = "\x1b[3m";
44}
45
46#[derive(Clone)]
48pub struct SyntaxHighlighter {
49 syntax_set: Arc<SyntaxSet>,
50 theme_set: Arc<ThemeSet>,
51}
52
53impl Default for SyntaxHighlighter {
54 fn default() -> Self {
55 Self {
56 syntax_set: Arc::new(SyntaxSet::load_defaults_newlines()),
57 theme_set: Arc::new(ThemeSet::load_defaults()),
58 }
59 }
60}
61
62impl SyntaxHighlighter {
63 pub fn highlight(&self, code: &str, lang: &str) -> String {
65 let syntax = self
66 .syntax_set
67 .find_syntax_by_token(lang)
68 .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
69 let theme = &self.theme_set.themes["base16-ocean.dark"];
70 let mut hl = HighlightLines::new(syntax, theme);
71
72 code.lines()
73 .filter_map(|line| hl.highlight_line(line, &self.syntax_set).ok())
74 .map(|ranges| format!("{}\x1b[0m", as_24_bit_terminal_escaped(&ranges, false)))
75 .collect::<Vec<_>>()
76 .join("\n")
77 }
78}
79
80#[derive(Clone, Debug)]
82struct CodeBlock {
83 code: String,
84 lang: String,
85}
86
87struct CodeBlockParser {
89 markdown: String,
90 blocks: Vec<CodeBlock>,
91}
92
93impl CodeBlockParser {
94 fn parse(content: &str) -> Self {
96 let mut blocks = Vec::new();
97 let mut result = String::new();
98 let mut in_code_block = false;
99 let mut code_lines: Vec<&str> = Vec::new();
100 let mut current_lang = String::new();
101
102 for line in content.lines() {
103 if line.trim_start().starts_with("```") {
104 if in_code_block {
105 result.push_str(&format!("\x00{}\x00\n", blocks.len()));
107 blocks.push(CodeBlock {
108 code: code_lines.join("\n"),
109 lang: current_lang.clone(),
110 });
111 code_lines.clear();
112 current_lang.clear();
113 in_code_block = false;
114 } else {
115 current_lang = line.trim_start().strip_prefix("```").unwrap_or("").to_string();
117 in_code_block = true;
118 }
119 } else if in_code_block {
120 code_lines.push(line);
121 } else {
122 result.push_str(line);
123 result.push('\n');
124 }
125 }
126
127 if in_code_block && !code_lines.is_empty() {
129 result.push_str(&format!("\x00{}\x00\n", blocks.len()));
130 blocks.push(CodeBlock {
131 code: code_lines.join("\n"),
132 lang: current_lang,
133 });
134 }
135
136 Self { markdown: result, blocks }
137 }
138
139 fn markdown(&self) -> &str {
141 &self.markdown
142 }
143
144 fn restore(&self, highlighter: &SyntaxHighlighter, mut rendered: String) -> String {
146 for (i, block) in self.blocks.iter().enumerate() {
147 let highlighted = highlighter.highlight(&block.code, &block.lang);
149 let code_block = format!("\n{}\n", highlighted);
150 rendered = rendered.replace(&format!("\x00{i}\x00"), &code_block);
151 }
152 rendered
153 }
154}
155
156pub struct MarkdownFormat {
158 skin: MadSkin,
159 highlighter: SyntaxHighlighter,
160}
161
162impl Default for MarkdownFormat {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168impl MarkdownFormat {
169 pub fn new() -> Self {
171 let mut skin = MadSkin::default();
172
173 skin.inline_code = CompoundStyle::new(Some(Color::Cyan), None, Default::default());
175
176 skin.code_block = LineStyle::new(
178 CompoundStyle::new(None, None, Default::default()),
179 Default::default(),
180 );
181
182 let mut h1_style = CompoundStyle::new(Some(Color::Magenta), None, Default::default());
184 h1_style.add_attr(Attribute::Bold);
185 skin.headers[0] = LineStyle::new(h1_style.clone(), Default::default());
186 skin.headers[1] = LineStyle::new(h1_style.clone(), Default::default());
187
188 let h3_style = CompoundStyle::new(Some(Color::Magenta), None, Default::default());
189 skin.headers[2] = LineStyle::new(h3_style, Default::default());
190
191 let mut bold_style = CompoundStyle::new(Some(Color::Magenta), None, Default::default());
193 bold_style.add_attr(Attribute::Bold);
194 skin.bold = bold_style;
195
196 skin.italic = CompoundStyle::with_attr(Attribute::Italic);
198
199 let mut strikethrough = CompoundStyle::with_attr(Attribute::CrossedOut);
201 strikethrough.add_attr(Attribute::Dim);
202 skin.strikeout = strikethrough;
203
204 Self {
205 skin,
206 highlighter: SyntaxHighlighter::default(),
207 }
208 }
209
210 pub fn render(&self, content: impl Into<String>) -> String {
212 let content = content.into();
213 let content = content.trim();
214
215 if content.is_empty() {
216 return String::new();
217 }
218
219 let parsed = CodeBlockParser::parse(content);
221
222 let rendered = self.skin.term_text(parsed.markdown()).to_string();
224
225 parsed.restore(&self.highlighter, rendered).trim().to_string()
227 }
228}
229
230pub struct ResponseFormatter;
232
233impl ResponseFormatter {
234 pub fn print_response(text: &str) {
236 println!();
238 Self::print_header();
239 println!();
240
241 let formatter = MarkdownFormat::new();
243 let rendered = formatter.render(text);
244
245 for line in rendered.lines() {
247 println!(" {}", line);
248 }
249
250 println!();
252 Self::print_separator();
253 }
254
255 fn print_header() {
257 print!(
258 "{}{}╭─ 🤖 Syncable AI ",
259 brand::PURPLE,
260 brand::BOLD
261 );
262 println!(
263 "{}─────────────────────────────────────────────────────╮{}",
264 brand::DIM,
265 brand::RESET
266 );
267 }
268
269 fn print_separator() {
271 println!(
272 "{}╰───────────────────────────────────────────────────────────────────╯{}",
273 brand::DIM,
274 brand::RESET
275 );
276 }
277}
278
279pub struct SimpleResponse;
281
282impl SimpleResponse {
283 pub fn print(text: &str) {
285 println!();
286 println!("{}{} Syncable AI:{}", brand::PURPLE, brand::BOLD, brand::RESET);
287 let formatter = MarkdownFormat::new();
288 println!("{}", formatter.render(text));
289 println!();
290 }
291}
292
293pub struct ToolProgress {
295 tools_executed: Vec<ToolExecution>,
296}
297
298#[derive(Clone)]
299struct ToolExecution {
300 name: String,
301 description: String,
302 status: ToolStatus,
303}
304
305#[derive(Clone, Copy)]
306enum ToolStatus {
307 Running,
308 Success,
309 Error,
310}
311
312impl ToolProgress {
313 pub fn new() -> Self {
314 Self {
315 tools_executed: Vec::new(),
316 }
317 }
318
319 pub fn tool_start(&mut self, name: &str, description: &str) {
321 self.tools_executed.push(ToolExecution {
322 name: name.to_string(),
323 description: description.to_string(),
324 status: ToolStatus::Running,
325 });
326 self.redraw();
327 }
328
329 pub fn tool_complete(&mut self, success: bool) {
331 if let Some(tool) = self.tools_executed.last_mut() {
332 tool.status = if success { ToolStatus::Success } else { ToolStatus::Error };
333 }
334 self.redraw();
335 }
336
337 fn redraw(&self) {
339 for tool in &self.tools_executed {
340 let (icon, color) = match tool.status {
341 ToolStatus::Running => ("", brand::YELLOW),
342 ToolStatus::Success => ("", brand::SUCCESS),
343 ToolStatus::Error => ("", "\x1b[38;5;196m"),
344 };
345 println!(
346 " {} {}{}{} {}{}{}",
347 icon,
348 color,
349 tool.name,
350 brand::RESET,
351 brand::DIM,
352 tool.description,
353 brand::RESET
354 );
355 }
356 }
357
358 pub fn print_summary(&self) {
360 if !self.tools_executed.is_empty() {
361 let success_count = self.tools_executed
362 .iter()
363 .filter(|t| matches!(t.status, ToolStatus::Success))
364 .count();
365 println!(
366 "\n{} {} tools executed successfully{}",
367 brand::DIM,
368 success_count,
369 brand::RESET
370 );
371 }
372 }
373}
374
375impl Default for ToolProgress {
376 fn default() -> Self {
377 Self::new()
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_markdown_render_empty() {
387 let formatter = MarkdownFormat::new();
388 assert!(formatter.render("").is_empty());
389 }
390
391 #[test]
392 fn test_markdown_render_simple() {
393 let formatter = MarkdownFormat::new();
394 let result = formatter.render("Hello world");
395 assert!(!result.is_empty());
396 }
397
398 #[test]
399 fn test_code_block_extraction() {
400 let parsed = CodeBlockParser::parse("Hello\n```rust\nfn main() {}\n```\nWorld");
401 assert_eq!(parsed.blocks.len(), 1);
402 assert_eq!(parsed.blocks[0].lang, "rust");
403 assert_eq!(parsed.blocks[0].code, "fn main() {}");
404 }
405
406 #[test]
407 fn test_syntax_highlighter() {
408 let hl = SyntaxHighlighter::default();
409 let result = hl.highlight("fn main() {}", "rust");
410 assert!(result.contains("\x1b["));
412 }
413}