1#[cfg(feature = "prompt")]
2use std::collections::VecDeque;
3use std::io::{self, IsTerminal as _};
4use std::ops::ControlFlow;
5use std::sync::{Arc, Mutex, Weak};
6use std::thread::JoinHandle;
7
8#[cfg(feature = "prompt")]
9use oneshot::{Receiver as OnceRecv, Sender as OnceSend};
10
11#[cfg(feature = "prompt")]
12use crate::cli::ctrlc;
13use crate::cli::fmt::{self, FormatBuffer, ansi};
14use crate::cli::progress::{BarFormatter, BarResult, ProgressBar};
15#[cfg(feature = "prompt")]
16use crate::cli::prompter;
17use crate::cli::{THREAD_NAME, TICK_INTERVAL, Tick};
18use crate::lv;
19
20pub(crate) static PRINTER: Mutex<Option<Printer>> = Mutex::new(None);
22pub(crate) struct Printer {
23 #[allow(unused)]
24 is_stdin_terminal: bool,
25 stdout: io::Stdout,
27 stderr: io::Stderr,
29 colors: ansi::Colors,
31
32 print_task: PrintingThreadHandle,
33 print_target: Target,
35 anime_target: Option<Target>,
39 bars: Vec<Weak<ProgressBar>>,
40 #[cfg(feature = "prompt")]
41 pending_prompts: VecDeque<PromptTask>,
42
43 format_buffer: FormatBuffer,
45 buffered: String,
47}
48impl Printer {
49 pub fn new(use_color: bool) -> Self {
50 let colors = ansi::colors(use_color);
51 let stdout = io::stdout();
52 let stderr = io::stderr();
53 let is_stdin_terminal = io::stdin().is_terminal();
54 let anime_target = if cfg!(feature = "__test") {
55 Some(Target::Stdout)
56 } else {
57 if stdout.is_terminal() {
58 Some(Target::Stdout)
59 } else if stderr.is_terminal() {
60 Some(Target::Stderr)
61 } else {
62 None
63 }
64 };
65
66 Self {
67 is_stdin_terminal,
68 stdout,
69 stderr,
70 colors,
71
72 print_task: Default::default(),
73 print_target: Target::Stdout,
74 anime_target,
75 bars: Default::default(),
76 #[cfg(feature = "prompt")]
77 pending_prompts: Default::default(),
78
79 format_buffer: FormatBuffer::new(),
80 buffered: String::new(),
81 }
82 }
83
84 pub(crate) fn set_print_target(&mut self, target: Target) {
85 self.print_target = target;
86 }
87 pub(crate) fn set_animation_target(&mut self, target: Option<Target>) {
88 self.anime_target = target;
89 }
90
91 #[cfg(feature = "prompt")]
92 pub(crate) fn show_prompt(
93 &mut self,
94 prompt: &str,
95 is_password: bool,
96 ) -> OnceRecv<cu::Result<Option<cu::ZString>>> {
97 let mut lines = prompt.lines();
99 self.format_buffer.reset(self.colors.gray, self.colors.cyan);
100 self.format_buffer.push_control(self.colors.cyan);
101 self.format_buffer.push('!', 1);
102 self.format_buffer.push(']', 1);
103 if let Some(line) = lines.next() {
104 self.format_buffer.push(' ', 1);
105 self.format_buffer.push_str(line);
106 }
107 for line in lines {
108 self.format_buffer.new_line();
109 self.format_buffer.push_str(line);
110 }
111 if is_password {
112 self.format_buffer.push_str(": ");
113 } else {
114 self.format_buffer.push_lf();
115 self.format_buffer.push_control(self.colors.reset);
116 self.format_buffer.push_control("-: ");
117 }
118
119 let (send, recv) = oneshot::channel();
120 self.pending_prompts.push_back(PromptTask {
121 send,
122 prompt: self.format_buffer.take(),
123 is_password,
124 });
125 self.start_print_task_if_needed();
126 recv
127 }
128
129 pub(crate) fn add_progress_bar(&mut self, bar: &Arc<ProgressBar>) {
131 if lv::PRINT_LEVEL.get() < lv::Print::Quiet {
132 return;
133 }
134 if self.anime_target.is_none() {
135 return;
136 }
137 self.bars.push(Arc::downgrade(bar));
139 self.start_print_task_if_needed();
140 }
141
142 fn start_print_task_if_needed(&mut self) {
143 if !self.print_task.active() {
144 self.print_task.join();
145 self.print_task.assign(PrintingThread::spawn());
146 }
147 }
148 pub(crate) fn print_bar_done(&mut self, result: &BarResult, is_root: bool) {
150 if lv::PRINT_LEVEL.get() < lv::Print::Normal {
151 return;
152 }
153 if !is_root && self.anime_target.is_some() {
154 return;
156 }
157 let message = match result {
158 BarResult::DontKeep => return,
159 BarResult::Done(message) => {
160 self.format_buffer
161 .reset(self.colors.gray, self.colors.green);
162 self.format_buffer.push_control(self.colors.green);
163 message
164 }
165 BarResult::Interrupted(message) => {
166 self.format_buffer
167 .reset(self.colors.gray, self.colors.yellow);
168 self.format_buffer.push_control(self.colors.yellow);
169 message
170 }
171 };
172 self.format_buffer.push_control("\u{283f}]");
173 if !message.starts_with('[') {
174 self.format_buffer.push_control(" ");
175 }
176 self.format_buffer.push_str(message);
177 self.format_buffer.push_lf();
178 self.print_format_buffer();
179 }
180
181 pub(crate) fn print_message(&mut self, lv: lv::Lv, message: &str) {
183 let Some(payload) = PrintPayload::new_level_message(lv, &self.colors, message) else {
184 return;
185 };
186 self.print_payload(payload);
187 }
188 pub(crate) fn print_payload(&mut self, payload: PrintPayload<'_>) {
189 self.format_buffer
190 .reset(self.colors.gray, payload.text_control);
191 self.format_buffer.push_control(payload.level_char_control);
192 self.format_buffer.push(payload.level_char, 1);
193 self.format_buffer.push_control(payload.sep_char_control);
194 self.format_buffer.push(payload.sep_char, 1);
195 THREAD_NAME.with_borrow(|x| {
196 if let Some(x) = x {
197 self.format_buffer.push_control(self.colors.magenta);
198 self.format_buffer.push('[', 1);
199 self.format_buffer.push_str(x);
200 self.format_buffer.push(']', 1);
201 }
202 });
203 self.format_buffer.push_control(payload.text_control);
204 let mut lines = payload.message.lines();
205 if let Some(line) = lines.next() {
206 self.format_buffer.push(' ', 1);
207 self.format_buffer.push_str(line);
208 }
209 for line in lines {
210 self.format_buffer.new_line();
211 self.format_buffer.push_str(line);
212 }
213 self.format_buffer.push_lf();
214 self.print_format_buffer();
215 }
216 fn print_format_buffer(&mut self) {
217 if !self.print_task.active() {
218 use std::io::Write;
219 match self.print_target {
220 Target::Stdout => {
221 let _ = write!(
222 self.stdout,
223 "{}{}",
224 self.format_buffer.as_str(),
225 self.colors.reset
226 );
227 let _ = self.stdout.flush();
228 }
229 Target::Stderr => {
230 let _ = write!(
231 self.stderr,
232 "{}{}",
233 self.format_buffer.as_str(),
234 self.colors.reset
235 );
236 let _ = self.stderr.flush();
237 }
238 }
239 } else {
240 self.buffered.push_str(self.format_buffer.as_str());
241 self.buffered.push_str(self.colors.reset);
242 }
243 }
244 fn flush_buffered_to_print_target(&mut self) {
245 use std::io::Write as _;
246 match self.print_target {
247 Target::Stdout => {
248 let _ = write!(self.stdout, "{}", self.buffered);
249 let _ = self.stdout.flush();
250 }
251 Target::Stderr => {
252 let _ = write!(self.stderr, "{}", self.buffered);
253 let _ = self.stderr.flush();
254 }
255 }
256 self.buffered.clear();
257 }
258
259 pub(crate) fn take_print_task_if_should_join(&mut self) -> Option<JoinHandle<()>> {
260 if self.print_task.needs_join {
261 return self.print_task.take();
262 }
263 let bar_strong_count = self.bars.iter().filter(|x| x.upgrade().is_some()).count();
268 if bar_strong_count != 0 {
269 return None;
270 }
271 #[cfg(feature = "prompt")]
272 if !self.pending_prompts.is_empty() {
273 return None;
274 }
275 self.print_task.take()
276 }
277}
278
279pub(crate) struct PrintPayload<'a> {
280 pub level_char_control: &'static str,
281 pub level_char: char,
282 pub sep_char_control: &'static str,
283 pub sep_char: char,
284 pub text_control: &'static str,
285 pub message: &'a str,
286}
287impl<'a> PrintPayload<'a> {
288 pub fn new_level_message(
289 level: lv::Lv,
290 colors: &ansi::Colors,
291 message: &'a str,
292 ) -> Option<Self> {
293 let payload = match level {
294 lv::Lv::Off => return None,
295 lv::Lv::Error => Self {
296 level_char_control: colors.red,
297 level_char: 'E',
298 sep_char_control: "",
299 sep_char: ']',
300 text_control: colors.red,
301 message,
302 },
303 lv::Lv::Hint => Self {
304 level_char_control: colors.cyan,
305 level_char: 'H',
306 sep_char_control: colors.gray,
307 sep_char: ']',
308 text_control: colors.yellow,
309 message,
310 },
311 lv::Lv::Print => Self {
312 level_char_control: colors.gray,
313 level_char: ':',
314 sep_char_control: "",
315 sep_char: ':',
316 text_control: colors.reset,
317 message,
318 },
319 lv::Lv::Warn => Self {
320 level_char_control: colors.yellow,
321 level_char: 'W',
322 sep_char_control: "",
323 sep_char: ']',
324 text_control: colors.yellow,
325 message,
326 },
327 lv::Lv::Info => Self {
328 level_char_control: colors.green,
329 level_char: 'I',
330 sep_char_control: colors.gray,
331 sep_char: ']',
332 text_control: colors.reset,
333 message,
334 },
335 lv::Lv::Debug => Self {
336 level_char_control: colors.gray,
337 level_char: 'D',
338 sep_char_control: "",
339 sep_char: ']',
340 text_control: colors.cyan,
341 message,
342 },
343 lv::Lv::Trace => Self {
344 level_char_control: colors.magenta,
345 level_char: '*',
346 sep_char_control: "",
347 sep_char: ']',
348 text_control: colors.magenta,
349 message,
350 },
351 };
352 Some(payload)
353 }
354}
355
356#[cfg(feature = "prompt")]
357struct PromptTask {
358 send: OnceSend<cu::Result<Option<cu::ZString>>>,
359 prompt: String,
360 is_password: bool,
361}
362
363#[derive(Default)]
365struct PrintingThreadHandle {
366 needs_join: bool,
367 handle: Option<JoinHandle<()>>,
371}
372impl PrintingThreadHandle {
373 fn take(&mut self) -> Option<JoinHandle<()>> {
375 self.needs_join = false;
376 self.handle.take()
377 }
378 fn mark_join(&mut self) {
380 self.needs_join = true;
381 }
382 fn active(&self) -> bool {
384 !self.needs_join && self.handle.is_some()
385 }
386 fn join(&mut self) {
388 self.needs_join = false;
389 if let Some(handle) = self.handle.take() {
390 let _: Result<_, _> = handle.join();
391 }
392 }
393 fn assign(&mut self, handle: Option<JoinHandle<()>>) {
395 self.needs_join = handle.is_none();
396 self.handle = handle;
397 }
398}
399
400struct PrintingThread {
401 buffer: String,
403 temp: String,
405 lines: i32,
408 tick: Tick,
410}
411
412impl PrintingThread {
413 fn spawn() -> Option<JoinHandle<()>> {
414 std::thread::Builder::new()
415 .name("cu-printer".to_string())
416 .spawn(move || {
417 let mut state = PrintingThread {
418 buffer: String::new(),
419 temp: String::new(),
420 lines: 0,
421 tick: 0,
422 };
423 loop {
424 match state.run_loop() {
425 ControlFlow::Break(_) => break,
426 _ => {
427 std::thread::sleep(TICK_INTERVAL);
428 state.tick = state.tick.wrapping_add(1);
429 }
430 };
431 }
432 })
433 .ok()
434 }
435 fn run_loop(&mut self) -> ControlFlow<()> {
436 #[cfg(not(feature = "prompt"))]
439 let mut printer_guard = {
440 let Ok(printer_guard) = PRINTER.lock() else {
441 return ControlFlow::Break(());
442 };
443 printer_guard
444 };
445 #[cfg(feature = "prompt")]
446 let mut printer_guard = {
447 let Ok(mut printer_guard) = PRINTER.lock() else {
448 return ControlFlow::Break(());
449 };
450 let Some(printer) = printer_guard.as_mut() else {
451 return ControlFlow::Break(());
452 };
453 let is_stdin_terminal = printer.is_stdin_terminal;
454 let task = printer.pending_prompts.pop_front();
456 if let Some(mut task) = task {
457 use std::io::Write as _;
458 if printer.anime_target.is_none() {
466 if !printer.buffered.is_empty() {
467 printer.flush_buffered_to_print_target();
468 }
469 let _ = write!(printer.stderr, "{}", task.prompt);
471 let _ = printer.stderr.flush();
472 drop(printer_guard);
475 let result = read_prompt(&task);
477 let _ = task.send.send(result);
480 } else {
481 if !printer.buffered.is_empty() {
482 self.format_clear_codes();
484 self.flush_buffered(printer);
489 self.format_bars(printer);
490 }
491 self.buffer.push_str("\r\x1b[K"); self.buffer.push_str(&task.prompt);
493 self.print_buffer_to_anime_target(printer);
494
495 drop(printer_guard);
498
499 let mut l = task.prompt.lines().count() as i32;
503 let mut no_newline_after_prompt = !is_stdin_terminal && !task.is_password;
507
508 let result = read_prompt(&task);
510 if let Ok(None) = &result {
511 no_newline_after_prompt = true;
513 }
514 if no_newline_after_prompt {
515 l = l.saturating_sub(1)
516 }
517 self.lines += l;
518
519 if !task.is_password {
522 while !task.prompt.ends_with('\n') {
523 task.prompt.pop();
524 }
525 task.prompt.pop(); }
527 if let Ok(mut printer) = PRINTER.lock() {
529 if let Some(printer) = printer.as_mut() {
530 printer.buffered.push_str(&task.prompt);
531 printer.buffered.push('\n');
532 }
533 }
534 let _ = task.send.send(result);
536 }
537 let Ok(printer_guard) = PRINTER.lock() else {
541 return ControlFlow::Break(());
542 };
543 printer_guard
544 } else {
545 printer_guard
546 }
547 };
548 let Some(printer) = printer_guard.as_mut() else {
549 return ControlFlow::Break(());
550 };
551 if printer.anime_target.is_some() {
552 self.format_clear_codes();
554 self.flush_buffered(printer);
555 self.format_bars(printer);
556 self.print_buffer_to_anime_target(printer);
557 } else {
558 printer.flush_buffered_to_print_target();
559 }
560 let bars_empty = printer.bars.is_empty();
561 #[cfg(feature = "prompt")]
562 let prompts_empty = printer.pending_prompts.is_empty();
563 #[cfg(not(feature = "prompt"))]
564 let prompts_empty = true;
565
566 if bars_empty && printer.anime_target.is_some() {
567 self.format_clear_codes();
569 self.print_buffer_to_anime_target(printer);
570 }
571
572 if bars_empty && prompts_empty {
574 printer.print_task.mark_join();
578 return ControlFlow::Break(());
582 }
583
584 ControlFlow::Continue(())
585 }
586
587 fn format_bars(&mut self, printer: &mut Printer) {
588 let width = fmt::term_width_or_max();
590 self.buffer.push_str(printer.colors.yellow);
591
592 #[rustfmt::skip]
594 const CHARS: [char; 30] = [
595 '\u{280b}', '\u{280b}', '\u{280b}', '\u{280b}', '\u{280b}',
596 '\u{2819}', '\u{2819}', '\u{2819}', '\u{2819}', '\u{2819}',
597 '\u{2838}', '\u{2838}', '\u{2838}', '\u{2838}', '\u{2838}',
598 '\u{2834}', '\u{2834}', '\u{2834}', '\u{2834}', '\u{2834}',
599 '\u{2826}', '\u{2826}', '\u{2826}', '\u{2826}', '\u{2826}',
600 '\u{2807}', '\u{2807}', '\u{2807}', '\u{2807}', '\u{2807}',
601 ];
602 let anime = CHARS[(self.tick as usize) % CHARS.len()];
603
604 let mut formatter = BarFormatter {
605 colors: printer.colors,
606 bar_color: printer.colors.yellow,
607 width,
608 tick: self.tick,
609 now: &mut None,
610 out: &mut self.buffer,
611 temp: &mut self.temp,
612 };
613
614 printer.bars.retain(|bar| {
615 let Some(bar) = bar.upgrade() else {
616 return false;
618 };
619 if width >= 2 {
620 formatter.out.push(anime);
621 formatter.out.push(']');
622 self.lines += bar.format(&mut formatter);
623 } else {
624 formatter.out.push('\n');
625 self.lines += 1;
626 }
627
628 true
629 });
630 self.buffer.push_str(printer.colors.reset);
631 }
632
633 #[inline]
635 fn format_clear_codes(&mut self) {
636 self.buffer.push_str("\r\x1b[K"); for _ in 0..self.lines {
638 self.buffer.push_str("\x1b[1A\x1b[K"); }
640 self.lines = 0;
641 }
642
643 fn print_buffer_to_anime_target(&mut self, printer: &mut Printer) {
645 use std::io::Write as _;
646 match printer.anime_target {
647 None => {}
648 Some(Target::Stdout) => {
649 let _ = write!(printer.stdout, "{}", self.buffer);
650 let _ = printer.stdout.flush();
651 }
652 Some(Target::Stderr) => {
653 let _ = write!(printer.stderr, "{}", self.buffer);
654 let _ = printer.stderr.flush();
655 }
656 }
657 self.buffer.clear();
658 }
659
660 fn flush_buffered(&mut self, printer: &mut Printer) {
662 if printer.anime_target == Some(printer.print_target) {
668 self.buffer.push_str(&printer.buffered);
669 printer.buffered.clear();
670 return;
671 }
672 printer.flush_buffered_to_print_target();
673 }
674}
675
676#[derive(Clone, Copy, PartialEq, Eq)]
678pub enum Target {
679 Stdout,
681 Stderr,
683}
684#[cfg(feature = "prompt")]
685fn read_prompt(task: &PromptTask) -> cu::Result<Option<cu::ZString>> {
686 let is_password = task.is_password;
687 let result = ctrlc::ctrlc_frame().execute(|ctrlc| {
688 if is_password {
689 prompter::read_password(ctrlc)
690 } else {
691 prompter::read_plaintext(ctrlc)
692 }
693 })?;
694 match result {
695 None | Some(None) => Ok(None),
696 Some(Some(x)) => Ok(Some(x)),
697 }
698}
699
700pub fn print_to(target: Target) {
715 if let Ok(mut printer) = PRINTER.lock() {
716 if let Some(printer) = printer.as_mut() {
717 printer.set_print_target(target);
718 }
719 }
720}
721
722pub fn animate_to(target: Option<Target>) {
737 if let Ok(mut printer) = PRINTER.lock() {
738 if let Some(printer) = printer.as_mut() {
739 printer.set_animation_target(target);
740 }
741 }
742}