Skip to main content

scope/display/
terminal.rs

1//! # Terminal Display Helpers
2//!
3//! Styled output utilities for rich, user-friendly terminal presentation.
4//! Uses `owo-colors` for color and `unicode` box-drawing characters for structure.
5//! All helpers respect non-TTY contexts (piped output) by falling back to plain text.
6
7use owo_colors::OwoColorize;
8use std::io::IsTerminal;
9
10// ============================================================================
11// Color-aware helpers
12// ============================================================================
13
14/// Returns `true` when stdout is an interactive terminal (not piped).
15pub fn is_tty() -> bool {
16    std::io::stdout().is_terminal()
17}
18
19/// Section header with colored title and box-drawing underline.
20///
21/// ```text
22/// ┌─ Token Health ─────────────────────────────
23/// ```
24pub fn section_header(title: &str) -> String {
25    let width: usize = 50;
26    let pad = width.saturating_sub(title.len() + 4);
27    if is_tty() {
28        format!(
29            "{}",
30            format!("\n┌─ {} {}", title.bold(), "─".repeat(pad)).cyan()
31        )
32    } else {
33        format!("\n┌─ {} {}", title, "─".repeat(pad))
34    }
35}
36
37/// Sub-section header (lighter weight).
38///
39/// ```text
40/// │
41/// ├── DEX Analytics
42/// ```
43pub fn subsection_header(title: &str) -> String {
44    if is_tty() {
45        format!("{}\n{}", "│".cyan(), format!("├── {}", title.bold()).cyan())
46    } else {
47        format!("│\n├── {}", title)
48    }
49}
50
51/// A key-value row inside a section, with aligned values.
52///
53/// ```text
54/// │  Price            $0.9999
55/// ```
56pub fn kv_row(key: &str, value: &str) -> String {
57    if is_tty() {
58        format!("{}  {:<18}{}", "│".cyan(), key.dimmed(), value)
59    } else {
60        format!("│  {:<18}{}", key, value)
61    }
62}
63
64/// A key-value row where the value is colored based on positive/negative.
65pub fn kv_row_delta(key: &str, value: f64, formatted: &str) -> String {
66    if is_tty() {
67        let colored_val = if value > 0.0 {
68            format!("{}", formatted.green())
69        } else if value < 0.0 {
70            format!("{}", formatted.red())
71        } else {
72            format!("{}", formatted.dimmed())
73        };
74        format!("{}  {:<18}{}", "│".cyan(), key.dimmed(), colored_val)
75    } else {
76        format!("│  {:<18}{}", key, formatted)
77    }
78}
79
80/// Health check pass line.
81///
82/// ```text
83/// │  ✓ No sells below peg
84/// ```
85pub fn check_pass(msg: &str) -> String {
86    if is_tty() {
87        format!("{}  {} {}", "│".cyan(), "✓".green(), msg)
88    } else {
89        format!("│  ✓ {}", msg)
90    }
91}
92
93/// Health check fail line.
94///
95/// ```text
96/// │  ✗ Bid depth: 0 USDT < 3000 USDT minimum
97/// ```
98pub fn check_fail(msg: &str) -> String {
99    if is_tty() {
100        format!("{}  {} {}", "│".cyan(), "✗".red(), msg)
101    } else {
102        format!("│  ✗ {}", msg)
103    }
104}
105
106/// Overall status line (healthy / unhealthy).
107pub fn status_line(healthy: bool) -> String {
108    if is_tty() {
109        if healthy {
110            format!("{}  {}", "│".cyan(), "HEALTHY".green().bold())
111        } else {
112            format!("{}  {}", "│".cyan(), "UNHEALTHY".red().bold())
113        }
114    } else if healthy {
115        "│  HEALTHY".to_string()
116    } else {
117        "│  UNHEALTHY".to_string()
118    }
119}
120
121/// Section footer (closing box line).
122///
123/// ```text
124/// └──────────────────────────────────────────────
125/// ```
126pub fn section_footer() -> String {
127    let line = "─".repeat(50);
128    if is_tty() {
129        format!("{}", format!("└{}", line).cyan())
130    } else {
131        format!("└{}", line)
132    }
133}
134
135/// A separator row inside a section.
136///
137/// ```text
138/// ├──────────────────────────────────────────────
139/// ```
140pub fn separator() -> String {
141    let line = "─".repeat(50);
142    if is_tty() {
143        format!("{}", format!("├{}", line).cyan())
144    } else {
145        format!("├{}", line)
146    }
147}
148
149/// Format a price with color for peg deviation.
150/// Green if within 0.1% of target, yellow if within 0.5%, red otherwise.
151pub fn format_price_peg(price: f64, target: f64) -> String {
152    let deviation = ((price - target) / target).abs();
153    let text = format!("{:.4}", price);
154    if !is_tty() {
155        return text;
156    }
157    if deviation < 0.001 {
158        format!("{}", text.green())
159    } else if deviation < 0.005 {
160        format!("{}", text.yellow())
161    } else {
162        format!("{}", text.red())
163    }
164}
165
166/// Empty line with continuation bar.
167pub fn blank_row() -> String {
168    if is_tty() {
169        format!("{}", "│".cyan())
170    } else {
171        "│".to_string()
172    }
173}
174
175/// An order book level row with price coloring relative to peg.
176pub fn orderbook_level(price: f64, quantity: f64, base: &str, value: f64, peg: f64) -> String {
177    let price_str = format_price_peg(price, peg);
178    if is_tty() {
179        format!(
180            "{}    {}  {:>10.2} {}  {:>10.2} USDT",
181            "│".cyan(),
182            price_str,
183            quantity,
184            base.dimmed(),
185            value
186        )
187    } else {
188        format!(
189            "│    {:.4}  {:>10.2} {}  {:>10.2} USDT",
190            price, quantity, base, value
191        )
192    }
193}
194
195// ============================================================================
196// Unit Tests
197// ============================================================================
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_section_header_contains_title() {
205        let header = section_header("Token Health");
206        assert!(header.contains("Token Health"));
207        assert!(header.contains("┌─"));
208    }
209
210    #[test]
211    fn test_subsection_header_contains_title() {
212        let header = subsection_header("DEX Analytics");
213        assert!(header.contains("DEX Analytics"));
214        assert!(header.contains("├──"));
215    }
216
217    #[test]
218    fn test_kv_row_contains_key_value() {
219        let row = kv_row("Price", "$1.00");
220        assert!(row.contains("Price"));
221        assert!(row.contains("$1.00"));
222        assert!(row.contains("│"));
223    }
224
225    #[test]
226    fn test_kv_row_delta_positive() {
227        let row = kv_row_delta("24h Change", 5.0, "+5.00%");
228        assert!(row.contains("+5.00%"));
229    }
230
231    #[test]
232    fn test_kv_row_delta_negative() {
233        let row = kv_row_delta("24h Change", -3.0, "-3.00%");
234        assert!(row.contains("-3.00%"));
235    }
236
237    #[test]
238    fn test_check_pass() {
239        let line = check_pass("No sells below peg");
240        assert!(line.contains("✓"));
241        assert!(line.contains("No sells below peg"));
242    }
243
244    #[test]
245    fn test_check_fail() {
246        let line = check_fail("Bid depth too low");
247        assert!(line.contains("✗"));
248        assert!(line.contains("Bid depth too low"));
249    }
250
251    #[test]
252    fn test_status_line_healthy() {
253        let line = status_line(true);
254        assert!(line.contains("HEALTHY"));
255    }
256
257    #[test]
258    fn test_status_line_unhealthy() {
259        let line = status_line(false);
260        assert!(line.contains("UNHEALTHY"));
261    }
262
263    #[test]
264    fn test_section_footer() {
265        let footer = section_footer();
266        assert!(footer.contains("└"));
267    }
268
269    #[test]
270    fn test_separator() {
271        let sep = separator();
272        assert!(sep.contains("├"));
273    }
274
275    #[test]
276    fn test_format_price_peg_near() {
277        let s = format_price_peg(1.0001, 1.0);
278        assert!(s.contains("1.0001"));
279    }
280
281    #[test]
282    fn test_format_price_peg_far() {
283        let s = format_price_peg(0.95, 1.0);
284        assert!(s.contains("0.9500"));
285    }
286
287    #[test]
288    fn test_blank_row() {
289        let row = blank_row();
290        assert!(row.contains("│"));
291    }
292
293    #[test]
294    fn test_orderbook_level() {
295        let row = orderbook_level(1.0001, 500.0, "PUSD", 500.05, 1.0);
296        assert!(row.contains("PUSD"));
297        assert!(row.contains("USDT"));
298    }
299}