#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(
not(any(feature = "async_output", feature = "static_output")),
allow(unused_imports),
allow(dead_code)
)]
#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::doc_markdown)]
mod events;
use events::Event;
#[cfg(feature = "async_output")]
mod async_pager;
pub mod error;
mod init;
pub mod input;
#[cfg(feature = "search")]
mod search;
#[cfg(feature = "static_output")]
mod static_pager;
#[cfg(feature = "threads_output")]
mod threads_pager;
mod utils;
#[cfg(feature = "async_output")]
pub use async_pager::async_paging;
#[cfg(feature = "static_output")]
pub use static_pager::page_all;
#[cfg(feature = "threads_output")]
pub use threads_pager::threads_paging;
use crossbeam_channel::{Receiver, Sender};
use crossterm::{terminal, tty::IsTty};
pub use error::MinusError;
use error::TermError;
#[cfg(feature = "search")]
pub use search::SearchMode;
use std::string::ToString;
use std::{fmt, io::stdout};
pub use utils::LineNumbers;
pub type ExitCallbacks = Vec<Box<dyn FnMut() + Send + Sync + 'static>>;
#[derive(Clone)]
pub struct Pager {
tx: Sender<Event>,
rx: Receiver<Event>,
}
impl Pager {
#[must_use]
pub fn new() -> Self {
let (tx, rx) = crossbeam_channel::unbounded();
Self { tx, rx }
}
pub fn set_text(&self, s: impl Into<String>) -> Result<(), MinusError> {
Ok(self.tx.send(Event::SetData(s.into()))?)
}
pub fn push_str(&self, s: impl Into<String>) -> Result<(), MinusError> {
Ok(self.tx.send(Event::AppendData(s.into()))?)
}
pub fn set_line_numbers(&self, l: LineNumbers) -> Result<(), MinusError> {
Ok(self.tx.send(Event::SetLineNumbers(l))?)
}
pub fn set_prompt(&self, text: impl Into<String>) -> Result<(), MinusError> {
let text = text.into();
assert!(!text.contains('\n'), "Prompt cannot contain newlines");
Ok(self.tx.send(Event::SetPrompt(text))?)
}
pub fn send_message(&self, text: impl Into<String>) -> Result<(), MinusError> {
let text = text.into();
assert!(!text.contains('\n'), "Message cannot contain newlines");
Ok(self.tx.send(Event::SendMessage(text))?)
}
pub fn set_exit_strategy(&self, es: ExitStrategy) -> Result<(), MinusError> {
Ok(self.tx.send(Event::SetExitStrategy(es))?)
}
#[cfg(feature = "static_output")]
#[cfg_attr(docsrs, feature = "static_output")]
pub fn set_run_no_overflow(&self, val: bool) -> Result<(), MinusError> {
Ok(self.tx.send(Event::SetRunNoOverflow(val))?)
}
pub fn set_input_classifier(
&self,
handler: Box<dyn input::InputClassifier + Send + Sync>,
) -> Result<(), MinusError> {
Ok(self.tx.send(Event::SetInputClassifier(handler))?)
}
pub fn add_exit_callback(
&self,
cb: Box<dyn FnMut() + Send + Sync + 'static>,
) -> Result<(), MinusError> {
Ok(self.tx.send(Event::AddExitCallback(cb))?)
}
}
impl Default for Pager {
fn default() -> Self {
Self::new()
}
}
pub struct PagerState {
lines: String,
formatted_lines: Vec<String>,
pub line_numbers: LineNumbers,
prompt: Vec<String>,
input_classifier: Box<dyn input::InputClassifier + Sync + Send>,
exit_callbacks: Vec<Box<dyn FnMut() + Send + Sync + 'static>>,
exit_strategy: ExitStrategy,
message: (Option<Vec<String>>, bool),
pub upper_mark: usize,
#[cfg(feature = "static_output")]
run_no_overflow: bool,
#[cfg(feature = "search")]
search_term: Option<regex::Regex>,
#[cfg(feature = "search")]
#[cfg_attr(docsrs, feature = "search")]
pub search_mode: SearchMode,
#[cfg(feature = "search")]
search_idx: Vec<usize>,
#[cfg(feature = "search")]
search_mark: usize,
pub rows: usize,
pub cols: usize,
pub prefix_num: String,
}
impl PagerState {
pub(crate) fn new() -> Result<Self, TermError> {
let (rows, cols);
if cfg!(test) {
cols = 80;
rows = 10;
} else if stdout().is_tty() {
let size = terminal::size()?;
cols = size.0 as usize;
rows = size.1 as usize;
} else {
cols = 1;
rows = 1;
};
Ok(Self {
lines: String::new(),
formatted_lines: Vec::new(),
line_numbers: LineNumbers::Disabled,
upper_mark: 0,
prompt: wrap_str("minus", cols),
exit_strategy: ExitStrategy::ProcessQuit,
input_classifier: Box::new(input::DefaultInputClassifier {}),
exit_callbacks: Vec::new(),
message: (None, false),
#[cfg(feature = "static_output")]
run_no_overflow: false,
#[cfg(feature = "search")]
search_term: None,
#[cfg(feature = "search")]
search_mode: SearchMode::Unknown,
#[cfg(feature = "search")]
search_idx: Vec::new(),
#[cfg(feature = "search")]
search_mark: 0,
cols,
rows,
prefix_num: String::new(),
})
}
pub(crate) fn num_lines(&self) -> usize {
self.formatted_lines.len()
}
pub(crate) fn formatted_line(
&self,
line: &str,
line_numbers: bool,
len_line_number: usize,
idx: usize,
) -> Vec<String> {
if line_numbers {
#[cfg_attr(not(feature = "search"), allow(unused_mut))]
wrap_str(line, self.cols.saturating_sub(len_line_number + 3))
.into_iter()
.map(|mut row| {
#[cfg(feature = "search")]
if let Some(st) = self.search_term.as_ref() {
row = search::highlight_line_matches(&row, st);
}
if cfg!(not(test)) {
format!(
" {bold}{number: >len$}.{reset} {row}",
bold = crossterm::style::Attribute::Bold,
number = idx + 1,
len = len_line_number,
reset = crossterm::style::Attribute::Reset,
row = row
)
} else {
format!(
" {number: >len$}. {row}",
number = idx + 1,
len = len_line_number,
row = row
)
}
})
.collect::<Vec<String>>()
} else {
#[cfg(feature = "search")]
if let Some(st) = self.search_term.as_ref() {
return wrap_str(&search::highlight_line_matches(line, st), self.cols);
}
wrap_str(line, self.cols)
}
}
pub(crate) fn format_lines(&mut self) {
let line_count = self.lines.lines().count();
let len_line_number = line_count.to_string().len();
self.formatted_lines = self
.lines
.lines()
.enumerate()
.flat_map(|(idx, line)| {
self.formatted_line(
line,
matches!(
self.line_numbers,
LineNumbers::AlwaysOn | LineNumbers::Enabled
),
len_line_number,
idx,
)
})
.collect::<Vec<String>>();
if self.message.0.is_some() {
rewrap(self.message.0.as_mut().unwrap(), self.cols);
}
rewrap(&mut self.prompt, self.cols);
#[cfg(feature = "search")]
search::set_match_indices(self);
}
pub(crate) fn get_flattened_lines_with_bounds(&self, start: usize, end: usize) -> &[String] {
if start >= self.num_lines() || start > end {
&[]
} else if end >= self.num_lines() {
&self.formatted_lines[start..]
} else {
&self.formatted_lines[start..end]
}
}
pub(crate) fn exit(&mut self) {
for func in &mut self.exit_callbacks {
func();
}
}
pub(crate) fn append_str(&mut self, text: &str) {
let newline = self.lines.ends_with('\n');
let ending_whitespace = self
.lines
.chars()
.rev()
.take_while(|c| c.is_whitespace() && *c != '\n')
.collect::<String>();
self.lines.push_str(text);
let len_line_number = self.lines.lines().count().to_string().len();
let (to_format, to_skip) = if newline {
(text.to_owned(), self.lines.lines().count())
} else {
let to_fmt = format!(
"{}{}{}",
self.formatted_lines.pop().unwrap_or_default(),
ending_whitespace,
text
);
(to_fmt, self.lines.lines().count().saturating_sub(1))
};
let mut to_append = to_format
.lines()
.enumerate()
.flat_map(|(idx, line)| {
self.formatted_line(
line,
matches!(
self.line_numbers,
LineNumbers::AlwaysOn | LineNumbers::Enabled
),
len_line_number,
idx + to_skip.saturating_sub(1),
)
})
.collect::<Vec<String>>();
self.formatted_lines.append(&mut to_append);
}
}
#[derive(PartialEq, Clone, Debug)]
pub enum ExitStrategy {
ProcessQuit,
PagerQuit,
}
impl fmt::Write for Pager {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.push_str(s).map_err(|_| fmt::Error)
}
}
pub(crate) fn rewrap(line: &mut Vec<String>, cols: usize) {
*line = textwrap::wrap(&line.join(" "), cols)
.iter()
.map(ToString::to_string)
.collect();
}
pub(crate) fn wrap_str(line: &str, cols: usize) -> Vec<String> {
textwrap::wrap(line, cols)
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
}
#[cfg(test)]
mod tests;