1pub mod brand {
10 pub const PURPLE: &str = "\x1b[38;5;141m";
12 pub const MAGENTA: &str = "\x1b[38;5;207m";
14 pub const LIGHT_PURPLE: &str = "\x1b[38;5;183m";
16 pub const CYAN: &str = "\x1b[38;5;51m";
18 pub const TEXT: &str = "\x1b[38;5;252m";
20 pub const DIM: &str = "\x1b[38;5;245m";
22 pub const SUCCESS: &str = "\x1b[38;5;114m";
24 pub const YELLOW: &str = "\x1b[38;5;221m";
26 pub const RESET: &str = "\x1b[0m";
28 pub const BOLD: &str = "\x1b[1m";
30 pub const ITALIC: &str = "\x1b[3m";
32}
33
34pub struct ResponseFormatter;
36
37impl ResponseFormatter {
38 pub fn print_response(text: &str) {
40 println!();
42 Self::print_header();
43 println!();
44
45 Self::format_markdown(text);
47
48 println!();
50 Self::print_separator();
51 }
52
53 fn print_header() {
55 print!(
56 "{}{}╭─ {} Syncable AI {}{}",
57 brand::PURPLE,
58 brand::BOLD,
59 "🤖",
60 brand::RESET,
61 brand::DIM
62 );
63 println!("─────────────────────────────────────────────────────╮{}", brand::RESET);
64 }
65
66 fn print_separator() {
68 println!(
69 "{}╰───────────────────────────────────────────────────────────────────╯{}",
70 brand::DIM,
71 brand::RESET
72 );
73 }
74
75 fn format_markdown(text: &str) {
77 let mut in_code_block = false;
78 let mut code_lang = String::new();
79 let mut list_depth = 0;
80
81 for line in text.lines() {
82 let trimmed = line.trim();
83
84 if trimmed.starts_with("```") {
86 if in_code_block {
87 println!(
89 "{} └────────────────────────────────────────────────────────────┘{}",
90 brand::DIM,
91 brand::RESET
92 );
93 in_code_block = false;
94 code_lang.clear();
95 } else {
96 code_lang = trimmed.strip_prefix("```").unwrap_or("").to_string();
98 let lang_display = if code_lang.is_empty() {
99 "code".to_string()
100 } else {
101 code_lang.clone()
102 };
103 println!(
104 "{} ┌─ {}{}{} ──────────────────────────────────────────────────────┐{}",
105 brand::DIM,
106 brand::CYAN,
107 lang_display,
108 brand::DIM,
109 brand::RESET
110 );
111 in_code_block = true;
112 }
113 continue;
114 }
115
116 if in_code_block {
117 println!("{} │ {}{}{} │", brand::DIM, brand::CYAN, line, brand::RESET);
119 continue;
120 }
121
122 if let Some(header) = Self::parse_header(trimmed) {
124 Self::print_formatted_header(header.0, header.1);
125 continue;
126 }
127
128 if let Some(bullet) = Self::parse_bullet(trimmed) {
130 Self::print_bullet(bullet.0, bullet.1, &mut list_depth);
131 continue;
132 }
133
134 Self::print_formatted_text(line);
136 }
137 }
138
139 fn parse_header(line: &str) -> Option<(usize, &str)> {
141 if line.starts_with("### ") {
142 Some((3, line.strip_prefix("### ").unwrap()))
143 } else if line.starts_with("## ") {
144 Some((2, line.strip_prefix("## ").unwrap()))
145 } else if line.starts_with("# ") {
146 Some((1, line.strip_prefix("# ").unwrap()))
147 } else {
148 None
149 }
150 }
151
152 fn print_formatted_header(level: usize, content: &str) {
154 match level {
155 1 => {
156 println!();
157 println!(
158 "{}{} ▓▓ {} {}",
159 brand::PURPLE,
160 brand::BOLD,
161 content.to_uppercase(),
162 brand::RESET
163 );
164 println!(
165 "{} ════════════════════════════════════════════════════════{}",
166 brand::PURPLE,
167 brand::RESET
168 );
169 }
170 2 => {
171 println!();
172 println!(
173 "{}{} ▸ {} {}",
174 brand::LIGHT_PURPLE,
175 brand::BOLD,
176 content,
177 brand::RESET
178 );
179 println!(
180 "{} ────────────────────────────────────────────────────────{}",
181 brand::DIM,
182 brand::RESET
183 );
184 }
185 _ => {
186 println!();
187 println!(
188 "{}{} ◦ {} {}",
189 brand::MAGENTA,
190 brand::BOLD,
191 content,
192 brand::RESET
193 );
194 }
195 }
196 }
197
198 fn parse_bullet(line: &str) -> Option<(usize, &str)> {
200 let trimmed = line.trim_start();
201 let indent = line.len() - trimmed.len();
202 let depth = indent / 2;
203
204 if trimmed.starts_with("- ") {
205 Some((depth, trimmed.strip_prefix("- ").unwrap()))
206 } else if trimmed.starts_with("* ") {
207 Some((depth, trimmed.strip_prefix("* ").unwrap()))
208 } else if trimmed.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false)
209 && trimmed.chars().nth(1) == Some('.')
210 {
211 Some((depth, trimmed.split_once(". ").map(|(_, rest)| rest).unwrap_or(trimmed)))
212 } else {
213 None
214 }
215 }
216
217 fn print_bullet(depth: usize, content: &str, _list_depth: &mut usize) {
219 let indent = " ".repeat(depth + 1);
220 let bullet_char = match depth {
221 0 => "●",
222 1 => "○",
223 _ => "◦",
224 };
225 let bullet_color = match depth {
226 0 => brand::PURPLE,
227 1 => brand::MAGENTA,
228 _ => brand::DIM,
229 };
230
231 let formatted = Self::format_inline(content);
233 println!("{}{}{} {}{}", indent, bullet_color, bullet_char, brand::TEXT, formatted);
234 print!("{}", brand::RESET);
235 }
236
237 fn print_formatted_text(line: &str) {
239 if line.trim().is_empty() {
240 println!();
241 return;
242 }
243
244 let formatted = Self::format_inline(line);
245 println!("{} {}{}", brand::TEXT, formatted, brand::RESET);
246 }
247
248 fn format_inline(text: &str) -> String {
250 let mut result = String::new();
251 let chars: Vec<char> = text.chars().collect();
252 let mut i = 0;
253
254 while i < chars.len() {
255 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
257 if let Some(end) = Self::find_closing(&chars, i + 2, "**") {
258 let bold_text: String = chars[i + 2..end].iter().collect();
259 result.push_str(brand::BOLD);
260 result.push_str(brand::LIGHT_PURPLE);
261 result.push_str(&bold_text);
262 result.push_str(brand::RESET);
263 result.push_str(brand::TEXT);
264 i = end + 2;
265 continue;
266 }
267 }
268
269 if chars[i] == '`' && (i + 1 >= chars.len() || chars[i + 1] != '`') {
271 if let Some(end) = chars[i + 1..].iter().position(|&c| c == '`') {
272 let code_text: String = chars[i + 1..i + 1 + end].iter().collect();
273 result.push_str(brand::CYAN);
274 result.push_str("`");
275 result.push_str(&code_text);
276 result.push_str("`");
277 result.push_str(brand::RESET);
278 result.push_str(brand::TEXT);
279 i = i + 2 + end;
280 continue;
281 }
282 }
283
284 result.push(chars[i]);
285 i += 1;
286 }
287
288 result
289 }
290
291 fn find_closing(chars: &[char], start: usize, marker: &str) -> Option<usize> {
293 let marker_chars: Vec<char> = marker.chars().collect();
294 let marker_len = marker_chars.len();
295
296 for i in start..=chars.len() - marker_len {
297 let matches = (0..marker_len).all(|j| chars[i + j] == marker_chars[j]);
298 if matches {
299 return Some(i);
300 }
301 }
302 None
303 }
304}
305
306pub struct SimpleResponse;
308
309impl SimpleResponse {
310 pub fn print(text: &str) {
312 println!();
313 println!("{}{}🤖 Syncable AI:{}", brand::PURPLE, brand::BOLD, brand::RESET);
314 println!("{}{}{}", brand::TEXT, text, brand::RESET);
315 println!();
316 }
317}
318
319pub struct ToolProgress {
321 tools_executed: Vec<ToolExecution>,
322}
323
324#[derive(Clone)]
325struct ToolExecution {
326 name: String,
327 description: String,
328 status: ToolStatus,
329}
330
331#[derive(Clone, Copy)]
332enum ToolStatus {
333 Running,
334 Success,
335 Error,
336}
337
338impl ToolProgress {
339 pub fn new() -> Self {
340 Self {
341 tools_executed: Vec::new(),
342 }
343 }
344
345 pub fn tool_start(&mut self, name: &str, description: &str) {
347 self.tools_executed.push(ToolExecution {
348 name: name.to_string(),
349 description: description.to_string(),
350 status: ToolStatus::Running,
351 });
352 self.redraw();
353 }
354
355 pub fn tool_complete(&mut self, success: bool) {
357 if let Some(tool) = self.tools_executed.last_mut() {
358 tool.status = if success { ToolStatus::Success } else { ToolStatus::Error };
359 }
360 self.redraw();
361 }
362
363 fn redraw(&self) {
365 for tool in &self.tools_executed {
367 let (icon, color) = match tool.status {
368 ToolStatus::Running => ("◐", brand::YELLOW),
369 ToolStatus::Success => ("✓", brand::SUCCESS),
370 ToolStatus::Error => ("✗", "\x1b[38;5;196m"),
371 };
372 println!(
373 " {} {}{}{} {}{}{}",
374 icon,
375 color,
376 tool.name,
377 brand::RESET,
378 brand::DIM,
379 tool.description,
380 brand::RESET
381 );
382 }
383 }
384
385 pub fn print_summary(&self) {
387 if !self.tools_executed.is_empty() {
388 let success_count = self.tools_executed
389 .iter()
390 .filter(|t| matches!(t.status, ToolStatus::Success))
391 .count();
392 println!(
393 "\n{} {} tools executed successfully{}",
394 brand::DIM,
395 success_count,
396 brand::RESET
397 );
398 }
399 }
400}
401
402impl Default for ToolProgress {
403 fn default() -> Self {
404 Self::new()
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn test_parse_header() {
414 assert_eq!(ResponseFormatter::parse_header("# Hello"), Some((1, "Hello")));
415 assert_eq!(ResponseFormatter::parse_header("## World"), Some((2, "World")));
416 assert_eq!(ResponseFormatter::parse_header("### Test"), Some((3, "Test")));
417 assert_eq!(ResponseFormatter::parse_header("Not a header"), None);
418 }
419
420 #[test]
421 fn test_format_inline_bold() {
422 let result = ResponseFormatter::format_inline("This is **bold** text");
423 assert!(result.contains("bold"));
424 }
425}