Skip to main content

pistonite_cu/cli/
printer.rs

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
20/// Global printer state
21pub(crate) static PRINTER: Mutex<Option<Printer>> = Mutex::new(None);
22pub(crate) struct Printer {
23    #[allow(unused)]
24    is_stdin_terminal: bool,
25    /// Handle to stdout
26    stdout: io::Stdout,
27    /// Handle to stderr
28    stderr: io::Stderr,
29    /// Color codes
30    colors: ansi::Colors,
31
32    print_task: PrintingThreadHandle,
33    /// Target for regular print messages
34    print_target: Target,
35    /// Target for showing animation (bars and prompts)
36    /// If None, bar animation will not be printed, and prompts
37    /// will still be printed to stderr
38    anime_target: Option<Target>,
39    bars: Vec<Weak<ProgressBar>>,
40    #[cfg(feature = "prompt")]
41    pending_prompts: VecDeque<PromptTask>,
42
43    /// Buffer for automatically do certain formatting
44    format_buffer: FormatBuffer,
45    /// Place to buffer prints while printing is blocked
46    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        // format the prompt
98        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    /// Spawn a progress bar, starting a print task if not already
130    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        // start the bar
138        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    /// Print a progress bar done message
149    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            // if bar is animated, don't print child's done messages
155            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    /// Format and print the message
182    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        // if there are no bars and no prompts, then eventually the task will end
264        // we have to check the strong count and not the bars size, because
265        // we need to force the last bar to join the printing thread before
266        // the program exits
267        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/// For synchornizing with the printer
364#[derive(Default)]
365struct PrintingThreadHandle {
366    needs_join: bool,
367    /// Handle for the printing task, None means
368    /// either no printing task is running, or, the printing
369    /// task is terminating
370    handle: Option<JoinHandle<()>>,
371}
372impl PrintingThreadHandle {
373    /// Take the handle for joining
374    fn take(&mut self) -> Option<JoinHandle<()>> {
375        self.needs_join = false;
376        self.handle.take()
377    }
378    /// Mark the task as will end, so it can be joined
379    fn mark_join(&mut self) {
380        self.needs_join = true;
381    }
382    /// If the task is active
383    fn active(&self) -> bool {
384        !self.needs_join && self.handle.is_some()
385    }
386    /// Blockingly join the task on the current thread
387    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    /// Assign a new handle
394    fn assign(&mut self, handle: Option<JoinHandle<()>>) {
395        self.needs_join = handle.is_none();
396        self.handle = handle;
397    }
398}
399
400struct PrintingThread {
401    /// main buffer to format outputs into
402    buffer: String,
403    /// temporary buffer to help formatting stuff
404    temp: String,
405    /// track how many lines are animated
406    /// i.e. needs to be erased on the next cycle
407    lines: i32,
408    /// animation tick counter
409    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        // first check if there are any pending prompts
437        // scope for locking the printer for checking prompts
438        #[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            // extract the next prompt task
455            let task = printer.pending_prompts.pop_front();
456            if let Some(mut task) = task {
457                use std::io::Write as _;
458                // if a task exists, check if there are any currently buffered messages
459                // while the print thread is active, code might print some messages
460                // which will be buffered, then call prompt.
461                // we need to ensure those messages are displayed before the prompt
462                // note if the buffer is currently empty, then
463                // we can save the extra cost of re-print the bars and just proceed
464                // with the prompt
465                if printer.anime_target.is_none() {
466                    if !printer.buffered.is_empty() {
467                        printer.flush_buffered_to_print_target();
468                    }
469                    // still print the prompt to stderr, but don't print control characters
470                    let _ = write!(printer.stderr, "{}", task.prompt);
471                    let _ = printer.stderr.flush();
472                    // now the prompt is printed, we can drop the print guard while blocking
473                    // for user input
474                    drop(printer_guard);
475                    // process this prompt
476                    let result = read_prompt(&task);
477                    // since there is no animation, we don't need to re-print the prompt
478                    // send the result of the prompt
479                    let _ = task.send.send(result);
480                } else {
481                    if !printer.buffered.is_empty() {
482                        // clear previous bars and prompts
483                        self.format_clear_codes();
484                        // print buffered messages to stdout
485                        // this won't cause conflict with stderr
486                        // because animation will also print to stdout
487                        // if stdout is terminal
488                        self.flush_buffered(printer);
489                        self.format_bars(printer);
490                    }
491                    self.buffer.push_str("\r\x1b[K"); // erase the last spacing line
492                    self.buffer.push_str(&task.prompt);
493                    self.print_buffer_to_anime_target(printer);
494
495                    // now the prompt is printed, we can drop the print guard while blocking
496                    // for user input
497                    drop(printer_guard);
498
499                    // we know the prompt string does not end with a new line (because of
500                    // the prompt prefix), so the number of lines to display
501                    // is exactly .lines().count()
502                    let mut l = task.prompt.lines().count() as i32;
503                    // however, if stdin is not terminal, then user won't press enter,
504                    // and we actually have 1 fewer line
505                    // if it's password, it's always read from terminal
506                    let mut no_newline_after_prompt = !is_stdin_terminal && !task.is_password;
507
508                    // process this prompt
509                    let result = read_prompt(&task);
510                    if let Ok(None) = &result {
511                        // user cancelled, so there is no newline input from the user
512                        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                    // now we need to re-print the prompt above the progress bars
520                    // by adding it to the buffer
521                    if !task.is_password {
522                        while !task.prompt.ends_with('\n') {
523                            task.prompt.pop();
524                        }
525                        task.prompt.pop(); // pop the final new line
526                    }
527                    // add the prompt to the print buffer
528                    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                    // send the result of the prompt
535                    let _ = task.send.send(result);
536                }
537                // we only process one prompt at a time, let the progress bar update
538
539                // lock the printer again for printing progress bars
540                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            // clear previous bars and prompts
553            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            // erase the bars
568            self.format_clear_codes();
569            self.print_buffer_to_anime_target(printer);
570        }
571
572        // check exit
573        if bars_empty && prompts_empty {
574            // nothing else to do, mark the task done,
575            // so the printer knows to join this thread (after we drop the lock guard)
576            // whenever someone calls, even before this thread is done
577            printer.print_task.mark_join();
578            // we know the printer buffer is empty
579            // because we just printed all of it while having
580            // the lock on the printer, no need to print again
581            return ControlFlow::Break(());
582        }
583
584        ControlFlow::Continue(())
585    }
586
587    fn format_bars(&mut self, printer: &mut Printer) {
588        // remeasure terminal width on every cycle
589        let width = fmt::term_width_or_max();
590        self.buffer.push_str(printer.colors.yellow);
591
592        // progress bar animation chars
593        #[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                // bar is done
617                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    /// Format codes to clear the animated part
634    #[inline]
635    fn format_clear_codes(&mut self) {
636        self.buffer.push_str("\r\x1b[K"); // erase the last spacing line
637        for _ in 0..self.lines {
638            self.buffer.push_str("\x1b[1A\x1b[K"); // move up one line and erase it
639        }
640        self.lines = 0;
641    }
642
643    /// Print `buffer` to progress bar target
644    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    /// Flush the printer buffered messages
661    fn flush_buffered(&mut self, printer: &mut Printer) {
662        // if the animation target is the same as the print target
663        // it's important that we take the buffered messages
664        // into our own buffer, so we don't immediately
665        // flush stdout. this prevents flushing partial output,
666        // which cause the progress animation to flicker
667        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/// Target for printing output
677#[derive(Clone, Copy, PartialEq, Eq)]
678pub enum Target {
679    /// Print to Stdout
680    Stdout,
681    /// Print to Stderr
682    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
700/// Set the target for printing messages
701///
702/// This is useful if `stdout` needs to emit structured messages for other tools.
703/// Then you can programmatically change everything to emit to `stderr`.
704/// Note this will only affect messages printed after the call.
705///
706/// By default, all print messages go to `stdout`.
707///
708/// See also [`cu::cli::animate_to`]
709///
710/// ```rust
711/// # use pistonite_cu as cu;
712/// cu::cli::print_to(cu::cli::Target::Stderr);
713/// ```
714pub 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
722/// Set the target for animation messages (prompts and bars)
723///
724/// By default, animation messages emit to `stdout` if `stdout` is terminal,
725/// and `stderr` if `stderr` is terminal. Otherwise, animation messages are
726/// not emitted. You can turn off animation messages by passing in `None`.
727/// Note this will only affect messages printed after the call.
728///
729/// See also [`cu::cli::print_to`]
730///
731/// ```rust
732/// # use pistonite_cu as cu;
733/// // force animation to go to stderr even if stdout is terminal
734/// cu::cli::animate_to(Some(cu::cli::Target::Stderr));
735/// ```
736pub 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}