#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(
not(any(
feature = "tokio_lib",
feature = "async_std_lib",
feature = "static_output"
)),
allow(unused_imports),
allow(dead_code)
)]
#![deny(clippy::all)]
#![warn(clippy::pedantic)]
pub mod error;
mod init;
pub mod input;
#[cfg(any(feature = "tokio_lib", feature = "async_std_lib"))]
mod rt_wrappers;
#[cfg(feature = "search")]
mod search;
#[cfg(feature = "static_output")]
mod static_pager;
mod utils;
#[cfg(any(feature = "tokio_lib", feature = "async_std_lib"))]
use async_mutex::Mutex;
use crossterm::{terminal, tty::IsTty};
use error::AlternateScreenPagingError;
#[cfg(any(feature = "tokio_lib", feature = "async_std_lib"))]
pub use rt_wrappers::*;
#[cfg(feature = "search")]
pub use search::SearchMode;
#[cfg(feature = "static_output")]
pub use static_pager::page_all;
use std::{fmt, io::stdout};
use std::{iter::Flatten, string::ToString, vec::IntoIter};
pub use utils::LineNumbers;
#[cfg(any(feature = "tokio_lib", feature = "async_std_lib"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio_lib", feature = "async_std_lib")))
)]
pub type PagerMutex = std::sync::Arc<Mutex<Pager>>;
pub type ExitCallbacks = Vec<Box<dyn FnMut() + Send + Sync + 'static>>;
pub struct Pager {
wrap_lines: Vec<Vec<String>>,
pub(crate) line_numbers: LineNumbers,
prompt: Vec<String>,
lines: String,
input_handler: Box<dyn input::InputHandler + Sync + Send>,
exit_callbacks: Vec<Box<dyn FnMut() + Send + Sync + 'static>>,
exit_strategy: ExitStrategy,
end_stream: bool,
message: (Option<Vec<String>>, bool),
pub(crate) upper_mark: usize,
pub(crate) run_no_overflow: bool,
#[cfg(feature = "search")]
search_term: Option<regex::Regex>,
#[cfg(feature = "search")]
search_mode: SearchMode,
#[cfg(feature = "search")]
pub(crate) search_idx: Vec<u16>,
pub(crate) rows: usize,
pub(crate) cols: usize,
}
impl Pager {
pub fn new() -> Result<Self, error::AlternateScreenPagingError> {
let (rows, cols);
if cfg!(test) {
cols = 80;
rows = 10;
} else if stdout().is_tty() {
let size = terminal::size()?;
cols = size.0;
rows = size.1;
} else {
cols = 1;
rows = 1;
};
Ok(Pager {
wrap_lines: Vec::new(),
line_numbers: LineNumbers::Disabled,
upper_mark: 0,
prompt: wrap_str("minus", cols.into()),
exit_strategy: ExitStrategy::ProcessQuit,
input_handler: Box::new(input::DefaultInputHandler {}),
exit_callbacks: Vec::new(),
run_no_overflow: false,
message: (None, false),
lines: String::new(),
end_stream: false,
#[cfg(feature = "search")]
search_term: None,
#[cfg(feature = "search")]
search_mode: SearchMode::Unknown,
#[cfg(feature = "search")]
search_idx: Vec::new(),
cols: cols as usize,
rows: rows as usize,
})
}
pub fn set_text(&mut self, text: impl Into<String>) {
let text: String = text.into();
self.wrap_lines = text.lines().map(|l| wrap_str(l, self.cols)).collect();
}
pub fn set_line_numbers(&mut self, l: LineNumbers) {
self.line_numbers = l;
}
pub fn send_message(&mut self, text: impl Into<String>) {
let message = text.into();
if message.contains('\n') {
panic!("Prompt text cannot contain newlines")
}
self.message.0 = Some(wrap_str(&message, self.cols));
self.message.1 = true;
}
pub fn set_prompt(&mut self, t: impl Into<String>) {
let prompt = t.into();
if prompt.contains('\n') {
panic!("Prompt text cannot contain newlines")
}
self.prompt = wrap_str(&prompt, self.cols);
}
#[must_use]
#[cfg(any(feature = "tokio_lib", feature = "async_std_lib"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio_lib", feature = "async_std_lib")))
)]
pub fn finish(self) -> PagerMutex {
std::sync::Arc::new(Mutex::new(self))
}
pub fn set_exit_strategy(&mut self, strategy: ExitStrategy) {
self.exit_strategy = strategy;
}
pub(crate) fn get_lines(&self) -> Vec<Vec<String>> {
self.wrap_lines.clone()
}
pub fn set_run_no_overflow(&mut self, value: bool) {
self.run_no_overflow = value;
}
pub fn push_str(&mut self, string: impl Into<String>) {
let string = string.into();
if string.ends_with('\n') {
self.wrap_lines.append(
&mut self
.lines
.lines()
.map(|l| wrap_str(l, self.cols))
.collect::<Vec<Vec<String>>>(),
);
self.lines.clear();
} else if string.contains('\n') {
let mut lines = string.lines().collect::<Vec<&str>>();
let line_count = lines.len();
let push_lines = &mut lines[0..line_count - 1];
self.wrap_lines.append(
&mut push_lines
.iter()
.map(|l| wrap_str(l, self.cols))
.collect::<Vec<Vec<String>>>(),
);
self.lines.push_str(lines[line_count - 1]);
} else {
self.lines.push_str(&string);
}
}
pub fn end_data_stream(&mut self) {
self.end_stream = true;
}
pub(crate) fn readjust_wraps(&mut self) {
rewrap_lines(&mut self.wrap_lines, self.cols);
if self.message.0.is_some() {
rewrap(&mut self.message.0.as_mut().unwrap(), self.cols);
}
rewrap(&mut self.prompt, self.cols);
}
pub(crate) fn get_flattened_lines(&self) -> Flatten<IntoIter<Vec<String>>> {
self.get_lines().into_iter().flatten()
}
pub(crate) fn num_lines(&self) -> usize {
self.get_flattened_lines().count()
}
pub fn set_input_handler(&mut self, handler: Box<dyn input::InputHandler + Send + Sync>) {
self.input_handler = handler;
}
pub(crate) fn exit(&mut self) {
for func in &mut self.exit_callbacks {
func();
}
}
pub fn add_exit_callback(&mut self, cb: impl FnMut() + Send + Sync + 'static) {
self.exit_callbacks.push(Box::new(cb));
}
}
impl std::default::Default for Pager {
fn default() -> Self {
Pager::new().unwrap()
}
}
#[derive(PartialEq, Clone)]
pub enum ExitStrategy {
ProcessQuit,
PagerQuit,
}
pub(crate) fn rewrap_lines(lines: &mut Vec<Vec<String>>, cols: usize) {
for line in lines {
rewrap(line, cols);
}
}
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>>()
}
impl fmt::Write for Pager {
fn write_str(&mut self, string: &str) -> fmt::Result {
self.push_str(string);
Ok(())
}
}
#[cfg(test)]
mod tests;