cli: Add pager to `rad diff`
Invokes a pager when the output doesn't fit on the screen.
This commit is contained in:
parent
135725d6f6
commit
84a95848b0
|
|
@ -1824,6 +1824,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"shlex",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"timeago",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
pub mod commands;
|
||||
pub mod git;
|
||||
pub mod pager;
|
||||
pub mod project;
|
||||
pub mod terminal;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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`"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue