just playing with tangled
at diffedit3 630 lines 20 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::io::{IsTerminal as _, Stderr, StderrLock, Stdout, StdoutLock, Write}; 16use std::process::{Child, ChildStdin, Stdio}; 17use std::str::FromStr; 18use std::thread::JoinHandle; 19use std::{env, fmt, io, mem}; 20 21use minus::Pager as MinusPager; 22use tracing::instrument; 23 24use crate::command_error::{config_error_with_message, CommandError}; 25use crate::config::CommandNameAndArgs; 26use crate::formatter::{Formatter, FormatterFactory, HeadingLabeledWriter, LabeledWriter}; 27 28const BUILTIN_PAGER_NAME: &str = ":builtin"; 29 30enum UiOutput { 31 Terminal { 32 stdout: Stdout, 33 stderr: Stderr, 34 }, 35 Paged { 36 child: Child, 37 child_stdin: ChildStdin, 38 }, 39 BuiltinPaged { 40 pager: BuiltinPager, 41 }, 42} 43 44/// A builtin pager 45pub struct BuiltinPager { 46 pager: MinusPager, 47 dynamic_pager_thread: JoinHandle<()>, 48} 49 50impl std::io::Write for &BuiltinPager { 51 fn flush(&mut self) -> io::Result<()> { 52 // no-op since this is being run in a dynamic pager mode. 53 Ok(()) 54 } 55 56 fn write(&mut self, buf: &[u8]) -> io::Result<usize> { 57 let string = std::str::from_utf8(buf).map_err(std::io::Error::other)?; 58 self.pager.push_str(string).map_err(std::io::Error::other)?; 59 Ok(buf.len()) 60 } 61} 62 63impl Default for BuiltinPager { 64 fn default() -> Self { 65 Self::new() 66 } 67} 68 69impl BuiltinPager { 70 pub fn finalize(self) { 71 let dynamic_pager_thread = self.dynamic_pager_thread; 72 dynamic_pager_thread.join().unwrap(); 73 } 74 75 pub fn new() -> Self { 76 let pager = MinusPager::new(); 77 // Prefer to be cautious and only kill the pager instead of the whole process 78 // like minus does by default. 79 pager 80 .set_exit_strategy(minus::ExitStrategy::PagerQuit) 81 .expect("Able to set the exit strategy"); 82 let pager_handle = pager.clone(); 83 84 BuiltinPager { 85 pager, 86 dynamic_pager_thread: std::thread::spawn(move || { 87 // This thread handles the actual paging. 88 minus::dynamic_paging(pager_handle).unwrap(); 89 }), 90 } 91 } 92} 93 94impl UiOutput { 95 fn new_builtin() -> UiOutput { 96 UiOutput::BuiltinPaged { 97 pager: BuiltinPager::new(), 98 } 99 } 100 fn new_terminal() -> UiOutput { 101 UiOutput::Terminal { 102 stdout: io::stdout(), 103 stderr: io::stderr(), 104 } 105 } 106 107 fn new_paged(pager_cmd: &CommandNameAndArgs) -> io::Result<UiOutput> { 108 let mut child = pager_cmd.to_command().stdin(Stdio::piped()).spawn()?; 109 let child_stdin = child.stdin.take().unwrap(); 110 Ok(UiOutput::Paged { child, child_stdin }) 111 } 112} 113 114pub enum UiStdout<'a> { 115 Terminal(StdoutLock<'static>), 116 Paged(&'a ChildStdin), 117 Builtin(&'a BuiltinPager), 118} 119 120pub enum UiStderr<'a> { 121 Terminal(StderrLock<'static>), 122 Paged(&'a ChildStdin), 123 Builtin(&'a BuiltinPager), 124} 125 126macro_rules! for_outputs { 127 ($ty:ident, $output:expr, $pat:pat => $expr:expr) => { 128 match $output { 129 $ty::Terminal($pat) => $expr, 130 $ty::Paged($pat) => $expr, 131 $ty::Builtin($pat) => $expr, 132 } 133 }; 134} 135 136impl Write for UiStdout<'_> { 137 fn write(&mut self, buf: &[u8]) -> io::Result<usize> { 138 for_outputs!(Self, self, w => w.write(buf)) 139 } 140 141 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { 142 for_outputs!(Self, self, w => w.write_all(buf)) 143 } 144 145 fn flush(&mut self) -> io::Result<()> { 146 for_outputs!(Self, self, w => w.flush()) 147 } 148} 149 150impl Write for UiStderr<'_> { 151 fn write(&mut self, buf: &[u8]) -> io::Result<usize> { 152 for_outputs!(Self, self, w => w.write(buf)) 153 } 154 155 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { 156 for_outputs!(Self, self, w => w.write_all(buf)) 157 } 158 159 fn flush(&mut self) -> io::Result<()> { 160 for_outputs!(Self, self, w => w.flush()) 161 } 162} 163 164pub struct Ui { 165 color: bool, 166 quiet: bool, 167 pager_cmd: CommandNameAndArgs, 168 paginate: PaginationChoice, 169 progress_indicator: bool, 170 formatter_factory: FormatterFactory, 171 output: UiOutput, 172} 173 174fn progress_indicator_setting(config: &config::Config) -> bool { 175 config.get_bool("ui.progress-indicator").unwrap_or(true) 176} 177 178#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 179pub enum ColorChoice { 180 Always, 181 Never, 182 #[default] 183 Auto, 184} 185 186impl FromStr for ColorChoice { 187 type Err = &'static str; 188 189 fn from_str(s: &str) -> Result<Self, Self::Err> { 190 match s { 191 "always" => Ok(ColorChoice::Always), 192 "never" => Ok(ColorChoice::Never), 193 "auto" => Ok(ColorChoice::Auto), 194 _ => Err("must be one of always, never, or auto"), 195 } 196 } 197} 198 199impl fmt::Display for ColorChoice { 200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 201 let s = match self { 202 ColorChoice::Always => "always", 203 ColorChoice::Never => "never", 204 ColorChoice::Auto => "auto", 205 }; 206 write!(f, "{s}") 207 } 208} 209 210fn color_setting(config: &config::Config) -> ColorChoice { 211 config 212 .get_string("ui.color") 213 .ok() 214 .and_then(|s| s.parse().ok()) 215 .unwrap_or_default() 216} 217 218fn use_color(choice: ColorChoice) -> bool { 219 match choice { 220 ColorChoice::Always => true, 221 ColorChoice::Never => false, 222 ColorChoice::Auto => io::stdout().is_terminal(), 223 } 224} 225 226fn be_quiet(config: &config::Config) -> bool { 227 config.get_bool("ui.quiet").unwrap_or_default() 228} 229 230#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize)] 231#[serde(rename_all(deserialize = "kebab-case"))] 232pub enum PaginationChoice { 233 Never, 234 #[default] 235 Auto, 236} 237 238fn pagination_setting(config: &config::Config) -> Result<PaginationChoice, CommandError> { 239 config 240 .get::<PaginationChoice>("ui.paginate") 241 .map_err(|err| config_error_with_message("Invalid `ui.paginate`", err)) 242} 243 244fn pager_setting(config: &config::Config) -> Result<CommandNameAndArgs, CommandError> { 245 config 246 .get::<CommandNameAndArgs>("ui.pager") 247 .map_err(|err| config_error_with_message("Invalid `ui.pager`", err)) 248} 249 250impl Ui { 251 pub fn with_config(config: &config::Config) -> Result<Ui, CommandError> { 252 let color = use_color(color_setting(config)); 253 let quiet = be_quiet(config); 254 // Sanitize ANSI escape codes if we're printing to a terminal. Doesn't affect 255 // ANSI escape codes that originate from the formatter itself. 256 let sanitize = io::stdout().is_terminal(); 257 let formatter_factory = FormatterFactory::prepare(config, color, sanitize)?; 258 let progress_indicator = progress_indicator_setting(config); 259 Ok(Ui { 260 color, 261 quiet, 262 formatter_factory, 263 pager_cmd: pager_setting(config)?, 264 paginate: pagination_setting(config)?, 265 progress_indicator, 266 output: UiOutput::new_terminal(), 267 }) 268 } 269 270 pub fn reset(&mut self, config: &config::Config) -> Result<(), CommandError> { 271 self.color = use_color(color_setting(config)); 272 self.quiet = be_quiet(config); 273 self.paginate = pagination_setting(config)?; 274 self.pager_cmd = pager_setting(config)?; 275 self.progress_indicator = progress_indicator_setting(config); 276 let sanitize = io::stdout().is_terminal(); 277 self.formatter_factory = FormatterFactory::prepare(config, self.color, sanitize)?; 278 Ok(()) 279 } 280 281 /// Switches the output to use the pager, if allowed. 282 #[instrument(skip_all)] 283 pub fn request_pager(&mut self) { 284 match self.paginate { 285 PaginationChoice::Never => return, 286 PaginationChoice::Auto => {} 287 } 288 289 match self.output { 290 UiOutput::Terminal { .. } if io::stdout().is_terminal() => { 291 if self.pager_cmd == CommandNameAndArgs::String(BUILTIN_PAGER_NAME.into()) { 292 self.output = UiOutput::new_builtin(); 293 return; 294 } 295 296 match UiOutput::new_paged(&self.pager_cmd) { 297 Ok(pager_output) => { 298 self.output = pager_output; 299 } 300 Err(e) => { 301 // The pager executable couldn't be found or couldn't be run 302 writeln!( 303 self.warning_default(), 304 "Failed to spawn pager '{name}': {e}. Consider using the `:builtin` \ 305 pager.", 306 name = self.pager_cmd.split_name(), 307 ) 308 .ok(); 309 } 310 } 311 } 312 UiOutput::Terminal { .. } | UiOutput::BuiltinPaged { .. } | UiOutput::Paged { .. } => {} 313 } 314 } 315 316 pub fn color(&self) -> bool { 317 self.color 318 } 319 320 pub fn new_formatter<'output, W: Write + 'output>( 321 &self, 322 output: W, 323 ) -> Box<dyn Formatter + 'output> { 324 self.formatter_factory.new_formatter(output) 325 } 326 327 /// Locked stdout stream. 328 pub fn stdout(&self) -> UiStdout<'_> { 329 match &self.output { 330 UiOutput::Terminal { stdout, .. } => UiStdout::Terminal(stdout.lock()), 331 UiOutput::Paged { child_stdin, .. } => UiStdout::Paged(child_stdin), 332 UiOutput::BuiltinPaged { pager } => UiStdout::Builtin(pager), 333 } 334 } 335 336 /// Creates a formatter for the locked stdout stream. 337 /// 338 /// Labels added to the returned formatter should be removed by caller. 339 /// Otherwise the last color would persist. 340 pub fn stdout_formatter(&self) -> Box<dyn Formatter + '_> { 341 for_outputs!(UiStdout, self.stdout(), w => self.new_formatter(w)) 342 } 343 344 /// Locked stderr stream. 345 pub fn stderr(&self) -> UiStderr<'_> { 346 match &self.output { 347 UiOutput::Terminal { stderr, .. } => UiStderr::Terminal(stderr.lock()), 348 UiOutput::Paged { child_stdin, .. } => UiStderr::Paged(child_stdin), 349 UiOutput::BuiltinPaged { pager } => UiStderr::Builtin(pager), 350 } 351 } 352 353 /// Creates a formatter for the locked stderr stream. 354 pub fn stderr_formatter(&self) -> Box<dyn Formatter + '_> { 355 for_outputs!(UiStderr, self.stderr(), w => self.new_formatter(w)) 356 } 357 358 /// Stderr stream to be attached to a child process. 359 pub fn stderr_for_child(&self) -> io::Result<Stdio> { 360 match &self.output { 361 UiOutput::Terminal { .. } => Ok(Stdio::inherit()), 362 UiOutput::Paged { child_stdin, .. } => Ok(duplicate_child_stdin(child_stdin)?.into()), 363 // Stderr does not get redirected through the built-in pager. 364 UiOutput::BuiltinPaged { .. } => Ok(Stdio::inherit()), 365 } 366 } 367 368 /// Whether continuous feedback should be displayed for long-running 369 /// operations 370 pub fn use_progress_indicator(&self) -> bool { 371 match &self.output { 372 UiOutput::Terminal { stderr, .. } => self.progress_indicator && stderr.is_terminal(), 373 UiOutput::Paged { .. } => false, 374 UiOutput::BuiltinPaged { .. } => false, 375 } 376 } 377 378 pub fn progress_output(&self) -> Option<ProgressOutput> { 379 self.use_progress_indicator().then(|| ProgressOutput { 380 output: io::stderr(), 381 }) 382 } 383 384 /// Writer to print an update that's not part of the command's main output. 385 pub fn status(&self) -> Box<dyn Write + '_> { 386 if self.quiet { 387 Box::new(std::io::sink()) 388 } else { 389 Box::new(self.stderr()) 390 } 391 } 392 393 /// A formatter to print an update that's not part of the command's main 394 /// output. Returns `None` if `--quiet` was requested. 395 pub fn status_formatter(&self) -> Option<Box<dyn Formatter + '_>> { 396 (!self.quiet).then(|| self.stderr_formatter()) 397 } 398 399 /// Writer to print hint with the default "Hint: " heading. 400 pub fn hint_default( 401 &self, 402 ) -> Option<HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, &'static str>> { 403 self.hint_with_heading("Hint: ") 404 } 405 406 /// Writer to print hint without the "Hint: " heading. 407 pub fn hint_no_heading(&self) -> Option<LabeledWriter<Box<dyn Formatter + '_>, &'static str>> { 408 (!self.quiet).then(|| LabeledWriter::new(self.stderr_formatter(), "hint")) 409 } 410 411 /// Writer to print hint with the given heading. 412 pub fn hint_with_heading<H: fmt::Display>( 413 &self, 414 heading: H, 415 ) -> Option<HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H>> { 416 (!self.quiet).then(|| HeadingLabeledWriter::new(self.stderr_formatter(), "hint", heading)) 417 } 418 419 /// Writer to print warning with the default "Warning: " heading. 420 pub fn warning_default( 421 &self, 422 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, &'static str> { 423 self.warning_with_heading("Warning: ") 424 } 425 426 /// Writer to print warning without the "Warning: " heading. 427 pub fn warning_no_heading(&self) -> LabeledWriter<Box<dyn Formatter + '_>, &'static str> { 428 LabeledWriter::new(self.stderr_formatter(), "warning") 429 } 430 431 /// Writer to print warning with the given heading. 432 pub fn warning_with_heading<H: fmt::Display>( 433 &self, 434 heading: H, 435 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H> { 436 HeadingLabeledWriter::new(self.stderr_formatter(), "warning", heading) 437 } 438 439 /// Writer to print error without the "Error: " heading. 440 pub fn error_no_heading(&self) -> LabeledWriter<Box<dyn Formatter + '_>, &'static str> { 441 LabeledWriter::new(self.stderr_formatter(), "error") 442 } 443 444 /// Writer to print error with the given heading. 445 pub fn error_with_heading<H: fmt::Display>( 446 &self, 447 heading: H, 448 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H> { 449 HeadingLabeledWriter::new(self.stderr_formatter(), "error", heading) 450 } 451 452 /// Waits for the pager exits. 453 #[instrument(skip_all)] 454 pub fn finalize_pager(&mut self) { 455 match mem::replace(&mut self.output, UiOutput::new_terminal()) { 456 UiOutput::Paged { 457 mut child, 458 child_stdin, 459 } => { 460 drop(child_stdin); 461 if let Err(e) = child.wait() { 462 // It's possible (though unlikely) that this write fails, but 463 // this function gets called so late that there's not much we 464 // can do about it. 465 writeln!(self.warning_default(), "Failed to wait on pager: {e}").ok(); 466 } 467 } 468 UiOutput::BuiltinPaged { pager } => { 469 pager.finalize(); 470 } 471 _ => { /* no-op */ } 472 } 473 } 474 475 pub fn can_prompt() -> bool { 476 io::stdout().is_terminal() 477 || env::var("JJ_INTERACTIVE") 478 .map(|v| v == "1") 479 .unwrap_or(false) 480 } 481 482 #[allow(unknown_lints)] // XXX FIXME (aseipp): nightly bogons; re-test this occasionally 483 #[allow(clippy::assigning_clones)] 484 pub fn prompt(&self, prompt: &str) -> io::Result<String> { 485 if !Self::can_prompt() { 486 return Err(io::Error::new( 487 io::ErrorKind::Unsupported, 488 "Cannot prompt for input since the output is not connected to a terminal", 489 )); 490 } 491 write!(self.stdout(), "{prompt}: ")?; 492 self.stdout().flush()?; 493 let mut buf = String::new(); 494 io::stdin().read_line(&mut buf)?; 495 496 if let Some(trimmed) = buf.strip_suffix('\n') { 497 buf = trimmed.to_owned(); 498 } else if buf.is_empty() { 499 return Err(io::Error::new( 500 io::ErrorKind::UnexpectedEof, 501 "Prompt cancelled by EOF", 502 )); 503 } 504 505 Ok(buf) 506 } 507 508 /// Repeat the given prompt until the input is one of the specified choices. 509 pub fn prompt_choice( 510 &self, 511 prompt: &str, 512 choices: &[impl AsRef<str>], 513 default: Option<&str>, 514 ) -> io::Result<String> { 515 if !Self::can_prompt() { 516 if let Some(default) = default { 517 // Choose the default automatically without waiting. 518 writeln!(self.stdout(), "{prompt}: {default}")?; 519 return Ok(default.to_owned()); 520 } 521 } 522 523 loop { 524 let choice = self.prompt(prompt)?.trim().to_owned(); 525 if choice.is_empty() { 526 if let Some(default) = default { 527 return Ok(default.to_owned()); 528 } 529 } 530 if choices.iter().any(|c| choice == c.as_ref()) { 531 return Ok(choice); 532 } 533 534 writeln!(self.warning_no_heading(), "unrecognized response")?; 535 } 536 } 537 538 /// Prompts for a yes-or-no response, with yes = true and no = false. 539 pub fn prompt_yes_no(&self, prompt: &str, default: Option<bool>) -> io::Result<bool> { 540 let default_str = match &default { 541 Some(true) => "(Yn)", 542 Some(false) => "(yN)", 543 None => "(yn)", 544 }; 545 let default_choice = default.map(|c| if c { "Y" } else { "N" }); 546 547 let choice = self.prompt_choice( 548 &format!("{} {}", prompt, default_str), 549 &["y", "n", "yes", "no", "Yes", "No", "YES", "NO"], 550 default_choice, 551 )?; 552 Ok(choice.starts_with(['y', 'Y'])) 553 } 554 555 pub fn prompt_password(&self, prompt: &str) -> io::Result<String> { 556 if !io::stdout().is_terminal() { 557 return Err(io::Error::new( 558 io::ErrorKind::Unsupported, 559 "Cannot prompt for input since the output is not connected to a terminal", 560 )); 561 } 562 rpassword::prompt_password(format!("{prompt}: ")) 563 } 564 565 pub fn term_width(&self) -> Option<u16> { 566 term_width() 567 } 568} 569 570#[derive(Debug)] 571pub struct ProgressOutput { 572 output: Stderr, 573} 574 575impl ProgressOutput { 576 pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> { 577 self.output.write_fmt(fmt) 578 } 579 580 pub fn flush(&mut self) -> io::Result<()> { 581 self.output.flush() 582 } 583 584 pub fn term_width(&self) -> Option<u16> { 585 // Terminal can be resized while progress is displayed, so don't cache it. 586 term_width() 587 } 588 589 /// Construct a guard object which writes `text` when dropped. Useful for 590 /// restoring terminal state. 591 pub fn output_guard(&self, text: String) -> OutputGuard { 592 OutputGuard { 593 text, 594 output: io::stderr(), 595 } 596 } 597} 598 599pub struct OutputGuard { 600 text: String, 601 output: Stderr, 602} 603 604impl Drop for OutputGuard { 605 #[instrument(skip_all)] 606 fn drop(&mut self) { 607 _ = self.output.write_all(self.text.as_bytes()); 608 _ = self.output.flush(); 609 } 610} 611 612#[cfg(unix)] 613fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::fd::OwnedFd> { 614 use std::os::fd::AsFd as _; 615 stdin.as_fd().try_clone_to_owned() 616} 617 618#[cfg(windows)] 619fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::windows::io::OwnedHandle> { 620 use std::os::windows::io::AsHandle as _; 621 stdin.as_handle().try_clone_to_owned() 622} 623 624fn term_width() -> Option<u16> { 625 if let Some(cols) = env::var("COLUMNS").ok().and_then(|s| s.parse().ok()) { 626 Some(cols) 627 } else { 628 crossterm::terminal::size().ok().map(|(cols, _)| cols) 629 } 630}