1use crossterm::cursor;
2use crossterm::style;
3use crossterm::terminal;
4use crossterm::QueueableCommand;
5use parking_lot::Mutex;
6use std::io::stderr;
7use std::io::stdout;
8use std::io::Stderr;
9use std::io::Stdout;
10use std::io::Write;
11use std::sync::Arc;
12
13pub enum LoggerTextItem {
14 Text(String),
15 HangingText { text: String, indent: u16 },
16}
17
18#[derive(PartialOrd, Ord, PartialEq, Eq)]
19pub(crate) enum LoggerRefreshItemKind {
20 ProgressBars = 0,
22 Selection = 1,
23}
24
25struct LoggerRefreshItem {
26 kind: LoggerRefreshItemKind,
27 text_items: Vec<LoggerTextItem>,
28}
29
30#[derive(Clone)]
31pub struct LoggerOptions {
32 pub initial_context_name: String,
33 pub is_stdout_machine_readable: bool,
35}
36
37#[derive(Clone)]
38pub struct Logger {
39 output_lock: Arc<Mutex<LoggerState>>,
40 is_stdout_machine_readable: bool,
41}
42
43struct LoggerState {
44 last_context_name: String,
45 std_out: Stdout,
46 std_err: Stderr,
47 refresh_items: Vec<LoggerRefreshItem>,
48 last_terminal_size: Option<(u16, u16)>,
49}
50
51impl Logger {
52 pub fn new(options: &LoggerOptions) -> Self {
53 Logger {
54 output_lock: Arc::new(Mutex::new(LoggerState {
55 last_context_name: options.initial_context_name.clone(),
56 std_out: stdout(),
57 std_err: stderr(),
58 refresh_items: Vec::new(),
59 last_terminal_size: None,
60 })),
61 is_stdout_machine_readable: options.is_stdout_machine_readable,
62 }
63 }
64
65 pub fn log(&self, text: &str, context_name: &str) {
66 if self.is_stdout_machine_readable {
67 return;
68 }
69 let mut state = self.output_lock.lock();
70 self.inner_log(&mut state, true, text, context_name);
71 }
72
73 pub fn log_machine_readable(&self, text: &str) {
74 let mut state = self.output_lock.lock();
75 let last_context_name = state.last_context_name.clone(); self.inner_log(&mut state, true, text, &last_context_name);
77 }
78
79 pub fn log_err(&self, text: &str, context_name: &str) {
80 let mut state = self.output_lock.lock();
81 self.inner_log(&mut state, false, text, context_name);
82 }
83
84 pub fn log_text_items(&self, text_items: &[LoggerTextItem], context_name: &str, terminal_width: Option<u16>) {
85 let text = render_text_items_with_width(text_items, terminal_width);
86 self.log(&text, context_name);
87 }
88
89 fn inner_log(&self, state: &mut LoggerState, is_std_out: bool, text: &str, context_name: &str) {
90 if !state.refresh_items.is_empty() {
91 self.inner_queue_clear_previous_draws(state);
92 }
93
94 let mut output_text = String::new();
95 if state.last_context_name != context_name {
96 if !is_std_out || !self.is_stdout_machine_readable {
98 output_text.push_str(&format!("[{}]\n", context_name));
99 }
100 state.last_context_name = context_name.to_string();
101 }
102
103 output_text.push_str(text);
104
105 if !output_text.ends_with('\n') {
107 output_text.push('\n');
108 }
109
110 if is_std_out {
111 state.std_out.queue(style::Print(output_text)).unwrap();
112 } else {
113 state.std_err.queue(style::Print(output_text)).unwrap();
114 }
115
116 if !state.refresh_items.is_empty() {
117 self.inner_queue_draw_items(state);
118 }
119
120 if is_std_out {
121 state.std_out.flush().unwrap();
122 if !state.refresh_items.is_empty() {
123 state.std_err.flush().unwrap();
124 }
125 } else {
126 state.std_err.flush().unwrap();
127 }
128 }
129
130 pub(crate) fn set_refresh_item(&self, kind: LoggerRefreshItemKind, text_items: Vec<LoggerTextItem>) {
131 self.with_update_refresh_items(move |refresh_items| match refresh_items.binary_search_by(|i| i.kind.cmp(&kind)) {
132 Ok(pos) => {
133 let mut refresh_item = refresh_items.get_mut(pos).unwrap();
134 refresh_item.text_items = text_items;
135 }
136 Err(pos) => {
137 let refresh_item = LoggerRefreshItem { kind, text_items };
138 refresh_items.insert(pos, refresh_item);
139 }
140 });
141 }
142
143 pub(crate) fn remove_refresh_item(&self, kind: LoggerRefreshItemKind) {
144 self.with_update_refresh_items(move |refresh_items| {
145 if let Ok(pos) = refresh_items.binary_search_by(|i| i.kind.cmp(&kind)) {
146 refresh_items.remove(pos);
147 } else {
148 }
150 });
151 }
152
153 fn with_update_refresh_items(&self, update_refresh_items: impl FnOnce(&mut Vec<LoggerRefreshItem>)) {
154 let mut state = self.output_lock.lock();
155
156 if state.refresh_items.is_empty() {
158 state.std_err.queue(cursor::Hide).unwrap();
159 }
160
161 self.inner_queue_clear_previous_draws(&mut state);
162
163 update_refresh_items(&mut state.refresh_items);
164
165 self.inner_queue_draw_items(&mut state);
166
167 if state.refresh_items.is_empty() {
169 state.std_err.queue(cursor::Show).unwrap();
170 }
171 state.std_err.flush().unwrap();
172 }
173
174 fn inner_queue_clear_previous_draws(&self, state: &mut LoggerState) {
175 let terminal_width = crate::terminal::get_terminal_width().unwrap();
176 let text_items = state.refresh_items.iter().flat_map(|item| item.text_items.iter());
177 let rendered_text = render_text_items_truncated_to_height(text_items, state.last_terminal_size);
178 let last_line_count = get_text_line_count(&rendered_text, terminal_width);
179
180 if last_line_count > 0 {
181 if last_line_count > 1 {
182 state.std_err.queue(cursor::MoveUp(last_line_count - 1)).unwrap();
183 }
184 state.std_err.queue(cursor::MoveToColumn(0)).unwrap();
185 state.std_err.queue(terminal::Clear(terminal::ClearType::FromCursorDown)).unwrap();
186 }
187
188 state.std_err.queue(cursor::MoveToColumn(0)).unwrap();
189 }
190
191 fn inner_queue_draw_items(&self, state: &mut LoggerState) {
192 let terminal_size = crate::terminal::get_terminal_size();
193 let text_items = state.refresh_items.iter().flat_map(|item| item.text_items.iter());
194 let rendered_text = render_text_items_truncated_to_height(text_items, terminal_size);
195 state.std_err.queue(style::Print(&rendered_text)).unwrap();
196 state.std_err.queue(cursor::MoveToColumn(0)).unwrap();
197 state.last_terminal_size = terminal_size;
198 }
199}
200
201pub fn render_text_items_with_width(text_items: &[LoggerTextItem], terminal_width: Option<u16>) -> String {
203 render_text_items_to_lines(text_items.iter(), terminal_width).join("\n")
204}
205
206fn render_text_items_truncated_to_height<'a>(text_items: impl Iterator<Item = &'a LoggerTextItem>, terminal_size: Option<(u16, u16)>) -> String {
207 let lines = render_text_items_to_lines(text_items, terminal_size.map(|(width, _)| width));
208 if let Some(height) = terminal_size.map(|(_, height)| height) {
209 let max_height = height as usize;
210 if lines.len() > max_height {
211 return lines[lines.len() - max_height..].join("\n");
212 }
213 }
214 lines.join("\n")
215}
216
217fn render_text_items_to_lines<'a>(text_items: impl Iterator<Item = &'a LoggerTextItem>, terminal_width: Option<u16>) -> Vec<String> {
218 let mut result = Vec::new();
219 for (_, item) in text_items.enumerate() {
220 match item {
221 LoggerTextItem::Text(text) => result.extend(render_text_to_lines(text, 0, terminal_width)),
222 LoggerTextItem::HangingText { text, indent } => {
223 result.extend(render_text_to_lines(text, *indent, terminal_width));
224 }
225 }
226 }
227 result
228}
229
230fn render_text_to_lines(text: &str, hanging_indent: u16, terminal_width: Option<u16>) -> Vec<String> {
231 let mut lines = Vec::new();
232 if let Some(terminal_width) = terminal_width {
233 let mut current_line = String::new();
234 let mut line_width: u16 = 0;
235 let mut current_whitespace = String::new();
236 for token in tokenize_words(text) {
237 match token {
238 WordToken::Word((word, word_width)) => {
239 let is_word_longer_than_line = hanging_indent + word_width > terminal_width;
240 if is_word_longer_than_line {
241 if !current_whitespace.is_empty() {
243 if line_width < terminal_width {
244 current_line.push_str(¤t_whitespace);
245 }
246 current_whitespace = String::new();
247 }
248 for c in word.chars() {
249 if line_width == terminal_width {
250 lines.push(current_line);
251 current_line = String::new();
252 current_line.push_str(&" ".repeat(hanging_indent as usize));
253 line_width = hanging_indent;
254 }
255 current_line.push(c);
256 line_width += 1;
257 }
258 } else {
259 if line_width + word_width > terminal_width {
260 lines.push(current_line);
261 current_line = String::new();
262 current_line.push_str(&" ".repeat(hanging_indent as usize));
263 line_width = hanging_indent;
264 current_whitespace = String::new();
265 }
266 if !current_whitespace.is_empty() {
267 current_line.push_str(¤t_whitespace);
268 current_whitespace = String::new();
269 }
270 current_line.push_str(word);
271 line_width += word_width;
272 }
273 }
274 WordToken::WhiteSpace(space_char) => {
275 current_whitespace.push(space_char);
276 line_width += 1;
277 }
278 WordToken::NewLine => {
279 lines.push(current_line);
280 current_line = String::new();
281 line_width = 0;
282 }
283 }
284 }
285 if !current_line.is_empty() {
286 lines.push(current_line);
287 }
288 } else {
289 for line in text.lines() {
290 lines.push(line.to_string());
291 }
292 }
293 lines
294}
295
296enum WordToken<'a> {
297 Word((&'a str, u16)),
298 WhiteSpace(char),
299 NewLine,
300}
301
302fn tokenize_words(text: &str) -> Vec<WordToken> {
303 let mut start_index = 0;
305 let mut tokens = Vec::new();
306 let mut word_width = 0;
307 for (index, c) in text.char_indices() {
308 if c.is_whitespace() || c == '\n' {
309 if word_width > 0 {
310 tokens.push(WordToken::Word((&text[start_index..index], word_width)));
311 word_width = 0;
312 }
313
314 if c == '\n' {
315 tokens.push(WordToken::NewLine);
316 } else {
317 tokens.push(WordToken::WhiteSpace(c));
318 }
319
320 start_index = index + c.len_utf8(); } else if !c.is_ascii_control() {
322 word_width += 1;
323 }
324 }
325 if word_width > 0 {
326 tokens.push(WordToken::Word((&text[start_index..text.len()], word_width)));
327 }
328 tokens
329}
330
331fn get_text_line_count(text: &str, terminal_width: u16) -> u16 {
332 let mut line_count: u16 = 0;
333 let mut line_width: u16 = 0;
334 for c in text.chars() {
335 if c.is_ascii_control() && !c.is_ascii_whitespace() {
336 } else if c == '\n' {
338 line_count += 1;
339 line_width = 0;
340 } else if line_width == terminal_width {
341 line_width = 0;
342 line_count += 1;
343 } else {
344 line_width += 1;
345 }
346 }
347 line_count + 1
348}