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",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
"shlex",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"timeago",
|
"timeago",
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// 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 {
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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`"),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue