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
117 .trim_start()
118 .strip_prefix("```")
119 .unwrap_or("")
120 .to_string();
121 in_code_block = true;
122 }
123 } else if in_code_block {
124 code_lines.push(line);
125 } else {
126 result.push_str(line);
127 result.push('\n');
128 }
129 }
130
131 if in_code_block && !code_lines.is_empty() {
133 result.push_str(&format!("\x00{}\x00\n", blocks.len()));
134 blocks.push(CodeBlock {
135 code: code_lines.join("\n"),
136 lang: current_lang,
137 });
138 }
139
140 Self {
141 markdown: result,
142 blocks,
143 }
144 }
145
146 fn markdown(&self) -> &str {
148 &self.markdown
149 }
150
151 fn restore(&self, highlighter: &SyntaxHighlighter, mut rendered: String) -> String {
153 for (i, block) in self.blocks.iter().enumerate() {
154 let highlighted = highlighter.highlight(&block.code, &block.lang);
156 let code_block = format!("\n{}\n", highlighted);
157 rendered = rendered.replace(&format!("\x00{i}\x00"), &code_block);
158 }
159 rendered
160 }
161}
162
163pub struct MarkdownFormat {
165 skin: MadSkin,
166 highlighter: SyntaxHighlighter,
167}
168
169impl Default for MarkdownFormat {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175impl MarkdownFormat {
176 pub fn new() -> Self {
178 let mut skin = MadSkin::default();
179
180 skin.inline_code = CompoundStyle::new(Some(Color::Cyan), None, Default::default());
182
183 skin.code_block = LineStyle::new(
185 CompoundStyle::new(None, None, Default::default()),
186 Default::default(),
187 );
188
189 let mut h1_style = CompoundStyle::new(Some(Color::Magenta), None, Default::default());
191 h1_style.add_attr(Attribute::Bold);
192 skin.headers[0] = LineStyle::new(h1_style.clone(), Default::default());
193 skin.headers[1] = LineStyle::new(h1_style.clone(), Default::default());
194
195 let h3_style = CompoundStyle::new(Some(Color::Magenta), None, Default::default());
196 skin.headers[2] = LineStyle::new(h3_style, Default::default());
197
198 let mut bold_style = CompoundStyle::new(Some(Color::Magenta), None, Default::default());
200 bold_style.add_attr(Attribute::Bold);
201 skin.bold = bold_style;
202
203 skin.italic = CompoundStyle::with_attr(Attribute::Italic);
205
206 let mut strikethrough = CompoundStyle::with_attr(Attribute::CrossedOut);
208 strikethrough.add_attr(Attribute::Dim);
209 skin.strikeout = strikethrough;
210
211 Self {
212 skin,
213 highlighter: SyntaxHighlighter::default(),
214 }
215 }
216
217 pub fn render(&self, content: impl Into<String>) -> String {
219 let content = content.into();
220 let content = content.trim();
221
222 if content.is_empty() {
223 return String::new();
224 }
225
226 let parsed = CodeBlockParser::parse(content);
228
229 let rendered = self.skin.term_text(parsed.markdown()).to_string();
231
232 parsed
234 .restore(&self.highlighter, rendered)
235 .trim()
236 .to_string()
237 }
238}
239
240pub struct ResponseFormatter;
242
243impl ResponseFormatter {
244 pub fn print_response(text: &str) {
246 println!();
248 Self::print_header();
249 println!();
250
251 let formatter = MarkdownFormat::new();
253 let rendered = formatter.render(text);
254
255 for line in rendered.lines() {
257 println!(" {}", line);
258 }
259
260 println!();
262 Self::print_separator();
263 }
264
265 fn print_header() {
267 print!("{}{}╭─ 🤖 Syncable AI ", brand::PURPLE, brand::BOLD);
268 println!(
269 "{}─────────────────────────────────────────────────────╮{}",
270 brand::DIM,
271 brand::RESET
272 );
273 }
274
275 fn print_separator() {
277 println!(
278 "{}╰───────────────────────────────────────────────────────────────────╯{}",
279 brand::DIM,
280 brand::RESET
281 );
282 }
283}
284
285pub struct SimpleResponse;
287
288impl SimpleResponse {
289 pub fn print(text: &str) {
291 println!();
292 println!(
293 "{}{} Syncable AI:{}",
294 brand::PURPLE,
295 brand::BOLD,
296 brand::RESET
297 );
298 let formatter = MarkdownFormat::new();
299 println!("{}", formatter.render(text));
300 println!();
301 }
302}
303
304pub struct ToolProgress {
306 tools_executed: Vec<ToolExecution>,
307}
308
309#[derive(Clone)]
310struct ToolExecution {
311 name: String,
312 description: String,
313 status: ToolStatus,
314}
315
316#[derive(Clone, Copy)]
317enum ToolStatus {
318 Running,
319 Success,
320 Error,
321}
322
323impl ToolProgress {
324 pub fn new() -> Self {
325 Self {
326 tools_executed: Vec::new(),
327 }
328 }
329
330 pub fn tool_start(&mut self, name: &str, description: &str) {
332 self.tools_executed.push(ToolExecution {
333 name: name.to_string(),
334 description: description.to_string(),
335 status: ToolStatus::Running,
336 });
337 self.redraw();
338 }
339
340 pub fn tool_complete(&mut self, success: bool) {
342 if let Some(tool) = self.tools_executed.last_mut() {
343 tool.status = if success {
344 ToolStatus::Success
345 } else {
346 ToolStatus::Error
347 };
348 }
349 self.redraw();
350 }
351
352 fn redraw(&self) {
354 for tool in &self.tools_executed {
355 let (icon, color) = match tool.status {
356 ToolStatus::Running => ("", brand::YELLOW),
357 ToolStatus::Success => ("", brand::SUCCESS),
358 ToolStatus::Error => ("", "\x1b[38;5;196m"),
359 };
360 println!(
361 " {} {}{}{} {}{}{}",
362 icon,
363 color,
364 tool.name,
365 brand::RESET,
366 brand::DIM,
367 tool.description,
368 brand::RESET
369 );
370 }
371 }
372
373 pub fn print_summary(&self) {
375 if !self.tools_executed.is_empty() {
376 let success_count = self
377 .tools_executed
378 .iter()
379 .filter(|t| matches!(t.status, ToolStatus::Success))
380 .count();
381 println!(
382 "\n{} {} tools executed successfully{}",
383 brand::DIM,
384 success_count,
385 brand::RESET
386 );
387 }
388 }
389}
390
391impl Default for ToolProgress {
392 fn default() -> Self {
393 Self::new()
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_markdown_render_empty() {
403 let formatter = MarkdownFormat::new();
404 assert!(formatter.render("").is_empty());
405 }
406
407 #[test]
408 fn test_markdown_render_simple() {
409 let formatter = MarkdownFormat::new();
410 let result = formatter.render("Hello world");
411 assert!(!result.is_empty());
412 }
413
414 #[test]
415 fn test_code_block_extraction() {
416 let parsed = CodeBlockParser::parse("Hello\n```rust\nfn main() {}\n```\nWorld");
417 assert_eq!(parsed.blocks.len(), 1);
418 assert_eq!(parsed.blocks[0].lang, "rust");
419 assert_eq!(parsed.blocks[0].code, "fn main() {}");
420 }
421
422 #[test]
423 fn test_syntax_highlighter() {
424 let hl = SyntaxHighlighter::default();
425 let result = hl.highlight("fn main() {}", "rust");
426 assert!(result.contains("\x1b["));
428 }
429}