use crossterm::{
cursor::{self, MoveTo},
event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent},
execute,
style::Attribute,
terminal::{self, Clear, ClearType},
};
use std::io::{self, Write as _};
use crate::{
error::{CleanupError, SetupError},
Pager,
};
pub(crate) fn setup(stdout: &io::Stdout, dynamic: bool) -> std::result::Result<usize, SetupError> {
let mut out = stdout.lock();
if dynamic {
use crossterm::tty::IsTty;
if out.is_tty() {
Ok(())
} else {
Err(SetupError::InvalidTerminal)
}?;
}
execute!(out, terminal::EnterAlternateScreen)
.map_err(|e| SetupError::AlternateScreen(e.into()))?;
terminal::enable_raw_mode().map_err(|e| SetupError::RawMode(e.into()))?;
execute!(out, cursor::Hide).map_err(|e| SetupError::HideCursor(e.into()))?;
execute!(out, event::EnableMouseCapture)
.map_err(|e| SetupError::EnableMouseCapture(e.into()))?;
let (_, rows) = terminal::size().map_err(|e| SetupError::TerminalSize(e.into()))?;
Ok(rows as usize)
}
pub(crate) fn cleanup(
mut out: impl io::Write,
es: &crate::ExitStrategy,
) -> std::result::Result<(), CleanupError> {
execute!(out, event::DisableMouseCapture)
.map_err(|e| CleanupError::DisableMouseCapture(e.into()))?;
execute!(out, cursor::Show).map_err(|e| CleanupError::ShowCursor(e.into()))?;
terminal::disable_raw_mode().map_err(|e| CleanupError::DisableRawMode(e.into()))?;
execute!(out, terminal::LeaveAlternateScreen)
.map_err(|e| CleanupError::LeaveAlternateScreen(e.into()))?;
if *es == crate::ExitStrategy::ProcessQuit {
std::process::exit(0);
} else {
Ok(())
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum InputEvent {
Exit,
UpdateRows(usize),
UpdateUpperMark(usize),
UpdateLineNumber(LineNumbers),
#[cfg(feature = "search")]
Search(SearchMode),
#[cfg(feature = "search")]
NextMatch,
#[cfg(feature = "search")]
PrevMatch,
}
#[derive(PartialEq, Clone, Copy, Debug)]
#[cfg(feature = "search")]
pub enum SearchMode {
Forward,
Reverse,
Unknown,
}
pub(crate) fn handle_input(
ev: Event,
upper_mark: usize,
#[cfg(feature = "search")] search_mode: SearchMode,
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),
#[cfg(feature = "search")]
Event::Key(KeyEvent {
code: KeyCode::Char('/'),
modifiers: KeyModifiers::NONE,
}) => Some(InputEvent::Search(SearchMode::Forward)),
#[cfg(feature = "search")]
Event::Key(KeyEvent {
code: KeyCode::Char('?'),
modifiers: KeyModifiers::NONE,
}) => Some(InputEvent::Search(SearchMode::Reverse)),
#[cfg(feature = "search")]
Event::Key(KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::NONE,
}) => {
if search_mode == SearchMode::Reverse {
Some(InputEvent::PrevMatch)
} else {
Some(InputEvent::NextMatch)
}
}
#[cfg(feature = "search")]
Event::Key(KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::NONE,
}) => {
if search_mode == SearchMode::Reverse {
Some(InputEvent::NextMatch)
} else {
Some(InputEvent::PrevMatch)
}
}
_ => None,
}
}
pub(crate) fn draw(out: &mut impl io::Write, mut pager: &mut 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 Pager,
rows: usize,
) -> io::Result<()> {
let lines = pager.get_lines();
let lines = lines.lines();
let line_count = lines.clone().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 = 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;