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