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