1use owo_colors::OwoColorize;
9use std::io::IsTerminal;
10
11pub fn is_tty() -> bool {
17 std::io::stdout().is_terminal()
18}
19
20pub fn terminal_width() -> usize {
24 crossterm::terminal::size()
25 .map(|(w, _)| w as usize)
26 .unwrap_or(80)
27}
28
29pub fn wrap_lines(text: &str, content_width: usize) -> Vec<String> {
42 if content_width == 0 {
43 return vec![text.to_string()];
44 }
45
46 let mut lines: Vec<String> = Vec::new();
47 let mut current_line = String::new();
48 let mut current_len = 0usize;
49
50 for word in text.split_whitespace() {
51 let word_len = word.len();
52
53 if current_len == 0 {
54 current_line.push_str(word);
56 current_len = word_len;
57 } else if current_len + 1 + word_len <= content_width {
58 current_line.push(' ');
60 current_line.push_str(word);
61 current_len += 1 + word_len;
62 } else {
63 lines.push(current_line);
65 current_line = word.to_string();
66 current_len = word_len;
67 }
68 }
69
70 if !current_line.is_empty() {
71 lines.push(current_line);
72 }
73
74 if lines.is_empty() {
75 lines.push(String::new());
76 }
77
78 lines
79}
80
81fn content_width_for(prefix_width: usize) -> usize {
87 terminal_width().saturating_sub(prefix_width)
88}
89
90pub fn section_header(title: &str) -> String {
100 section_header_styled(title, is_tty())
101}
102
103fn section_header_styled(title: &str, tty: bool) -> String {
104 let width: usize = 50;
105 let pad = width.saturating_sub(title.len() + 4);
106 if tty {
107 format!(
108 "\n{} {} {}",
109 "┌─".cyan(),
110 title.bold().bright_white(),
111 "─".repeat(pad).cyan()
112 )
113 } else {
114 format!("\n┌─ {} {}", title, "─".repeat(pad))
115 }
116}
117
118pub fn subsection_header(title: &str) -> String {
125 subsection_header_styled(title, is_tty())
126}
127
128fn subsection_header_styled(title: &str, tty: bool) -> String {
129 if tty {
130 format!("{}\n{}", "│".cyan(), format!("├── {}", title.bold()).cyan())
131 } else {
132 format!("│\n├── {}", title)
133 }
134}
135
136pub fn kv_row(key: &str, value: &str) -> String {
142 kv_row_styled(key, value, is_tty())
143}
144
145fn kv_row_styled(key: &str, value: &str, tty: bool) -> String {
146 let prefix_cols = 21;
148 let avail = terminal_width().saturating_sub(prefix_cols);
149 let wrapped = wrap_lines(value, avail);
150
151 let mut out = if tty {
152 format!("{} {:<18}{}", "│".cyan(), key.dimmed(), wrapped[0])
153 } else {
154 format!("│ {:<18}{}", key, wrapped[0])
155 };
156
157 let cont_prefix = format!("│ {:<18}", "");
159 for line in &wrapped[1..] {
160 if tty {
161 out.push_str(&format!("\n{} {:<18}{}", "│".cyan(), "", line));
162 } else {
163 out.push_str(&format!("\n{}{}", cont_prefix, line));
164 }
165 }
166 out
167}
168
169pub fn kv_row_delta(key: &str, value: f64, formatted: &str) -> String {
171 kv_row_delta_styled(key, value, formatted, is_tty())
172}
173
174fn kv_row_delta_styled(key: &str, value: f64, formatted: &str, tty: bool) -> String {
175 if tty {
176 let colored_val = if value > 0.0 {
177 format!("{}", formatted.green())
178 } else if value < 0.0 {
179 format!("{}", formatted.red())
180 } else {
181 format!("{}", formatted.dimmed())
182 };
183 format!("{} {:<18}{}", "│".cyan(), key.dimmed(), colored_val)
184 } else {
185 format!("│ {:<18}{}", key, formatted)
186 }
187}
188
189pub fn check_pass(msg: &str) -> String {
195 check_pass_styled(msg, is_tty())
196}
197
198fn check_pass_styled(msg: &str, tty: bool) -> String {
199 let avail = content_width_for(5);
201 let wrapped = wrap_lines(msg, avail);
202
203 let mut out = if tty {
204 format!("{} {} {}", "│".cyan(), "✓".green(), wrapped[0])
205 } else {
206 format!("│ ✓ {}", wrapped[0])
207 };
208
209 for line in &wrapped[1..] {
210 if tty {
211 out.push_str(&format!("\n{} {}", "│".cyan(), line));
212 } else {
213 out.push_str(&format!("\n│ {}", line));
214 }
215 }
216 out
217}
218
219pub fn check_fail(msg: &str) -> String {
225 check_fail_styled(msg, is_tty())
226}
227
228fn check_fail_styled(msg: &str, tty: bool) -> String {
229 let avail = content_width_for(5);
231 let wrapped = wrap_lines(msg, avail);
232
233 let mut out = if tty {
234 format!("{} {} {}", "│".cyan(), "✗".red(), wrapped[0])
235 } else {
236 format!("│ ✗ {}", wrapped[0])
237 };
238
239 for line in &wrapped[1..] {
240 if tty {
241 out.push_str(&format!("\n{} {}", "│".cyan(), line));
242 } else {
243 out.push_str(&format!("\n│ {}", line));
244 }
245 }
246 out
247}
248
249pub fn status_line(healthy: bool) -> String {
251 status_line_styled(healthy, is_tty())
252}
253
254fn status_line_styled(healthy: bool, tty: bool) -> String {
255 if tty {
256 if healthy {
257 format!("{} {}", "│".cyan(), "HEALTHY".green().bold())
258 } else {
259 format!("{} {}", "│".cyan(), "UNHEALTHY".red().bold())
260 }
261 } else if healthy {
262 "│ HEALTHY".to_string()
263 } else {
264 "│ UNHEALTHY".to_string()
265 }
266}
267
268pub fn section_footer() -> String {
274 section_footer_styled(is_tty())
275}
276
277fn section_footer_styled(tty: bool) -> String {
278 let line = "─".repeat(50);
279 if tty {
280 format!("{}", format!("└{}", line).cyan())
281 } else {
282 format!("└{}", line)
283 }
284}
285
286pub fn separator() -> String {
292 separator_styled(is_tty())
293}
294
295fn separator_styled(tty: bool) -> String {
296 let line = "─".repeat(50);
297 if tty {
298 format!("{}", format!("├{}", line).cyan())
299 } else {
300 format!("├{}", line)
301 }
302}
303
304pub fn format_price_peg(price: f64, target: f64) -> String {
307 format_price_peg_styled(price, target, is_tty())
308}
309
310fn format_price_peg_styled(price: f64, target: f64, tty: bool) -> String {
311 let deviation = ((price - target) / target).abs();
312 let text = format!("{:.4}", price);
313 if !tty {
314 return text;
315 }
316 if deviation < 0.001 {
317 format!("{}", text.green())
318 } else if deviation < 0.005 {
319 format!("{}", text.yellow())
320 } else {
321 format!("{}", text.red())
322 }
323}
324
325pub fn blank_row() -> String {
327 blank_row_styled(is_tty())
328}
329
330fn blank_row_styled(tty: bool) -> String {
331 if tty {
332 format!("{}", "│".cyan())
333 } else {
334 "│".to_string()
335 }
336}
337
338pub fn orderbook_level(price: f64, quantity: f64, base: &str, value: f64, peg: f64) -> String {
340 orderbook_level_styled(price, quantity, base, value, peg, is_tty())
341}
342
343fn orderbook_level_styled(
344 price: f64,
345 quantity: f64,
346 base: &str,
347 value: f64,
348 peg: f64,
349 tty: bool,
350) -> String {
351 let price_str = format_price_peg_styled(price, peg, tty);
352 if tty {
353 format!(
354 "{} {} {:>10.2} {} {:>10.2} USDT",
355 "│".cyan(),
356 price_str,
357 quantity,
358 base.dimmed(),
359 value
360 )
361 } else {
362 format!(
363 "│ {:.4} {:>10.2} {} {:>10.2} USDT",
364 price, quantity, base, value
365 )
366 }
367}
368
369pub fn score_bar(label: &str, score: u32, max: u32) -> String {
379 score_bar_styled(label, score, max, is_tty())
380}
381
382fn score_bar_styled(label: &str, score: u32, max: u32, tty: bool) -> String {
383 let width = 20usize;
384 let filled = ((score as f64 / max as f64) * width as f64).round() as usize;
385 let filled = filled.min(width);
386 let empty = width - filled;
387 let bar = format!(
388 "[{}{}] {}/{}",
389 "█".repeat(filled),
390 "─".repeat(empty),
391 score,
392 max
393 );
394 if tty {
395 let colored_bar = if score >= 80 {
396 format!("{}", bar.green())
397 } else if score >= 50 {
398 format!("{}", bar.yellow())
399 } else {
400 format!("{}", bar.red())
401 };
402 format!("{} {:<18}{}", "│".cyan(), label.dimmed(), colored_bar)
403 } else {
404 format!("│ {:<18}{}", label, bar)
405 }
406}
407
408pub fn severity_label(severity: &str) -> String {
413 severity_label_styled(severity, is_tty())
414}
415
416fn severity_label_styled(severity: &str, tty: bool) -> String {
417 if !tty {
418 return severity.to_string();
419 }
420 match severity.to_lowercase().as_str() {
421 "critical" => format!("{}", severity.red().bold()),
422 "high" => format!("{}", severity.red()),
423 "medium" => format!("{}", severity.yellow()),
424 "low" => format!("{}", severity.cyan()),
425 _ => format!("{}", severity.dimmed()),
426 }
427}
428
429pub fn warning_row(msg: &str) -> String {
435 warning_row_styled(msg, is_tty())
436}
437
438fn warning_row_styled(msg: &str, tty: bool) -> String {
439 let avail = content_width_for(5);
441 let wrapped = wrap_lines(msg, avail);
442
443 let mut out = if tty {
444 format!(
445 "{} {} {}",
446 "│".cyan(),
447 "⚠".yellow().bold(),
448 wrapped[0].yellow()
449 )
450 } else {
451 format!("│ ⚠ {}", wrapped[0])
452 };
453
454 for line in &wrapped[1..] {
455 if tty {
456 out.push_str(&format!("\n{} {}", "│".cyan(), line.yellow()));
457 } else {
458 out.push_str(&format!("\n│ {}", line));
459 }
460 }
461 out
462}
463
464pub fn info_row(msg: &str) -> String {
470 info_row_styled(msg, is_tty())
471}
472
473fn info_row_styled(msg: &str, tty: bool) -> String {
474 let avail = content_width_for(5);
476 let wrapped = wrap_lines(msg, avail);
477
478 let mut out = if tty {
479 format!("{} {} {}", "│".cyan(), "ℹ".blue(), wrapped[0].dimmed())
480 } else {
481 format!("│ ℹ {}", wrapped[0])
482 };
483
484 for line in &wrapped[1..] {
485 if tty {
486 out.push_str(&format!("\n{} {}", "│".cyan(), line.dimmed()));
487 } else {
488 out.push_str(&format!("\n│ {}", line));
489 }
490 }
491 out
492}
493
494pub fn link_row(label: &str, url: &str) -> String {
500 link_row_styled(label, url, is_tty())
501}
502
503fn link_row_styled(label: &str, url: &str, tty: bool) -> String {
504 if tty {
505 format!("{} {:<18}{}", "│".cyan(), label.dimmed(), url.underline())
506 } else {
507 format!("│ {:<18}{}", label, url)
508 }
509}
510
511pub fn detail_row(msg: &str) -> String {
517 detail_row_styled(msg, is_tty())
518}
519
520fn detail_row_styled(msg: &str, tty: bool) -> String {
521 let avail = content_width_for(7);
523 let wrapped = wrap_lines(msg, avail);
524
525 let mut out = if tty {
526 format!("{} {}", "│".cyan(), wrapped[0].dimmed())
527 } else {
528 format!("│ {}", wrapped[0])
529 };
530
531 for line in &wrapped[1..] {
532 if tty {
533 out.push_str(&format!("\n{} {}", "│".cyan(), line.dimmed()));
534 } else {
535 out.push_str(&format!("\n│ {}", line));
536 }
537 }
538 out
539}
540
541pub fn bullet_row(msg: &str) -> String {
547 bullet_row_styled(msg, is_tty())
548}
549
550fn bullet_row_styled(msg: &str, tty: bool) -> String {
551 let avail = content_width_for(7);
553 let wrapped = wrap_lines(msg, avail);
554
555 let mut out = if tty {
556 format!("{} {} {}", "│".cyan(), "•".dimmed(), wrapped[0])
557 } else {
558 format!("│ • {}", wrapped[0])
559 };
560
561 for line in &wrapped[1..] {
562 if tty {
563 out.push_str(&format!("\n{} {}", "│".cyan(), line));
564 } else {
565 out.push_str(&format!("\n│ {}", line));
566 }
567 }
568 out
569}
570
571pub struct Col<'a> {
577 pub label: &'a str,
579 pub width: usize,
581 pub align: char,
583}
584
585pub fn table_header(cols: &[Col]) -> String {
592 table_header_styled(cols, is_tty())
593}
594
595fn table_header_styled(cols: &[Col], tty: bool) -> String {
596 let mut header = String::new();
597 for col in cols {
598 if col.align == '>' {
599 header.push_str(&format!("{:>width$} ", col.label, width = col.width));
600 } else {
601 header.push_str(&format!("{:<width$} ", col.label, width = col.width));
602 }
603 }
604 let header = header.trim_end().to_string();
605 let rule_len = cols.iter().map(|c| c.width + 2).sum::<usize>();
606 let rule = "─".repeat(rule_len);
607
608 if tty {
609 format!(
610 "{} {}\n{} {}",
611 "│".cyan(),
612 header.dimmed(),
613 "│".cyan(),
614 rule.cyan()
615 )
616 } else {
617 format!("│ {}\n│ {}", header, rule)
618 }
619}
620
621pub fn table_row(cols: &[Col], values: &[&str]) -> String {
629 table_row_styled(cols, values, is_tty())
630}
631
632fn table_row_styled(cols: &[Col], values: &[&str], tty: bool) -> String {
633 let mut row = String::new();
634 for (i, col) in cols.iter().enumerate() {
635 let val = values.get(i).copied().unwrap_or("");
636 if col.align == '>' {
637 row.push_str(&format!("{:>width$} ", val, width = col.width));
638 } else {
639 row.push_str(&format!("{:<width$} ", val, width = col.width));
640 }
641 }
642 let row = row.trim_end().to_string();
643
644 if tty {
645 format!("{} {}", "│".cyan(), row)
646 } else {
647 format!("│ {}", row)
648 }
649}
650
651pub fn numbered_row(index: usize, msg: &str) -> String {
657 numbered_row_styled(index, msg, is_tty())
658}
659
660fn numbered_row_styled(index: usize, msg: &str, tty: bool) -> String {
661 let prefix_len = 4 + format!("{}.", index).len();
663 let avail = terminal_width().saturating_sub(prefix_len);
664 let wrapped = wrap_lines(msg, avail);
665
666 let num = format!("{}.", index);
667 let mut out = if tty {
668 format!("{} {} {}", "│".cyan(), num.dimmed(), wrapped[0])
669 } else {
670 format!("│ {} {}", num, wrapped[0])
671 };
672
673 let cont_pad = " ".repeat(num.len() + 1);
674 for line in &wrapped[1..] {
675 if tty {
676 out.push_str(&format!("\n{} {}{}", "│".cyan(), cont_pad, line));
677 } else {
678 out.push_str(&format!("\n│ {}{}", cont_pad, line));
679 }
680 }
681 out
682}
683
684#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn test_section_header_contains_title() {
694 let header = section_header("Token Health");
695 assert!(header.contains("Token Health"));
696 assert!(header.contains("┌─"));
697 }
698
699 #[test]
700 fn test_subsection_header_contains_title() {
701 let header = subsection_header("DEX Analytics");
702 assert!(header.contains("DEX Analytics"));
703 assert!(header.contains("├──"));
704 }
705
706 #[test]
707 fn test_kv_row_contains_key_value() {
708 let row = kv_row("Price", "$1.00");
709 assert!(row.contains("Price"));
710 assert!(row.contains("$1.00"));
711 assert!(row.contains("│"));
712 }
713
714 #[test]
715 fn test_kv_row_delta_positive() {
716 let row = kv_row_delta("24h Change", 5.0, "+5.00%");
717 assert!(row.contains("+5.00%"));
718 }
719
720 #[test]
721 fn test_kv_row_delta_negative() {
722 let row = kv_row_delta("24h Change", -3.0, "-3.00%");
723 assert!(row.contains("-3.00%"));
724 }
725
726 #[test]
727 fn test_check_pass() {
728 let line = check_pass("No sells below peg");
729 assert!(line.contains("✓"));
730 assert!(line.contains("No sells below peg"));
731 }
732
733 #[test]
734 fn test_check_fail() {
735 let line = check_fail("Bid depth too low");
736 assert!(line.contains("✗"));
737 assert!(line.contains("Bid depth too low"));
738 }
739
740 #[test]
741 fn test_status_line_healthy() {
742 let line = status_line(true);
743 assert!(line.contains("HEALTHY"));
744 }
745
746 #[test]
747 fn test_status_line_unhealthy() {
748 let line = status_line(false);
749 assert!(line.contains("UNHEALTHY"));
750 }
751
752 #[test]
753 fn test_section_footer() {
754 let footer = section_footer();
755 assert!(footer.contains("└"));
756 }
757
758 #[test]
759 fn test_separator() {
760 let sep = separator();
761 assert!(sep.contains("├"));
762 }
763
764 #[test]
765 fn test_format_price_peg_near() {
766 let s = format_price_peg(1.0001, 1.0);
767 assert!(s.contains("1.0001"));
768 }
769
770 #[test]
771 fn test_format_price_peg_far() {
772 let s = format_price_peg(0.95, 1.0);
773 assert!(s.contains("0.9500"));
774 }
775
776 #[test]
777 fn test_blank_row() {
778 let row = blank_row();
779 assert!(row.contains("│"));
780 }
781
782 #[test]
783 fn test_orderbook_level() {
784 let row = orderbook_level(1.0001, 500.0, "DAI", 500.05, 1.0);
785 assert!(row.contains("DAI"));
786 assert!(row.contains("USDT"));
787 }
788
789 #[test]
790 fn test_kv_row_delta_zero_value() {
791 let row = kv_row_delta("Change", 0.0, "0.00%");
793 assert!(row.contains("Change"));
794 assert!(row.contains("0.00%"));
795 assert!(row.contains("│"));
796 }
797
798 #[test]
799 fn test_format_price_peg_moderate_deviation() {
800 let s = format_price_peg(1.002, 1.0);
802 assert!(s.contains("1.0020"));
803 }
804
805 #[test]
806 fn test_orderbook_level_various_prices() {
807 let row_low = orderbook_level(0.9990, 100.0, "DAI", 99.90, 1.0);
808 let row_mid = orderbook_level(1.0000, 100.0, "DAI", 100.0, 1.0);
809 let row_high = orderbook_level(1.0015, 100.0, "DAI", 100.15, 1.0);
810 assert!(row_low.contains("0.9990"));
811 assert!(row_mid.contains("1.0000"));
812 assert!(row_high.contains("1.0015"));
813 assert!(row_low.contains("│"));
814 assert!(row_mid.contains("│"));
815 assert!(row_high.contains("│"));
816 }
817
818 #[test]
819 fn test_non_tty_returns_unicode_box_characters() {
820 let header = section_header("Test");
822 let sub = subsection_header("Sub");
823 let kv = kv_row("Key", "Val");
824 let pass = check_pass("ok");
825 let fail = check_fail("err");
826 let footer = section_footer();
827 let sep = separator();
828 let blank = blank_row();
829 let status_healthy = status_line(true);
830 let status_unhealthy = status_line(false);
831
832 assert!(header.contains('┌'), "section_header should contain ┌");
833 assert!(header.contains('─'), "section_header should contain ─");
834 assert!(sub.contains('│'), "subsection_header should contain │");
835 assert!(sub.contains('├'), "subsection_header should contain ├");
836 assert!(kv.contains('│'), "kv_row should contain │");
837 assert!(pass.contains('✓'), "check_pass should contain ✓");
838 assert!(fail.contains('✗'), "check_fail should contain ✗");
839 assert!(footer.contains('└'), "section_footer should contain └");
840 assert!(sep.contains('├'), "separator should contain ├");
841 assert!(blank.contains('│'), "blank_row should contain │");
842 assert!(status_healthy.contains("HEALTHY"));
843 assert!(status_unhealthy.contains("UNHEALTHY"));
844 }
845
846 #[test]
851 fn test_section_header_tty() {
852 let header = section_header_styled("Token Health", true);
853 assert!(header.contains("Token Health"));
854 assert!(header.contains("┌─"));
855 }
856
857 #[test]
858 fn test_subsection_header_tty() {
859 let header = subsection_header_styled("DEX", true);
860 assert!(header.contains("DEX"));
861 assert!(header.contains("├──"));
862 }
863
864 #[test]
865 fn test_kv_row_tty() {
866 let row = kv_row_styled("Price", "$1.00", true);
867 assert!(row.contains("Price"));
868 assert!(row.contains("$1.00"));
869 }
870
871 #[test]
872 fn test_kv_row_delta_positive_tty() {
873 let row = kv_row_delta_styled("Change", 5.0, "+5%", true);
874 assert!(row.contains("+5%"));
875 }
876
877 #[test]
878 fn test_kv_row_delta_negative_tty() {
879 let row = kv_row_delta_styled("Change", -3.0, "-3%", true);
880 assert!(row.contains("-3%"));
881 }
882
883 #[test]
884 fn test_kv_row_delta_zero_tty() {
885 let row = kv_row_delta_styled("Change", 0.0, "0.00%", true);
886 assert!(row.contains("0.00%"));
887 }
888
889 #[test]
890 fn test_check_pass_tty() {
891 let line = check_pass_styled("ok", true);
892 assert!(line.contains("✓"));
893 assert!(line.contains("ok"));
894 }
895
896 #[test]
897 fn test_check_fail_tty() {
898 let line = check_fail_styled("err", true);
899 assert!(line.contains("✗"));
900 assert!(line.contains("err"));
901 }
902
903 #[test]
904 fn test_status_line_healthy_tty() {
905 let line = status_line_styled(true, true);
906 assert!(line.contains("HEALTHY"));
907 }
908
909 #[test]
910 fn test_status_line_unhealthy_tty() {
911 let line = status_line_styled(false, true);
912 assert!(line.contains("UNHEALTHY"));
913 }
914
915 #[test]
916 fn test_section_footer_tty() {
917 let footer = section_footer_styled(true);
918 assert!(footer.contains("└"));
919 }
920
921 #[test]
922 fn test_separator_tty() {
923 let sep = separator_styled(true);
924 assert!(sep.contains("├"));
925 }
926
927 #[test]
928 fn test_format_price_peg_tty_near() {
929 let s = format_price_peg_styled(1.0001, 1.0, true);
930 assert!(s.contains("1.0001"));
931 }
932
933 #[test]
934 fn test_format_price_peg_tty_moderate() {
935 let s = format_price_peg_styled(1.003, 1.0, true);
936 assert!(s.contains("1.0030"));
937 }
938
939 #[test]
940 fn test_format_price_peg_tty_far() {
941 let s = format_price_peg_styled(0.95, 1.0, true);
942 assert!(s.contains("0.9500"));
943 }
944
945 #[test]
946 fn test_blank_row_tty() {
947 let row = blank_row_styled(true);
948 assert!(row.contains("│"));
949 }
950
951 #[test]
952 fn test_orderbook_level_tty() {
953 let row = orderbook_level_styled(1.0001, 500.0, "DAI", 500.05, 1.0, true);
954 assert!(row.contains("DAI"));
955 assert!(row.contains("USDT"));
956 }
957
958 #[test]
963 fn test_score_bar_high() {
964 let bar = score_bar("Security Score", 85, 100);
965 assert!(bar.contains("85/100"));
966 assert!(bar.contains("│"));
967 assert!(bar.contains("█"));
968 }
969
970 #[test]
971 fn test_score_bar_low() {
972 let bar = score_bar("Security Score", 20, 100);
973 assert!(bar.contains("20/100"));
974 }
975
976 #[test]
977 fn test_score_bar_zero() {
978 let bar = score_bar("Score", 0, 100);
979 assert!(bar.contains("0/100"));
980 assert!(bar.contains("│"));
981 }
982
983 #[test]
984 fn test_score_bar_max() {
985 let bar = score_bar("Score", 100, 100);
986 assert!(bar.contains("100/100"));
987 }
988
989 #[test]
990 fn test_severity_label_critical() {
991 let label = severity_label("Critical");
992 assert!(label.contains("Critical"));
993 }
994
995 #[test]
996 fn test_severity_label_high() {
997 let label = severity_label("High");
998 assert!(label.contains("High"));
999 }
1000
1001 #[test]
1002 fn test_severity_label_medium() {
1003 let label = severity_label("Medium");
1004 assert!(label.contains("Medium"));
1005 }
1006
1007 #[test]
1008 fn test_severity_label_low() {
1009 let label = severity_label("Low");
1010 assert!(label.contains("Low"));
1011 }
1012
1013 #[test]
1014 fn test_severity_label_informational() {
1015 let label = severity_label("Informational");
1016 assert!(label.contains("Informational"));
1017 }
1018
1019 #[test]
1020 fn test_warning_row() {
1021 let row = warning_row("Source code is NOT verified");
1022 assert!(row.contains("⚠"));
1023 assert!(row.contains("Source code is NOT verified"));
1024 assert!(row.contains("│"));
1025 }
1026
1027 #[test]
1028 fn test_info_row() {
1029 let row = info_row("No findings triggered");
1030 assert!(row.contains("ℹ"));
1031 assert!(row.contains("No findings triggered"));
1032 assert!(row.contains("│"));
1033 }
1034
1035 #[test]
1036 fn test_link_row() {
1037 let row = link_row("Explorer", "https://etherscan.io");
1038 assert!(row.contains("Explorer"));
1039 assert!(row.contains("https://etherscan.io"));
1040 assert!(row.contains("│"));
1041 }
1042
1043 #[test]
1044 fn test_detail_row() {
1045 let row = detail_row("Contract is not verified");
1046 assert!(row.contains("Contract is not verified"));
1047 assert!(row.contains("│"));
1048 }
1049
1050 #[test]
1051 fn test_bullet_row() {
1052 let row = bullet_row("mint (Critical): Can mint tokens");
1053 assert!(row.contains("•"));
1054 assert!(row.contains("mint (Critical): Can mint tokens"));
1055 assert!(row.contains("│"));
1056 }
1057
1058 #[test]
1063 fn test_score_bar_tty_high() {
1064 let bar = score_bar_styled("Security Score", 85, 100, true);
1065 assert!(bar.contains("85/100"));
1066 assert!(bar.contains("█"));
1067 }
1068
1069 #[test]
1070 fn test_score_bar_tty_medium() {
1071 let bar = score_bar_styled("Security Score", 55, 100, true);
1072 assert!(bar.contains("55/100"));
1073 }
1074
1075 #[test]
1076 fn test_score_bar_tty_low() {
1077 let bar = score_bar_styled("Security Score", 20, 100, true);
1078 assert!(bar.contains("20/100"));
1079 }
1080
1081 #[test]
1082 fn test_severity_label_tty_critical() {
1083 let label = severity_label_styled("Critical", true);
1084 assert!(label.contains("Critical"));
1085 }
1086
1087 #[test]
1088 fn test_severity_label_tty_low() {
1089 let label = severity_label_styled("Low", true);
1090 assert!(label.contains("Low"));
1091 }
1092
1093 #[test]
1094 fn test_severity_label_tty_unknown() {
1095 let label = severity_label_styled("Unknown", true);
1096 assert!(label.contains("Unknown"));
1097 }
1098
1099 #[test]
1100 fn test_warning_row_tty() {
1101 let row = warning_row_styled("Alert!", true);
1102 assert!(row.contains("⚠"));
1103 assert!(row.contains("Alert!"));
1104 }
1105
1106 #[test]
1107 fn test_info_row_tty() {
1108 let row = info_row_styled("Note", true);
1109 assert!(row.contains("ℹ"));
1110 assert!(row.contains("Note"));
1111 }
1112
1113 #[test]
1114 fn test_link_row_tty() {
1115 let row = link_row_styled("Explorer", "https://example.com", true);
1116 assert!(row.contains("Explorer"));
1117 assert!(row.contains("https://example.com"));
1118 }
1119
1120 #[test]
1121 fn test_detail_row_tty() {
1122 let row = detail_row_styled("Some detail", true);
1123 assert!(row.contains("Some detail"));
1124 assert!(row.contains("│"));
1125 }
1126
1127 #[test]
1128 fn test_bullet_row_tty() {
1129 let row = bullet_row_styled("Item one", true);
1130 assert!(row.contains("•"));
1131 assert!(row.contains("Item one"));
1132 }
1133
1134 #[test]
1139 fn test_wrap_lines_short_text() {
1140 let lines = wrap_lines("hello world", 80);
1141 assert_eq!(lines, vec!["hello world"]);
1142 }
1143
1144 #[test]
1145 fn test_wrap_lines_exact_fit() {
1146 let lines = wrap_lines("abcde fghij", 11);
1147 assert_eq!(lines, vec!["abcde fghij"]);
1148 }
1149
1150 #[test]
1151 fn test_wrap_lines_wraps_at_word_boundary() {
1152 let lines = wrap_lines("hello world foo", 11);
1153 assert_eq!(lines, vec!["hello world", "foo"]);
1154 }
1155
1156 #[test]
1157 fn test_wrap_lines_multiple_wraps() {
1158 let lines = wrap_lines("a b c d e f g", 3);
1159 assert_eq!(lines, vec!["a b", "c d", "e f", "g"]);
1160 }
1161
1162 #[test]
1163 fn test_wrap_lines_long_word_exceeds_width() {
1164 let lines = wrap_lines(
1166 "https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7",
1167 40,
1168 );
1169 assert_eq!(lines.len(), 1);
1170 assert!(lines[0].starts_with("https://"));
1171 }
1172
1173 #[test]
1174 fn test_wrap_lines_long_word_after_short() {
1175 let lines = wrap_lines(
1176 "Explorer: https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7",
1177 30,
1178 );
1179 assert_eq!(lines.len(), 2);
1180 assert_eq!(lines[0], "Explorer:");
1181 assert!(lines[1].starts_with("https://"));
1182 }
1183
1184 #[test]
1185 fn test_wrap_lines_empty_string() {
1186 let lines = wrap_lines("", 80);
1187 assert_eq!(lines, vec![""]);
1188 }
1189
1190 #[test]
1191 fn test_wrap_lines_zero_width() {
1192 let lines = wrap_lines("hello", 0);
1193 assert_eq!(lines, vec!["hello"]);
1194 }
1195
1196 #[test]
1197 fn test_wrap_lines_single_word() {
1198 let lines = wrap_lines("hello", 80);
1199 assert_eq!(lines, vec!["hello"]);
1200 }
1201
1202 #[test]
1203 fn test_wrap_lines_preserves_all_words() {
1204 let input = "Contract source code is not verified. Full vulnerability analysis requires verified source code.";
1205 let lines = wrap_lines(input, 40);
1206 let reassembled: String = lines.join(" ");
1207 assert_eq!(reassembled, input);
1208 }
1209
1210 #[test]
1211 fn test_terminal_width_returns_positive() {
1212 let w = terminal_width();
1213 assert!(w > 0);
1214 }
1215
1216 #[test]
1217 fn test_content_width_for_reasonable_prefix() {
1218 let w = content_width_for(7);
1219 assert!(w > 0 || terminal_width() <= 7);
1221 }
1222
1223 #[test]
1228 fn test_detail_row_wraps_long_text() {
1229 let long = "Contract source code is not verified and full vulnerability analysis requires verified source code for accurate results";
1231 let row = detail_row_styled(long, false);
1232 let line_count = row.lines().count();
1234 assert!(row.contains("│"), "should contain box-drawing prefix");
1236 for line in row.lines() {
1238 assert!(
1239 line.starts_with('│'),
1240 "continuation line should start with │: {}",
1241 line
1242 );
1243 }
1244 assert!(row.contains("Contract"));
1246 assert!(row.contains("results"));
1247 if terminal_width() < 80 {
1249 assert!(line_count > 1, "should wrap on narrow terminal");
1250 }
1251 }
1252
1253 #[test]
1254 fn test_check_pass_wraps_long_text() {
1255 let long = "No sells detected below the configured peg target during the monitoring window across all tracked pairs";
1256 let row = check_pass_styled(long, false);
1257 assert!(row.contains("✓"));
1258 assert!(row.contains("No sells"));
1259 assert!(row.contains("pairs"));
1260 for line in row.lines() {
1261 assert!(line.starts_with('│'));
1262 }
1263 }
1264
1265 #[test]
1266 fn test_check_fail_wraps_long_text() {
1267 let long = "Bid depth is significantly below the minimum threshold required for healthy market conditions on this trading pair";
1268 let row = check_fail_styled(long, false);
1269 assert!(row.contains("✗"));
1270 for line in row.lines() {
1271 assert!(line.starts_with('│'));
1272 }
1273 }
1274
1275 #[test]
1276 fn test_warning_row_wraps_long_text() {
1277 let long = "Source code is NOT verified — unable to perform source-level analysis. Consider requesting verification.";
1278 let row = warning_row_styled(long, false);
1279 assert!(row.contains("⚠"));
1280 for line in row.lines() {
1281 assert!(line.starts_with('│'));
1282 }
1283 }
1284
1285 #[test]
1286 fn test_info_row_wraps_long_text() {
1287 let long = "No audit reports found. Check block explorer and auditor databases manually for third-party audit information.";
1288 let row = info_row_styled(long, false);
1289 assert!(row.contains("ℹ"));
1290 for line in row.lines() {
1291 assert!(line.starts_with('│'));
1292 }
1293 }
1294
1295 #[test]
1296 fn test_bullet_row_wraps_long_text() {
1297 let long = "Uniswap V3 integration detected with slippage protection enabled and deadline protection enabled for all swap calls";
1298 let row = bullet_row_styled(long, false);
1299 assert!(row.contains("•"));
1300 for line in row.lines() {
1301 assert!(line.starts_with('│'));
1302 }
1303 }
1304
1305 #[test]
1306 fn test_kv_row_wraps_long_value() {
1307 let long_val = "Verified contract with comprehensive access controls and multiple security features including role-based permissions";
1308 let row = kv_row_styled("Summary", long_val, false);
1309 assert!(row.contains("Summary"));
1310 assert!(row.contains("Verified"));
1311 assert!(row.contains("permissions"));
1312 for line in row.lines() {
1313 assert!(line.starts_with('│'));
1314 }
1315 }
1316
1317 #[test]
1318 fn test_kv_row_short_value_no_wrap() {
1319 let row = kv_row_styled("Chain", "ethereum", false);
1320 assert_eq!(row.lines().count(), 1);
1321 assert!(row.contains("Chain"));
1322 assert!(row.contains("ethereum"));
1323 }
1324
1325 #[test]
1326 fn test_detail_row_wraps_tty() {
1327 let long = "Contract source code is not verified and full vulnerability analysis requires verified source code for accurate results";
1328 let row = detail_row_styled(long, true);
1329 assert!(row.contains("│"));
1330 assert!(row.contains("Contract"));
1331 assert!(row.contains("results"));
1332 }
1333
1334 #[test]
1335 fn test_bullet_row_wraps_tty() {
1336 let long = "Uniswap V3 integration detected with slippage protection enabled and deadline protection enabled for all swap calls";
1337 let row = bullet_row_styled(long, true);
1338 assert!(row.contains("•"));
1339 assert!(row.contains("Uniswap"));
1340 assert!(row.contains("calls"));
1341 }
1342
1343 #[test]
1344 fn test_kv_row_wraps_tty() {
1345 let long_val = "Verified contract with comprehensive access controls and multiple security features including role-based permissions";
1346 let row = kv_row_styled("Summary", long_val, true);
1347 assert!(row.contains("Summary"));
1348 assert!(row.contains("Verified"));
1349 assert!(row.contains("permissions"));
1350 }
1351
1352 #[test]
1357 fn test_table_header_contains_labels() {
1358 let cols = &[
1359 Col {
1360 label: "Rank",
1361 width: 6,
1362 align: '>',
1363 },
1364 Col {
1365 label: "Name",
1366 width: 20,
1367 align: '<',
1368 },
1369 ];
1370 let header = table_header(cols);
1371 assert!(header.contains("Rank"));
1372 assert!(header.contains("Name"));
1373 assert!(header.contains("│"));
1374 assert!(header.contains("─"));
1375 }
1376
1377 #[test]
1378 fn test_table_row_contains_values() {
1379 let cols = &[
1380 Col {
1381 label: "Rank",
1382 width: 6,
1383 align: '>',
1384 },
1385 Col {
1386 label: "Name",
1387 width: 20,
1388 align: '<',
1389 },
1390 ];
1391 let row = table_row(cols, &["1", "TestToken"]);
1392 assert!(row.contains("1"));
1393 assert!(row.contains("TestToken"));
1394 assert!(row.contains("│"));
1395 }
1396
1397 #[test]
1398 fn test_table_row_missing_values() {
1399 let cols = &[
1400 Col {
1401 label: "A",
1402 width: 5,
1403 align: '<',
1404 },
1405 Col {
1406 label: "B",
1407 width: 5,
1408 align: '<',
1409 },
1410 ];
1411 let row = table_row(cols, &["only"]);
1412 assert!(row.contains("only"));
1413 assert!(row.contains("│"));
1414 }
1415
1416 #[test]
1417 fn test_table_header_tty() {
1418 let cols = &[Col {
1419 label: "Price",
1420 width: 10,
1421 align: '>',
1422 }];
1423 let header = table_header_styled(cols, true);
1424 assert!(header.contains("Price"));
1425 assert!(header.contains("│"));
1426 }
1427
1428 #[test]
1429 fn test_table_row_tty() {
1430 let cols = &[Col {
1431 label: "Price",
1432 width: 10,
1433 align: '>',
1434 }];
1435 let row = table_row_styled(cols, &["$1.00"], true);
1436 assert!(row.contains("$1.00"));
1437 assert!(row.contains("│"));
1438 }
1439
1440 #[test]
1441 fn test_numbered_row_basic() {
1442 let row = numbered_row(1, "First item");
1443 assert!(row.contains("1."));
1444 assert!(row.contains("First item"));
1445 assert!(row.contains("│"));
1446 }
1447
1448 #[test]
1449 fn test_numbered_row_wraps() {
1450 let long = "This is a very long description that should eventually wrap to the next line when the terminal width is narrow enough";
1451 let row = numbered_row(1, long);
1452 assert!(row.contains("1."));
1453 assert!(row.contains("This"));
1454 assert!(row.contains("enough"));
1455 for line in row.lines() {
1456 assert!(line.contains('│'));
1457 }
1458 }
1459
1460 #[test]
1461 fn test_numbered_row_tty() {
1462 let row = numbered_row_styled(5, "Fifth item", true);
1463 assert!(row.contains("5."));
1464 assert!(row.contains("Fifth item"));
1465 }
1466
1467 #[test]
1468 fn test_numbered_row_double_digits() {
1469 let row = numbered_row(12, "Twelfth item");
1470 assert!(row.contains("12."));
1471 assert!(row.contains("Twelfth item"));
1472 }
1473
1474 #[test]
1478 fn test_check_pass_wraps_tty() {
1479 let long = "No sells detected below the configured peg target during the monitoring window across all tracked pairs and venues in scope";
1480 let row = check_pass_styled(long, true);
1481 assert!(row.contains("✓"));
1482 assert!(row.lines().count() > 1, "should wrap to multiple lines");
1483 }
1484
1485 #[test]
1486 fn test_check_fail_wraps_tty() {
1487 let long = "Bid depth is significantly below the minimum threshold required for healthy market conditions on this particular trading pair";
1488 let row = check_fail_styled(long, true);
1489 assert!(row.contains("✗"));
1490 assert!(row.lines().count() > 1, "should wrap to multiple lines");
1491 }
1492
1493 #[test]
1494 fn test_warning_row_wraps_tty() {
1495 let long = "Source code is NOT verified — unable to perform full source-level analysis on this contract. Consider requesting verification from the deployer.";
1496 let row = warning_row_styled(long, true);
1497 assert!(row.contains("⚠"));
1498 assert!(row.lines().count() > 1, "should wrap to multiple lines");
1499 }
1500
1501 #[test]
1502 fn test_info_row_wraps_tty() {
1503 let long = "No audit reports found in any public database. Check block explorer and auditor databases manually for third-party audit information and verification.";
1504 let row = info_row_styled(long, true);
1505 assert!(row.contains("ℹ"));
1506 assert!(row.lines().count() > 1, "should wrap to multiple lines");
1507 }
1508
1509 #[test]
1510 fn test_numbered_row_wraps_tty() {
1511 let long = "This is a very long description that should eventually wrap to the next line when the terminal width is narrow enough to force it";
1512 let row = numbered_row_styled(1, long, true);
1513 assert!(row.contains("1."));
1514 assert!(row.lines().count() > 1, "should wrap to multiple lines");
1515 }
1516}