term: Implement content-aware pager
This is a pager (like `less`) that takes an `Element`, and dynamically re-renders the element when the terminal size changes. This ensures that the content is always properly wrapped and rendered.
This commit is contained in:
parent
2801403182
commit
40f4383bbc
|
|
@ -2623,14 +2623,17 @@ version = "0.9.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle-query",
|
"anstyle-query",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"crossbeam-channel",
|
||||||
"git2",
|
"git2",
|
||||||
"inquire",
|
"inquire",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
|
"radicle-signals",
|
||||||
"shlex",
|
"shlex",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"termion 3.0.0",
|
"termion 3.0.0",
|
||||||
|
"thiserror",
|
||||||
"unicode-display-width",
|
"unicode-display-width",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
|
|
@ -2644,6 +2647,7 @@ dependencies = [
|
||||||
"radicle",
|
"radicle",
|
||||||
"radicle-cli",
|
"radicle-cli",
|
||||||
"radicle-git-ext",
|
"radicle-git-ext",
|
||||||
|
"radicle-term",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,13 @@ default = ["git2"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1" }
|
anyhow = { version = "1" }
|
||||||
anstyle-query = { version = "1.0.0" }
|
anstyle-query = { version = "1.0.0" }
|
||||||
|
crossbeam-channel = { version = "0.5.6" }
|
||||||
inquire = { version = "0.7.4", default-features = false, features = ["termion", "editor"] }
|
inquire = { version = "0.7.4", default-features = false, features = ["termion", "editor"] }
|
||||||
libc = { version = "0.2" }
|
libc = { version = "0.2" }
|
||||||
once_cell = { version = "1.13" }
|
once_cell = { version = "1.13" }
|
||||||
shlex = { version = "1.1" }
|
shlex = { version = "1.1" }
|
||||||
termion = { version = "3" }
|
termion = { version = "3" }
|
||||||
|
thiserror = { version = "1" }
|
||||||
unicode-display-width = { version = "0.3.0" }
|
unicode-display-width = { version = "0.3.0" }
|
||||||
unicode-segmentation = { version = "1.7.1" }
|
unicode-segmentation = { version = "1.7.1" }
|
||||||
zeroize = { version = "1.1" }
|
zeroize = { version = "1.1" }
|
||||||
|
|
@ -29,6 +31,10 @@ default-features = false
|
||||||
features = ["vendored-libgit2"]
|
features = ["vendored-libgit2"]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.radicle-signals]
|
||||||
|
path = "../radicle-signals"
|
||||||
|
version = "0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { version = "1.3.0" }
|
pretty_assertions = { version = "1.3.0" }
|
||||||
tempfile = { version = "3.3.0" }
|
tempfile = { version = "3.3.0" }
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub mod format;
|
||||||
pub mod hstack;
|
pub mod hstack;
|
||||||
pub mod io;
|
pub mod io;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
|
pub mod pager;
|
||||||
pub mod spinner;
|
pub mod spinner;
|
||||||
pub mod table;
|
pub mod table;
|
||||||
pub mod textarea;
|
pub mod textarea;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
use std::io::{IsTerminal, Write};
|
||||||
|
use std::{io, thread};
|
||||||
|
|
||||||
|
use crate::element::Size;
|
||||||
|
use crate::{Constraint, Element, Line, Paint};
|
||||||
|
|
||||||
|
use crossbeam_channel as chan;
|
||||||
|
use radicle_signals as signals;
|
||||||
|
use termion::event::{Event, Key, MouseButton, MouseEvent};
|
||||||
|
use termion::{input::TermRead, raw::IntoRawMode, screen::IntoAlternateScreen};
|
||||||
|
|
||||||
|
/// How many lines to scroll when the mouse wheel is used.
|
||||||
|
const MOUSE_SCROLL_LINES: usize = 3;
|
||||||
|
|
||||||
|
/// Pager error.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Channel(#[from] chan::RecvError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pager for the given element. Re-renders the element when the terminal is resized so that
|
||||||
|
/// it doesn't wrap. If the output device is not a TTY, just prints the element via
|
||||||
|
/// [`Element::print`].
|
||||||
|
pub fn page<E: Element + Send + 'static>(element: E) -> Result<(), Error> {
|
||||||
|
let (events_tx, events_rx) = chan::unbounded();
|
||||||
|
let (signals_tx, signals_rx) = chan::unbounded();
|
||||||
|
|
||||||
|
signals::install(signals_tx)?;
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
for e in io::stdin().events() {
|
||||||
|
events_tx.send(e).ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
thread::spawn(move || main(element, signals_rx, events_rx))
|
||||||
|
.join()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main<E: Element>(
|
||||||
|
element: E,
|
||||||
|
signals_rx: chan::Receiver<signals::Signal>,
|
||||||
|
events_rx: chan::Receiver<Result<Event, io::Error>>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
if !stdout.is_terminal() {
|
||||||
|
element.print();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let raw = stdout.into_raw_mode()?;
|
||||||
|
let mut stdout = termion::input::MouseTerminal::from(raw).into_alternate_screen()?;
|
||||||
|
let (mut width, mut height) = termion::terminal_size()?;
|
||||||
|
let mut lines = element.render(Constraint::max(Size::new(width as usize, height as usize)));
|
||||||
|
let mut line = 0;
|
||||||
|
|
||||||
|
render(&mut stdout, lines.as_slice(), line, (width, height))?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
chan::select! {
|
||||||
|
recv(signals_rx) -> signal => {
|
||||||
|
match signal? {
|
||||||
|
signals::Signal::WindowChanged => {
|
||||||
|
let (w, h) = termion::terminal_size()?;
|
||||||
|
|
||||||
|
lines = element.render(Constraint::max(Size::new(w as usize, h as usize)));
|
||||||
|
width = w;
|
||||||
|
height = h;
|
||||||
|
}
|
||||||
|
signals::Signal::Interrupt | signals::Signal::Terminate => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recv(events_rx) -> event => {
|
||||||
|
let event = event??;
|
||||||
|
let page = height as usize - 1; // Don't count the status bar.
|
||||||
|
let end = lines.len() - page;
|
||||||
|
let prev = line;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Key(key) => match key {
|
||||||
|
Key::Up | Key::Char('k') => {
|
||||||
|
line = line.saturating_sub(1);
|
||||||
|
}
|
||||||
|
Key::Home => {
|
||||||
|
line = 0;
|
||||||
|
}
|
||||||
|
Key::End | Key::Char('G') => {
|
||||||
|
line = end;
|
||||||
|
}
|
||||||
|
Key::PageUp | Key::Char('b') => {
|
||||||
|
line = line.saturating_sub(page);
|
||||||
|
}
|
||||||
|
Key::PageDown | Key::Char(' ') => {
|
||||||
|
line = (line + page).min(end);
|
||||||
|
}
|
||||||
|
Key::Down | Key::Char('j') => {
|
||||||
|
if line < end {
|
||||||
|
line += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::Char('q') => break,
|
||||||
|
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
Event::Mouse(MouseEvent::Press(MouseButton::WheelDown, _, _)) => {
|
||||||
|
if line < end {
|
||||||
|
line += MOUSE_SCROLL_LINES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(MouseEvent::Press(MouseButton::WheelUp, _, _)) => {
|
||||||
|
line = line.saturating_sub(MOUSE_SCROLL_LINES);
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
// Don't re-render if there's no change in line.
|
||||||
|
if line == prev {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render(&mut stdout, &lines, line, (width, height))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<W: Write>(
|
||||||
|
out: &mut W,
|
||||||
|
lines: &[Line],
|
||||||
|
start_line: usize,
|
||||||
|
(width, height): (u16, u16),
|
||||||
|
) -> io::Result<()> {
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
"{}{}",
|
||||||
|
termion::clear::All,
|
||||||
|
termion::cursor::Goto(1, 1)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let content_length = lines.len();
|
||||||
|
let window_size = height as usize - 1;
|
||||||
|
let end_line = if start_line + window_size > content_length {
|
||||||
|
content_length
|
||||||
|
} else {
|
||||||
|
start_line + window_size
|
||||||
|
};
|
||||||
|
// Render content.
|
||||||
|
for (ix, line) in lines[start_line..end_line].iter().enumerate() {
|
||||||
|
write!(out, "{}{}", termion::cursor::Goto(1, ix as u16 + 1), line)?;
|
||||||
|
}
|
||||||
|
// Render progress meter.
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
"{}{}",
|
||||||
|
termion::cursor::Goto(width - 3, height),
|
||||||
|
Paint::new(format!(
|
||||||
|
"{:.0}%",
|
||||||
|
end_line as f64 / lines.len() as f64 * 100.
|
||||||
|
))
|
||||||
|
.dim()
|
||||||
|
)?;
|
||||||
|
// Render cursor input area.
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
"{}{}",
|
||||||
|
termion::cursor::Goto(1, height),
|
||||||
|
Paint::new(":").dim()
|
||||||
|
)?;
|
||||||
|
out.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,10 @@ path = "../radicle"
|
||||||
version = "0"
|
version = "0"
|
||||||
path = "../radicle-cli"
|
path = "../radicle-cli"
|
||||||
|
|
||||||
|
[dependencies.radicle-term]
|
||||||
|
version = "0"
|
||||||
|
path = "../radicle-term"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "rad-init"
|
name = "rad-init"
|
||||||
path = "src/rad-init.rs"
|
path = "src/rad-init.rs"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ fn main() -> anyhow::Result<()> {
|
||||||
"Choose something to try out:",
|
"Choose something to try out:",
|
||||||
&[
|
&[
|
||||||
"confirm",
|
"confirm",
|
||||||
|
"pager",
|
||||||
"spinner",
|
"spinner",
|
||||||
"spinner-drop",
|
"spinner-drop",
|
||||||
"spinner-error",
|
"spinner-error",
|
||||||
|
|
@ -22,6 +23,17 @@ fn main() -> anyhow::Result<()> {
|
||||||
terminal::success!("You said 'yes'");
|
terminal::success!("You said 'yes'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"pager" => {
|
||||||
|
let mut table = radicle_term::Table::<1, radicle_term::Label>::new(
|
||||||
|
radicle_term::TableOptions::bordered(),
|
||||||
|
);
|
||||||
|
let rows = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/rad-cli-demo.rs"));
|
||||||
|
|
||||||
|
for row in rows.lines() {
|
||||||
|
table.push([row.into()]);
|
||||||
|
}
|
||||||
|
radicle_term::pager::page(table)?;
|
||||||
|
}
|
||||||
"editor" => {
|
"editor" => {
|
||||||
let output = terminal::editor::Editor::new()
|
let output = terminal::editor::Editor::new()
|
||||||
.extension("rs")
|
.extension("rs")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue