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