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