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