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
14pub 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}"); }
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#[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; 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}