Skip to main content

tftio_cli_common/
output.rs

1//! Output utilities for consistent terminal formatting.
2
3use colored::Colorize;
4use is_terminal::IsTerminal;
5use std::io;
6
7/// Check if stdout is a TTY (terminal).
8///
9/// Returns `true` if stdout is connected to a terminal, `false` if piped/redirected.
10/// This is used to determine whether to use colored output and fancy formatting.
11#[must_use]
12pub fn is_tty() -> bool {
13    io::stdout().is_terminal()
14}
15
16/// Check if stderr is a TTY (terminal).
17#[must_use]
18pub fn stderr_is_tty() -> bool {
19    io::stderr().is_terminal()
20}
21
22/// Format a success message with green checkmark.
23///
24/// Returns colored output if stdout is a TTY, plain text otherwise.
25#[must_use]
26pub fn success(msg: &str) -> String {
27    if is_tty() {
28        format!("{} {}", "✅".green(), msg.green())
29    } else {
30        format!("[OK] {msg}")
31    }
32}
33
34/// Format an error message with red X.
35///
36/// Returns colored output if stdout is a TTY, plain text otherwise.
37#[must_use]
38pub fn error(msg: &str) -> String {
39    if is_tty() {
40        format!("{} {}", "❌".red(), msg.red().bold())
41    } else {
42        format!("[ERROR] {msg}")
43    }
44}
45
46/// Format a warning message with yellow warning sign.
47///
48/// Returns colored output if stdout is a TTY, plain text otherwise.
49#[must_use]
50pub fn warning(msg: &str) -> String {
51    if is_tty() {
52        format!("{} {}", "⚠️".yellow(), msg.yellow())
53    } else {
54        format!("[WARNING] {msg}")
55    }
56}
57
58/// Format an info message with blue info sign.
59///
60/// Returns colored output if stdout is a TTY, plain text otherwise.
61#[must_use]
62pub fn info(msg: &str) -> String {
63    if is_tty() {
64        format!("{} {}", "ℹ️".blue(), msg.blue())
65    } else {
66        format!("[INFO] {msg}")
67    }
68}
69
70/// Format a header with separator line.
71///
72/// Returns colored output if stdout is a TTY, plain text otherwise.
73#[must_use]
74pub fn header(title: &str, width: usize) -> String {
75    if is_tty() {
76        format!("{}\n{}", title.bold().cyan(), "=".repeat(width).cyan())
77    } else {
78        format!("{title}\n{}", "=".repeat(width))
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_is_tty_returns_bool() {
88        // Just verify it returns a boolean without panicking
89        let _result = is_tty();
90        // Function executed successfully if we get here
91    }
92
93    #[test]
94    fn test_stderr_is_tty_returns_bool() {
95        let _result = stderr_is_tty();
96    }
97
98    #[test]
99    fn test_success_format() {
100        let msg = success("test message");
101        // Should contain the message
102        assert!(msg.contains("test message"));
103        // Should contain either emoji or [OK]
104        assert!(msg.contains("✅") || msg.contains("[OK]"));
105    }
106
107    #[test]
108    fn test_error_format() {
109        let msg = error("test error");
110        assert!(msg.contains("test error"));
111        assert!(msg.contains("❌") || msg.contains("[ERROR]"));
112    }
113
114    #[test]
115    fn test_warning_format() {
116        let msg = warning("test warning");
117        assert!(msg.contains("test warning"));
118        assert!(msg.contains("⚠️") || msg.contains("[WARNING]"));
119    }
120
121    #[test]
122    fn test_info_format() {
123        let msg = info("test info");
124        assert!(msg.contains("test info"));
125        assert!(msg.contains("ℹ️") || msg.contains("[INFO]"));
126    }
127
128    #[test]
129    fn test_header_format() {
130        let msg = header("Test Header", 20);
131        assert!(msg.contains("Test Header"));
132        assert!(msg.contains("===================="));
133    }
134}