Skip to main content

clitest_lib/
term.rs

1use std::sync::Mutex;
2pub use termcolor::Color;
3use termcolor::{ColorChoice, StandardStream};
4use unicode_segmentation::UnicodeSegmentation;
5use unicode_width::UnicodeWidthStr;
6
7pub static STDOUT: std::sync::LazyLock<Mutex<StandardStream>> =
8    std::sync::LazyLock::new(|| Mutex::new(StandardStream::stdout(ColorChoice::Auto)));
9
10pub static IS_UTF8: std::sync::LazyLock<bool> = std::sync::LazyLock::new(|| {
11    utf8_supported::utf8_supported() == utf8_supported::Utf8Support::UTF8
12});
13
14/// Estimate the width of the terminal. Falls back to 79 if the width cannot be
15/// determined.
16pub fn term_width() -> usize {
17    termsize::get().map(|s| (s.cols - 1) as usize).unwrap_or(79)
18}
19
20#[allow(clippy::while_let_loop)]
21pub fn compute_rule_string(message: &str, max_width: usize) -> String {
22    if message.width() <= max_width {
23        message.to_string()
24    } else {
25        let mut chars = message.graphemes(true);
26
27        let mut start = String::with_capacity(max_width / 2);
28        let mut end = String::with_capacity(max_width / 2);
29        let ellipsis = "…";
30
31        loop {
32            if let Some(grapheme) = chars.next() {
33                let prev_len = start.len();
34                start.push_str(grapheme);
35                if start.width() + end.width() + ellipsis.width() > max_width {
36                    start.truncate(prev_len);
37                    break;
38                }
39            } else {
40                break;
41            }
42            if let Some(grapheme) = chars.next_back() {
43                let prev_len = end.len();
44                end.insert_str(0, grapheme);
45                if start.width() + end.width() + ellipsis.width() > max_width {
46                    end.drain(0..(end.len() - prev_len));
47                    break;
48                }
49            } else {
50                break;
51            }
52        }
53
54        let s = format!("{start}{ellipsis}{end}");
55        debug_assert!(s.width() <= max_width);
56        s
57    }
58}
59
60#[macro_export]
61macro_rules! println {
62    ($($arg:tt)*) => {
63        {
64            use std::io::Write;
65            _ = writeln!($crate::term::STDOUT.lock().unwrap(), $($arg)*);
66        }
67    };
68}
69
70#[macro_export]
71macro_rules! cwriteln {
72    ($stream:expr) => {
73        {
74            use termcolor::{WriteColor, ColorSpec};
75            let stream: &mut dyn WriteColor = &mut $stream;
76            _ = stream.set_color(&ColorSpec::new());
77            _ = writeln!(stream);
78        }
79    };
80    ($stream:expr, $(fg=$fg:expr,)? $(bg=$bg:expr,)? $(bold=$bold:expr,)? $(dimmed=$dimmed:expr,)? $literal:literal $($arg:tt)*) => {
81        {
82            #[allow(unused_imports)]
83            use std::io::Write;
84            $crate::cwrite!($stream, $(fg=$fg,)? $(bg=$bg,)? $(bold=$bold,)? $(dimmed=$dimmed,)? $literal $($arg)*);
85            _ = writeln!($stream);
86        }
87    };
88}
89
90#[macro_export]
91macro_rules! cwrite {
92    ($stream:expr, $(fg=$fg:expr,)? $(bg=$bg:expr,)? $(bold=$bold:expr,)? $(dimmed=$dimmed:expr,)? $literal:literal $($arg:tt)*) => {
93        {
94            #[allow(unused_imports)]
95            use termcolor::{WriteColor, ColorSpec};
96            #[allow(unused_imports)]
97            use std::io::Write;
98
99            #[allow(unused_mut)]
100            let mut color = ColorSpec::new();
101            $(
102                color.set_bg(Some($bg));
103            )?
104            $(
105                color.set_fg(Some($fg));
106            )?
107            $(
108                color.set_bold($bold);
109            )?
110            $(
111                color.set_dimmed($dimmed);
112            )?
113            _ = $stream.set_color(&color);
114            let mut s = format!($literal $($arg)*);
115            if s.contains('\x1b') {
116                s = s.replace('\x1b', "\u{241B}"); // "ESC"
117            }
118            _ = write!($stream, "{s}");
119            _ = $stream.set_color(&ColorSpec::new());
120        }
121    };
122}
123
124#[macro_export]
125macro_rules! cprintln {
126    () => {
127        {
128            let mut stdout = $crate::term::STDOUT.lock().unwrap();
129            $crate::cwriteln!(&mut *stdout);
130        }
131    };
132    ($(fg=$fg:expr,)? $(bg=$bg:expr,)? $(bold=$bold:expr,)? $(dimmed=$dimmed:expr,)? $literal:literal $($arg:tt)*) => {
133        {
134            let mut stdout = $crate::term::STDOUT.lock().unwrap();
135            $crate::cwriteln!(&mut stdout, $(fg=$fg,)? $(bg=$bg,)? $(bold=$bold,)? $(dimmed=$dimmed,)? $literal $($arg)*);
136        }
137    };
138}
139
140#[macro_export]
141macro_rules! cprint {
142    ($(fg=$fg:expr,)? $(bg=$bg:expr,)? $(bold=$bold:expr,)? $(dimmed=$dimmed:expr,)? $literal:literal $($arg:tt)*) => {
143        {
144            let mut stdout = $crate::term::STDOUT.lock().unwrap();
145            $crate::cwrite!(&mut stdout, $(fg=$fg,)? $(bg=$bg,)? $(bold=$bold,)? $(dimmed=$dimmed,)? $literal $($arg)*);
146        }
147    };
148}
149
150/// Print a rule of dashes, optionally with an embedded message.
151///
152/// ```nocompile
153/// -[messsage]----------------- ...
154/// ```
155#[macro_export]
156macro_rules! cwriteln_rule {
157    ($stream:expr) => {
158        let is_utf8 = *$crate::term::IS_UTF8;
159
160        if is_utf8 {
161            $crate::cwriteln!(
162                $stream,
163                dimmed = true,
164                "{:─>count$}",
165                "",
166                count = $crate::term::term_width() - 1
167            );
168        } else {
169            $crate::cwriteln!(
170                $stream,
171                dimmed = true,
172                "{:->count$}",
173                "",
174                count = $crate::term::term_width() - 1
175            );
176        }
177    };
178    ($stream:expr, $(fg=$fg:expr,)? $(bg=$bg:expr,)? $(bold=$bold:expr,)? $(dimmed=$dimmed:expr,)? $literal:literal $($arg:tt)*) => {
179        use ::unicode_width::UnicodeWidthStr;
180
181        let message = format!($literal $($arg)*);
182        const UNTOUCHABLE: usize = 1 + 8; // --[ ... ]--
183
184        // If there's not enough space, just skip printing the extra rule overlay.
185        if $crate::term::term_width() > UNTOUCHABLE {
186            let max_width = $crate::term::term_width() - UNTOUCHABLE;
187            let message = $crate::term::compute_rule_string(&message, max_width);
188            let message_width = message.width();
189
190            let is_utf8 = *$crate::term::IS_UTF8;
191
192            if is_utf8 {
193                $crate::cwrite!($stream, dimmed = true, "{:─>count$}", "", count = max_width - message_width);
194            } else {
195                $crate::cwrite!($stream, dimmed = true, "{:->count$}", "", count = max_width - message_width);
196            }
197
198            if is_utf8 {
199                $crate::cwrite!($stream, dimmed = true, "┨ ");
200            } else {
201                $crate::cwrite!($stream, dimmed = true, "[ ");
202            }
203            $crate::cwrite!($stream, $(fg = $fg,)? $(bg = $bg,)? $(bold = $bold,)? $(dimmed = $dimmed,)? "{message}");
204            if is_utf8 {
205                $crate::cwrite!($stream, dimmed = true, " ┣");
206            } else {
207                $crate::cwrite!($stream, dimmed = true, " ]");
208            }
209
210            if is_utf8 {
211                $crate::cwrite!($stream, dimmed = true, "━━");
212            } else {
213                $crate::cwrite!($stream, dimmed = true, "--");
214            }
215            $crate::cwriteln!($stream);
216        } else {
217            $crate::cwriteln_rule!($stream);
218        }
219    }
220}
221
222#[macro_export]
223macro_rules! cprintln_rule {
224    () => {
225        {
226            let mut stdout = $crate::term::STDOUT.lock().unwrap();
227            $crate::cwriteln_rule!(&mut stdout);
228        }
229    };
230    ($(fg=$fg:expr,)? $(bg=$bg:expr,)? $(bold=$bold:expr,)? $(dimmed=$dimmed:expr,)? $literal:literal $($arg:tt)*) => {
231        {
232            let mut stdout = $crate::term::STDOUT.lock().unwrap();
233            $crate::cwriteln_rule!(&mut *stdout, $(fg=$fg,)? $(bg=$bg,)? $(bold=$bold,)? $(dimmed=$dimmed,)? $literal $($arg)*);
234        }
235    };
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_compute_rule_string() {
244        assert_eq!(compute_rule_string("Hello, world!", 10), "Hello…rld!");
245        assert_eq!(compute_rule_string("Hello, world!", 11), "Hello…orld!");
246        assert_eq!(compute_rule_string("Hello, world!", 12), "Hello,…orld!");
247        assert_eq!(compute_rule_string("Hello, world!", 13), "Hello, world!");
248        assert_eq!(compute_rule_string("Hello, world!", 14), "Hello, world!");
249    }
250}