1use owo_colors::OwoColorize;
8use std::io::IsTerminal;
9
10pub fn is_tty() -> bool {
16 std::io::stdout().is_terminal()
17}
18
19pub fn section_header(title: &str) -> String {
25 section_header_styled(title, is_tty())
26}
27
28fn section_header_styled(title: &str, tty: bool) -> String {
29 let width: usize = 50;
30 let pad = width.saturating_sub(title.len() + 4);
31 if tty {
32 format!(
33 "{}",
34 format!("\n┌─ {} {}", title.bold(), "─".repeat(pad)).cyan()
35 )
36 } else {
37 format!("\n┌─ {} {}", title, "─".repeat(pad))
38 }
39}
40
41pub fn subsection_header(title: &str) -> String {
48 subsection_header_styled(title, is_tty())
49}
50
51fn subsection_header_styled(title: &str, tty: bool) -> String {
52 if tty {
53 format!("{}\n{}", "│".cyan(), format!("├── {}", title.bold()).cyan())
54 } else {
55 format!("│\n├── {}", title)
56 }
57}
58
59pub fn kv_row(key: &str, value: &str) -> String {
65 kv_row_styled(key, value, is_tty())
66}
67
68fn kv_row_styled(key: &str, value: &str, tty: bool) -> String {
69 if tty {
70 format!("{} {:<18}{}", "│".cyan(), key.dimmed(), value)
71 } else {
72 format!("│ {:<18}{}", key, value)
73 }
74}
75
76pub fn kv_row_delta(key: &str, value: f64, formatted: &str) -> String {
78 kv_row_delta_styled(key, value, formatted, is_tty())
79}
80
81fn kv_row_delta_styled(key: &str, value: f64, formatted: &str, tty: bool) -> String {
82 if tty {
83 let colored_val = if value > 0.0 {
84 format!("{}", formatted.green())
85 } else if value < 0.0 {
86 format!("{}", formatted.red())
87 } else {
88 format!("{}", formatted.dimmed())
89 };
90 format!("{} {:<18}{}", "│".cyan(), key.dimmed(), colored_val)
91 } else {
92 format!("│ {:<18}{}", key, formatted)
93 }
94}
95
96pub fn check_pass(msg: &str) -> String {
102 check_pass_styled(msg, is_tty())
103}
104
105fn check_pass_styled(msg: &str, tty: bool) -> String {
106 if tty {
107 format!("{} {} {}", "│".cyan(), "✓".green(), msg)
108 } else {
109 format!("│ ✓ {}", msg)
110 }
111}
112
113pub fn check_fail(msg: &str) -> String {
119 check_fail_styled(msg, is_tty())
120}
121
122fn check_fail_styled(msg: &str, tty: bool) -> String {
123 if tty {
124 format!("{} {} {}", "│".cyan(), "✗".red(), msg)
125 } else {
126 format!("│ ✗ {}", msg)
127 }
128}
129
130pub fn status_line(healthy: bool) -> String {
132 status_line_styled(healthy, is_tty())
133}
134
135fn status_line_styled(healthy: bool, tty: bool) -> String {
136 if tty {
137 if healthy {
138 format!("{} {}", "│".cyan(), "HEALTHY".green().bold())
139 } else {
140 format!("{} {}", "│".cyan(), "UNHEALTHY".red().bold())
141 }
142 } else if healthy {
143 "│ HEALTHY".to_string()
144 } else {
145 "│ UNHEALTHY".to_string()
146 }
147}
148
149pub fn section_footer() -> String {
155 section_footer_styled(is_tty())
156}
157
158fn section_footer_styled(tty: bool) -> String {
159 let line = "─".repeat(50);
160 if tty {
161 format!("{}", format!("└{}", line).cyan())
162 } else {
163 format!("└{}", line)
164 }
165}
166
167pub fn separator() -> String {
173 separator_styled(is_tty())
174}
175
176fn separator_styled(tty: bool) -> String {
177 let line = "─".repeat(50);
178 if tty {
179 format!("{}", format!("├{}", line).cyan())
180 } else {
181 format!("├{}", line)
182 }
183}
184
185pub fn format_price_peg(price: f64, target: f64) -> String {
188 format_price_peg_styled(price, target, is_tty())
189}
190
191fn format_price_peg_styled(price: f64, target: f64, tty: bool) -> String {
192 let deviation = ((price - target) / target).abs();
193 let text = format!("{:.4}", price);
194 if !tty {
195 return text;
196 }
197 if deviation < 0.001 {
198 format!("{}", text.green())
199 } else if deviation < 0.005 {
200 format!("{}", text.yellow())
201 } else {
202 format!("{}", text.red())
203 }
204}
205
206pub fn blank_row() -> String {
208 blank_row_styled(is_tty())
209}
210
211fn blank_row_styled(tty: bool) -> String {
212 if tty {
213 format!("{}", "│".cyan())
214 } else {
215 "│".to_string()
216 }
217}
218
219pub fn orderbook_level(price: f64, quantity: f64, base: &str, value: f64, peg: f64) -> String {
221 orderbook_level_styled(price, quantity, base, value, peg, is_tty())
222}
223
224fn orderbook_level_styled(
225 price: f64,
226 quantity: f64,
227 base: &str,
228 value: f64,
229 peg: f64,
230 tty: bool,
231) -> String {
232 let price_str = format_price_peg_styled(price, peg, tty);
233 if tty {
234 format!(
235 "{} {} {:>10.2} {} {:>10.2} USDT",
236 "│".cyan(),
237 price_str,
238 quantity,
239 base.dimmed(),
240 value
241 )
242 } else {
243 format!(
244 "│ {:.4} {:>10.2} {} {:>10.2} USDT",
245 price, quantity, base, value
246 )
247 }
248}
249
250#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_section_header_contains_title() {
260 let header = section_header("Token Health");
261 assert!(header.contains("Token Health"));
262 assert!(header.contains("┌─"));
263 }
264
265 #[test]
266 fn test_subsection_header_contains_title() {
267 let header = subsection_header("DEX Analytics");
268 assert!(header.contains("DEX Analytics"));
269 assert!(header.contains("├──"));
270 }
271
272 #[test]
273 fn test_kv_row_contains_key_value() {
274 let row = kv_row("Price", "$1.00");
275 assert!(row.contains("Price"));
276 assert!(row.contains("$1.00"));
277 assert!(row.contains("│"));
278 }
279
280 #[test]
281 fn test_kv_row_delta_positive() {
282 let row = kv_row_delta("24h Change", 5.0, "+5.00%");
283 assert!(row.contains("+5.00%"));
284 }
285
286 #[test]
287 fn test_kv_row_delta_negative() {
288 let row = kv_row_delta("24h Change", -3.0, "-3.00%");
289 assert!(row.contains("-3.00%"));
290 }
291
292 #[test]
293 fn test_check_pass() {
294 let line = check_pass("No sells below peg");
295 assert!(line.contains("✓"));
296 assert!(line.contains("No sells below peg"));
297 }
298
299 #[test]
300 fn test_check_fail() {
301 let line = check_fail("Bid depth too low");
302 assert!(line.contains("✗"));
303 assert!(line.contains("Bid depth too low"));
304 }
305
306 #[test]
307 fn test_status_line_healthy() {
308 let line = status_line(true);
309 assert!(line.contains("HEALTHY"));
310 }
311
312 #[test]
313 fn test_status_line_unhealthy() {
314 let line = status_line(false);
315 assert!(line.contains("UNHEALTHY"));
316 }
317
318 #[test]
319 fn test_section_footer() {
320 let footer = section_footer();
321 assert!(footer.contains("└"));
322 }
323
324 #[test]
325 fn test_separator() {
326 let sep = separator();
327 assert!(sep.contains("├"));
328 }
329
330 #[test]
331 fn test_format_price_peg_near() {
332 let s = format_price_peg(1.0001, 1.0);
333 assert!(s.contains("1.0001"));
334 }
335
336 #[test]
337 fn test_format_price_peg_far() {
338 let s = format_price_peg(0.95, 1.0);
339 assert!(s.contains("0.9500"));
340 }
341
342 #[test]
343 fn test_blank_row() {
344 let row = blank_row();
345 assert!(row.contains("│"));
346 }
347
348 #[test]
349 fn test_orderbook_level() {
350 let row = orderbook_level(1.0001, 500.0, "PUSD", 500.05, 1.0);
351 assert!(row.contains("PUSD"));
352 assert!(row.contains("USDT"));
353 }
354
355 #[test]
356 fn test_kv_row_delta_zero_value() {
357 let row = kv_row_delta("Change", 0.0, "0.00%");
359 assert!(row.contains("Change"));
360 assert!(row.contains("0.00%"));
361 assert!(row.contains("│"));
362 }
363
364 #[test]
365 fn test_format_price_peg_moderate_deviation() {
366 let s = format_price_peg(1.002, 1.0);
368 assert!(s.contains("1.0020"));
369 }
370
371 #[test]
372 fn test_orderbook_level_various_prices() {
373 let row_low = orderbook_level(0.9990, 100.0, "PUSD", 99.90, 1.0);
374 let row_mid = orderbook_level(1.0000, 100.0, "PUSD", 100.0, 1.0);
375 let row_high = orderbook_level(1.0015, 100.0, "PUSD", 100.15, 1.0);
376 assert!(row_low.contains("0.9990"));
377 assert!(row_mid.contains("1.0000"));
378 assert!(row_high.contains("1.0015"));
379 assert!(row_low.contains("│"));
380 assert!(row_mid.contains("│"));
381 assert!(row_high.contains("│"));
382 }
383
384 #[test]
385 fn test_non_tty_returns_unicode_box_characters() {
386 let header = section_header("Test");
388 let sub = subsection_header("Sub");
389 let kv = kv_row("Key", "Val");
390 let pass = check_pass("ok");
391 let fail = check_fail("err");
392 let footer = section_footer();
393 let sep = separator();
394 let blank = blank_row();
395 let status_healthy = status_line(true);
396 let status_unhealthy = status_line(false);
397
398 assert!(header.contains('┌'), "section_header should contain ┌");
399 assert!(header.contains('─'), "section_header should contain ─");
400 assert!(sub.contains('│'), "subsection_header should contain │");
401 assert!(sub.contains('├'), "subsection_header should contain ├");
402 assert!(kv.contains('│'), "kv_row should contain │");
403 assert!(pass.contains('✓'), "check_pass should contain ✓");
404 assert!(fail.contains('✗'), "check_fail should contain ✗");
405 assert!(footer.contains('└'), "section_footer should contain └");
406 assert!(sep.contains('├'), "separator should contain ├");
407 assert!(blank.contains('│'), "blank_row should contain │");
408 assert!(status_healthy.contains("HEALTHY"));
409 assert!(status_unhealthy.contains("UNHEALTHY"));
410 }
411
412 #[test]
417 fn test_section_header_tty() {
418 let header = section_header_styled("Token Health", true);
419 assert!(header.contains("Token Health"));
420 assert!(header.contains("┌─"));
421 }
422
423 #[test]
424 fn test_subsection_header_tty() {
425 let header = subsection_header_styled("DEX", true);
426 assert!(header.contains("DEX"));
427 assert!(header.contains("├──"));
428 }
429
430 #[test]
431 fn test_kv_row_tty() {
432 let row = kv_row_styled("Price", "$1.00", true);
433 assert!(row.contains("Price"));
434 assert!(row.contains("$1.00"));
435 }
436
437 #[test]
438 fn test_kv_row_delta_positive_tty() {
439 let row = kv_row_delta_styled("Change", 5.0, "+5%", true);
440 assert!(row.contains("+5%"));
441 }
442
443 #[test]
444 fn test_kv_row_delta_negative_tty() {
445 let row = kv_row_delta_styled("Change", -3.0, "-3%", true);
446 assert!(row.contains("-3%"));
447 }
448
449 #[test]
450 fn test_kv_row_delta_zero_tty() {
451 let row = kv_row_delta_styled("Change", 0.0, "0.00%", true);
452 assert!(row.contains("0.00%"));
453 }
454
455 #[test]
456 fn test_check_pass_tty() {
457 let line = check_pass_styled("ok", true);
458 assert!(line.contains("✓"));
459 assert!(line.contains("ok"));
460 }
461
462 #[test]
463 fn test_check_fail_tty() {
464 let line = check_fail_styled("err", true);
465 assert!(line.contains("✗"));
466 assert!(line.contains("err"));
467 }
468
469 #[test]
470 fn test_status_line_healthy_tty() {
471 let line = status_line_styled(true, true);
472 assert!(line.contains("HEALTHY"));
473 }
474
475 #[test]
476 fn test_status_line_unhealthy_tty() {
477 let line = status_line_styled(false, true);
478 assert!(line.contains("UNHEALTHY"));
479 }
480
481 #[test]
482 fn test_section_footer_tty() {
483 let footer = section_footer_styled(true);
484 assert!(footer.contains("└"));
485 }
486
487 #[test]
488 fn test_separator_tty() {
489 let sep = separator_styled(true);
490 assert!(sep.contains("├"));
491 }
492
493 #[test]
494 fn test_format_price_peg_tty_near() {
495 let s = format_price_peg_styled(1.0001, 1.0, true);
496 assert!(s.contains("1.0001"));
497 }
498
499 #[test]
500 fn test_format_price_peg_tty_moderate() {
501 let s = format_price_peg_styled(1.003, 1.0, true);
502 assert!(s.contains("1.0030"));
503 }
504
505 #[test]
506 fn test_format_price_peg_tty_far() {
507 let s = format_price_peg_styled(0.95, 1.0, true);
508 assert!(s.contains("0.9500"));
509 }
510
511 #[test]
512 fn test_blank_row_tty() {
513 let row = blank_row_styled(true);
514 assert!(row.contains("│"));
515 }
516
517 #[test]
518 fn test_orderbook_level_tty() {
519 let row = orderbook_level_styled(1.0001, 500.0, "PUSD", 500.05, 1.0, true);
520 assert!(row.contains("PUSD"));
521 assert!(row.contains("USDT"));
522 }
523}