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_json",
"serde_yaml",
"shlex",
"tempfile",
"thiserror",
"timeago",

View File

@ -26,6 +26,7 @@ radicle-surf = { version = "0.17.0" }
serde = { version = "1.0" }
serde_json = { version = "1" }
serde_yaml = { version = "0.8" }
shlex = { version = "1.1.0" }
tempfile = { version = "3.3.0" }
thiserror = { version = "1" }
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::Rev;
use crate::pager;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
use crate::terminal::highlight::Highlighter;
use crate::terminal::{Constraint, Element as _};
pub const HELP: Help = Help {
name: "diff",
@ -145,7 +145,7 @@ pub fn run(options: Options, _ctx: impl term::Context) -> anyhow::Result<()> {
let mut hi = Highlighter::default();
let pretty = diff.pretty(&mut hi, &(), &repo);
pretty.write(Constraint::from_env().unwrap_or_default());
pager::run(pretty)?;
Ok(())
}

View File

@ -3,5 +3,6 @@
#![allow(clippy::too_many_arguments)]
pub mod commands;
pub mod git;
pub mod pager;
pub mod project;
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.
pub fn tight(size: Size) -> Self {
/// A constraint that can only be satisfied by a single column size.
/// The rows are unconstrained.
pub fn tight(cols: usize) -> Self {
Self {
min: size,
max: size,
}
}
/// Return a new constraint that forces objects to the maximum size.
pub fn maximize(self) -> Self {
Self {
min: self.max,
max: self.max,
min: Size::new(cols, 1),
max: Size::new(cols, usize::MAX),
}
}
@ -96,11 +89,12 @@ pub trait Element: fmt::Debug {
}
}
/// Write to a writer.
fn write(&self, constraints: Constraint) {
for line in self.render(constraints) {
println!("{}", line.to_string().trim_end());
}
/// Write using the given constraints to `stdout`.
fn write(&self, constraints: Constraint) -> io::Result<()>
where
Self: Sized,
{
self::write_to(self, &mut io::stdout(), constraints)
}
#[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.
#[derive(Clone, Default, Debug)]
pub struct Line {

View File

@ -92,6 +92,10 @@ pub fn columns() -> Option<usize> {
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> {
termion::terminal_size()
.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,
}
}
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`].
@ -109,7 +116,7 @@ impl<'a> VStack<'a> {
fn outer(&self, c: Constraint) -> Size {
let padding = self.opts.padding * 2;
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.
if self.opts.border.is_some() {
@ -129,10 +136,7 @@ impl<'a> Element for VStack<'a> {
let mut lines = Vec::new();
let padding = self.opts.padding;
let inner = self.inner(parent);
let child = Constraint::tight(Size {
cols: inner.cols - padding * 2,
rows: usize::MAX,
});
let child = Constraint::tight(inner.cols - padding * 2);
if let Some(color) = self.opts.border {
lines.push(

View File

@ -40,9 +40,22 @@ pub mod env {
/// RNG seed. Must be convertible to a `u64`.
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.
pub fn passphrase() -> Option<super::Passphrase> {
let Ok(passphrase) = std::env::var(RAD_PASSPHRASE) else {
let Ok(passphrase) = var(RAD_PASSPHRASE) else {
return None;
};
Some(super::Passphrase::from(passphrase))
@ -50,7 +63,7 @@ pub mod env {
/// Get a random number generator from the environment.
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(
seed.parse()
.expect("env::rng: invalid seed specified in `RAD_RNG_SEED`"),