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            let mut s = format!($literal $($arg)*);
114            if s.contains('\x1b') {
115                s = s.replace('\x1b', "\u{241B}"); // "ESC"
116            }
117            _ = write!($stream, "{s}");
118            _ = $stream.set_color(&ColorSpec::new());
119        }
120    };
121}
122
123#[macro_export]
124macro_rules! cprintln {
125    () => {
126        {
127            let mut stdout = $crate::term::STDOUT.lock().unwrap();
128            $crate::cwriteln!(&mut *stdout);
129        }
130    };
131    ($(fg=$fg:expr,)? $(bg=$bg:expr,)? $(bold=$bold:expr,)? $(dimmed=$dimmed:expr,)? $literal:literal $($arg:tt)*) => {
132        {
133            let mut stdout = $crate::term::STDOUT.lock().unwrap();
134            $crate::cwriteln!(&mut stdout, $(fg=$fg,)? $(bg=$bg,)? $(bold=$bold,)? $(dimmed=$dimmed,)? $literal $($arg)*);
135        }
136    };
137}
138
139#[macro_export]
140macro_rules! cprint {
141    ($(fg=$fg:expr,)? $(bg=$bg:expr,)? $(bold=$bold:expr,)? $(dimmed=$dimmed:expr,)? $literal:literal $($arg:tt)*) => {
142        {
143            let mut stdout = $crate::term::STDOUT.lock().unwrap();
144            $crate::cwrite!(&mut stdout, $(fg=$fg,)? $(bg=$bg,)? $(bold=$bold,)? $(dimmed=$dimmed,)? $literal $($arg)*);
145        }
146    };
147}
148
149/// Print a rule of dashes, optionally with an embedded message.
150///
151/// ```nocompile
152/// -[messsage]----------------- ...
153/// ```
154#[macro_export]
155macro_rules! cwriteln_rule {
156    ($stream:expr) => {
157        let is_utf8 = *$crate::term::IS_UTF8;
158
159        if is_utf8 {
160            $crate::cwriteln!(
161                $stream,
162                dimmed = true,
163                "{:─>count$}",
164                "",
165                count = $crate::term::term_width() - 1
166            );
167        } else {
168            $crate::cwriteln!(
169                $stream,
170                dimmed = true,
171                "{:->count$}",
172                "",
173                count = $crate::term::term_width() - 1
174            );
175        }
176    };
177    ($stream:expr, $(fg=$fg:expr,)? $(bg=$bg:expr,)? $(bold=$bold:expr,)? $(dimmed=$dimmed:expr,)? $literal:literal $($arg:tt)*) => {
178        use ::unicode_width::UnicodeWidthStr;
179
180        let message = format!($literal $($arg)*);
181        const UNTOUCHABLE: usize = 1 + 8; // --[ ... ]--
182
183        // If there's not enough space, just skip printing the extra rule overlay.
184        if $crate::term::term_width() > UNTOUCHABLE {
185            let max_width = $crate::term::term_width() - UNTOUCHABLE;
186            let message = $crate::term::compute_rule_string(&message, max_width);
187            let message_width = message.width();
188
189            let is_utf8 = *$crate::term::IS_UTF8;
190
191            if is_utf8 {
192                $crate::cwrite!($stream, dimmed = true, "{:─>count$}", "", count = max_width - message_width);
193            } else {
194                $crate::cwrite!($stream, dimmed = true, "{:->count$}", "", count = max_width - message_width);
195            }
196
197            if is_utf8 {
198                $crate::cwrite!($stream, dimmed = true, "┨ ");
199            } else {
200                $crate::cwrite!($stream, dimmed = true, "[ ");
201            }
202            $crate::cwrite!($stream, $(fg = $fg,)? $(bg = $bg,)? $(bold = $bold,)? $(dimmed = $dimmed,)? "{message}");
203            if is_utf8 {
204                $crate::cwrite!($stream, dimmed = true, " ┣");
205            } else {
206                $crate::cwrite!($stream, dimmed = true, " ]");
207            }
208
209            if is_utf8 {
210                $crate::cwrite!($stream, dimmed = true, "━━");
211            } else {
212                $crate::cwrite!($stream, dimmed = true, "--");
213            }
214            $crate::cwriteln!($stream);
215        } else {
216            $crate::cwriteln_rule!($stream);
217        }
218    }
219}
220
221#[macro_export]
222macro_rules! cprintln_rule {
223    () => {
224        {
225            let mut stdout = $crate::term::STDOUT.lock().unwrap();
226            $crate::cwriteln_rule!(&mut stdout);
227        }
228    };
229    ($(fg=$fg:expr,)? $(bg=$bg:expr,)? $(bold=$bold:expr,)? $(dimmed=$dimmed:expr,)? $literal:literal $($arg:tt)*) => {
230        {
231            let mut stdout = $crate::term::STDOUT.lock().unwrap();
232            $crate::cwriteln_rule!(&mut *stdout, $(fg=$fg,)? $(bg=$bg,)? $(bold=$bold,)? $(dimmed=$dimmed,)? $literal $($arg)*);
233        }
234    };
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_compute_rule_string() {
243        assert_eq!(compute_rule_string("Hello, world!", 10), "Hello…rld!");
244        assert_eq!(compute_rule_string("Hello, world!", 11), "Hello…orld!");
245        assert_eq!(compute_rule_string("Hello, world!", 12), "Hello,…orld!");
246        assert_eq!(compute_rule_string("Hello, world!", 13), "Hello, world!");
247        assert_eq!(compute_rule_string("Hello, world!", 14), "Hello, world!");
248    }
249}