jj_cli/
ui.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::env;
16use std::error;
17use std::fmt;
18use std::io;
19use std::io::IsTerminal as _;
20use std::io::PipeWriter;
21use std::io::Stderr;
22use std::io::StderrLock;
23use std::io::Stdout;
24use std::io::StdoutLock;
25use std::io::Write;
26use std::iter;
27use std::mem;
28use std::process::Child;
29use std::process::ChildStdin;
30use std::process::Stdio;
31use std::thread;
32use std::thread::JoinHandle;
33
34use itertools::Itertools as _;
35use jj_lib::config::ConfigGetError;
36use jj_lib::config::StackedConfig;
37use tracing::instrument;
38
39use crate::command_error::CommandError;
40use crate::config::CommandNameAndArgs;
41use crate::formatter::Formatter;
42use crate::formatter::FormatterExt as _;
43use crate::formatter::FormatterFactory;
44use crate::formatter::HeadingLabeledWriter;
45use crate::formatter::LabeledScope;
46use crate::formatter::PlainTextFormatter;
47
48const BUILTIN_PAGER_NAME: &str = ":builtin";
49
50enum UiOutput {
51    Terminal {
52        stdout: Stdout,
53        stderr: Stderr,
54    },
55    Paged {
56        child: Child,
57        child_stdin: ChildStdin,
58    },
59    BuiltinPaged {
60        out_wr: PipeWriter,
61        err_wr: PipeWriter,
62        pager_thread: JoinHandle<streampager::Result<()>>,
63    },
64    Null,
65}
66
67impl UiOutput {
68    fn new_terminal() -> Self {
69        Self::Terminal {
70            stdout: io::stdout(),
71            stderr: io::stderr(),
72        }
73    }
74
75    fn new_paged(pager_cmd: &CommandNameAndArgs) -> io::Result<Self> {
76        let mut cmd = pager_cmd.to_command();
77        tracing::info!(?cmd, "spawning pager");
78        let mut child = cmd.stdin(Stdio::piped()).spawn()?;
79        let child_stdin = child.stdin.take().unwrap();
80        Ok(Self::Paged { child, child_stdin })
81    }
82
83    fn new_builtin_paged(config: &StreampagerConfig) -> streampager::Result<Self> {
84        let streampager_config = streampager::config::Config {
85            wrapping_mode: config.wrapping.into(),
86            interface_mode: config.streampager_interface_mode(),
87            show_ruler: config.show_ruler,
88            // We could make scroll-past-eof configurable, but I'm guessing people
89            // will not miss it. If we do make it configurable, we should mention
90            // that it's a bad idea to turn this on if `interface=quit-if-one-page`,
91            // as it can leave a lot of empty lines on the screen after exiting.
92            scroll_past_eof: false,
93            ..Default::default()
94        };
95        // Initialize with tty instead of stdin/stdout. We spawn pager so long
96        // as stdout is a tty, which means stdin may be redirected.
97        let mut pager =
98            streampager::Pager::new_using_system_terminal_with_config(streampager_config)?;
99
100        // Use native pipe, which can be attached to child process. The stdout
101        // stream could be an in-process channel, but the cost of extra syscalls
102        // wouldn't matter.
103        let (out_rd, out_wr) = io::pipe()?;
104        let (err_rd, err_wr) = io::pipe()?;
105        pager.add_stream(out_rd, "")?;
106        pager.add_error_stream(err_rd, "stderr")?;
107
108        Ok(Self::BuiltinPaged {
109            out_wr,
110            err_wr,
111            pager_thread: thread::spawn(|| pager.run()),
112        })
113    }
114
115    fn finalize(self, ui: &Ui) {
116        match self {
117            Self::Terminal { .. } => { /* no-op */ }
118            Self::Paged {
119                mut child,
120                child_stdin,
121            } => {
122                drop(child_stdin);
123                if let Err(err) = child.wait() {
124                    // It's possible (though unlikely) that this write fails, but
125                    // this function gets called so late that there's not much we
126                    // can do about it.
127                    writeln!(
128                        ui.warning_default(),
129                        "Failed to wait on pager: {err}",
130                        err = format_error_with_sources(&err),
131                    )
132                    .ok();
133                }
134            }
135            Self::BuiltinPaged {
136                out_wr,
137                err_wr,
138                pager_thread,
139            } => {
140                drop(out_wr);
141                drop(err_wr);
142                match pager_thread.join() {
143                    Ok(Ok(())) => {}
144                    Ok(Err(err)) => {
145                        writeln!(
146                            ui.warning_default(),
147                            "Failed to run builtin pager: {err}",
148                            err = format_error_with_sources(&err),
149                        )
150                        .ok();
151                    }
152                    Err(_) => {
153                        writeln!(ui.warning_default(), "Builtin pager crashed.").ok();
154                    }
155                }
156            }
157            Self::Null => {}
158        }
159    }
160}
161
162pub enum UiStdout<'a> {
163    Terminal(StdoutLock<'static>),
164    Paged(&'a ChildStdin),
165    Builtin(&'a PipeWriter),
166    Null(io::Sink),
167}
168
169pub enum UiStderr<'a> {
170    Terminal(StderrLock<'static>),
171    Paged(&'a ChildStdin),
172    Builtin(&'a PipeWriter),
173    Null(io::Sink),
174}
175
176macro_rules! for_outputs {
177    ($ty:ident, $output:expr, $pat:pat => $expr:expr) => {
178        match $output {
179            $ty::Terminal($pat) => $expr,
180            $ty::Paged($pat) => $expr,
181            $ty::Builtin($pat) => $expr,
182            $ty::Null($pat) => $expr,
183        }
184    };
185}
186
187impl Write for UiStdout<'_> {
188    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
189        for_outputs!(Self, self, w => w.write(buf))
190    }
191
192    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
193        for_outputs!(Self, self, w => w.write_all(buf))
194    }
195
196    fn flush(&mut self) -> io::Result<()> {
197        for_outputs!(Self, self, w => w.flush())
198    }
199}
200
201impl Write for UiStderr<'_> {
202    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
203        for_outputs!(Self, self, w => w.write(buf))
204    }
205
206    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
207        for_outputs!(Self, self, w => w.write_all(buf))
208    }
209
210    fn flush(&mut self) -> io::Result<()> {
211        for_outputs!(Self, self, w => w.flush())
212    }
213}
214
215pub struct Ui {
216    quiet: bool,
217    pager: PagerConfig,
218    progress_indicator: bool,
219    formatter_factory: FormatterFactory,
220    output: UiOutput,
221}
222
223#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, clap::ValueEnum)]
224#[serde(rename_all = "kebab-case")]
225pub enum ColorChoice {
226    Always,
227    Never,
228    Debug,
229    Auto,
230}
231
232impl fmt::Display for ColorChoice {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        let s = match self {
235            Self::Always => "always",
236            Self::Never => "never",
237            Self::Debug => "debug",
238            Self::Auto => "auto",
239        };
240        write!(f, "{s}")
241    }
242}
243
244fn prepare_formatter_factory(
245    config: &StackedConfig,
246    stdout: &Stdout,
247) -> Result<FormatterFactory, ConfigGetError> {
248    let terminal = stdout.is_terminal();
249    let (color, debug) = match config.get("ui.color")? {
250        ColorChoice::Always => (true, false),
251        ColorChoice::Never => (false, false),
252        ColorChoice::Debug => (true, true),
253        ColorChoice::Auto => (terminal, false),
254    };
255    if color {
256        FormatterFactory::color(config, debug)
257    } else if terminal {
258        // Sanitize ANSI escape codes if we're printing to a terminal. Doesn't
259        // affect ANSI escape codes that originate from the formatter itself.
260        Ok(FormatterFactory::sanitized())
261    } else {
262        Ok(FormatterFactory::plain_text())
263    }
264}
265
266#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
267#[serde(rename_all(deserialize = "kebab-case"))]
268pub enum PaginationChoice {
269    Never,
270    Auto,
271}
272
273#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
274#[serde(rename_all(deserialize = "kebab-case"))]
275pub enum StreampagerAlternateScreenMode {
276    QuitIfOnePage,
277    FullScreenClearOutput,
278    QuitQuicklyOrClearOutput,
279}
280
281#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
282#[serde(rename_all(deserialize = "kebab-case"))]
283enum StreampagerWrappingMode {
284    None,
285    Word,
286    Anywhere,
287}
288
289impl From<StreampagerWrappingMode> for streampager::config::WrappingMode {
290    fn from(val: StreampagerWrappingMode) -> Self {
291        match val {
292            StreampagerWrappingMode::None => Self::Unwrapped,
293            StreampagerWrappingMode::Word => Self::WordBoundary,
294            StreampagerWrappingMode::Anywhere => Self::GraphemeBoundary,
295        }
296    }
297}
298
299#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
300#[serde(rename_all(deserialize = "kebab-case"))]
301struct StreampagerConfig {
302    interface: StreampagerAlternateScreenMode,
303    wrapping: StreampagerWrappingMode,
304    show_ruler: bool,
305    // TODO: Add an `quit-quickly-delay-seconds` floating point option or a
306    // `quit-quickly-delay` option that takes a 's' or 'ms' suffix. Note that as
307    // of this writing, floating point numbers do not work with `--config`
308}
309
310impl StreampagerConfig {
311    fn streampager_interface_mode(&self) -> streampager::config::InterfaceMode {
312        use StreampagerAlternateScreenMode::*;
313        use streampager::config::InterfaceMode;
314        match self.interface {
315            // InterfaceMode::Direct not implemented
316            FullScreenClearOutput => InterfaceMode::FullScreen,
317            QuitIfOnePage => InterfaceMode::Hybrid,
318            QuitQuicklyOrClearOutput => InterfaceMode::Delayed(std::time::Duration::from_secs(2)),
319        }
320    }
321}
322
323enum PagerConfig {
324    Disabled,
325    Builtin(StreampagerConfig),
326    External(CommandNameAndArgs),
327}
328
329impl PagerConfig {
330    fn from_config(config: &StackedConfig) -> Result<Self, ConfigGetError> {
331        if matches!(config.get("ui.paginate")?, PaginationChoice::Never) {
332            return Ok(Self::Disabled);
333        };
334        let args: CommandNameAndArgs = config.get("ui.pager")?;
335        if args.as_str() == Some(BUILTIN_PAGER_NAME) {
336            Ok(Self::Builtin(config.get("ui.streampager")?))
337        } else {
338            Ok(Self::External(args))
339        }
340    }
341}
342
343impl Ui {
344    pub fn null() -> Self {
345        Self {
346            quiet: true,
347            pager: PagerConfig::Disabled,
348            progress_indicator: false,
349            formatter_factory: FormatterFactory::plain_text(),
350            output: UiOutput::Null,
351        }
352    }
353
354    pub fn with_config(config: &StackedConfig) -> Result<Self, CommandError> {
355        let formatter_factory = prepare_formatter_factory(config, &io::stdout())?;
356        Ok(Self {
357            quiet: config.get("ui.quiet")?,
358            formatter_factory,
359            pager: PagerConfig::from_config(config)?,
360            progress_indicator: config.get("ui.progress-indicator")?,
361            output: UiOutput::new_terminal(),
362        })
363    }
364
365    pub fn reset(&mut self, config: &StackedConfig) -> Result<(), CommandError> {
366        self.quiet = config.get("ui.quiet")?;
367        self.pager = PagerConfig::from_config(config)?;
368        self.progress_indicator = config.get("ui.progress-indicator")?;
369        self.formatter_factory = prepare_formatter_factory(config, &io::stdout())?;
370        Ok(())
371    }
372
373    /// Switches the output to use the pager, if allowed.
374    #[instrument(skip_all)]
375    pub fn request_pager(&mut self) {
376        if !matches!(&self.output, UiOutput::Terminal { stdout, .. } if stdout.is_terminal()) {
377            return;
378        }
379
380        let new_output = match &self.pager {
381            PagerConfig::Disabled => {
382                return;
383            }
384            PagerConfig::Builtin(streampager_config) => {
385                UiOutput::new_builtin_paged(streampager_config)
386                    .inspect_err(|err| {
387                        writeln!(
388                            self.warning_default(),
389                            "Failed to set up builtin pager: {err}",
390                            err = format_error_with_sources(err),
391                        )
392                        .ok();
393                    })
394                    .ok()
395            }
396            PagerConfig::External(command_name_and_args) => {
397                UiOutput::new_paged(command_name_and_args)
398                    .inspect_err(|err| {
399                        // The pager executable couldn't be found or couldn't be run
400                        writeln!(
401                            self.warning_default(),
402                            "Failed to spawn pager '{name}': {err}",
403                            name = command_name_and_args.split_name(),
404                            err = format_error_with_sources(err),
405                        )
406                        .ok();
407                        writeln!(self.hint_default(), "Consider using the `:builtin` pager.").ok();
408                    })
409                    .ok()
410            }
411        };
412        if let Some(output) = new_output {
413            self.output = output;
414        }
415    }
416
417    pub fn color(&self) -> bool {
418        self.formatter_factory.is_color()
419    }
420
421    pub fn new_formatter<'output, W: Write + 'output>(
422        &self,
423        output: W,
424    ) -> Box<dyn Formatter + 'output> {
425        self.formatter_factory.new_formatter(output)
426    }
427
428    /// Locked stdout stream.
429    pub fn stdout(&self) -> UiStdout<'_> {
430        match &self.output {
431            UiOutput::Terminal { stdout, .. } => UiStdout::Terminal(stdout.lock()),
432            UiOutput::Paged { child_stdin, .. } => UiStdout::Paged(child_stdin),
433            UiOutput::BuiltinPaged { out_wr, .. } => UiStdout::Builtin(out_wr),
434            UiOutput::Null => UiStdout::Null(io::sink()),
435        }
436    }
437
438    /// Creates a formatter for the locked stdout stream.
439    ///
440    /// Labels added to the returned formatter should be removed by caller.
441    /// Otherwise the last color would persist.
442    pub fn stdout_formatter(&self) -> Box<dyn Formatter + '_> {
443        for_outputs!(UiStdout, self.stdout(), w => self.new_formatter(w))
444    }
445
446    /// Locked stderr stream.
447    pub fn stderr(&self) -> UiStderr<'_> {
448        match &self.output {
449            UiOutput::Terminal { stderr, .. } => UiStderr::Terminal(stderr.lock()),
450            UiOutput::Paged { child_stdin, .. } => UiStderr::Paged(child_stdin),
451            UiOutput::BuiltinPaged { err_wr, .. } => UiStderr::Builtin(err_wr),
452            UiOutput::Null => UiStderr::Null(io::sink()),
453        }
454    }
455
456    /// Creates a formatter for the locked stderr stream.
457    pub fn stderr_formatter(&self) -> Box<dyn Formatter + '_> {
458        for_outputs!(UiStderr, self.stderr(), w => self.new_formatter(w))
459    }
460
461    /// Stderr stream to be attached to a child process.
462    pub fn stderr_for_child(&self) -> io::Result<Stdio> {
463        match &self.output {
464            UiOutput::Terminal { .. } => Ok(Stdio::inherit()),
465            UiOutput::Paged { child_stdin, .. } => Ok(duplicate_child_stdin(child_stdin)?.into()),
466            UiOutput::BuiltinPaged { err_wr, .. } => Ok(err_wr.try_clone()?.into()),
467            UiOutput::Null => Ok(Stdio::null()),
468        }
469    }
470
471    /// Whether continuous feedback should be displayed for long-running
472    /// operations
473    pub fn use_progress_indicator(&self) -> bool {
474        match &self.output {
475            UiOutput::Terminal { stderr, .. } => self.progress_indicator && stderr.is_terminal(),
476            UiOutput::Paged { .. } => false,
477            UiOutput::BuiltinPaged { .. } => false,
478            UiOutput::Null => false,
479        }
480    }
481
482    pub fn progress_output(&self) -> Option<ProgressOutput<std::io::Stderr>> {
483        self.use_progress_indicator()
484            .then(ProgressOutput::for_stderr)
485    }
486
487    /// Writer to print an update that's not part of the command's main output.
488    pub fn status(&self) -> Box<dyn Write + '_> {
489        if self.quiet {
490            Box::new(io::sink())
491        } else {
492            Box::new(self.stderr())
493        }
494    }
495
496    /// A formatter to print an update that's not part of the command's main
497    /// output. Returns `None` if `--quiet` was requested.
498    pub fn status_formatter(&self) -> Option<Box<dyn Formatter + '_>> {
499        (!self.quiet).then(|| self.stderr_formatter())
500    }
501
502    /// Writer to print hint with the default "Hint: " heading.
503    pub fn hint_default(&self) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str> {
504        self.hint_with_heading("Hint: ")
505    }
506
507    /// Writer to print hint without the "Hint: " heading.
508    pub fn hint_no_heading(&self) -> LabeledScope<Box<dyn Formatter + '_>> {
509        let formatter = self
510            .status_formatter()
511            .unwrap_or_else(|| Box::new(PlainTextFormatter::new(io::sink())));
512        formatter.into_labeled("hint")
513    }
514
515    /// Writer to print hint with the given heading.
516    pub fn hint_with_heading<H: fmt::Display>(
517        &self,
518        heading: H,
519    ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, H> {
520        self.hint_no_heading().with_heading(heading)
521    }
522
523    /// Writer to print warning with the default "Warning: " heading.
524    pub fn warning_default(&self) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str> {
525        self.warning_with_heading("Warning: ")
526    }
527
528    /// Writer to print warning without the "Warning: " heading.
529    pub fn warning_no_heading(&self) -> LabeledScope<Box<dyn Formatter + '_>> {
530        self.stderr_formatter().into_labeled("warning")
531    }
532
533    /// Writer to print warning with the given heading.
534    pub fn warning_with_heading<H: fmt::Display>(
535        &self,
536        heading: H,
537    ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, H> {
538        self.warning_no_heading().with_heading(heading)
539    }
540
541    /// Writer to print error without the "Error: " heading.
542    pub fn error_no_heading(&self) -> LabeledScope<Box<dyn Formatter + '_>> {
543        self.stderr_formatter().into_labeled("error")
544    }
545
546    /// Writer to print error with the given heading.
547    pub fn error_with_heading<H: fmt::Display>(
548        &self,
549        heading: H,
550    ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, H> {
551        self.error_no_heading().with_heading(heading)
552    }
553
554    /// Waits for the pager exits.
555    #[instrument(skip_all)]
556    pub fn finalize_pager(&mut self) {
557        let old_output = mem::replace(&mut self.output, UiOutput::new_terminal());
558        old_output.finalize(self);
559    }
560
561    pub fn can_prompt() -> bool {
562        io::stderr().is_terminal()
563            || env::var("JJ_INTERACTIVE")
564                .map(|v| v == "1")
565                .unwrap_or(false)
566    }
567
568    pub fn prompt(&self, prompt: &str) -> io::Result<String> {
569        if !Self::can_prompt() {
570            return Err(io::Error::new(
571                io::ErrorKind::Unsupported,
572                "Cannot prompt for input since the output is not connected to a terminal",
573            ));
574        }
575        write!(self.stderr(), "{prompt}: ")?;
576        self.stderr().flush()?;
577        let mut buf = String::new();
578        io::stdin().read_line(&mut buf)?;
579
580        if buf.is_empty() {
581            return Err(io::Error::new(
582                io::ErrorKind::UnexpectedEof,
583                "Prompt canceled by EOF",
584            ));
585        }
586
587        if let Some(trimmed) = buf.strip_suffix('\n') {
588            buf.truncate(trimmed.len());
589        }
590        Ok(buf)
591    }
592
593    /// Repeat the given prompt until the input is one of the specified choices.
594    /// Returns the index of the choice.
595    pub fn prompt_choice(
596        &self,
597        prompt: &str,
598        choices: &[impl AsRef<str>],
599        default_index: Option<usize>,
600    ) -> io::Result<usize> {
601        self.prompt_choice_with(
602            prompt,
603            default_index.map(|index| {
604                choices
605                    .get(index)
606                    .expect("default_index should be within range")
607                    .as_ref()
608            }),
609            |input| {
610                choices
611                    .iter()
612                    .position(|c| input == c.as_ref())
613                    .ok_or("unrecognized response")
614            },
615        )
616    }
617
618    /// Prompts for a yes-or-no response, with yes = true and no = false.
619    pub fn prompt_yes_no(&self, prompt: &str, default: Option<bool>) -> io::Result<bool> {
620        let default_str = match &default {
621            Some(true) => "(Yn)",
622            Some(false) => "(yN)",
623            None => "(yn)",
624        };
625        self.prompt_choice_with(
626            &format!("{prompt} {default_str}"),
627            default.map(|v| if v { "y" } else { "n" }),
628            |input| {
629                if input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") {
630                    Ok(true)
631                } else if input.eq_ignore_ascii_case("n") || input.eq_ignore_ascii_case("no") {
632                    Ok(false)
633                } else {
634                    Err("unrecognized response")
635                }
636            },
637        )
638    }
639
640    /// Repeats the given prompt until `parse(input)` returns a value.
641    ///
642    /// If the default `text` is given, an empty input will be mapped to it. It
643    /// will also be used in non-interactive session. The default `text` must
644    /// be parsable. If no default is given, this function will fail in
645    /// non-interactive session.
646    pub fn prompt_choice_with<T, E: fmt::Debug + fmt::Display>(
647        &self,
648        prompt: &str,
649        default: Option<&str>,
650        mut parse: impl FnMut(&str) -> Result<T, E>,
651    ) -> io::Result<T> {
652        // Parse the default to ensure that the text is valid.
653        let default = default.map(|text| (parse(text).expect("default should be valid"), text));
654
655        if !Self::can_prompt()
656            && let Some((value, text)) = default
657        {
658            // Choose the default automatically without waiting.
659            writeln!(self.stderr(), "{prompt}: {text}")?;
660            return Ok(value);
661        }
662
663        loop {
664            let input = self.prompt(prompt)?;
665            let input = input.trim();
666            if input.is_empty() {
667                if let Some((value, _)) = default {
668                    return Ok(value);
669                } else {
670                    continue;
671                }
672            }
673            match parse(input) {
674                Ok(value) => return Ok(value),
675                Err(err) => writeln!(self.warning_no_heading(), "{err}")?,
676            }
677        }
678    }
679
680    pub fn prompt_password(&self, prompt: &str) -> io::Result<String> {
681        if !io::stdout().is_terminal() {
682            return Err(io::Error::new(
683                io::ErrorKind::Unsupported,
684                "Cannot prompt for input since the output is not connected to a terminal",
685            ));
686        }
687        rpassword::prompt_password(format!("{prompt}: "))
688    }
689
690    pub fn term_width(&self) -> usize {
691        term_width().unwrap_or(80).into()
692    }
693}
694
695#[derive(Debug)]
696pub struct ProgressOutput<W> {
697    output: W,
698    term_width: Option<u16>,
699}
700
701impl ProgressOutput<io::Stderr> {
702    pub fn for_stderr() -> Self {
703        Self {
704            output: io::stderr(),
705            term_width: None,
706        }
707    }
708}
709
710impl<W> ProgressOutput<W> {
711    pub fn for_test(output: W, term_width: u16) -> Self {
712        Self {
713            output,
714            term_width: Some(term_width),
715        }
716    }
717
718    pub fn term_width(&self) -> Option<u16> {
719        // Terminal can be resized while progress is displayed, so don't cache it.
720        self.term_width.or_else(term_width)
721    }
722
723    /// Construct a guard object which writes `text` when dropped. Useful for
724    /// restoring terminal state.
725    pub fn output_guard(&self, text: String) -> OutputGuard {
726        OutputGuard {
727            text,
728            output: io::stderr(),
729        }
730    }
731}
732
733impl<W: Write> ProgressOutput<W> {
734    pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
735        self.output.write_fmt(fmt)
736    }
737
738    pub fn flush(&mut self) -> io::Result<()> {
739        self.output.flush()
740    }
741}
742
743pub struct OutputGuard {
744    text: String,
745    output: Stderr,
746}
747
748impl Drop for OutputGuard {
749    #[instrument(skip_all)]
750    fn drop(&mut self) {
751        self.output.write_all(self.text.as_bytes()).ok();
752        self.output.flush().ok();
753    }
754}
755
756#[cfg(unix)]
757fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::fd::OwnedFd> {
758    use std::os::fd::AsFd as _;
759    stdin.as_fd().try_clone_to_owned()
760}
761
762#[cfg(windows)]
763fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::windows::io::OwnedHandle> {
764    use std::os::windows::io::AsHandle as _;
765    stdin.as_handle().try_clone_to_owned()
766}
767
768fn format_error_with_sources(err: &dyn error::Error) -> impl fmt::Display {
769    iter::successors(Some(err), |&err| err.source()).format(": ")
770}
771
772fn term_width() -> Option<u16> {
773    if let Some(cols) = env::var("COLUMNS").ok().and_then(|s| s.parse().ok()) {
774        Some(cols)
775    } else {
776        crossterm::terminal::size().ok().map(|(cols, _)| cols)
777    }
778}