cli: Add pager to `rad diff`

Invokes a pager when the output doesn't fit on the screen.
This commit is contained in:
cloudhead 2023-11-01 14:34:29 +01:00
parent 135725d6f6
commit 84a95848b0
No known key found for this signature in database
9 changed files with 107 additions and 26 deletions

1
Cargo.lock generated
View File

@ -1824,6 +1824,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"shlex",
"tempfile", "tempfile",
"thiserror", "thiserror",
"timeago", "timeago",

View File

@ -26,6 +26,7 @@ radicle-surf = { version = "0.17.0" }
serde = { version = "1.0" } serde = { version = "1.0" }
serde_json = { version = "1" } serde_json = { version = "1" }
serde_yaml = { version = "0.8" } serde_yaml = { version = "0.8" }
shlex = { version = "1.1.0" }
tempfile = { version = "3.3.0" } tempfile = { version = "3.3.0" }
thiserror = { version = "1" } thiserror = { version = "1" }
timeago = { version = "0.3", default-features = false } timeago = { version = "0.3", default-features = false }

View File

@ -8,10 +8,10 @@ use radicle_surf as surf;
use crate::git::pretty_diff::ToPretty as _; use crate::git::pretty_diff::ToPretty as _;
use crate::git::Rev; use crate::git::Rev;
use crate::pager;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{Args, Error, Help};
use crate::terminal::highlight::Highlighter; use crate::terminal::highlight::Highlighter;
use crate::terminal::{Constraint, Element as _};
pub const HELP: Help = Help { pub const HELP: Help = Help {
name: "diff", name: "diff",
@ -145,7 +145,7 @@ pub fn run(options: Options, _ctx: impl term::Context) -> anyhow::Result<()> {
let mut hi = Highlighter::default(); let mut hi = Highlighter::default();
let pretty = diff.pretty(&mut hi, &(), &repo); let pretty = diff.pretty(&mut hi, &(), &repo);
pretty.write(Constraint::from_env().unwrap_or_default()); pager::run(pretty)?;
Ok(()) Ok(())
} }

View File

@ -3,5 +3,6 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
pub mod commands; pub mod commands;
pub mod git; pub mod git;
pub mod pager;
pub mod project; pub mod project;
pub mod terminal; pub mod terminal;

51
radicle-cli/src/pager.rs Normal file
View File

@ -0,0 +1,51 @@
use std::io;
use std::process::{Command, Stdio};
use radicle_term::element;
use radicle_term::{Constraint, Element};
use crate::terminal;
/// Output the given element through a pager, if necessary.
/// If it fits within the screen, don't run it through a pager.
pub fn run(elem: impl Element) -> io::Result<()> {
let Some(constraint) = Constraint::from_env() else {
return elem.write(Constraint::UNBOUNDED);
};
let Some(rows) = terminal::rows() else {
return elem.write(Constraint::UNBOUNDED);
};
if elem.size(Constraint::UNBOUNDED).rows <= rows {
return elem.write(Constraint::UNBOUNDED);
}
let Some(pager) = radicle::profile::env::pager() else {
return elem.write(Constraint::UNBOUNDED);
};
let Some(parts) = shlex::split(&pager) else {
return elem.write(Constraint::UNBOUNDED);
};
let Some((program, args)) = parts.split_first() else {
return elem.write(Constraint::UNBOUNDED);
};
let mut child = Command::new(program)
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.args(args)
.spawn()?;
let writer = child.stdin.as_mut().unwrap();
let result = element::write_to(&elem, writer, constraint);
child.wait()?;
match result {
// This error is expected when the pager is exited.
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {}
Err(e) => return Err(e),
Ok(_) => {}
}
Ok(())
}

View File

@ -43,19 +43,12 @@ impl Constraint {
} }
} }
/// A constraint that can only be satisfied by a single size. /// A constraint that can only be satisfied by a single column size.
pub fn tight(size: Size) -> Self { /// The rows are unconstrained.
pub fn tight(cols: usize) -> Self {
Self { Self {
min: size, min: Size::new(cols, 1),
max: size, max: Size::new(cols, usize::MAX),
}
}
/// Return a new constraint that forces objects to the maximum size.
pub fn maximize(self) -> Self {
Self {
min: self.max,
max: self.max,
} }
} }
@ -96,11 +89,12 @@ pub trait Element: fmt::Debug {
} }
} }
/// Write to a writer. /// Write using the given constraints to `stdout`.
fn write(&self, constraints: Constraint) { fn write(&self, constraints: Constraint) -> io::Result<()>
for line in self.render(constraints) { where
println!("{}", line.to_string().trim_end()); Self: Sized,
} {
self::write_to(self, &mut io::stdout(), constraints)
} }
#[must_use] #[must_use]
@ -143,6 +137,18 @@ impl<T: Element> Element for &T {
} }
} }
/// Write using the given constraints, to a writer.
pub fn write_to(
elem: &impl Element,
writer: &mut impl io::Write,
constraints: Constraint,
) -> io::Result<()> {
for line in elem.render(constraints) {
writeln!(writer, "{}", line.to_string().trim_end())?;
}
Ok(())
}
/// A line of text that has styling and can be displayed. /// A line of text that has styling and can be displayed.
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug)]
pub struct Line { pub struct Line {

View File

@ -92,6 +92,10 @@ pub fn columns() -> Option<usize> {
termion::terminal_size().map(|(cols, _)| cols as usize).ok() termion::terminal_size().map(|(cols, _)| cols as usize).ok()
} }
pub fn rows() -> Option<usize> {
termion::terminal_size().map(|(_, rows)| rows as usize).ok()
}
pub fn viewport() -> Option<Size> { pub fn viewport() -> Option<Size> {
termion::terminal_size() termion::terminal_size()
.map(|(cols, rows)| Size::new(cols as usize, rows as usize)) .map(|(cols, rows)| Size::new(cols as usize, rows as usize))

View File

@ -32,6 +32,13 @@ impl<'a> Row<'a> {
Self::Dividier => c.min.cols, Self::Dividier => c.min.cols,
} }
} }
fn height(&self, c: Constraint) -> usize {
match self {
Self::Element(e) => e.rows(c),
Self::Dividier => 1,
}
}
} }
/// Vertical stack of [`Element`] objects that implements [`Element`]. /// Vertical stack of [`Element`] objects that implements [`Element`].
@ -109,7 +116,7 @@ impl<'a> VStack<'a> {
fn outer(&self, c: Constraint) -> Size { fn outer(&self, c: Constraint) -> Size {
let padding = self.opts.padding * 2; let padding = self.opts.padding * 2;
let mut cols = self.rows.iter().map(|r| r.width(c)).max().unwrap_or(0) + padding; let mut cols = self.rows.iter().map(|r| r.width(c)).max().unwrap_or(0) + padding;
let mut rows = self.rows.len(); let mut rows = self.rows.iter().map(|r| r.height(c)).sum();
// Account for outer borders. // Account for outer borders.
if self.opts.border.is_some() { if self.opts.border.is_some() {
@ -129,10 +136,7 @@ impl<'a> Element for VStack<'a> {
let mut lines = Vec::new(); let mut lines = Vec::new();
let padding = self.opts.padding; let padding = self.opts.padding;
let inner = self.inner(parent); let inner = self.inner(parent);
let child = Constraint::tight(Size { let child = Constraint::tight(inner.cols - padding * 2);
cols: inner.cols - padding * 2,
rows: usize::MAX,
});
if let Some(color) = self.opts.border { if let Some(color) = self.opts.border {
lines.push( lines.push(

View File

@ -40,9 +40,22 @@ pub mod env {
/// RNG seed. Must be convertible to a `u64`. /// RNG seed. Must be convertible to a `u64`.
pub const RAD_RNG_SEED: &str = "RAD_RNG_SEED"; pub const RAD_RNG_SEED: &str = "RAD_RNG_SEED";
/// Get the configured pager program from the environment.
pub fn pager() -> Option<String> {
if let Ok(cfg) = git2::Config::open_default() {
if let Ok(pager) = cfg.get_string("core.pager") {
return Some(pager);
}
}
if let Ok(pager) = var("PAGER") {
return Some(pager);
}
None
}
/// Get the radicle passphrase from the environment. /// Get the radicle passphrase from the environment.
pub fn passphrase() -> Option<super::Passphrase> { pub fn passphrase() -> Option<super::Passphrase> {
let Ok(passphrase) = std::env::var(RAD_PASSPHRASE) else { let Ok(passphrase) = var(RAD_PASSPHRASE) else {
return None; return None;
}; };
Some(super::Passphrase::from(passphrase)) Some(super::Passphrase::from(passphrase))
@ -50,7 +63,7 @@ pub mod env {
/// Get a random number generator from the environment. /// Get a random number generator from the environment.
pub fn rng() -> fastrand::Rng { pub fn rng() -> fastrand::Rng {
if let Ok(seed) = std::env::var(RAD_RNG_SEED) { if let Ok(seed) = var(RAD_RNG_SEED) {
return fastrand::Rng::with_seed( return fastrand::Rng::with_seed(
seed.parse() seed.parse()
.expect("env::rng: invalid seed specified in `RAD_RNG_SEED`"), .expect("env::rng: invalid seed specified in `RAD_RNG_SEED`"),