just playing with tangled
at ig/vimdiffwarn 777 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 // 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}