1use super::theme::{Painter, Role};
10use crate::market::health::{PegBand, PegBands};
11use std::sync::atomic::{AtomicUsize, Ordering};
12use unicode_width::UnicodeWidthStr;
13
14pub use super::theme::{
20 color_enabled, color_enabled_stderr, color_enabled_with, set_color_override,
21};
22
23static WIDTH_OVERRIDE: AtomicUsize = AtomicUsize::new(0);
26
27pub fn set_width_override(width: Option<usize>) {
31 WIDTH_OVERRIDE.store(width.unwrap_or(0), Ordering::Relaxed);
32}
33
34pub fn width_override() -> Option<usize> {
36 match WIDTH_OVERRIDE.load(Ordering::Relaxed) {
37 0 => None,
38 w => Some(w),
39 }
40}
41
42pub fn terminal_width() -> usize {
48 width_or_real(width_override())
49}
50
51fn width_or_real(over: Option<usize>) -> usize {
53 over.unwrap_or_else(|| {
54 crossterm::terminal::size()
55 .map(|(w, _)| w as usize)
56 .unwrap_or(80)
57 })
58}
59
60#[cfg(test)]
63pub(crate) fn width_lock() -> std::sync::MutexGuard<'static, ()> {
64 static WIDTH_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
65 WIDTH_LOCK.lock().unwrap_or_else(|p| p.into_inner())
66}
67
68const RULE_WIDTH: usize = 50;
70
71fn rule_at(len: usize, prefix_cols: usize, cap: Option<usize>) -> String {
76 let len = match cap {
77 Some(w) => len.min(w.saturating_sub(prefix_cols)),
78 None => len,
79 };
80 "─".repeat(len)
81}
82
83pub fn wrap_lines(text: &str, content_width: usize) -> Vec<String> {
96 if content_width == 0 {
97 return vec![text.to_string()];
98 }
99
100 let mut lines: Vec<String> = Vec::new();
101 let mut current_line = String::new();
102 let mut current_len = 0usize;
103
104 for word in text.split_whitespace() {
105 let word_len = word.width();
106
107 if current_len == 0 {
108 current_line.push_str(word);
110 current_len = word_len;
111 } else if current_len + 1 + word_len <= content_width {
112 current_line.push(' ');
114 current_line.push_str(word);
115 current_len += 1 + word_len;
116 } else {
117 lines.push(current_line);
119 current_line = word.to_string();
120 current_len = word_len;
121 }
122 }
123
124 if !current_line.is_empty() {
125 lines.push(current_line);
126 }
127
128 if lines.is_empty() {
129 lines.push(String::new());
130 }
131
132 lines
133}
134
135fn content_width_for(prefix_width: usize) -> usize {
141 terminal_width().saturating_sub(prefix_width)
142}
143
144const MARK_PREFIX_COLS: usize = 5;
150
151const LABEL_COLS: usize = 18;
153
154fn rail(p: Painter) -> String {
156 p.paint(Role::Accent, "│")
157}
158
159fn label(p: Painter, text: &str) -> String {
162 p.paint(Role::Secondary, &format!("{:<LABEL_COLS$}", text))
163}
164
165fn marked_row(p: Painter, glyph: String, text_role: Role, msg: &str) -> String {
168 let wrapped = wrap_lines(msg, content_width_for(MARK_PREFIX_COLS));
169 let mut out = format!("{} {} {}", rail(p), glyph, p.paint(text_role, &wrapped[0]));
170 for line in &wrapped[1..] {
171 out.push_str(&format!("\n{} {}", rail(p), p.paint(text_role, line)));
172 }
173 out
174}
175
176pub fn section_header(title: &str) -> String {
182 section_header_styled(title, color_enabled())
183}
184
185fn section_header_styled(title: &str, tty: bool) -> String {
186 section_header_at(title, tty, width_override())
187}
188
189fn section_header_at(title: &str, tty: bool, cap: Option<usize>) -> String {
191 let p = Painter::new(tty);
192 let prefix = title.width() + 4;
194 let pad = rule_at(RULE_WIDTH.saturating_sub(prefix), prefix, cap);
195 format!(
196 "\n{} {} {}",
197 p.paint(Role::Accent, "┌─"),
198 p.paint_strong(Role::Accent, title),
199 p.paint(Role::Accent, &pad)
200 )
201}
202
203pub fn subsection_header(title: &str) -> String {
210 subsection_header_styled(title, color_enabled())
211}
212
213fn subsection_header_styled(title: &str, tty: bool) -> String {
214 let p = Painter::new(tty);
215 format!(
216 "{}\n{}{}",
217 rail(p),
218 p.paint(Role::Accent, "├── "),
219 p.paint_strong(Role::Accent, title)
220 )
221}
222
223pub fn kv_row(key: &str, value: &str) -> String {
229 kv_row_styled(key, value, color_enabled())
230}
231
232fn kv_row_styled(key: &str, value: &str, tty: bool) -> String {
233 let p = Painter::new(tty);
234 let prefix_cols = 3 + LABEL_COLS;
236 let avail = terminal_width().saturating_sub(prefix_cols);
237 let wrapped = wrap_lines(value, avail);
238
239 let mut out = format!("{} {}{}", rail(p), label(p, key), wrapped[0]);
240 for line in &wrapped[1..] {
242 out.push_str(&format!("\n{} {:<LABEL_COLS$}{}", rail(p), "", line));
243 }
244 out
245}
246
247pub fn kv_row_delta(key: &str, value: f64, formatted: &str) -> String {
249 kv_row_delta_styled(key, value, formatted, color_enabled())
250}
251
252fn kv_row_delta_styled(key: &str, value: f64, formatted: &str, tty: bool) -> String {
253 let p = Painter::new(tty);
254 let role = if value > 0.0 {
255 Role::Ok
256 } else if value < 0.0 {
257 Role::Err
258 } else {
259 Role::Secondary
260 };
261 format!("{} {}{}", rail(p), label(p, key), p.paint(role, formatted))
262}
263
264pub fn check_pass(msg: &str) -> String {
270 check_pass_styled(msg, color_enabled())
271}
272
273fn check_pass_styled(msg: &str, tty: bool) -> String {
274 let p = Painter::new(tty);
275 marked_row(p, p.paint(Role::Ok, "✓"), Role::Neutral, msg)
276}
277
278pub fn check_fail(msg: &str) -> String {
284 check_fail_styled(msg, color_enabled())
285}
286
287fn check_fail_styled(msg: &str, tty: bool) -> String {
288 let p = Painter::new(tty);
289 marked_row(p, p.paint(Role::Err, "✗"), Role::Neutral, msg)
290}
291
292pub fn check_na(msg: &str) -> String {
298 check_na_styled(msg, color_enabled())
299}
300
301fn check_na_styled(msg: &str, tty: bool) -> String {
302 let p = Painter::new(tty);
303 marked_row(p, p.paint(Role::Secondary, "–"), Role::Secondary, msg)
304}
305
306pub fn check_warn(msg: &str) -> String {
312 check_warn_styled(msg, color_enabled())
313}
314
315fn check_warn_styled(msg: &str, tty: bool) -> String {
316 let p = Painter::new(tty);
317 marked_row(p, p.paint_strong(Role::Warn, "!"), Role::Warn, msg)
318}
319
320pub fn status_line(healthy: bool) -> String {
322 status_line_styled(healthy, color_enabled())
323}
324
325fn status_line_styled(healthy: bool, tty: bool) -> String {
326 let p = Painter::new(tty);
327 let verdict = if healthy {
328 p.paint_strong(Role::Ok, "HEALTHY")
329 } else {
330 p.paint_strong(Role::Err, "UNHEALTHY")
331 };
332 format!("{} {}", rail(p), verdict)
333}
334
335pub fn section_footer() -> String {
341 section_footer_styled(color_enabled())
342}
343
344fn section_footer_styled(tty: bool) -> String {
345 section_footer_at(tty, width_override())
346}
347
348fn section_footer_at(tty: bool, cap: Option<usize>) -> String {
350 let line = rule_at(RULE_WIDTH, 1, cap);
351 Painter::new(tty).paint(Role::Accent, &format!("└{}", line))
352}
353
354pub fn separator() -> String {
360 separator_styled(color_enabled())
361}
362
363fn separator_styled(tty: bool) -> String {
364 separator_at(tty, width_override())
365}
366
367fn separator_at(tty: bool, cap: Option<usize>) -> String {
369 let line = rule_at(RULE_WIDTH, 1, cap);
370 Painter::new(tty).paint(Role::Accent, &format!("├{}", line))
371}
372
373pub fn format_price_peg(price: f64, peg: &PegBands) -> String {
377 format_price_peg_styled(price, peg, color_enabled())
378}
379
380fn format_price_peg_styled(price: f64, peg: &PegBands, tty: bool) -> String {
381 let role = match peg.classify(price) {
382 PegBand::OnPeg => Role::Ok,
383 PegBand::Slight => Role::Warn,
384 PegBand::Off => Role::Err,
385 };
386 let decimals = crate::display::format::price_decimals(peg.target());
388 Painter::new(tty).paint(role, &format!("{price:.decimals$}"))
389}
390
391#[cfg(test)]
393fn usd_peg() -> PegBands {
394 crate::market::health::HealthThresholds::default().peg_bands()
395}
396
397pub fn blank_row() -> String {
399 blank_row_styled(color_enabled())
400}
401
402fn blank_row_styled(tty: bool) -> String {
403 rail(Painter::new(tty))
404}
405
406pub fn orderbook_level(
408 price: f64,
409 quantity: f64,
410 base: &str,
411 value: f64,
412 quote: &str,
413 peg: &PegBands,
414) -> String {
415 orderbook_level_styled(price, quantity, base, value, quote, peg, color_enabled())
416}
417
418fn orderbook_level_styled(
419 price: f64,
420 quantity: f64,
421 base: &str,
422 value: f64,
423 quote: &str,
424 peg: &PegBands,
425 tty: bool,
426) -> String {
427 let p = Painter::new(tty);
428 format!(
429 "{} {} {:>10.2} {} {:>10.2} {quote}",
430 rail(p),
431 format_price_peg_styled(price, peg, tty),
432 quantity,
433 p.paint(Role::Secondary, base),
434 value
435 )
436}
437
438pub fn score_bar(label: &str, score: u32, max: u32) -> String {
448 score_bar_styled(label, score, max, color_enabled())
449}
450
451fn score_bar_styled(label_text: &str, score: u32, max: u32, tty: bool) -> String {
452 let p = Painter::new(tty);
453 let width = 20usize;
454 let filled = ((score as f64 / max as f64) * width as f64).round() as usize;
455 let filled = filled.min(width);
456 let empty = width - filled;
457 let bar = format!(
458 "[{}{}] {}/{}",
459 "█".repeat(filled),
460 "─".repeat(empty),
461 score,
462 max
463 );
464 let role = if score >= 80 {
465 Role::Ok
466 } else if score >= 50 {
467 Role::Warn
468 } else {
469 Role::Err
470 };
471 format!(
472 "{} {}{}",
473 rail(p),
474 label(p, label_text),
475 p.paint(role, &bar)
476 )
477}
478
479pub fn severity_label(severity: &str) -> String {
484 severity_label_styled(severity, color_enabled())
485}
486
487fn severity_label_styled(severity: &str, tty: bool) -> String {
488 let p = Painter::new(tty);
489 match severity.to_lowercase().as_str() {
490 "critical" => p.paint_strong(Role::Err, severity),
491 "high" => p.paint(Role::Err, severity),
492 "medium" => p.paint(Role::Warn, severity),
493 "low" => p.paint(Role::Accent, severity),
494 _ => p.paint(Role::Secondary, severity),
495 }
496}
497
498pub fn warning_row(msg: &str) -> String {
504 warning_row_styled(msg, color_enabled())
505}
506
507fn warning_row_styled(msg: &str, tty: bool) -> String {
508 let p = Painter::new(tty);
509 marked_row(p, p.paint_strong(Role::Warn, "⚠"), Role::Warn, msg)
510}
511
512pub fn info_row(msg: &str) -> String {
518 info_row_styled(msg, color_enabled())
519}
520
521fn info_row_styled(msg: &str, tty: bool) -> String {
522 let p = Painter::new(tty);
523 marked_row(p, p.paint(Role::Info, "ℹ"), Role::Secondary, msg)
524}
525
526pub fn link_row(label: &str, url: &str) -> String {
532 link_row_styled(label, url, color_enabled())
533}
534
535fn link_row_styled(label_text: &str, url: &str, tty: bool) -> String {
536 let p = Painter::new(tty);
537 format!(
538 "{} {}{}",
539 rail(p),
540 label(p, label_text),
541 p.paint(Role::Link, url)
542 )
543}
544
545pub fn detail_row(msg: &str) -> String {
551 detail_row_styled(msg, color_enabled())
552}
553
554fn detail_row_styled(msg: &str, tty: bool) -> String {
555 let p = Painter::new(tty);
556 let avail = content_width_for(7);
558 let wrapped = wrap_lines(msg, avail);
559
560 let mut out = String::new();
561 for (i, line) in wrapped.iter().enumerate() {
562 if i > 0 {
563 out.push('\n');
564 }
565 out.push_str(&format!(
566 "{} {}",
567 rail(p),
568 p.paint(Role::Secondary, line)
569 ));
570 }
571 out
572}
573
574pub fn bullet_row(msg: &str) -> String {
580 bullet_row_styled(msg, color_enabled())
581}
582
583fn bullet_row_styled(msg: &str, tty: bool) -> String {
584 let p = Painter::new(tty);
585 let avail = content_width_for(7);
587 let wrapped = wrap_lines(msg, avail);
588
589 let mut out = format!(
590 "{} {} {}",
591 rail(p),
592 p.paint(Role::Secondary, "•"),
593 wrapped[0]
594 );
595 for line in &wrapped[1..] {
596 out.push_str(&format!("\n{} {}", rail(p), line));
597 }
598 out
599}
600
601pub struct Col<'a> {
607 pub label: &'a str,
609 pub width: usize,
611 pub align: char,
613}
614
615pub fn table_header(cols: &[Col]) -> String {
622 table_header_styled(cols, color_enabled())
623}
624
625fn table_header_styled(cols: &[Col], tty: bool) -> String {
626 table_header_at(cols, tty, width_override())
627}
628
629fn table_header_at(cols: &[Col], tty: bool, cap: Option<usize>) -> String {
631 let p = Painter::new(tty);
632 let mut header = String::new();
633 for col in cols {
634 if col.align == '>' {
635 header.push_str(&format!("{:>width$} ", col.label, width = col.width));
636 } else {
637 header.push_str(&format!("{:<width$} ", col.label, width = col.width));
638 }
639 }
640 let header = header.trim_end().to_string();
641 let rule_len = cols.iter().map(|c| c.width + 2).sum::<usize>();
642 let line = rule_at(rule_len, 3, cap);
643
644 format!(
645 "{} {}\n{} {}",
646 rail(p),
647 p.paint_strong(Role::Accent, &header),
648 rail(p),
649 p.paint(Role::Accent, &line)
650 )
651}
652
653pub fn table_row(cols: &[Col], values: &[&str]) -> String {
661 table_row_styled(cols, values, color_enabled())
662}
663
664fn table_row_styled(cols: &[Col], values: &[&str], tty: bool) -> String {
665 let mut row = String::new();
666 for (i, col) in cols.iter().enumerate() {
667 let val = values.get(i).copied().unwrap_or("");
668 if col.align == '>' {
669 row.push_str(&format!("{:>width$} ", val, width = col.width));
670 } else {
671 row.push_str(&format!("{:<width$} ", val, width = col.width));
672 }
673 }
674 let row = row.trim_end().to_string();
675 format!("{} {}", rail(Painter::new(tty)), row)
676}
677
678pub fn numbered_row(index: usize, msg: &str) -> String {
684 numbered_row_styled(index, msg, color_enabled())
685}
686
687fn numbered_row_styled(index: usize, msg: &str, tty: bool) -> String {
688 let p = Painter::new(tty);
689 let prefix_len = 4 + format!("{}.", index).len();
691 let avail = terminal_width().saturating_sub(prefix_len);
692 let wrapped = wrap_lines(msg, avail);
693
694 let num = format!("{}.", index);
695 let mut out = format!(
696 "{} {} {}",
697 rail(p),
698 p.paint(Role::Secondary, &num),
699 wrapped[0]
700 );
701 let cont_pad = " ".repeat(num.len() + 1);
702 for line in &wrapped[1..] {
703 out.push_str(&format!("\n{} {}{}", rail(p), cont_pad, line));
704 }
705 out
706}
707
708#[cfg(test)]
713mod golden_tests;
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718
719 #[test]
720 fn test_section_header_contains_title() {
721 let header = section_header("Token Health");
722 assert!(header.contains("Token Health"));
723 assert!(header.contains("┌─"));
724 }
725
726 #[test]
727 fn test_subsection_header_contains_title() {
728 let header = subsection_header("DEX Analytics");
729 assert!(header.contains("DEX Analytics"));
730 assert!(header.contains("├──"));
731 }
732
733 #[test]
734 fn test_kv_row_contains_key_value() {
735 let row = kv_row("Price", "$1.00");
736 assert!(row.contains("Price"));
737 assert!(row.contains("$1.00"));
738 assert!(row.contains("│"));
739 }
740
741 #[test]
742 fn test_kv_row_delta_positive() {
743 let row = kv_row_delta("24h Change", 5.0, "+5.00%");
744 assert!(row.contains("+5.00%"));
745 }
746
747 #[test]
748 fn test_kv_row_delta_negative() {
749 let row = kv_row_delta("24h Change", -3.0, "-3.00%");
750 assert!(row.contains("-3.00%"));
751 }
752
753 #[test]
754 fn test_check_pass() {
755 let line = check_pass("No sells below peg");
756 assert!(line.contains("✓"));
757 assert!(line.contains("No sells below peg"));
758 }
759
760 #[test]
761 fn test_check_fail() {
762 let line = check_fail("Bid depth too low");
763 assert!(line.contains("✗"));
764 assert!(line.contains("Bid depth too low"));
765 }
766
767 #[test]
768 fn test_status_line_healthy() {
769 let line = status_line(true);
770 assert!(line.contains("HEALTHY"));
771 }
772
773 #[test]
774 fn test_status_line_unhealthy() {
775 let line = status_line(false);
776 assert!(line.contains("UNHEALTHY"));
777 }
778
779 #[test]
780 fn test_section_footer() {
781 let footer = section_footer();
782 assert!(footer.contains("└"));
783 }
784
785 #[test]
786 fn test_separator() {
787 let sep = separator();
788 assert!(sep.contains("├"));
789 }
790
791 #[test]
792 fn test_format_price_peg_near() {
793 let s = format_price_peg(1.0001, &usd_peg());
794 assert!(s.contains("1.0001"));
795 }
796
797 #[test]
798 fn test_format_price_peg_far() {
799 let s = format_price_peg(0.95, &usd_peg());
800 assert!(s.contains("0.9500"));
801 }
802
803 #[test]
804 fn test_blank_row() {
805 let row = blank_row();
806 assert!(row.contains("│"));
807 }
808
809 #[test]
810 fn test_orderbook_level() {
811 let row = orderbook_level(1.0001, 500.0, "DAI", 500.05, "USDT", &usd_peg());
812 assert!(row.contains("DAI"));
813 assert!(row.contains("USDT"));
814 }
815
816 #[test]
817 fn test_kv_row_delta_zero_value() {
818 let row = kv_row_delta("Change", 0.0, "0.00%");
820 assert!(row.contains("Change"));
821 assert!(row.contains("0.00%"));
822 assert!(row.contains("│"));
823 }
824
825 #[test]
826 fn test_format_price_peg_moderate_deviation() {
827 let s = format_price_peg(1.002, &usd_peg());
829 assert!(s.contains("1.0020"));
830 }
831
832 #[test]
833 fn test_orderbook_level_various_prices() {
834 let row_low = orderbook_level(0.9990, 100.0, "DAI", 99.90, "USDT", &usd_peg());
835 let row_mid = orderbook_level(1.0000, 100.0, "DAI", 100.0, "USDT", &usd_peg());
836 let row_high = orderbook_level(1.0015, 100.0, "DAI", 100.15, "USDT", &usd_peg());
837 assert!(row_low.contains("0.9990"));
838 assert!(row_mid.contains("1.0000"));
839 assert!(row_high.contains("1.0015"));
840 assert!(row_low.contains("│"));
841 assert!(row_mid.contains("│"));
842 assert!(row_high.contains("│"));
843 }
844
845 #[test]
846 fn test_non_tty_returns_unicode_box_characters() {
847 let header = section_header("Test");
849 let sub = subsection_header("Sub");
850 let kv = kv_row("Key", "Val");
851 let pass = check_pass("ok");
852 let fail = check_fail("err");
853 let footer = section_footer();
854 let sep = separator();
855 let blank = blank_row();
856 let status_healthy = status_line(true);
857 let status_unhealthy = status_line(false);
858
859 assert!(header.contains('┌'), "section_header should contain ┌");
860 assert!(header.contains('─'), "section_header should contain ─");
861 assert!(sub.contains('│'), "subsection_header should contain │");
862 assert!(sub.contains('├'), "subsection_header should contain ├");
863 assert!(kv.contains('│'), "kv_row should contain │");
864 assert!(pass.contains('✓'), "check_pass should contain ✓");
865 assert!(fail.contains('✗'), "check_fail should contain ✗");
866 assert!(footer.contains('└'), "section_footer should contain └");
867 assert!(sep.contains('├'), "separator should contain ├");
868 assert!(blank.contains('│'), "blank_row should contain │");
869 assert!(status_healthy.contains("HEALTHY"));
870 assert!(status_unhealthy.contains("UNHEALTHY"));
871 }
872
873 #[test]
878 fn test_section_header_tty() {
879 let header = section_header_styled("Token Health", true);
880 assert!(header.contains("Token Health"));
881 assert!(header.contains("┌─"));
882 }
883
884 #[test]
885 fn test_subsection_header_tty() {
886 let header = subsection_header_styled("DEX", true);
887 assert!(header.contains("DEX"));
888 assert!(header.contains("├──"));
889 }
890
891 #[test]
892 fn test_kv_row_tty() {
893 let row = kv_row_styled("Price", "$1.00", true);
894 assert!(row.contains("Price"));
895 assert!(row.contains("$1.00"));
896 }
897
898 #[test]
899 fn test_kv_row_delta_positive_tty() {
900 let row = kv_row_delta_styled("Change", 5.0, "+5%", true);
901 assert!(row.contains("+5%"));
902 }
903
904 #[test]
905 fn test_kv_row_delta_negative_tty() {
906 let row = kv_row_delta_styled("Change", -3.0, "-3%", true);
907 assert!(row.contains("-3%"));
908 }
909
910 #[test]
911 fn test_kv_row_delta_zero_tty() {
912 let row = kv_row_delta_styled("Change", 0.0, "0.00%", true);
913 assert!(row.contains("0.00%"));
914 }
915
916 #[test]
917 fn test_check_pass_tty() {
918 let line = check_pass_styled("ok", true);
919 assert!(line.contains("✓"));
920 assert!(line.contains("ok"));
921 }
922
923 #[test]
924 fn test_check_fail_tty() {
925 let line = check_fail_styled("err", true);
926 assert!(line.contains("✗"));
927 assert!(line.contains("err"));
928 }
929
930 #[test]
931 fn test_status_line_healthy_tty() {
932 let line = status_line_styled(true, true);
933 assert!(line.contains("HEALTHY"));
934 }
935
936 #[test]
937 fn test_status_line_unhealthy_tty() {
938 let line = status_line_styled(false, true);
939 assert!(line.contains("UNHEALTHY"));
940 }
941
942 #[test]
943 fn test_section_footer_tty() {
944 let footer = section_footer_styled(true);
945 assert!(footer.contains("└"));
946 }
947
948 #[test]
949 fn test_separator_tty() {
950 let sep = separator_styled(true);
951 assert!(sep.contains("├"));
952 }
953
954 #[test]
955 fn test_format_price_peg_tty_near() {
956 let s = format_price_peg_styled(1.0001, &usd_peg(), true);
957 assert!(s.contains("1.0001"));
958 }
959
960 #[test]
961 fn test_format_price_peg_tty_moderate() {
962 let s = format_price_peg_styled(1.003, &usd_peg(), true);
963 assert!(s.contains("1.0030"));
964 }
965
966 #[test]
967 fn test_format_price_peg_tty_far() {
968 let s = format_price_peg_styled(0.95, &usd_peg(), true);
969 assert!(s.contains("0.9500"));
970 }
971
972 #[test]
973 fn test_blank_row_tty() {
974 let row = blank_row_styled(true);
975 assert!(row.contains("│"));
976 }
977
978 #[test]
979 fn test_orderbook_level_tty() {
980 let row = orderbook_level_styled(1.0001, 500.0, "DAI", 500.05, "USDT", &usd_peg(), true);
981 assert!(row.contains("DAI"));
982 assert!(row.contains("USDT"));
983 }
984
985 #[test]
990 fn test_score_bar_high() {
991 let bar = score_bar("Security Score", 85, 100);
992 assert!(bar.contains("85/100"));
993 assert!(bar.contains("│"));
994 assert!(bar.contains("█"));
995 }
996
997 #[test]
998 fn test_score_bar_low() {
999 let bar = score_bar("Security Score", 20, 100);
1000 assert!(bar.contains("20/100"));
1001 }
1002
1003 #[test]
1004 fn test_score_bar_zero() {
1005 let bar = score_bar("Score", 0, 100);
1006 assert!(bar.contains("0/100"));
1007 assert!(bar.contains("│"));
1008 }
1009
1010 #[test]
1011 fn test_score_bar_max() {
1012 let bar = score_bar("Score", 100, 100);
1013 assert!(bar.contains("100/100"));
1014 }
1015
1016 #[test]
1017 fn test_severity_label_critical() {
1018 let label = severity_label("Critical");
1019 assert!(label.contains("Critical"));
1020 }
1021
1022 #[test]
1023 fn test_severity_label_high() {
1024 let label = severity_label("High");
1025 assert!(label.contains("High"));
1026 }
1027
1028 #[test]
1029 fn test_severity_label_medium() {
1030 let label = severity_label("Medium");
1031 assert!(label.contains("Medium"));
1032 }
1033
1034 #[test]
1035 fn test_severity_label_low() {
1036 let label = severity_label("Low");
1037 assert!(label.contains("Low"));
1038 }
1039
1040 #[test]
1041 fn test_severity_label_informational() {
1042 let label = severity_label("Informational");
1043 assert!(label.contains("Informational"));
1044 }
1045
1046 #[test]
1047 fn test_warning_row() {
1048 let row = warning_row("Source code is NOT verified");
1049 assert!(row.contains("⚠"));
1050 assert!(row.contains("Source code is NOT verified"));
1051 assert!(row.contains("│"));
1052 }
1053
1054 #[test]
1055 fn test_info_row() {
1056 let row = info_row("No findings triggered");
1057 assert!(row.contains("ℹ"));
1058 assert!(row.contains("No findings triggered"));
1059 assert!(row.contains("│"));
1060 }
1061
1062 #[test]
1063 fn test_link_row() {
1064 let row = link_row("Explorer", "https://etherscan.io");
1065 assert!(row.contains("Explorer"));
1066 assert!(row.contains("https://etherscan.io"));
1067 assert!(row.contains("│"));
1068 }
1069
1070 #[test]
1071 fn test_detail_row() {
1072 let row = detail_row("Contract is not verified");
1073 assert!(row.contains("Contract is not verified"));
1074 assert!(row.contains("│"));
1075 }
1076
1077 #[test]
1078 fn test_bullet_row() {
1079 let row = bullet_row("mint (Critical): Can mint tokens");
1080 assert!(row.contains("•"));
1081 assert!(row.contains("mint (Critical): Can mint tokens"));
1082 assert!(row.contains("│"));
1083 }
1084
1085 #[test]
1090 fn test_score_bar_tty_high() {
1091 let bar = score_bar_styled("Security Score", 85, 100, true);
1092 assert!(bar.contains("85/100"));
1093 assert!(bar.contains("█"));
1094 }
1095
1096 #[test]
1097 fn test_score_bar_tty_medium() {
1098 let bar = score_bar_styled("Security Score", 55, 100, true);
1099 assert!(bar.contains("55/100"));
1100 }
1101
1102 #[test]
1103 fn test_score_bar_tty_low() {
1104 let bar = score_bar_styled("Security Score", 20, 100, true);
1105 assert!(bar.contains("20/100"));
1106 }
1107
1108 #[test]
1109 fn test_severity_label_tty_critical() {
1110 let label = severity_label_styled("Critical", true);
1111 assert!(label.contains("Critical"));
1112 }
1113
1114 #[test]
1115 fn test_severity_label_tty_low() {
1116 let label = severity_label_styled("Low", true);
1117 assert!(label.contains("Low"));
1118 }
1119
1120 #[test]
1121 fn test_severity_label_tty_unknown() {
1122 let label = severity_label_styled("Unknown", true);
1123 assert!(label.contains("Unknown"));
1124 }
1125
1126 #[test]
1127 fn test_warning_row_tty() {
1128 let row = warning_row_styled("Alert!", true);
1129 assert!(row.contains("⚠"));
1130 assert!(row.contains("Alert!"));
1131 }
1132
1133 #[test]
1134 fn test_info_row_tty() {
1135 let row = info_row_styled("Note", true);
1136 assert!(row.contains("ℹ"));
1137 assert!(row.contains("Note"));
1138 }
1139
1140 #[test]
1141 fn test_link_row_tty() {
1142 let row = link_row_styled("Explorer", "https://example.com", true);
1143 assert!(row.contains("Explorer"));
1144 assert!(row.contains("https://example.com"));
1145 }
1146
1147 #[test]
1148 fn test_detail_row_tty() {
1149 let row = detail_row_styled("Some detail", true);
1150 assert!(row.contains("Some detail"));
1151 assert!(row.contains("│"));
1152 }
1153
1154 #[test]
1155 fn test_bullet_row_tty() {
1156 let row = bullet_row_styled("Item one", true);
1157 assert!(row.contains("•"));
1158 assert!(row.contains("Item one"));
1159 }
1160
1161 #[test]
1166 fn test_wrap_lines_short_text() {
1167 let lines = wrap_lines("hello world", 80);
1168 assert_eq!(lines, vec!["hello world"]);
1169 }
1170
1171 #[test]
1172 fn test_wrap_lines_exact_fit() {
1173 let lines = wrap_lines("abcde fghij", 11);
1174 assert_eq!(lines, vec!["abcde fghij"]);
1175 }
1176
1177 #[test]
1178 fn test_wrap_lines_wraps_at_word_boundary() {
1179 let lines = wrap_lines("hello world foo", 11);
1180 assert_eq!(lines, vec!["hello world", "foo"]);
1181 }
1182
1183 #[test]
1184 fn test_wrap_lines_multiple_wraps() {
1185 let lines = wrap_lines("a b c d e f g", 3);
1186 assert_eq!(lines, vec!["a b", "c d", "e f", "g"]);
1187 }
1188
1189 #[test]
1190 fn test_wrap_lines_long_word_exceeds_width() {
1191 let lines = wrap_lines(
1193 "https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7",
1194 40,
1195 );
1196 assert_eq!(lines.len(), 1);
1197 assert!(lines[0].starts_with("https://"));
1198 }
1199
1200 #[test]
1201 fn test_wrap_lines_long_word_after_short() {
1202 let lines = wrap_lines(
1203 "Explorer: https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7",
1204 30,
1205 );
1206 assert_eq!(lines.len(), 2);
1207 assert_eq!(lines[0], "Explorer:");
1208 assert!(lines[1].starts_with("https://"));
1209 }
1210
1211 #[test]
1212 fn test_wrap_lines_empty_string() {
1213 let lines = wrap_lines("", 80);
1214 assert_eq!(lines, vec![""]);
1215 }
1216
1217 #[test]
1218 fn test_wrap_lines_zero_width() {
1219 let lines = wrap_lines("hello", 0);
1220 assert_eq!(lines, vec!["hello"]);
1221 }
1222
1223 #[test]
1224 fn test_wrap_lines_single_word() {
1225 let lines = wrap_lines("hello", 80);
1226 assert_eq!(lines, vec!["hello"]);
1227 }
1228
1229 #[test]
1230 fn test_wrap_lines_preserves_all_words() {
1231 let input = "Contract source code is not verified. Full vulnerability analysis requires verified source code.";
1232 let lines = wrap_lines(input, 40);
1233 let reassembled: String = lines.join(" ");
1234 assert_eq!(reassembled, input);
1235 }
1236
1237 #[test]
1238 fn test_terminal_width_returns_positive() {
1239 let _lock = width_lock();
1240 let w = terminal_width();
1241 assert!(w > 0);
1242 }
1243
1244 #[test]
1245 fn test_content_width_for_reasonable_prefix() {
1246 let _lock = width_lock();
1247 let w = content_width_for(7);
1248 assert!(w > 0 || terminal_width() <= 7);
1250 }
1251
1252 #[test]
1257 fn test_detail_row_wraps_long_text() {
1258 let _lock = width_lock();
1259 let long = "Contract source code is not verified and full vulnerability analysis requires verified source code for accurate results";
1261 let row = detail_row_styled(long, false);
1262 let line_count = row.lines().count();
1264 assert!(row.contains("│"), "should contain box-drawing prefix");
1266 for line in row.lines() {
1268 assert!(
1269 line.starts_with('│'),
1270 "continuation line should start with │: {}",
1271 line
1272 );
1273 }
1274 assert!(row.contains("Contract"));
1276 assert!(row.contains("results"));
1277 if terminal_width() < 80 {
1279 assert!(line_count > 1, "should wrap on narrow terminal");
1280 }
1281 }
1282
1283 #[test]
1284 fn test_check_pass_wraps_long_text() {
1285 let _lock = width_lock();
1286 let long = "No sells detected below the configured peg target during the monitoring window across all tracked pairs";
1287 let row = check_pass_styled(long, false);
1288 assert!(row.contains("✓"));
1289 assert!(row.contains("No sells"));
1290 assert!(row.contains("pairs"));
1291 for line in row.lines() {
1292 assert!(line.starts_with('│'));
1293 }
1294 }
1295
1296 #[test]
1297 fn test_check_fail_wraps_long_text() {
1298 let _lock = width_lock();
1299 let long = "Bid depth is significantly below the minimum threshold required for healthy market conditions on this trading pair";
1300 let row = check_fail_styled(long, false);
1301 assert!(row.contains("✗"));
1302 for line in row.lines() {
1303 assert!(line.starts_with('│'));
1304 }
1305 }
1306
1307 #[test]
1308 fn test_warning_row_wraps_long_text() {
1309 let _lock = width_lock();
1310 let long = "Source code is NOT verified — unable to perform source-level analysis. Consider requesting verification.";
1311 let row = warning_row_styled(long, false);
1312 assert!(row.contains("⚠"));
1313 for line in row.lines() {
1314 assert!(line.starts_with('│'));
1315 }
1316 }
1317
1318 #[test]
1319 fn test_info_row_wraps_long_text() {
1320 let _lock = width_lock();
1321 let long = "No audit reports found. Check block explorer and auditor databases manually for third-party audit information.";
1322 let row = info_row_styled(long, false);
1323 assert!(row.contains("ℹ"));
1324 for line in row.lines() {
1325 assert!(line.starts_with('│'));
1326 }
1327 }
1328
1329 #[test]
1330 fn test_bullet_row_wraps_long_text() {
1331 let _lock = width_lock();
1332 let long = "Uniswap V3 integration detected with slippage protection enabled and deadline protection enabled for all swap calls";
1333 let row = bullet_row_styled(long, false);
1334 assert!(row.contains("•"));
1335 for line in row.lines() {
1336 assert!(line.starts_with('│'));
1337 }
1338 }
1339
1340 #[test]
1341 fn test_kv_row_wraps_long_value() {
1342 let _lock = width_lock();
1343 let long_val = "Verified contract with comprehensive access controls and multiple security features including role-based permissions";
1344 let row = kv_row_styled("Summary", long_val, false);
1345 assert!(row.contains("Summary"));
1346 assert!(row.contains("Verified"));
1347 assert!(row.contains("permissions"));
1348 for line in row.lines() {
1349 assert!(line.starts_with('│'));
1350 }
1351 }
1352
1353 #[test]
1354 fn test_kv_row_short_value_no_wrap() {
1355 let _lock = width_lock();
1356 let row = kv_row_styled("Chain", "ethereum", false);
1357 assert_eq!(row.lines().count(), 1);
1358 assert!(row.contains("Chain"));
1359 assert!(row.contains("ethereum"));
1360 }
1361
1362 #[test]
1363 fn test_detail_row_wraps_tty() {
1364 let _lock = width_lock();
1365 let long = "Contract source code is not verified and full vulnerability analysis requires verified source code for accurate results";
1366 let row = detail_row_styled(long, true);
1367 assert!(row.contains("│"));
1368 assert!(row.contains("Contract"));
1369 assert!(row.contains("results"));
1370 }
1371
1372 #[test]
1373 fn test_bullet_row_wraps_tty() {
1374 let _lock = width_lock();
1375 let long = "Uniswap V3 integration detected with slippage protection enabled and deadline protection enabled for all swap calls";
1376 let row = bullet_row_styled(long, true);
1377 assert!(row.contains("•"));
1378 assert!(row.contains("Uniswap"));
1379 assert!(row.contains("calls"));
1380 }
1381
1382 #[test]
1383 fn test_kv_row_wraps_tty() {
1384 let _lock = width_lock();
1385 let long_val = "Verified contract with comprehensive access controls and multiple security features including role-based permissions";
1386 let row = kv_row_styled("Summary", long_val, true);
1387 assert!(row.contains("Summary"));
1388 assert!(row.contains("Verified"));
1389 assert!(row.contains("permissions"));
1390 }
1391
1392 #[test]
1397 fn test_table_header_contains_labels() {
1398 let cols = &[
1399 Col {
1400 label: "Rank",
1401 width: 6,
1402 align: '>',
1403 },
1404 Col {
1405 label: "Name",
1406 width: 20,
1407 align: '<',
1408 },
1409 ];
1410 let header = table_header(cols);
1411 assert!(header.contains("Rank"));
1412 assert!(header.contains("Name"));
1413 assert!(header.contains("│"));
1414 assert!(header.contains("─"));
1415 }
1416
1417 #[test]
1418 fn test_table_row_contains_values() {
1419 let cols = &[
1420 Col {
1421 label: "Rank",
1422 width: 6,
1423 align: '>',
1424 },
1425 Col {
1426 label: "Name",
1427 width: 20,
1428 align: '<',
1429 },
1430 ];
1431 let row = table_row(cols, &["1", "TestToken"]);
1432 assert!(row.contains("1"));
1433 assert!(row.contains("TestToken"));
1434 assert!(row.contains("│"));
1435 }
1436
1437 #[test]
1438 fn test_table_row_missing_values() {
1439 let cols = &[
1440 Col {
1441 label: "A",
1442 width: 5,
1443 align: '<',
1444 },
1445 Col {
1446 label: "B",
1447 width: 5,
1448 align: '<',
1449 },
1450 ];
1451 let row = table_row(cols, &["only"]);
1452 assert!(row.contains("only"));
1453 assert!(row.contains("│"));
1454 }
1455
1456 #[test]
1457 fn test_table_header_tty() {
1458 let cols = &[Col {
1459 label: "Price",
1460 width: 10,
1461 align: '>',
1462 }];
1463 let header = table_header_styled(cols, true);
1464 assert!(header.contains("Price"));
1465 assert!(header.contains("│"));
1466 assert!(header.contains("\x1b[36m\x1b[1m"), "{header:?}");
1468 let plain = table_header_styled(cols, false);
1469 assert!(!plain.contains('\x1b'), "plain output has no escapes");
1470 }
1471
1472 #[test]
1473 fn test_table_row_tty() {
1474 let cols = &[Col {
1475 label: "Price",
1476 width: 10,
1477 align: '>',
1478 }];
1479 let row = table_row_styled(cols, &["$1.00"], true);
1480 assert!(row.contains("$1.00"));
1481 assert!(row.contains("│"));
1482 }
1483
1484 #[test]
1485 fn test_numbered_row_basic() {
1486 let row = numbered_row(1, "First item");
1487 assert!(row.contains("1."));
1488 assert!(row.contains("First item"));
1489 assert!(row.contains("│"));
1490 }
1491
1492 #[test]
1493 fn test_numbered_row_wraps() {
1494 let _lock = width_lock();
1495 let long = "This is a very long description that should eventually wrap to the next line when the terminal width is narrow enough";
1496 let row = numbered_row(1, long);
1497 assert!(row.contains("1."));
1498 assert!(row.contains("This"));
1499 assert!(row.contains("enough"));
1500 for line in row.lines() {
1501 assert!(line.contains('│'));
1502 }
1503 }
1504
1505 #[test]
1506 fn test_numbered_row_tty() {
1507 let row = numbered_row_styled(5, "Fifth item", true);
1508 assert!(row.contains("5."));
1509 assert!(row.contains("Fifth item"));
1510 }
1511
1512 #[test]
1513 fn test_numbered_row_double_digits() {
1514 let row = numbered_row(12, "Twelfth item");
1515 assert!(row.contains("12."));
1516 assert!(row.contains("Twelfth item"));
1517 }
1518
1519 #[test]
1523 fn test_check_pass_wraps_tty() {
1524 let _lock = width_lock();
1525 let long = &format!("{} ", "No sells detected below the configured peg target during the monitoring window across all tracked pairs and venues in scope").repeat(4);
1527 let row = check_pass_styled(long, true);
1528 assert!(row.contains("✓"));
1529 assert!(row.lines().count() > 1, "should wrap to multiple lines");
1530 }
1531
1532 #[test]
1533 fn test_check_fail_wraps_tty() {
1534 let _lock = width_lock();
1535 let long = &format!("{} ", "Bid depth is significantly below the minimum threshold required for healthy market conditions on this particular trading pair").repeat(4);
1537 let row = check_fail_styled(long, true);
1538 assert!(row.contains("✗"));
1539 assert!(row.lines().count() > 1, "should wrap to multiple lines");
1540 }
1541
1542 #[test]
1543 fn test_check_na_plain_and_tty_wrap() {
1544 let _lock = width_lock();
1545 let long = &format!("{} ", "Spread: n/a (synthetic AMM book; the real spread is the pool fee, which the data source does not report)").repeat(4);
1548 for tty in [false, true] {
1549 let row = check_na_styled(long, tty);
1550 assert!(row.contains("–"), "tty={}", tty);
1551 assert!(!row.contains("✓") && !row.contains("✗"), "tty={}", tty);
1552 assert!(row.lines().count() > 1, "should wrap, tty={}", tty);
1553 }
1554 assert!(check_na_styled("short", false).starts_with("│ – short"));
1555 }
1556
1557 #[test]
1558 fn test_warning_row_wraps_tty() {
1559 let _lock = width_lock();
1560 let long = &format!("{} ", "Source code is NOT verified — unable to perform full source-level analysis on this contract. Consider requesting verification from the deployer.").repeat(4);
1562 let row = warning_row_styled(long, true);
1563 assert!(row.contains("⚠"));
1564 assert!(row.lines().count() > 1, "should wrap to multiple lines");
1565 }
1566
1567 #[test]
1568 fn test_info_row_wraps_tty() {
1569 let _lock = width_lock();
1570 let long = &format!("{} ", "No audit reports found in any public database. Check block explorer and auditor databases manually for third-party audit information and verification.").repeat(4);
1572 let row = info_row_styled(long, true);
1573 assert!(row.contains("ℹ"));
1574 assert!(row.lines().count() > 1, "should wrap to multiple lines");
1575 }
1576
1577 #[test]
1578 fn test_numbered_row_wraps_tty() {
1579 let _lock = width_lock();
1580 let long = &format!("{} ", "This is a very long description that should eventually wrap to the next line when the terminal width is narrow enough to force it").repeat(4);
1582 let row = numbered_row_styled(1, long, true);
1583 assert!(row.contains("1."));
1584 assert!(row.lines().count() > 1, "should wrap to multiple lines");
1585 }
1586
1587 #[test]
1588 fn test_check_warn_styled_keeps_text_and_continuation_bar() {
1589 let msg = "word ".repeat(60);
1590 let plain = check_warn_styled(&msg, false);
1591 let styled = check_warn_styled(&msg, true);
1592 assert!(plain.starts_with("│ ! "));
1593 assert!(!plain.contains('\x1b'));
1594 assert!(styled.contains('\x1b'), "color on a color-enabled stream");
1595 assert!(plain.lines().count() > 1);
1597 assert_eq!(styled.lines().count(), plain.lines().count());
1598 let words = |s: &str| s.matches("word").count();
1599 assert_eq!(words(&plain), 60);
1600 assert_eq!(words(&styled), 60);
1601 }
1602
1603 fn widest(text: &str) -> usize {
1608 text.lines().map(UnicodeWidthStr::width).max().unwrap_or(0)
1609 }
1610
1611 fn two_cols() -> [Col<'static>; 2] {
1612 [
1613 Col {
1614 label: "Rank",
1615 width: 150,
1616 align: '<',
1617 },
1618 Col {
1619 label: "Value",
1620 width: 150,
1621 align: '>',
1622 },
1623 ]
1624 }
1625
1626 #[test]
1627 fn test_rules_fit_a_narrow_pane() {
1628 let cap = Some(40);
1630 assert!(widest(§ion_footer_at(false, cap)) <= 40);
1631 assert!(widest(&separator_at(false, cap)) <= 40);
1632 assert!(widest(§ion_header_at("Available Venues", false, cap)) <= 40);
1633 let header = table_header_at(&two_cols(), false, cap);
1634 let rule = header.lines().nth(1).unwrap_or_default();
1635 assert_eq!(rule.width(), 40, "{header}");
1636 }
1637
1638 #[test]
1639 fn test_a_wide_pane_keeps_the_fixed_rules() {
1640 let cap = Some(200);
1642 assert_eq!(section_footer_at(false, cap).width(), 51);
1643 assert_eq!(separator_at(false, cap).width(), 51);
1644 let header = section_header_at("Available Venues", false, cap);
1645 assert_eq!(header.trim().width(), 50);
1646 }
1647
1648 #[test]
1649 fn test_without_an_override_rules_keep_their_fixed_widths() {
1650 let _lock = width_lock();
1653 assert_eq!(width_override(), None);
1654 assert_eq!(section_footer_styled(false).width(), 51);
1655 assert_eq!(separator_styled(false).width(), 51);
1656 let header = section_header_styled("Available Venues", false);
1657 assert_eq!(header.trim().width(), 50);
1658 let table = table_header_styled(&two_cols(), false);
1661 assert_eq!(table.lines().nth(1).unwrap_or_default().width(), 3 + 304);
1662 assert_eq!(section_footer_at(false, None), section_footer_styled(false));
1663 }
1664
1665 #[test]
1666 fn test_terminal_width_uses_the_override_first() {
1667 assert_eq!(width_or_real(Some(40)), 40);
1668 let real = crossterm::terminal::size()
1669 .map(|(w, _)| w as usize)
1670 .unwrap_or(80);
1671 assert_eq!(width_or_real(None), real);
1672 }
1673
1674 #[test]
1675 fn test_width_override_set_and_clear() {
1676 let _lock = width_lock();
1679 let real = terminal_width();
1680 set_width_override(Some(real));
1681 assert_eq!(width_override(), Some(real));
1682 set_width_override(Some(0));
1683 assert_eq!(width_override(), None, "Some(0) removes it");
1684 set_width_override(Some(real));
1685 set_width_override(None);
1686 assert_eq!(width_override(), None);
1687 assert_eq!(terminal_width(), real);
1688 }
1689
1690 #[test]
1691 fn test_wrap_lines_counts_display_width_of_cjk() {
1692 let lines = wrap_lines("日本 語 です", 7);
1695 assert_eq!(lines, vec!["日本 語", "です"]);
1696 for l in &lines {
1697 assert!(l.width() <= 7, "{l}");
1698 }
1699 }
1700}