scope/display/
terminal.rs1use 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 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
37pub 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
51pub 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
64pub 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
80pub fn check_pass(msg: &str) -> String {
86 if is_tty() {
87 format!("{} {} {}", "│".cyan(), "✓".green(), msg)
88 } else {
89 format!("│ ✓ {}", msg)
90 }
91}
92
93pub fn check_fail(msg: &str) -> String {
99 if is_tty() {
100 format!("{} {} {}", "│".cyan(), "✗".red(), msg)
101 } else {
102 format!("│ ✗ {}", msg)
103 }
104}
105
106pub 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
121pub 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
135pub 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
149pub 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
166pub fn blank_row() -> String {
168 if is_tty() {
169 format!("{}", "│".cyan())
170 } else {
171 "│".to_string()
172 }
173}
174
175pub 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#[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}