use std::env;
use std::error;
use std::fmt;
use std::io;
use std::io::IsTerminal as _;
use std::io::Stderr;
use std::io::StderrLock;
use std::io::Stdout;
use std::io::StdoutLock;
use std::io::Write;
use std::iter;
use std::mem;
use std::process::Child;
use std::process::ChildStdin;
use std::process::Stdio;
use std::thread;
use std::thread::JoinHandle;
use itertools::Itertools as _;
use jj_lib::config::ConfigGetError;
use jj_lib::config::StackedConfig;
use os_pipe::PipeWriter;
use tracing::instrument;
use crate::command_error::CommandError;
use crate::config::CommandNameAndArgs;
use crate::formatter::Formatter;
use crate::formatter::FormatterFactory;
use crate::formatter::HeadingLabeledWriter;
use crate::formatter::LabeledWriter;
use crate::formatter::PlainTextFormatter;
const BUILTIN_PAGER_NAME: &str = ":builtin";
enum UiOutput {
Terminal {
stdout: Stdout,
stderr: Stderr,
},
Paged {
child: Child,
child_stdin: ChildStdin,
},
BuiltinPaged {
out_wr: PipeWriter,
err_wr: PipeWriter,
pager_thread: JoinHandle<streampager::Result<()>>,
},
Null,
}
impl UiOutput {
fn new_terminal() -> UiOutput {
UiOutput::Terminal {
stdout: io::stdout(),
stderr: io::stderr(),
}
}
fn new_paged(pager_cmd: &CommandNameAndArgs) -> io::Result<UiOutput> {
let mut cmd = pager_cmd.to_command();
tracing::info!(?cmd, "spawning pager");
let mut child = cmd.stdin(Stdio::piped()).spawn()?;
let child_stdin = child.stdin.take().unwrap();
Ok(UiOutput::Paged { child, child_stdin })
}
fn new_builtin_paged(config: &StreampagerConfig) -> streampager::Result<UiOutput> {
let streampager_config = streampager::config::Config {
wrapping_mode: config.wrapping.into(),
interface_mode: config.streampager_interface_mode(),
show_ruler: config.show_ruler,
scroll_past_eof: false,
..Default::default()
};
let mut pager = streampager::Pager::new_using_stdio_with_config(streampager_config)?;
let (out_rd, out_wr) = os_pipe::pipe()?;
let (err_rd, err_wr) = os_pipe::pipe()?;
pager.add_stream(out_rd, "")?;
pager.add_error_stream(err_rd, "stderr")?;
Ok(UiOutput::BuiltinPaged {
out_wr,
err_wr,
pager_thread: thread::spawn(|| pager.run()),
})
}
fn finalize(self, ui: &Ui) {
match self {
UiOutput::Terminal { .. } => { }
UiOutput::Paged {
mut child,
child_stdin,
} => {
drop(child_stdin);
if let Err(err) = child.wait() {
writeln!(
ui.warning_default(),
"Failed to wait on pager: {err}",
err = format_error_with_sources(&err),
)
.ok();
}
}
UiOutput::BuiltinPaged {
out_wr,
err_wr,
pager_thread,
} => {
drop(out_wr);
drop(err_wr);
match pager_thread.join() {
Ok(Ok(())) => {}
Ok(Err(err)) => {
writeln!(
ui.warning_default(),
"Failed to run builtin pager: {err}",
err = format_error_with_sources(&err),
)
.ok();
}
Err(_) => {
writeln!(ui.warning_default(), "Builtin pager crashed.").ok();
}
}
}
UiOutput::Null => {}
}
}
}
pub enum UiStdout<'a> {
Terminal(StdoutLock<'static>),
Paged(&'a ChildStdin),
Builtin(&'a PipeWriter),
Null(io::Sink),
}
pub enum UiStderr<'a> {
Terminal(StderrLock<'static>),
Paged(&'a ChildStdin),
Builtin(&'a PipeWriter),
Null(io::Sink),
}
macro_rules! for_outputs {
($ty:ident, $output:expr, $pat:pat => $expr:expr) => {
match $output {
$ty::Terminal($pat) => $expr,
$ty::Paged($pat) => $expr,
$ty::Builtin($pat) => $expr,
$ty::Null($pat) => $expr,
}
};
}
impl Write for UiStdout<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
for_outputs!(Self, self, w => w.write(buf))
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
for_outputs!(Self, self, w => w.write_all(buf))
}
fn flush(&mut self) -> io::Result<()> {
for_outputs!(Self, self, w => w.flush())
}
}
impl Write for UiStderr<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
for_outputs!(Self, self, w => w.write(buf))
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
for_outputs!(Self, self, w => w.write_all(buf))
}
fn flush(&mut self) -> io::Result<()> {
for_outputs!(Self, self, w => w.flush())
}
}
pub struct Ui {
quiet: bool,
pager: PagerConfig,
progress_indicator: bool,
formatter_factory: FormatterFactory,
output: UiOutput,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, clap::ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum ColorChoice {
Always,
Never,
Debug,
Auto,
}
impl fmt::Display for ColorChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ColorChoice::Always => "always",
ColorChoice::Never => "never",
ColorChoice::Debug => "debug",
ColorChoice::Auto => "auto",
};
write!(f, "{s}")
}
}
fn prepare_formatter_factory(
config: &StackedConfig,
stdout: &Stdout,
) -> Result<FormatterFactory, ConfigGetError> {
let terminal = stdout.is_terminal();
let (color, debug) = match config.get("ui.color")? {
ColorChoice::Always => (true, false),
ColorChoice::Never => (false, false),
ColorChoice::Debug => (true, true),
ColorChoice::Auto => (terminal, false),
};
if color {
FormatterFactory::color(config, debug)
} else if terminal {
Ok(FormatterFactory::sanitized())
} else {
Ok(FormatterFactory::plain_text())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
pub enum PaginationChoice {
Never,
Auto,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
pub enum StreampagerAlternateScreenMode {
QuitIfOnePage,
FullScreenClearOutput,
QuitQuicklyOrClearOutput,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
enum StreampagerWrappingMode {
None,
Word,
Anywhere,
}
impl From<StreampagerWrappingMode> for streampager::config::WrappingMode {
fn from(val: StreampagerWrappingMode) -> Self {
use streampager::config::WrappingMode;
match val {
StreampagerWrappingMode::None => WrappingMode::Unwrapped,
StreampagerWrappingMode::Word => WrappingMode::WordBoundary,
StreampagerWrappingMode::Anywhere => WrappingMode::GraphemeBoundary,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
struct StreampagerConfig {
interface: StreampagerAlternateScreenMode,
wrapping: StreampagerWrappingMode,
show_ruler: bool,
}
impl StreampagerConfig {
fn streampager_interface_mode(&self) -> streampager::config::InterfaceMode {
use streampager::config::InterfaceMode;
use StreampagerAlternateScreenMode::*;
match self.interface {
FullScreenClearOutput => InterfaceMode::FullScreen,
QuitIfOnePage => InterfaceMode::Hybrid,
QuitQuicklyOrClearOutput => InterfaceMode::Delayed(std::time::Duration::from_secs(2)),
}
}
}
enum PagerConfig {
Disabled,
Builtin(StreampagerConfig),
External(CommandNameAndArgs),
}
impl PagerConfig {
fn from_config(config: &StackedConfig) -> Result<PagerConfig, ConfigGetError> {
if matches!(config.get("ui.paginate")?, PaginationChoice::Never) {
return Ok(PagerConfig::Disabled);
};
match config.get("ui.pager")? {
CommandNameAndArgs::String(name) if name == BUILTIN_PAGER_NAME => {
Ok(PagerConfig::Builtin(config.get("ui.streampager")?))
}
pager_command => Ok(PagerConfig::External(pager_command)),
}
}
}
impl Ui {
pub fn null() -> Ui {
Ui {
quiet: true,
pager: PagerConfig::Disabled,
progress_indicator: false,
formatter_factory: FormatterFactory::plain_text(),
output: UiOutput::Null,
}
}
pub fn with_config(config: &StackedConfig) -> Result<Ui, CommandError> {
let formatter_factory = prepare_formatter_factory(config, &io::stdout())?;
Ok(Ui {
quiet: config.get("ui.quiet")?,
formatter_factory,
pager: PagerConfig::from_config(config)?,
progress_indicator: config.get("ui.progress-indicator")?,
output: UiOutput::new_terminal(),
})
}
pub fn reset(&mut self, config: &StackedConfig) -> Result<(), CommandError> {
self.quiet = config.get("ui.quiet")?;
self.pager = PagerConfig::from_config(config)?;
self.progress_indicator = config.get("ui.progress-indicator")?;
self.formatter_factory = prepare_formatter_factory(config, &io::stdout())?;
Ok(())
}
#[instrument(skip_all)]
pub fn request_pager(&mut self) {
if !matches!(&self.output, UiOutput::Terminal { stdout, .. } if stdout.is_terminal()) {
return;
}
let new_output = match &self.pager {
PagerConfig::Disabled => {
return;
}
PagerConfig::Builtin(streampager_config) => {
UiOutput::new_builtin_paged(streampager_config)
.inspect_err(|err| {
writeln!(
self.warning_default(),
"Failed to set up builtin pager: {err}",
err = format_error_with_sources(err),
)
.ok();
})
.ok()
}
PagerConfig::External(command_name_and_args) => {
UiOutput::new_paged(command_name_and_args)
.inspect_err(|err| {
writeln!(
self.warning_default(),
"Failed to spawn pager '{name}': {err}",
name = command_name_and_args.split_name(),
err = format_error_with_sources(err),
)
.ok();
writeln!(self.hint_default(), "Consider using the `:builtin` pager.").ok();
})
.ok()
}
};
if let Some(output) = new_output {
self.output = output;
}
}
pub fn color(&self) -> bool {
self.formatter_factory.is_color()
}
pub fn new_formatter<'output, W: Write + 'output>(
&self,
output: W,
) -> Box<dyn Formatter + 'output> {
self.formatter_factory.new_formatter(output)
}
pub fn stdout(&self) -> UiStdout<'_> {
match &self.output {
UiOutput::Terminal { stdout, .. } => UiStdout::Terminal(stdout.lock()),
UiOutput::Paged { child_stdin, .. } => UiStdout::Paged(child_stdin),
UiOutput::BuiltinPaged { out_wr, .. } => UiStdout::Builtin(out_wr),
UiOutput::Null => UiStdout::Null(io::sink()),
}
}
pub fn stdout_formatter(&self) -> Box<dyn Formatter + '_> {
for_outputs!(UiStdout, self.stdout(), w => self.new_formatter(w))
}
pub fn stderr(&self) -> UiStderr<'_> {
match &self.output {
UiOutput::Terminal { stderr, .. } => UiStderr::Terminal(stderr.lock()),
UiOutput::Paged { child_stdin, .. } => UiStderr::Paged(child_stdin),
UiOutput::BuiltinPaged { err_wr, .. } => UiStderr::Builtin(err_wr),
UiOutput::Null => UiStderr::Null(io::sink()),
}
}
pub fn stderr_formatter(&self) -> Box<dyn Formatter + '_> {
for_outputs!(UiStderr, self.stderr(), w => self.new_formatter(w))
}
pub fn stderr_for_child(&self) -> io::Result<Stdio> {
match &self.output {
UiOutput::Terminal { .. } => Ok(Stdio::inherit()),
UiOutput::Paged { child_stdin, .. } => Ok(duplicate_child_stdin(child_stdin)?.into()),
UiOutput::BuiltinPaged { err_wr, .. } => Ok(err_wr.try_clone()?.into()),
UiOutput::Null => Ok(Stdio::null()),
}
}
pub fn use_progress_indicator(&self) -> bool {
match &self.output {
UiOutput::Terminal { stderr, .. } => self.progress_indicator && stderr.is_terminal(),
UiOutput::Paged { .. } => false,
UiOutput::BuiltinPaged { .. } => false,
UiOutput::Null => false,
}
}
pub fn progress_output(&self) -> Option<ProgressOutput<std::io::Stderr>> {
self.use_progress_indicator()
.then(ProgressOutput::for_stderr)
}
pub fn status(&self) -> Box<dyn Write + '_> {
if self.quiet {
Box::new(io::sink())
} else {
Box::new(self.stderr())
}
}
pub fn status_formatter(&self) -> Option<Box<dyn Formatter + '_>> {
(!self.quiet).then(|| self.stderr_formatter())
}
pub fn hint_default(
&self,
) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, &'static str> {
self.hint_with_heading("Hint: ")
}
pub fn hint_no_heading(&self) -> LabeledWriter<Box<dyn Formatter + '_>, &'static str> {
let formatter = self
.status_formatter()
.unwrap_or_else(|| Box::new(PlainTextFormatter::new(io::sink())));
LabeledWriter::new(formatter, "hint")
}
pub fn hint_with_heading<H: fmt::Display>(
&self,
heading: H,
) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H> {
self.hint_no_heading().with_heading(heading)
}
pub fn warning_default(
&self,
) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, &'static str> {
self.warning_with_heading("Warning: ")
}
pub fn warning_no_heading(&self) -> LabeledWriter<Box<dyn Formatter + '_>, &'static str> {
LabeledWriter::new(self.stderr_formatter(), "warning")
}
pub fn warning_with_heading<H: fmt::Display>(
&self,
heading: H,
) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H> {
self.warning_no_heading().with_heading(heading)
}
pub fn error_no_heading(&self) -> LabeledWriter<Box<dyn Formatter + '_>, &'static str> {
LabeledWriter::new(self.stderr_formatter(), "error")
}
pub fn error_with_heading<H: fmt::Display>(
&self,
heading: H,
) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H> {
self.error_no_heading().with_heading(heading)
}
#[instrument(skip_all)]
pub fn finalize_pager(&mut self) {
let old_output = mem::replace(&mut self.output, UiOutput::new_terminal());
old_output.finalize(self);
}
pub fn can_prompt() -> bool {
io::stderr().is_terminal()
|| env::var("JJ_INTERACTIVE")
.map(|v| v == "1")
.unwrap_or(false)
}
pub fn prompt(&self, prompt: &str) -> io::Result<String> {
if !Self::can_prompt() {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"Cannot prompt for input since the output is not connected to a terminal",
));
}
write!(self.stderr(), "{prompt}: ")?;
self.stderr().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
if buf.is_empty() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Prompt cancelled by EOF",
));
}
if let Some(trimmed) = buf.strip_suffix('\n') {
buf.truncate(trimmed.len());
}
Ok(buf)
}
pub fn prompt_choice(
&self,
prompt: &str,
choices: &[impl AsRef<str>],
default_index: Option<usize>,
) -> io::Result<usize> {
self.prompt_choice_with(
prompt,
default_index.map(|index| {
choices
.get(index)
.expect("default_index should be within range")
.as_ref()
}),
|input| {
choices
.iter()
.position(|c| input == c.as_ref())
.ok_or("unrecognized response")
},
)
}
pub fn prompt_yes_no(&self, prompt: &str, default: Option<bool>) -> io::Result<bool> {
let default_str = match &default {
Some(true) => "(Yn)",
Some(false) => "(yN)",
None => "(yn)",
};
self.prompt_choice_with(
&format!("{prompt} {default_str}"),
default.map(|v| if v { "y" } else { "n" }),
|input| {
if input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") {
Ok(true)
} else if input.eq_ignore_ascii_case("n") || input.eq_ignore_ascii_case("no") {
Ok(false)
} else {
Err("unrecognized response")
}
},
)
}
pub fn prompt_choice_with<T, E: fmt::Debug + fmt::Display>(
&self,
prompt: &str,
default: Option<&str>,
mut parse: impl FnMut(&str) -> Result<T, E>,
) -> io::Result<T> {
let default = default.map(|text| (parse(text).expect("default should be valid"), text));
if !Self::can_prompt() {
if let Some((value, text)) = default {
writeln!(self.stderr(), "{prompt}: {text}")?;
return Ok(value);
}
}
loop {
let input = self.prompt(prompt)?;
let input = input.trim();
if input.is_empty() {
if let Some((value, _)) = default {
return Ok(value);
} else {
continue;
}
}
match parse(input) {
Ok(value) => return Ok(value),
Err(err) => writeln!(self.warning_no_heading(), "{err}")?,
}
}
}
pub fn prompt_password(&self, prompt: &str) -> io::Result<String> {
if !io::stdout().is_terminal() {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"Cannot prompt for input since the output is not connected to a terminal",
));
}
rpassword::prompt_password(format!("{prompt}: "))
}
pub fn term_width(&self) -> usize {
term_width().unwrap_or(80).into()
}
}
#[derive(Debug)]
pub struct ProgressOutput<W> {
output: W,
term_width: Option<u16>,
}
impl ProgressOutput<io::Stderr> {
pub fn for_stderr() -> ProgressOutput<io::Stderr> {
ProgressOutput {
output: io::stderr(),
term_width: None,
}
}
}
impl<W> ProgressOutput<W> {
pub fn for_test(output: W, term_width: u16) -> Self {
Self {
output,
term_width: Some(term_width),
}
}
pub fn term_width(&self) -> Option<u16> {
self.term_width.or_else(term_width)
}
pub fn output_guard(&self, text: String) -> OutputGuard {
OutputGuard {
text,
output: io::stderr(),
}
}
}
impl<W: Write> ProgressOutput<W> {
pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
self.output.write_fmt(fmt)
}
pub fn flush(&mut self) -> io::Result<()> {
self.output.flush()
}
}
pub struct OutputGuard {
text: String,
output: Stderr,
}
impl Drop for OutputGuard {
#[instrument(skip_all)]
fn drop(&mut self) {
_ = self.output.write_all(self.text.as_bytes());
_ = self.output.flush();
}
}
#[cfg(unix)]
fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::fd::OwnedFd> {
use std::os::fd::AsFd as _;
stdin.as_fd().try_clone_to_owned()
}
#[cfg(windows)]
fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::windows::io::OwnedHandle> {
use std::os::windows::io::AsHandle as _;
stdin.as_handle().try_clone_to_owned()
}
fn format_error_with_sources(err: &dyn error::Error) -> impl fmt::Display + use<'_> {
iter::successors(Some(err), |&err| err.source()).format(": ")
}
fn term_width() -> Option<u16> {
if let Some(cols) = env::var("COLUMNS").ok().and_then(|s| s.parse().ok()) {
Some(cols)
} else {
crossterm::terminal::size().ok().map(|(cols, _)| cols)
}
}