just playing with tangled
at main 779 lines 26 kB view raw
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}