use crossterm::{
cursor::{self, MoveTo},
event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent},
style::Attribute,
terminal::{self, Clear, ClearType},
};
use crate::error::{AlternateScreenPagingError, CleanupError, SetupError};
use std::io::{self, Write as _};
fn setup(stdout: &io::Stdout) -> std::result::Result<(io::StdoutLock<'_>, usize), SetupError> {
let mut out = stdout.lock();
#[cfg(any(feature = "tokio_lib", feature = "async_std_lib"))]
{
use crossterm::tty::IsTty;
if out.is_tty() {
Ok(())
} else {
Err(SetupError::InvalidTerminal)
}?;
}
crossterm::execute!(out, terminal::EnterAlternateScreen)
.map_err(|e| SetupError::AlternateScreen(e.into()))?;
terminal::enable_raw_mode().map_err(|e| SetupError::RawMode(e.into()))?;
crossterm::execute!(out, cursor::Hide).map_err(|e| SetupError::HideCursor(e.into()))?;
crossterm::execute!(out, event::EnableMouseCapture)
.map_err(|e| SetupError::EnableMouseCapture(e.into()))?;
let (_, rows) = terminal::size().map_err(|e| SetupError::TerminalSize(e.into()))?;
Ok((out, rows as usize))
}
fn cleanup(mut out: impl io::Write) -> std::result::Result<(), CleanupError> {
crossterm::execute!(out, event::DisableMouseCapture)
.map_err(|e| CleanupError::DisableMouseCapture(e.into()))?;
crossterm::execute!(out, cursor::Show).map_err(|e| CleanupError::ShowCursor(e.into()))?;
terminal::disable_raw_mode().map_err(|e| CleanupError::DisableRawMode(e.into()))?;
crossterm::execute!(out, terminal::LeaveAlternateScreen)
.map_err(|e| CleanupError::LeaveAlternateScreen(e.into()))?;
Ok(())
}
#[cfg(feature = "static_output")]
pub(crate) fn static_paging(mut pager: crate::Pager) -> Result<(), AlternateScreenPagingError> {
let stdout = io::stdout();
let (mut out, mut rows) = setup(&stdout)?;
loop {
draw(&mut out, &mut pager, rows)?;
if event::poll(std::time::Duration::from_millis(10))
.map_err(|e| AlternateScreenPagingError::HandleEvent(e.into()))?
{
let input = handle_input(
event::read().map_err(|e| AlternateScreenPagingError::HandleEvent(e.into()))?,
pager.upper_mark,
pager.line_numbers,
rows,
);
match input {
None => continue,
Some(InputEvent::Exit) => return Ok(cleanup(out)?),
Some(InputEvent::UpdateRows(r)) => rows = r,
Some(InputEvent::UpdateUpperMark(um)) => pager.upper_mark = um,
Some(InputEvent::UpdateLineNumber(l)) => {
pager.line_numbers = l;
}
}
draw(&mut out, &mut pager, rows)?;
}
}
}
#[cfg(any(feature = "async_std_lib", feature = "tokio_lib"))]
pub(crate) fn dynamic_paging<P, F>(
p: &P,
get: F,
) -> std::result::Result<(), AlternateScreenPagingError>
where
F: Fn(&P) -> std::sync::MutexGuard<crate::Pager>,
{
let stdout = io::stdout();
let (mut out, mut rows) = setup(&stdout)?;
let mut last_printed = String::new();
loop {
let lock = get(&p);
let mut pager = lock.clone();
drop(lock);
if pager.lines != last_printed {
draw(&mut out, &mut pager, rows)?;
last_printed = pager.lines.clone();
}
if event::poll(std::time::Duration::from_millis(10))
.map_err(|e| AlternateScreenPagingError::HandleEvent(e.into()))?
{
let input = handle_input(
event::read().map_err(|e| AlternateScreenPagingError::HandleEvent(e.into()))?,
pager.upper_mark,
pager.line_numbers,
rows,
);
let mut lock = get(&p);
match input {
None => continue,
Some(InputEvent::Exit) => return Ok(cleanup(out)?),
Some(InputEvent::UpdateRows(r)) => rows = r,
Some(InputEvent::UpdateUpperMark(um)) => lock.upper_mark = um,
Some(InputEvent::UpdateLineNumber(l)) => {
lock.line_numbers = l;
}
}
let mut pager = lock.clone();
draw(&mut out, &mut pager, rows)?;
*lock = pager;
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
enum InputEvent {
Exit,
UpdateRows(usize),
UpdateUpperMark(usize),
UpdateLineNumber(LineNumbers),
}
fn handle_input(ev: Event, upper_mark: usize, ln: LineNumbers, rows: usize) -> Option<InputEvent> {
match ev {
Event::Key(KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE,
})
| Event::Key(KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::NONE,
}) => Some(InputEvent::UpdateUpperMark(upper_mark.saturating_sub(1))),
Event::Key(KeyEvent {
code: KeyCode::Down,
modifiers: KeyModifiers::NONE,
})
| Event::Key(KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::NONE,
}) => Some(InputEvent::UpdateUpperMark(upper_mark.saturating_add(1))),
Event::Mouse(MouseEvent::ScrollUp(_, _, _)) => {
Some(InputEvent::UpdateUpperMark(upper_mark.saturating_sub(5)))
}
Event::Mouse(MouseEvent::ScrollDown(_, _, _)) => {
Some(InputEvent::UpdateUpperMark(upper_mark.saturating_add(5)))
}
Event::Key(KeyEvent {
code: KeyCode::Char('g'),
modifiers: KeyModifiers::NONE,
}) => Some(InputEvent::UpdateUpperMark(0)),
Event::Key(KeyEvent {
code: KeyCode::Char('g'),
modifiers: KeyModifiers::SHIFT,
})
| Event::Key(KeyEvent {
code: KeyCode::Char('G'),
modifiers: KeyModifiers::SHIFT,
})
| Event::Key(KeyEvent {
code: KeyCode::Char('G'),
modifiers: KeyModifiers::NONE,
}) => Some(InputEvent::UpdateUpperMark(usize::MAX)),
Event::Key(KeyEvent {
code: KeyCode::PageUp,
modifiers: KeyModifiers::NONE,
}) => Some(InputEvent::UpdateUpperMark(
upper_mark.saturating_sub(rows - 1),
)),
Event::Key(KeyEvent {
code: KeyCode::PageDown,
modifiers: KeyModifiers::NONE,
}) => Some(InputEvent::UpdateUpperMark(
upper_mark.saturating_add(rows - 1),
)),
Event::Resize(_, height) => Some(InputEvent::UpdateRows(height as usize)),
Event::Key(KeyEvent {
code: KeyCode::Char('l'),
modifiers: KeyModifiers::CONTROL,
}) => Some(InputEvent::UpdateLineNumber(!ln)),
Event::Key(KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::NONE,
})
| Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
}) => Some(InputEvent::Exit),
_ => None,
}
}
fn draw(out: &mut impl io::Write, mut pager: &mut crate::Pager, rows: usize) -> io::Result<()> {
write!(out, "{}{}", Clear(ClearType::All), MoveTo(0, 0))?;
write_lines(out, &mut pager, rows.saturating_sub(1))?;
#[allow(clippy::cast_possible_truncation)]
{
write!(
out,
"{mv}\r{rev}{prompt}{reset}",
mv = MoveTo(0, rows as u16),
rev = Attribute::Reverse,
prompt = pager.prompt,
reset = Attribute::Reset,
)?;
}
out.flush()
}
pub(crate) fn write_lines(
out: &mut impl io::Write,
pager: &mut crate::Pager,
rows: usize,
) -> io::Result<()> {
let line_count = pager.lines.lines().count();
let lower_mark = pager.upper_mark.saturating_add(rows.min(line_count));
if lower_mark > line_count {
pager.upper_mark = if line_count < rows {
0
} else {
line_count.saturating_sub(rows)
};
}
let displayed_lines = pager
.lines
.lines()
.skip(pager.upper_mark)
.take(rows.min(line_count));
match pager.line_numbers {
LineNumbers::AlwaysOff | LineNumbers::Disabled => {
for line in displayed_lines {
writeln!(out, "\r{}", line)?;
}
}
LineNumbers::AlwaysOn | LineNumbers::Enabled => {
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
{
let len_line_number = (line_count as f64).log10().floor() as usize + 1;
debug_assert_eq!(line_count.to_string().len(), len_line_number);
for (idx, line) in displayed_lines.enumerate() {
writeln!(
out,
"\r{number: >len$}. {line}",
number = pager.upper_mark + idx + 1,
len = len_line_number,
line = line
)?;
}
}
}
}
Ok(())
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum LineNumbers {
AlwaysOn,
Enabled,
Disabled,
AlwaysOff,
}
impl LineNumbers {
#[allow(dead_code)]
fn is_invertible(self) -> bool {
matches!(self, Self::Enabled | Self::Disabled)
}
}
impl std::ops::Not for LineNumbers {
type Output = Self;
fn not(self) -> Self::Output {
use LineNumbers::{Disabled, Enabled};
match self {
Enabled => Disabled,
Disabled => Enabled,
ln => ln,
}
}
}
#[cfg(test)]
mod tests;