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