1use crate::client::{CodeExample, DocSection, Documentation, SearchResult};
2use crate::config::Config;
3use anyhow::Result;
4use colored::*;
5use indicatif::{ProgressBar, ProgressStyle};
6use std::io;
7
8pub struct Renderer {
9 quiet_mode: bool,
10 terminal_width: usize,
11 config: Option<Config>,
12}
13
14impl Renderer {
15 pub fn new(quiet: bool) -> Self {
16 let terminal_width = termsize::get().map(|size| size.cols as usize).unwrap_or(80);
17 let config = Config::load().ok();
18
19 Self {
20 quiet_mode: quiet,
21 terminal_width,
22 config,
23 }
24 }
25
26 pub fn render_search_results(&self, results: &[SearchResult]) -> io::Result<()> {
27 self.render_search_results_with_library(results, None, None)
28 }
29
30 pub fn render_search_results_with_library(
31 &self,
32 results: &[SearchResult],
33 library_info: Option<(&str, &str)>,
34 limit: Option<usize>,
35 ) -> io::Result<()> {
36 if self.quiet_mode {
37 println!("{}", serde_json::to_string_pretty(results)?);
39 return Ok(());
40 }
41
42 if results.is_empty() {
43 println!("{}", "No results found.".yellow());
44 return Ok(());
45 }
46
47 println!(
48 "{} {} found:",
49 results.len().to_string().cyan().bold(),
50 if results.len() == 1 {
51 "result"
52 } else {
53 "results"
54 }
55 );
56
57 if let Some((library_title, library_id)) = library_info {
58 println!(
59 "š Using library: {} ({})\n",
60 library_title.bright_blue(),
61 library_id.dimmed()
62 );
63 } else {
64 println!();
65 }
66
67 let display_limit = limit.unwrap_or(10);
69 let total_results = results.len();
70 let results_to_show = if display_limit == 0 {
71 results.iter().take(total_results)
72 } else {
73 results.iter().take(display_limit)
74 };
75
76 for (idx, result) in results_to_show.enumerate() {
77 self.render_search_result(idx + 1, result)?;
78 }
79
80 if display_limit > 0 && total_results > display_limit {
81 println!(
82 "\n{}",
83 format!(
84 "... and {} more results. Use --limit 0 to show all, or --save-all to export.",
85 total_results - display_limit
86 )
87 .yellow()
88 );
89 }
90
91 println!(
92 "\n{}",
93 "Tip: Use 'manx get <id>' to expand a result.".dimmed()
94 );
95 Ok(())
96 }
97
98 fn render_search_result(&self, num: usize, result: &SearchResult) -> io::Result<()> {
99 let separator = "ā".repeat(self.terminal_width.min(60));
100
101 println!(
102 "{} {} {}",
103 format!("[{}]", num).cyan().bold(),
104 result.title.white().bold(),
105 format!("({})", result.library).dimmed()
106 );
107
108 println!(" {}: {}", "ID".dimmed(), result.id.yellow());
109
110 if let Some(url) = &result.url {
111 println!(" {}: {}", "URL".dimmed(), url.blue().underline());
112 }
113
114 println!();
115
116 if result.excerpt.contains("CODE SNIPPETS") {
118 self.render_context7_excerpt(&result.excerpt)?;
119 } else {
120 let max_width = self.terminal_width.max(100) - 4;
122 println!(" {}", self.truncate_text(&result.excerpt, max_width));
123 }
124
125 println!("{}\n", separator.dimmed());
126 Ok(())
127 }
128
129 fn render_context7_excerpt(&self, content: &str) -> io::Result<()> {
130 let lines: Vec<&str> = content.lines().collect();
132 let mut found_title = false;
133
134 for line in lines.iter().take(10) {
135 if line.starts_with("TITLE: ") && !found_title {
137 let title = &line[7..];
138 println!(" {}", title.white().bold());
139 found_title = true;
140 } else if line.starts_with("DESCRIPTION: ") && found_title {
141 let desc = &line[13..];
142 let truncated = self.truncate_text(desc, self.terminal_width - 4);
143 println!(" {}", truncated.dimmed());
144 break;
145 }
146 }
147
148 if !found_title {
149 println!(" {}", "Documentation snippets available...".dimmed());
150 }
151
152 Ok(())
153 }
154
155 pub fn render_documentation(&self, doc: &Documentation) -> io::Result<()> {
156 if self.quiet_mode {
157 println!("{}", serde_json::to_string_pretty(doc)?);
158 return Ok(());
159 }
160
161 println!(
163 "\nš {} {}",
164 doc.library.name.cyan().bold(),
165 doc.library
166 .version
167 .as_ref()
168 .map(|v| format!("v{}", v))
169 .unwrap_or_default()
170 .dimmed()
171 );
172
173 if let Some(desc) = &doc.library.description {
174 println!("{}\n", desc.dimmed());
175 }
176
177 for section in &doc.sections {
179 self.render_doc_section(section)?;
180 }
181
182 Ok(())
183 }
184
185 fn render_doc_section(&self, section: &DocSection) -> io::Result<()> {
186 println!("\n{}", section.title.green().bold());
187
188 if let Some(url) = §ion.url {
189 println!("{}: {}", "Source".dimmed(), url.blue().underline());
190 }
191
192 println!("\n{}", section.content);
193
194 for example in §ion.code_examples {
196 self.render_code_example(example)?;
197 }
198
199 Ok(())
200 }
201
202 fn render_code_example(&self, example: &CodeExample) -> io::Result<()> {
203 println!(
204 "\n{} {}:",
205 "ā¶".cyan(),
206 example
207 .description
208 .as_ref()
209 .unwrap_or(&"Example".to_string())
210 .yellow()
211 );
212
213 println!("{}", format!("```{}", example.language).dimmed());
214
215 let highlighted = self.highlight_code(&example.code, &example.language);
217 println!("{}", highlighted);
218
219 println!("{}", "```".dimmed());
220 Ok(())
221 }
222
223 fn highlight_code(&self, code: &str, language: &str) -> String {
224 if self.quiet_mode {
225 return code.to_string();
226 }
227
228 match language {
230 "python" | "py" => self.highlight_python(code),
231 "javascript" | "js" | "typescript" | "ts" => self.highlight_javascript(code),
232 "rust" | "rs" => self.highlight_rust(code),
233 _ => code.to_string(),
234 }
235 }
236
237 fn highlight_python(&self, code: &str) -> String {
238 let keywords = [
239 "def", "class", "import", "from", "return", "if", "else", "elif", "for", "while", "in",
240 "as", "with", "try", "except", "finally", "raise", "yield", "lambda",
241 ];
242
243 let mut highlighted = code.to_string();
244 for keyword in &keywords {
245 let _pattern = format!(r"\b{}\b", keyword);
246 highlighted = highlighted.replace(keyword, &keyword.magenta().to_string());
247 }
248 highlighted
249 }
250
251 fn highlight_javascript(&self, code: &str) -> String {
252 let keywords = [
253 "function", "const", "let", "var", "return", "if", "else", "for", "while", "class",
254 "extends", "import", "export", "async", "await", "try", "catch", "throw", "new",
255 ];
256
257 let mut highlighted = code.to_string();
258 for keyword in &keywords {
259 let _pattern = format!(r"\b{}\b", keyword);
260 highlighted = highlighted.replace(keyword, &keyword.blue().to_string());
261 }
262 highlighted
263 }
264
265 fn highlight_rust(&self, code: &str) -> String {
266 let keywords = [
267 "fn", "let", "mut", "const", "use", "mod", "pub", "impl", "struct", "enum", "trait",
268 "where", "async", "await", "match", "if", "else", "for", "while", "loop", "return",
269 ];
270
271 let mut highlighted = code.to_string();
272 for keyword in &keywords {
273 let _pattern = format!(r"\b{}\b", keyword);
274 highlighted = highlighted.replace(keyword, &keyword.red().to_string());
275 }
276 highlighted
277 }
278
279 fn truncate_text(&self, text: &str, max_len: usize) -> String {
280 if text.len() <= max_len {
281 text.to_string()
282 } else {
283 let truncate_at = max_len - 3;
285 if let Some(last_space) = text[..truncate_at].rfind(' ') {
286 format!("{}...", &text[..last_space])
287 } else {
288 format!("{}...", &text[..truncate_at])
289 }
290 }
291 }
292
293 pub fn show_progress(&self, message: &str) -> ProgressBar {
294 if self.quiet_mode {
295 return ProgressBar::hidden();
296 }
297
298 let pb = ProgressBar::new_spinner();
299 pb.set_style(
300 ProgressStyle::default_spinner()
301 .tick_chars("ā ā ā ā”ā¢ā ā ā ")
302 .template("{spinner:.cyan} {msg}")
303 .unwrap(),
304 );
305 pb.set_message(message.to_string());
306 pb.enable_steady_tick(std::time::Duration::from_millis(100));
307 pb
308 }
309
310 pub fn print_error(&self, error: &str) {
311 if self.quiet_mode {
312 eprintln!("{{\"error\": \"{}\"}}", error);
313 } else {
314 eprintln!("{} {}", "ā".red().bold(), error.red());
315 }
316 }
317
318 pub fn print_success(&self, message: &str) {
319 if !self.quiet_mode {
320 println!("{} {}", "ā".green().bold(), message.green());
321 }
322 }
323
324 pub fn render_context7_documentation(&self, library: &str, content: &str) -> io::Result<()> {
325 self.render_context7_documentation_with_limit(library, content, None)
326 }
327
328 pub fn render_context7_documentation_with_limit(
329 &self,
330 library: &str,
331 content: &str,
332 limit: Option<usize>,
333 ) -> io::Result<()> {
334 if self.quiet_mode {
335 println!("{}", content);
336 return Ok(());
337 }
338
339 println!(
341 "\n{} {} {}",
342 "š".cyan().bold(),
343 library.white().bold(),
344 "Documentation".dimmed()
345 );
346
347 self.parse_and_render_context7_content_with_limit(content, limit)?;
349
350 let sections = self.extract_doc_sections(content);
352 if self.cache_doc_sections(library, §ions).is_err() {
353 }
355
356 Ok(())
357 }
358
359 fn parse_and_render_context7_content_with_limit(
360 &self,
361 content: &str,
362 limit: Option<usize>,
363 ) -> io::Result<()> {
364 let lines: Vec<&str> = content.lines().collect();
365 let mut i = 0;
366 let mut sections_shown = 0;
367 let section_limit = limit.unwrap_or(10); while i < lines.len() {
370 if limit.is_some() && limit.unwrap() > 0 && sections_shown >= section_limit {
372 let remaining = self.count_remaining_sections(&lines[i..]);
373 if remaining > 0 {
374 println!(
375 "\n{}",
376 format!(
377 "... and {} more sections. Use --limit 0 to show all.",
378 remaining
379 )
380 .yellow()
381 );
382 }
383 break;
384 }
385 let line = lines[i];
386
387 if line.starts_with("========================") {
389 if i + 1 < lines.len() && lines[i + 1].starts_with("CODE SNIPPETS") {
390 println!("\n{}", "š Code Examples & Snippets".green().bold());
391 i += 2;
392 continue;
393 }
394 i += 1;
395 continue;
396 }
397
398 if let Some(title) = line.strip_prefix("TITLE: ") {
400 sections_shown += 1;
401 println!(
402 "\n{} {}",
403 format!("[{}]", sections_shown).cyan().bold(),
404 title.white().bold()
405 );
406 i += 1;
407
408 if i < lines.len() && lines[i].starts_with("DESCRIPTION: ") {
410 let desc = &lines[i][13..];
411 println!("{}", desc.dimmed());
412 i += 1;
413 }
414
415 while i < lines.len() && lines[i].trim().is_empty() {
417 i += 1;
418 }
419
420 while i < lines.len() && lines[i].starts_with("SOURCE: ") {
422 let source = &lines[i][8..];
423 println!("{}: {}", "Source".dimmed(), source.blue());
424 i += 1;
425 }
426
427 while i < lines.len() && lines[i].trim().is_empty() {
429 i += 1;
430 }
431
432 if i < lines.len() && lines[i].starts_with("LANGUAGE: ") {
434 let language = &lines[i][10..];
435 i += 1;
436
437 if i < lines.len() && lines[i].starts_with("CODE:") {
439 i += 1;
440 }
441
442 if i < lines.len() && lines[i].starts_with("```") {
444 println!("\n{} {}:", "ā¶".cyan(), language.yellow());
445 println!("{}", lines[i].dimmed());
446 i += 1;
447
448 while i < lines.len() && !lines[i].starts_with("```") {
450 let highlighted =
451 self.highlight_code(lines[i], &language.to_lowercase());
452 println!("{}", highlighted);
453 i += 1;
454 }
455
456 if i < lines.len() && lines[i].starts_with("```") {
458 println!("{}", lines[i].dimmed());
459 i += 1;
460 }
461 }
462 }
463
464 while i < lines.len() && (lines[i].trim().is_empty() || lines[i].starts_with("---"))
466 {
467 if lines[i].starts_with("---") {
468 let separator = "ā".repeat(self.terminal_width.min(60));
469 println!("\n{}", separator.dimmed());
470 }
471 i += 1;
472 }
473
474 continue;
475 }
476
477 i += 1;
478 }
479
480 if sections_shown > 0 {
482 println!(
483 "\n{}",
484 "Tip: Use 'manx open <section-id>' to expand a specific section.".dimmed()
485 );
486 }
487
488 Ok(())
489 }
490
491 fn count_remaining_sections(&self, lines: &[&str]) -> usize {
492 lines
493 .iter()
494 .filter(|line| line.starts_with("TITLE: "))
495 .count()
496 }
497
498 fn extract_doc_sections(&self, content: &str) -> Vec<String> {
499 let lines: Vec<&str> = content.lines().collect();
500 let mut sections = Vec::new();
501 let mut i = 0;
502
503 while i < lines.len() {
504 let line = lines[i];
505
506 if let Some(_title) = line.strip_prefix("TITLE: ") {
508 let section_start = i;
509 let mut section_end = lines.len();
510
511 for (j, line) in lines.iter().enumerate().skip(i + 1) {
513 if line.starts_with("TITLE: ") {
514 section_end = j;
515 break;
516 }
517 }
518
519 let section_lines = &lines[section_start..section_end];
521 let section_content = section_lines.join("\n").trim().to_string();
522
523 if !section_content.is_empty() {
524 sections.push(section_content);
525 }
526
527 i = section_end;
528 } else {
529 i += 1;
530 }
531 }
532
533 sections
534 }
535
536 pub fn render_open_section(&self, id: &str, content: &str) -> io::Result<()> {
537 if self.quiet_mode {
538 println!("{}", content);
539 return Ok(());
540 }
541
542 println!(
543 "\n{} {} {}",
544 "š".cyan().bold(),
545 id.yellow().bold(),
546 "Documentation Section".dimmed()
547 );
548
549 self.render_single_section(content)?;
551
552 Ok(())
553 }
554
555 fn render_single_section(&self, content: &str) -> io::Result<()> {
556 let lines: Vec<&str> = content.lines().collect();
557 let mut i = 0;
558
559 while i < lines.len() {
560 let line = lines[i];
561
562 if line.starts_with("========================") {
564 if i + 1 < lines.len() && lines[i + 1].starts_with("CODE SNIPPETS") {
565 println!("\n{}", "š Code Examples & Snippets".green().bold());
566 i += 2;
567 continue;
568 }
569 i += 1;
570 continue;
571 }
572
573 if let Some(title) = line.strip_prefix("TITLE: ") {
575 println!("\n{}", title.white().bold());
576 i += 1;
577
578 if i < lines.len() && lines[i].starts_with("DESCRIPTION: ") {
580 let desc = &lines[i][13..];
581 println!("{}", desc.dimmed());
582 i += 1;
583 }
584
585 while i < lines.len() && lines[i].trim().is_empty() {
587 i += 1;
588 }
589
590 while i < lines.len() && lines[i].starts_with("SOURCE: ") {
592 let source = &lines[i][8..];
593 println!("{}: {}", "Source".dimmed(), source.blue());
594 i += 1;
595 }
596
597 while i < lines.len() && lines[i].trim().is_empty() {
599 i += 1;
600 }
601
602 if i < lines.len() && lines[i].starts_with("LANGUAGE: ") {
604 let language = &lines[i][10..];
605 i += 1;
606
607 if i < lines.len() && lines[i].starts_with("CODE:") {
609 i += 1;
610 }
611
612 if i < lines.len() && lines[i].starts_with("```") {
614 println!("\n{} {}:", "ā¶".cyan(), language.yellow());
615 println!("{}", lines[i].dimmed());
616 i += 1;
617
618 while i < lines.len() && !lines[i].starts_with("```") {
620 let highlighted =
621 self.highlight_code(lines[i], &language.to_lowercase());
622 println!("{}", highlighted);
623 i += 1;
624 }
625
626 if i < lines.len() && lines[i].starts_with("```") {
628 println!("{}", lines[i].dimmed());
629 i += 1;
630 }
631 }
632 }
633
634 while i < lines.len() && (lines[i].trim().is_empty() || lines[i].starts_with("---"))
636 {
637 if lines[i].starts_with("---") {
638 let separator = "ā".repeat(self.terminal_width.min(60));
639 println!("\n{}", separator.dimmed());
640 }
641 i += 1;
642 }
643
644 continue;
645 }
646
647 i += 1;
648 }
649
650 Ok(())
651 }
652
653 fn cache_doc_sections(&self, library: &str, sections: &[String]) -> Result<()> {
654 if let Some(config) = &self.config {
655 if config.auto_cache_enabled {
656 if let Ok(cache_manager) = crate::cache::CacheManager::new() {
657 let library_clean = library.to_string();
658 let sections_clone = sections.to_vec();
659
660 tokio::spawn(async move {
661 for (idx, section) in sections_clone.iter().enumerate() {
662 let cache_key = format!("{}_doc-{}", library_clean, idx + 1);
663 let _ = cache_manager.set("doc_sections", &cache_key, section).await;
664 }
665 });
666 }
667 }
668 }
669 Ok(())
670 }
671}