use std::{
fs::File,
io::{BufRead, BufReader, Stdin, Stdout, Write, stdin, stdout},
panic::set_hook,
path::Path,
time::Duration,
};
use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
use crossterm::{
ExecutableCommand,
QueueableCommand, cursor::{Hide, MoveTo, Show},
event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
style::Attribute,
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
tty::IsTty,
};
use uucore::error::{UResult, USimpleError, UUsageError};
use uucore::{display::Quotable, show};
use uucore::{format_usage, help_about, help_usage};
const ABOUT: &str = help_about!("more.md");
const USAGE: &str = help_usage!("more.md");
const BELL: char = '\x07';
const MULTI_FILE_TOP_PROMPT: &str = "\r::::::::::::::\n\r{}\n\r::::::::::::::\n";
const HELP_MESSAGE: &str = "[Press space to continue, 'q' to quit.]";
pub mod options {
pub const SILENT: &str = "silent";
pub const LOGICAL: &str = "logical";
pub const EXIT_ON_EOF: &str = "exit-on-eof";
pub const NO_PAUSE: &str = "no-pause";
pub const PRINT_OVER: &str = "print-over";
pub const CLEAN_PRINT: &str = "clean-print";
pub const SQUEEZE: &str = "squeeze";
pub const PLAIN: &str = "plain";
pub const LINES: &str = "lines";
pub const NUMBER: &str = "number";
pub const PATTERN: &str = "pattern";
pub const FROM_LINE: &str = "from-line";
pub const FILES: &str = "files";
}
struct Options {
silent: bool,
_logical: bool, _exit_on_eof: bool, _no_pause: bool, print_over: bool,
clean_print: bool,
squeeze: bool,
lines: Option<u16>,
from_line: usize,
pattern: Option<String>,
}
impl Options {
fn from(matches: &ArgMatches) -> Self {
let lines = match (
matches.get_one::<u16>(options::LINES).copied(),
matches.get_one::<u16>(options::NUMBER).copied(),
) {
(Some(n), _) | (None, Some(n)) if n > 0 => Some(n + 1),
_ => None, };
let from_line = match matches.get_one::<usize>(options::FROM_LINE).copied() {
Some(number) => number.saturating_sub(1),
_ => 0,
};
let pattern = matches.get_one::<String>(options::PATTERN).cloned();
Self {
silent: matches.get_flag(options::SILENT),
_logical: matches.get_flag(options::LOGICAL),
_exit_on_eof: matches.get_flag(options::EXIT_ON_EOF),
_no_pause: matches.get_flag(options::NO_PAUSE),
print_over: matches.get_flag(options::PRINT_OVER),
clean_print: matches.get_flag(options::CLEAN_PRINT),
squeeze: matches.get_flag(options::SQUEEZE),
lines,
from_line,
pattern,
}
}
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
set_hook(Box::new(|panic_info| {
print!("\r");
println!("{panic_info}");
}));
let matches = uu_app().try_get_matches_from(args)?;
let mut options = Options::from(&matches);
if let Some(files) = matches.get_many::<String>(options::FILES) {
let length = files.len();
let mut files_iter = files.map(|s| s.as_str()).peekable();
while let (Some(file), next_file) = (files_iter.next(), files_iter.peek()) {
let file = Path::new(file);
if file.is_dir() {
show!(UUsageError::new(
0,
format!("{} is a directory.", file.quote()),
));
continue;
}
if !file.exists() {
show!(USimpleError::new(
0,
format!("cannot open {}: No such file or directory", file.quote()),
));
continue;
}
let opened_file = match File::open(file) {
Err(why) => {
show!(USimpleError::new(
0,
format!("cannot open {}: {}", file.quote(), why.kind()),
));
continue;
}
Ok(opened_file) => opened_file,
};
more(
InputType::File(BufReader::new(opened_file)),
length > 1,
file.to_str(),
next_file.copied(),
&mut options,
)?;
}
} else {
let stdin = stdin();
if stdin.is_tty() {
return Err(UUsageError::new(1, "bad usage"));
}
more(InputType::Stdin(stdin), false, None, None, &mut options)?;
}
Ok(())
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.version(uucore::crate_version!())
.infer_long_args(true)
.arg(
Arg::new(options::SILENT)
.short('d')
.long(options::SILENT)
.action(ArgAction::SetTrue)
.help("Display help instead of ringing bell when an illegal key is pressed"),
)
.arg(
Arg::new(options::LOGICAL)
.short('l')
.long(options::LOGICAL)
.action(ArgAction::SetTrue)
.help("Do not pause after any line containing a ^L (form feed)"),
)
.arg(
Arg::new(options::EXIT_ON_EOF)
.short('e')
.long(options::EXIT_ON_EOF)
.action(ArgAction::SetTrue)
.help("Exit on End-Of-File"),
)
.arg(
Arg::new(options::NO_PAUSE)
.short('f')
.long(options::NO_PAUSE)
.action(ArgAction::SetTrue)
.help("Count logical lines, rather than screen lines"),
)
.arg(
Arg::new(options::PRINT_OVER)
.short('p')
.long(options::PRINT_OVER)
.action(ArgAction::SetTrue)
.help("Do not scroll, clear screen and display text"),
)
.arg(
Arg::new(options::CLEAN_PRINT)
.short('c')
.long(options::CLEAN_PRINT)
.action(ArgAction::SetTrue)
.help("Do not scroll, display text and clean line ends"),
)
.arg(
Arg::new(options::SQUEEZE)
.short('s')
.long(options::SQUEEZE)
.action(ArgAction::SetTrue)
.help("Squeeze multiple blank lines into one"),
)
.arg(
Arg::new(options::PLAIN)
.short('u')
.long(options::PLAIN)
.action(ArgAction::SetTrue)
.hide(true)
.help("Suppress underlining"),
)
.arg(
Arg::new(options::LINES)
.short('n')
.long(options::LINES)
.value_name("number")
.num_args(1)
.value_parser(value_parser!(u16).range(0..))
.help("The number of lines per screen full"),
)
.arg(
Arg::new(options::NUMBER)
.long(options::NUMBER)
.num_args(1)
.value_parser(value_parser!(u16).range(0..))
.help("Same as --lines option argument"),
)
.arg(
Arg::new(options::FROM_LINE)
.short('F')
.long(options::FROM_LINE)
.num_args(1)
.value_name("number")
.value_parser(value_parser!(usize))
.help("Start displaying each file at line number"),
)
.arg(
Arg::new(options::PATTERN)
.short('P')
.long(options::PATTERN)
.allow_hyphen_values(true)
.required(false)
.value_name("pattern")
.help("The string to be searched in each file before starting to display it"),
)
.arg(
Arg::new(options::FILES)
.required(false)
.action(ArgAction::Append)
.help("Path to the files to be read")
.value_hint(clap::ValueHint::FilePath),
)
}
enum InputType {
File(BufReader<File>),
Stdin(Stdin),
}
impl InputType {
fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> {
match self {
InputType::File(reader) => reader.read_line(buf),
InputType::Stdin(stdin) => stdin.read_line(buf),
}
}
fn len(&self) -> std::io::Result<Option<u64>> {
let len = match self {
InputType::File(reader) => Some(reader.get_ref().metadata()?.len()),
InputType::Stdin(_) => None,
};
Ok(len)
}
}
enum OutputType {
Tty(Stdout),
Pipe(Box<dyn Write>),
#[cfg(test)]
Test(Vec<u8>),
}
impl IsTty for OutputType {
fn is_tty(&self) -> bool {
matches!(self, Self::Tty(_))
}
}
impl Write for OutputType {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
Self::Tty(stdout) => stdout.write(buf),
Self::Pipe(writer) => writer.write(buf),
#[cfg(test)]
Self::Test(vec) => vec.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
Self::Tty(stdout) => stdout.flush(),
Self::Pipe(writer) => writer.flush(),
#[cfg(test)]
Self::Test(vec) => vec.flush(),
}
}
}
fn setup_term() -> UResult<OutputType> {
let mut stdout = stdout();
if stdout.is_tty() {
terminal::enable_raw_mode()?;
stdout.execute(EnterAlternateScreen)?.execute(Hide)?;
Ok(OutputType::Tty(stdout))
} else {
Ok(OutputType::Pipe(Box::new(stdout)))
}
}
#[cfg(target_os = "fuchsia")]
#[inline(always)]
fn setup_term() -> UResult<OutputType> {
Ok(OutputType::Pipe(Box::new(stdout())))
}
fn reset_term() -> UResult<()> {
let mut stdout = stdout();
if stdout.is_tty() {
stdout.queue(Show)?.queue(LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
} else {
stdout.queue(Clear(ClearType::CurrentLine))?;
write!(stdout, "\r")?;
}
stdout.flush()?;
Ok(())
}
#[cfg(target_os = "fuchsia")]
#[inline(always)]
fn reset_term() -> UResult<()> {
Ok(())
}
struct TerminalGuard;
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = reset_term();
}
}
fn more(
input: InputType,
multiple_file: bool,
file_name: Option<&str>,
next_file: Option<&str>,
options: &mut Options,
) -> UResult<()> {
let out = setup_term()?;
let _guard = TerminalGuard;
let (_cols, mut rows) = terminal::size()?;
if let Some(number) = options.lines {
rows = number;
}
let mut pager = Pager::new(input, rows, file_name, next_file, options, out)?;
pager.handle_from_line()?;
pager.handle_pattern_search()?;
if multiple_file {
pager.display_multi_file_header()?;
}
pager.draw(None)?;
if multiple_file {
pager.reset_multi_file_header();
options.from_line = 0;
}
pager.process_events(options)
}
struct Pager<'a> {
input: InputType,
file_size: Option<u64>,
lines: Vec<String>,
cumulative_line_sizes: Vec<u64>,
upper_mark: usize,
content_rows: usize,
lines_squeezed: usize,
pattern: Option<String>,
file_name: Option<&'a str>,
next_file: Option<&'a str>,
eof_reached: bool,
silent: bool,
squeeze: bool,
stdout: OutputType,
}
impl<'a> Pager<'a> {
fn new(
input: InputType,
rows: u16,
file_name: Option<&'a str>,
next_file: Option<&'a str>,
options: &Options,
stdout: OutputType,
) -> UResult<Self> {
let content_rows = rows.saturating_sub(1).max(1) as usize;
let file_size = input.len()?;
let pager = Self {
input,
file_size,
lines: Vec::with_capacity(content_rows),
cumulative_line_sizes: Vec::new(),
upper_mark: options.from_line,
content_rows,
lines_squeezed: 0,
pattern: options.pattern.clone(),
file_name,
next_file,
eof_reached: false,
silent: options.silent,
squeeze: options.squeeze,
stdout,
};
Ok(pager)
}
fn handle_from_line(&mut self) -> UResult<()> {
if !self.read_until_line(self.upper_mark)? {
write!(
self.stdout,
"\r{}Cannot seek to line number {} (press RETURN){}",
Attribute::Reverse,
self.upper_mark + 1,
Attribute::Reset,
)?;
self.stdout.flush()?;
self.wait_for_enter_key()?;
self.upper_mark = 0;
}
Ok(())
}
fn read_until_line(&mut self, target_line: usize) -> UResult<bool> {
let mut line = String::new();
while self.lines.len() <= target_line {
let bytes_read = self.input.read_line(&mut line)?;
if bytes_read == 0 {
return Ok(false); }
let last_pos = self.cumulative_line_sizes.last().copied().unwrap_or(0);
self.cumulative_line_sizes
.push(last_pos + bytes_read as u64);
line = line.trim_end().to_string();
self.lines.push(std::mem::take(&mut line));
}
Ok(true)
}
fn wait_for_enter_key(&self) -> UResult<()> {
if !self.stdout.is_tty() {
return Ok(());
}
loop {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(KeyEvent {
code: KeyCode::Enter,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
}) = event::read()?
{
return Ok(());
}
}
}
}
fn handle_pattern_search(&mut self) -> UResult<()> {
if self.pattern.is_none() {
return Ok(());
};
match self.search_pattern_in_file() {
Some(line) => self.upper_mark = line,
None => {
self.pattern = None;
write!(
self.stdout,
"\r{}Pattern not found (press RETURN){}",
Attribute::Reverse,
Attribute::Reset,
)?;
self.stdout.flush()?;
self.wait_for_enter_key()?;
}
}
Ok(())
}
fn search_pattern_in_file(&mut self) -> Option<usize> {
let pattern = self.pattern.clone().expect("pattern should be set");
let mut line_num = self.upper_mark;
loop {
match self.get_line(line_num) {
Some(line) if line.contains(&pattern) => return Some(line_num),
Some(_) => line_num += 1,
None => return None,
}
}
}
fn get_line(&mut self, index: usize) -> Option<&String> {
match self.read_until_line(index) {
Ok(true) => self.lines.get(index),
_ => None,
}
}
fn display_multi_file_header(&mut self) -> UResult<()> {
self.stdout.queue(Clear(ClearType::CurrentLine))?;
self.stdout.write_all(
MULTI_FILE_TOP_PROMPT
.replace("{}", self.file_name.unwrap_or_default())
.as_bytes(),
)?;
self.content_rows = self
.content_rows
.saturating_sub(MULTI_FILE_TOP_PROMPT.lines().count());
Ok(())
}
fn reset_multi_file_header(&mut self) {
self.content_rows = self
.content_rows
.saturating_add(MULTI_FILE_TOP_PROMPT.lines().count());
}
fn update_display(&mut self, options: &Options) -> UResult<()> {
if options.print_over {
self.stdout
.execute(MoveTo(0, 0))?
.execute(Clear(ClearType::FromCursorDown))?;
} else if options.clean_print {
self.stdout
.execute(Clear(ClearType::All))?
.execute(MoveTo(0, 0))?;
}
Ok(())
}
fn process_events(&mut self, options: &Options) -> UResult<()> {
loop {
if !event::poll(Duration::from_millis(100))? {
continue;
}
let mut wrong_key = None;
match event::read()? {
Event::Key(
KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
},
) => {
reset_term()?;
std::process::exit(0);
}
Event::Key(KeyEvent {
code: KeyCode::Down | KeyCode::PageDown | KeyCode::Char(' '),
modifiers: KeyModifiers::NONE,
..
}) => {
if self.eof_reached {
return Ok(());
}
self.page_down();
}
Event::Key(KeyEvent {
code: KeyCode::Enter | KeyCode::Char('j'),
modifiers: KeyModifiers::NONE,
..
}) => {
if self.eof_reached {
return Ok(());
}
self.next_line();
}
Event::Key(KeyEvent {
code: KeyCode::Up | KeyCode::PageUp,
modifiers: KeyModifiers::NONE,
..
}) => {
self.page_up();
}
Event::Key(KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::NONE,
..
}) => {
self.prev_line();
}
Event::Resize(col, row) => {
self.page_resize(col, row, options.lines);
}
Event::Key(KeyEvent {
kind: KeyEventKind::Release,
..
}) => continue,
Event::Key(KeyEvent {
code: KeyCode::Char(k),
..
}) => wrong_key = Some(k),
_ => continue,
}
self.update_display(options)?;
self.draw(wrong_key)?;
}
}
fn page_down(&mut self) {
self.upper_mark = self.upper_mark.saturating_add(self.content_rows);
}
fn next_line(&mut self) {
self.upper_mark = self.upper_mark.saturating_add(1);
}
fn page_up(&mut self) {
self.eof_reached = false;
self.upper_mark = self
.upper_mark
.saturating_sub(self.content_rows.saturating_add(self.lines_squeezed));
if self.squeeze {
while self.upper_mark > 0 {
let line = self.lines.get(self.upper_mark).expect("line should exist");
if !line.trim().is_empty() {
break;
}
self.upper_mark = self.upper_mark.saturating_sub(1);
}
}
}
fn prev_line(&mut self) {
self.eof_reached = false;
self.upper_mark = self.upper_mark.saturating_sub(1);
}
fn page_resize(&mut self, _col: u16, row: u16, option_line: Option<u16>) {
if option_line.is_none() {
self.content_rows = row.saturating_sub(1) as usize;
};
}
fn draw(&mut self, wrong_key: Option<char>) -> UResult<()> {
self.draw_lines()?;
self.draw_status_bar(wrong_key);
self.stdout.flush()?;
Ok(())
}
fn draw_lines(&mut self) -> UResult<()> {
self.stdout.queue(Clear(ClearType::CurrentLine))?;
self.lines_squeezed = 0;
let mut lines_printed = 0;
let mut index = self.upper_mark;
while lines_printed < self.content_rows {
if !self.read_until_line(index)? {
self.eof_reached = true;
self.upper_mark = index.saturating_sub(self.content_rows);
break;
}
if self.should_squeeze_line(index) {
self.lines_squeezed += 1;
index += 1;
continue;
}
let mut line = self.lines[index].clone();
if let Some(pattern) = &self.pattern {
line = line.replace(
pattern,
&format!("{}{pattern}{}", Attribute::Reverse, Attribute::Reset),
);
};
self.stdout.write_all(format!("\r{}\n", line).as_bytes())?;
lines_printed += 1;
index += 1;
}
while lines_printed < self.content_rows {
self.stdout.write_all(b"\r~\n")?;
lines_printed += 1;
}
Ok(())
}
fn should_squeeze_line(&self, index: usize) -> bool {
if !self.squeeze || index == 0 {
return false;
}
match (self.lines.get(index), self.lines.get(index - 1)) {
(Some(current), Some(previous)) => current.is_empty() && previous.is_empty(),
_ => false,
}
}
fn draw_status_bar(&mut self, wrong_key: Option<char>) {
let lower_mark =
(self.upper_mark + self.content_rows).min(self.lines.len().saturating_sub(1));
let progress_info = if self.eof_reached && self.next_file.is_some() {
format!(" (Next file: {})", self.next_file.unwrap())
} else if let Some(file_size) = self.file_size {
let position = self
.cumulative_line_sizes
.get(lower_mark)
.copied()
.unwrap_or_default();
if file_size == 0 {
" (END)".to_string()
} else {
let percentage = (position as f64 / file_size as f64 * 100.0).round() as u16;
if percentage >= 100 {
" (END)".to_string()
} else {
format!(" ({}%)", percentage)
}
}
} else {
String::new()
};
let file_name = self.file_name.unwrap_or(":");
let status = format!("{file_name}{progress_info}");
let banner = match (self.silent, wrong_key) {
(true, Some(key)) => format!(
"{status}[Unknown key: '{key}'. Press 'h' for instructions. (unimplemented)]"
),
(true, None) => format!("{status}{HELP_MESSAGE}"),
(false, Some(_)) => format!("{status}{BELL}"),
(false, None) => status,
};
write!(
self.stdout,
"\r{}{banner}{}",
Attribute::Reverse,
Attribute::Reset
)
.unwrap();
}
}
#[cfg(test)]
mod tests {
use std::{
io::Seek,
ops::{Deref, DerefMut},
};
use super::*;
use tempfile::tempfile;
impl Deref for OutputType {
type Target = Vec<u8>;
fn deref(&self) -> &Vec<u8> {
match self {
OutputType::Test(buf) => buf,
_ => unreachable!(),
}
}
}
impl DerefMut for OutputType {
fn deref_mut(&mut self) -> &mut Vec<u8> {
match self {
OutputType::Test(buf) => buf,
_ => unreachable!(),
}
}
}
struct TestPagerBuilder {
content: String,
options: Options,
rows: u16,
next_file: Option<&'static str>,
}
impl Default for TestPagerBuilder {
fn default() -> Self {
Self {
content: String::new(),
options: Options {
silent: false,
_logical: false,
_exit_on_eof: false,
_no_pause: false,
print_over: false,
clean_print: false,
squeeze: false,
lines: None,
from_line: 0,
pattern: None,
},
rows: 10,
next_file: None,
}
}
}
#[allow(dead_code)]
impl TestPagerBuilder {
fn new(content: &str) -> Self {
Self {
content: content.to_string(),
..Default::default()
}
}
fn build(mut self) -> Pager<'static> {
let mut tmpfile = tempfile().unwrap();
tmpfile.write_all(self.content.as_bytes()).unwrap();
tmpfile.rewind().unwrap();
let out = OutputType::Test(Vec::new());
if let Some(rows) = self.options.lines {
self.rows = rows;
}
Pager::new(
InputType::File(BufReader::new(tmpfile)),
self.rows,
None,
self.next_file,
&self.options,
out,
)
.unwrap()
}
fn silent(mut self) -> Self {
self.options.silent = true;
self
}
fn print_over(mut self) -> Self {
self.options.print_over = true;
self
}
fn clean_print(mut self) -> Self {
self.options.clean_print = true;
self
}
fn squeeze(mut self) -> Self {
self.options.squeeze = true;
self
}
fn lines(mut self, lines: u16) -> Self {
self.options.lines = Some(lines);
self
}
#[allow(clippy::wrong_self_convention)]
fn from_line(mut self, from_line: usize) -> Self {
self.options.from_line = from_line;
self
}
fn pattern(mut self, pattern: &str) -> Self {
self.options.pattern = Some(pattern.to_owned());
self
}
fn rows(mut self, rows: u16) -> Self {
self.rows = rows;
self
}
fn next_file(mut self, next_file: &'static str) -> Self {
self.next_file = Some(next_file);
self
}
}
#[test]
fn test_get_line_and_len() {
let content = "a\n\tb\nc\n";
let mut pager = TestPagerBuilder::new(content).build();
assert_eq!(pager.get_line(1).unwrap(), "\tb");
assert_eq!(pager.cumulative_line_sizes.len(), 2);
assert_eq!(pager.cumulative_line_sizes[1], 5);
}
#[test]
fn test_navigate_page() {
let content = (0..10).map(|i| i.to_string() + "\n").collect::<String>();
let mut pager = TestPagerBuilder::new(&content).build();
assert_eq!(pager.upper_mark, 0);
pager.page_down();
assert_eq!(pager.upper_mark, pager.content_rows);
pager.draw(None).unwrap();
let mut stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains("9\n"));
assert!(!stdout.contains("8\n"));
assert_eq!(pager.upper_mark, 1);
pager.page_up();
assert_eq!(pager.upper_mark, 0);
pager.next_line();
assert_eq!(pager.upper_mark, 1);
pager.prev_line();
assert_eq!(pager.upper_mark, 0);
pager.stdout.clear();
pager.draw(None).unwrap();
stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains("0\n"));
assert!(!stdout.contains("9\n")); }
#[test]
fn test_silent_mode() {
let content = (0..5).map(|i| i.to_string() + "\n").collect::<String>();
let mut pager = TestPagerBuilder::new(&content)
.from_line(3)
.silent()
.build();
pager.draw_status_bar(None);
let stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains(HELP_MESSAGE));
}
#[test]
fn test_squeeze() {
let content = "Line 0\n\n\n\nLine 4\n\n\nLine 7\n";
let mut pager = TestPagerBuilder::new(content).lines(6).squeeze().build();
assert_eq!(pager.content_rows, 5);
assert!(pager.read_until_line(7).unwrap());
assert!(pager.should_squeeze_line(2));
assert!(pager.should_squeeze_line(3));
assert!(pager.should_squeeze_line(6));
assert!(!pager.should_squeeze_line(0));
assert!(!pager.should_squeeze_line(1));
assert!(!pager.should_squeeze_line(4));
assert!(!pager.should_squeeze_line(5));
assert!(!pager.should_squeeze_line(7));
pager.draw(None).unwrap();
let stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains("Line 0"));
assert!(stdout.contains("Line 4"));
assert!(stdout.contains("Line 7"));
}
#[test]
fn test_lines_option() {
let content = (0..5).map(|i| i.to_string() + "\n").collect::<String>();
let mut pager = TestPagerBuilder::new(&content).lines(0).build();
pager.draw(None).unwrap();
let mut stdout = String::from_utf8_lossy(&pager.stdout);
assert!(!stdout.is_empty());
let mut pager = TestPagerBuilder::new(&content).lines(3).build();
assert_eq!(pager.content_rows, 3 - 1); pager.draw(None).unwrap();
stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains("0\n"));
assert!(stdout.contains("1\n"));
assert!(!stdout.contains("2\n"));
}
#[test]
fn test_from_line_option() {
let content = (0..5).map(|i| i.to_string() + "\n").collect::<String>();
let mut pager = TestPagerBuilder::new(&content).from_line(0).build();
assert!(pager.handle_from_line().is_ok());
pager.draw(None).unwrap();
let stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains("0\n"));
pager = TestPagerBuilder::new(&content).from_line(1).build();
assert!(pager.handle_from_line().is_ok());
pager.draw(None).unwrap();
let stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains("1\n"));
assert!(!stdout.contains("0\n"));
pager = TestPagerBuilder::new(&content).from_line(99).build();
assert!(pager.handle_from_line().is_ok());
assert_eq!(pager.upper_mark, 0);
let stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains("Cannot seek to line number 100"));
}
#[test]
fn test_search_pattern_found() {
let content = "foo\nbar\nbaz\n";
let mut pager = TestPagerBuilder::new(content).pattern("bar").build();
assert!(pager.handle_pattern_search().is_ok());
assert_eq!(pager.upper_mark, 1);
pager.draw(None).unwrap();
let stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains("bar"));
assert!(!stdout.contains("foo"));
}
#[test]
fn test_search_pattern_not_found() {
let content = "foo\nbar\nbaz\n";
let mut pager = TestPagerBuilder::new(content).pattern("qux").build();
assert!(pager.handle_pattern_search().is_ok());
let stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains("Pattern not found"));
assert_eq!(pager.pattern, None);
assert_eq!(pager.upper_mark, 0);
}
#[test]
fn test_wrong_key() {
let mut pager = TestPagerBuilder::default().silent().build();
pager.draw_status_bar(Some('x'));
let stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains("Unknown key: 'x'"));
pager = TestPagerBuilder::default().build();
pager.draw_status_bar(Some('x'));
let stdout = String::from_utf8_lossy(&pager.stdout);
assert!(stdout.contains(BELL));
}
}