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::Stderr;
21use std::io::StderrLock;
22use std::io::Stdout;
23use std::io::StdoutLock;
24use std::io::Write;
25use std::iter;
26use std::mem;
27use std::process::Child;
28use std::process::ChildStdin;
29use std::process::Stdio;
30use std::thread;
31use std::thread::JoinHandle;
32
33use itertools::Itertools as _;
34use jj_lib::config::ConfigGetError;
35use jj_lib::config::StackedConfig;
36use os_pipe::PipeWriter;
37use tracing::instrument;
38
39use crate::command_error::CommandError;
40use crate::config::CommandNameAndArgs;
41use crate::formatter::Formatter;
42use crate::formatter::FormatterFactory;
43use crate::formatter::HeadingLabeledWriter;
44use crate::formatter::LabeledWriter;
45use crate::formatter::PlainTextFormatter;
46
47const BUILTIN_PAGER_NAME: &str = ":builtin";
48
49enum UiOutput {
50    Terminal {
51        stdout: Stdout,
52        stderr: Stderr,
53    },
54    Paged {
55        child: Child,
56        child_stdin: ChildStdin,
57    },
58    BuiltinPaged {
59        out_wr: PipeWriter,
60        err_wr: PipeWriter,
61        pager_thread: JoinHandle<streampager::Result<()>>,
62    },
63    Null,
64}
65
66impl UiOutput {
67    fn new_terminal() -> UiOutput {
68        UiOutput::Terminal {
69            stdout: io::stdout(),
70            stderr: io::stderr(),
71        }
72    }
73
74    fn new_paged(pager_cmd: &CommandNameAndArgs) -> io::Result<UiOutput> {
75        let mut cmd = pager_cmd.to_command();
76        tracing::info!(?cmd, "spawning pager");
77        let mut child = cmd.stdin(Stdio::piped()).spawn()?;
78        let child_stdin = child.stdin.take().unwrap();
79        Ok(UiOutput::Paged { child, child_stdin })
80    }
81
82    fn new_builtin_paged(config: &StreampagerConfig) -> streampager::Result<UiOutput> {
83        let streampager_config = streampager::config::Config {
84            wrapping_mode: config.wrapping.into(),
85            interface_mode: config.streampager_interface_mode(),
86            show_ruler: config.show_ruler,
87            // We could make scroll-past-eof configurable, but I'm guessing people
88            // will not miss it. If we do make it configurable, we should mention
89            // that it's a bad idea to turn this on if `interface=quit-if-one-page`,
90            // as it can leave a lot of empty lines on the screen after exiting.
91            scroll_past_eof: false,
92            ..Default::default()
93        };
94        let mut pager = streampager::Pager::new_using_stdio_with_config(streampager_config)?;
95
96        // Use native pipe, which can be attached to child process. The stdout
97        // stream could be an in-process channel, but the cost of extra syscalls
98        // wouldn't matter.
99        let (out_rd, out_wr) = os_pipe::pipe()?;
100        let (err_rd, err_wr) = os_pipe::pipe()?;
101        pager.add_stream(out_rd, "")?;
102        pager.add_error_stream(err_rd, "stderr")?;
103
104        Ok(UiOutput::BuiltinPaged {
105            out_wr,
106            err_wr,
107            pager_thread: thread::spawn(|| pager.run()),
108        })
109    }
110
111    fn finalize(self, ui: &Ui) {
112        match self {
113            UiOutput::Terminal { .. } => { /* no-op */ }
114            UiOutput::Paged {
115                mut child,
116                child_stdin,
117            } => {
118                drop(child_stdin);
119                if let Err(err) = child.wait() {
120                    // It's possible (though unlikely) that this write fails, but
121                    // this function gets called so late that there's not much we
122                    // can do about it.
123                    writeln!(
124                        ui.warning_default(),
125                        "Failed to wait on pager: {err}",
126                        err = format_error_with_sources(&err),
127                    )
128                    .ok();
129                }
130            }
131            UiOutput::BuiltinPaged {
132                out_wr,
133                err_wr,
134                pager_thread,
135            } => {
136                drop(out_wr);
137                drop(err_wr);
138                match pager_thread.join() {
139                    Ok(Ok(())) => {}
140                    Ok(Err(err)) => {
141                        writeln!(
142                            ui.warning_default(),
143                            "Failed to run builtin pager: {err}",
144                            err = format_error_with_sources(&err),
145                        )
146                        .ok();
147                    }
148                    Err(_) => {
149                        writeln!(ui.warning_default(), "Builtin pager crashed.").ok();
150                    }
151                }
152            }
153            UiOutput::Null => {}
154        }
155    }
156}
157
158pub enum UiStdout<'a> {
159    Terminal(StdoutLock<'static>),
160    Paged(&'a ChildStdin),
161    Builtin(&'a PipeWriter),
162    Null(io::Sink),
163}
164
165pub enum UiStderr<'a> {
166    Terminal(StderrLock<'static>),
167    Paged(&'a ChildStdin),
168    Builtin(&'a PipeWriter),
169    Null(io::Sink),
170}
171
172macro_rules! for_outputs {
173    ($ty:ident, $output:expr, $pat:pat => $expr:expr) => {
174        match $output {
175            $ty::Terminal($pat) => $expr,
176            $ty::Paged($pat) => $expr,
177            $ty::Builtin($pat) => $expr,
178            $ty::Null($pat) => $expr,
179        }
180    };
181}
182
183impl Write for UiStdout<'_> {
184    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
185        for_outputs!(Self, self, w => w.write(buf))
186    }
187
188    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
189        for_outputs!(Self, self, w => w.write_all(buf))
190    }
191
192    fn flush(&mut self) -> io::Result<()> {
193        for_outputs!(Self, self, w => w.flush())
194    }
195}
196
197impl Write for UiStderr<'_> {
198    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
199        for_outputs!(Self, self, w => w.write(buf))
200    }
201
202    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
203        for_outputs!(Self, self, w => w.write_all(buf))
204    }
205
206    fn flush(&mut self) -> io::Result<()> {
207        for_outputs!(Self, self, w => w.flush())
208    }
209}
210
211pub struct Ui {
212    quiet: bool,
213    pager: PagerConfig,
214    progress_indicator: bool,
215    formatter_factory: FormatterFactory,
216    output: UiOutput,
217}
218
219#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, clap::ValueEnum)]
220#[serde(rename_all = "kebab-case")]
221pub enum ColorChoice {
222    Always,
223    Never,
224    Debug,
225    Auto,
226}
227
228impl fmt::Display for ColorChoice {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        let s = match self {
231            ColorChoice::Always => "always",
232            ColorChoice::Never => "never",
233            ColorChoice::Debug => "debug",
234            ColorChoice::Auto => "auto",
235        };
236        write!(f, "{s}")
237    }
238}
239
240fn prepare_formatter_factory(
241    config: &StackedConfig,
242    stdout: &Stdout,
243) -> Result<FormatterFactory, ConfigGetError> {
244    let terminal = stdout.is_terminal();
245    let (color, debug) = match config.get("ui.color")? {
246        ColorChoice::Always => (true, false),
247        ColorChoice::Never => (false, false),
248        ColorChoice::Debug => (true, true),
249        ColorChoice::Auto => (terminal, false),
250    };
251    if color {
252        FormatterFactory::color(config, debug)
253    } else if terminal {
254        // Sanitize ANSI escape codes if we're printing to a terminal. Doesn't
255        // affect ANSI escape codes that originate from the formatter itself.
256        Ok(FormatterFactory::sanitized())
257    } else {
258        Ok(FormatterFactory::plain_text())
259    }
260}
261
262#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
263#[serde(rename_all(deserialize = "kebab-case"))]
264pub enum PaginationChoice {
265    Never,
266    Auto,
267}
268
269#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
270#[serde(rename_all(deserialize = "kebab-case"))]
271pub enum StreampagerAlternateScreenMode {
272    QuitIfOnePage,
273    FullScreenClearOutput,
274    QuitQuicklyOrClearOutput,
275}
276
277#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
278#[serde(rename_all(deserialize = "kebab-case"))]
279enum StreampagerWrappingMode {
280    None,
281    Word,
282    Anywhere,
283}
284
285impl From<StreampagerWrappingMode> for streampager::config::WrappingMode {
286    fn from(val: StreampagerWrappingMode) -> Self {
287        use streampager::config::WrappingMode;
288        match val {
289            StreampagerWrappingMode::None => WrappingMode::Unwrapped,
290            StreampagerWrappingMode::Word => WrappingMode::WordBoundary,
291            StreampagerWrappingMode::Anywhere => WrappingMode::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 streampager::config::InterfaceMode;
310        use StreampagerAlternateScreenMode::*;
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<PagerConfig, ConfigGetError> {
328        if matches!(config.get("ui.paginate")?, PaginationChoice::Never) {
329            return Ok(PagerConfig::Disabled);
330        };
331        let args: CommandNameAndArgs = config.get("ui.pager")?;
332        if args.as_str() == Some(BUILTIN_PAGER_NAME) {
333            Ok(PagerConfig::Builtin(config.get("ui.streampager")?))
334        } else {
335            Ok(PagerConfig::External(args))
336        }
337    }
338}
339
340impl Ui {
341    pub fn null() -> Ui {
342        Ui {
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<Ui, CommandError> {
352        let formatter_factory = prepare_formatter_factory(config, &io::stdout())?;
353        Ok(Ui {
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(
501        &self,
502    ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, &'static str> {
503        self.hint_with_heading("Hint: ")
504    }
505
506    /// Writer to print hint without the "Hint: " heading.
507    pub fn hint_no_heading(&self) -> LabeledWriter<Box<dyn Formatter + '_>, &'static str> {
508        let formatter = self
509            .status_formatter()
510            .unwrap_or_else(|| Box::new(PlainTextFormatter::new(io::sink())));
511        LabeledWriter::new(formatter, "hint")
512    }
513
514    /// Writer to print hint with the given heading.
515    pub fn hint_with_heading<H: fmt::Display>(
516        &self,
517        heading: H,
518    ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H> {
519        self.hint_no_heading().with_heading(heading)
520    }
521
522    /// Writer to print warning with the default "Warning: " heading.
523    pub fn warning_default(
524        &self,
525    ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, &'static str> {
526        self.warning_with_heading("Warning: ")
527    }
528
529    /// Writer to print warning without the "Warning: " heading.
530    pub fn warning_no_heading(&self) -> LabeledWriter<Box<dyn Formatter + '_>, &'static str> {
531        LabeledWriter::new(self.stderr_formatter(), "warning")
532    }
533
534    /// Writer to print warning with the given heading.
535    pub fn warning_with_heading<H: fmt::Display>(
536        &self,
537        heading: H,
538    ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H> {
539        self.warning_no_heading().with_heading(heading)
540    }
541
542    /// Writer to print error without the "Error: " heading.
543    pub fn error_no_heading(&self) -> LabeledWriter<Box<dyn Formatter + '_>, &'static str> {
544        LabeledWriter::new(self.stderr_formatter(), "error")
545    }
546
547    /// Writer to print error with the given heading.
548    pub fn error_with_heading<H: fmt::Display>(
549        &self,
550        heading: H,
551    ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H> {
552        self.error_no_heading().with_heading(heading)
553    }
554
555    /// Waits for the pager exits.
556    #[instrument(skip_all)]
557    pub fn finalize_pager(&mut self) {
558        let old_output = mem::replace(&mut self.output, UiOutput::new_terminal());
559        old_output.finalize(self);
560    }
561
562    pub fn can_prompt() -> bool {
563        io::stderr().is_terminal()
564            || env::var("JJ_INTERACTIVE")
565                .map(|v| v == "1")
566                .unwrap_or(false)
567    }
568
569    pub fn prompt(&self, prompt: &str) -> io::Result<String> {
570        if !Self::can_prompt() {
571            return Err(io::Error::new(
572                io::ErrorKind::Unsupported,
573                "Cannot prompt for input since the output is not connected to a terminal",
574            ));
575        }
576        write!(self.stderr(), "{prompt}: ")?;
577        self.stderr().flush()?;
578        let mut buf = String::new();
579        io::stdin().read_line(&mut buf)?;
580
581        if buf.is_empty() {
582            return Err(io::Error::new(
583                io::ErrorKind::UnexpectedEof,
584                "Prompt cancelled by EOF",
585            ));
586        }
587
588        if let Some(trimmed) = buf.strip_suffix('\n') {
589            buf.truncate(trimmed.len());
590        }
591        Ok(buf)
592    }
593
594    /// Repeat the given prompt until the input is one of the specified choices.
595    /// Returns the index of the choice.
596    pub fn prompt_choice(
597        &self,
598        prompt: &str,
599        choices: &[impl AsRef<str>],
600        default_index: Option<usize>,
601    ) -> io::Result<usize> {
602        self.prompt_choice_with(
603            prompt,
604            default_index.map(|index| {
605                choices
606                    .get(index)
607                    .expect("default_index should be within range")
608                    .as_ref()
609            }),
610            |input| {
611                choices
612                    .iter()
613                    .position(|c| input == c.as_ref())
614                    .ok_or("unrecognized response")
615            },
616        )
617    }
618
619    /// Prompts for a yes-or-no response, with yes = true and no = false.
620    pub fn prompt_yes_no(&self, prompt: &str, default: Option<bool>) -> io::Result<bool> {
621        let default_str = match &default {
622            Some(true) => "(Yn)",
623            Some(false) => "(yN)",
624            None => "(yn)",
625        };
626        self.prompt_choice_with(
627            &format!("{prompt} {default_str}"),
628            default.map(|v| if v { "y" } else { "n" }),
629            |input| {
630                if input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") {
631                    Ok(true)
632                } else if input.eq_ignore_ascii_case("n") || input.eq_ignore_ascii_case("no") {
633                    Ok(false)
634                } else {
635                    Err("unrecognized response")
636                }
637            },
638        )
639    }
640
641    /// Repeats the given prompt until `parse(input)` returns a value.
642    ///
643    /// If the default `text` is given, an empty input will be mapped to it. It
644    /// will also be used in non-interactive session. The default `text` must
645    /// be parsable. If no default is given, this function will fail in
646    /// non-interactive session.
647    pub fn prompt_choice_with<T, E: fmt::Debug + fmt::Display>(
648        &self,
649        prompt: &str,
650        default: Option<&str>,
651        mut parse: impl FnMut(&str) -> Result<T, E>,
652    ) -> io::Result<T> {
653        // Parse the default to ensure that the text is valid.
654        let default = default.map(|text| (parse(text).expect("default should be valid"), text));
655
656        if !Self::can_prompt() {
657            if let Some((value, text)) = default {
658                // Choose the default automatically without waiting.
659                writeln!(self.stderr(), "{prompt}: {text}")?;
660                return Ok(value);
661            }
662        }
663
664        loop {
665            let input = self.prompt(prompt)?;
666            let input = input.trim();
667            if input.is_empty() {
668                if let Some((value, _)) = default {
669                    return Ok(value);
670                } else {
671                    continue;
672                }
673            }
674            match parse(input) {
675                Ok(value) => return Ok(value),
676                Err(err) => writeln!(self.warning_no_heading(), "{err}")?,
677            }
678        }
679    }
680
681    pub fn prompt_password(&self, prompt: &str) -> io::Result<String> {
682        if !io::stdout().is_terminal() {
683            return Err(io::Error::new(
684                io::ErrorKind::Unsupported,
685                "Cannot prompt for input since the output is not connected to a terminal",
686            ));
687        }
688        rpassword::prompt_password(format!("{prompt}: "))
689    }
690
691    pub fn term_width(&self) -> usize {
692        term_width().unwrap_or(80).into()
693    }
694}
695
696#[derive(Debug)]
697pub struct ProgressOutput<W> {
698    output: W,
699    term_width: Option<u16>,
700}
701
702impl ProgressOutput<io::Stderr> {
703    pub fn for_stderr() -> ProgressOutput<io::Stderr> {
704        ProgressOutput {
705            output: io::stderr(),
706            term_width: None,
707        }
708    }
709}
710
711impl<W> ProgressOutput<W> {
712    pub fn for_test(output: W, term_width: u16) -> Self {
713        Self {
714            output,
715            term_width: Some(term_width),
716        }
717    }
718
719    pub fn term_width(&self) -> Option<u16> {
720        // Terminal can be resized while progress is displayed, so don't cache it.
721        self.term_width.or_else(term_width)
722    }
723
724    /// Construct a guard object which writes `text` when dropped. Useful for
725    /// restoring terminal state.
726    pub fn output_guard(&self, text: String) -> OutputGuard {
727        OutputGuard {
728            text,
729            output: io::stderr(),
730        }
731    }
732}
733
734impl<W: Write> ProgressOutput<W> {
735    pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
736        self.output.write_fmt(fmt)
737    }
738
739    pub fn flush(&mut self) -> io::Result<()> {
740        self.output.flush()
741    }
742}
743
744pub struct OutputGuard {
745    text: String,
746    output: Stderr,
747}
748
749impl Drop for OutputGuard {
750    #[instrument(skip_all)]
751    fn drop(&mut self) {
752        _ = self.output.write_all(self.text.as_bytes());
753        _ = self.output.flush();
754    }
755}
756
757#[cfg(unix)]
758fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::fd::OwnedFd> {
759    use std::os::fd::AsFd as _;
760    stdin.as_fd().try_clone_to_owned()
761}
762
763#[cfg(windows)]
764fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::windows::io::OwnedHandle> {
765    use std::os::windows::io::AsHandle as _;
766    stdin.as_handle().try_clone_to_owned()
767}
768
769fn format_error_with_sources(err: &dyn error::Error) -> impl fmt::Display + use<'_> {
770    iter::successors(Some(err), |&err| err.source()).format(": ")
771}
772
773fn term_width() -> Option<u16> {
774    if let Some(cols) = env::var("COLUMNS").ok().and_then(|s| s.parse().ok()) {
775        Some(cols)
776    } else {
777        crossterm::terminal::size().ok().map(|(cols, _)| cols)
778    }
779}