Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Add pager to `rad diff`
cloudhead committed 2 years ago
commit 84a95848b02de5599f5a2dc905fa2ba4c185b7ab
parent 135725d6f6211f47feb26cf9a276ae2a13e7101b
9 files changed +107 -26
modified Cargo.lock
@@ -1824,6 +1824,7 @@ dependencies = [
 "serde",
 "serde_json",
 "serde_yaml",
+
 "shlex",
 "tempfile",
 "thiserror",
 "timeago",
modified radicle-cli/Cargo.toml
@@ -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 }
modified radicle-cli/src/commands/diff.rs
@@ -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(())
}
modified radicle-cli/src/lib.rs
@@ -3,5 +3,6 @@
#![allow(clippy::too_many_arguments)]
pub mod commands;
pub mod git;
+
pub mod pager;
pub mod project;
pub mod terminal;
added radicle-cli/src/pager.rs
@@ -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(())
+
}
modified radicle-term/src/element.rs
@@ -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 {
modified radicle-term/src/io.rs
@@ -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))
modified radicle-term/src/vstack.rs
@@ -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(
modified radicle/src/profile.rs
@@ -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`"),