1use std::{
7 ffi::OsString,
8 fs::File,
9 io::{BufRead, BufReader, Stdin, Stdout, Write, stdin, stdout},
10 panic::set_hook,
11 path::Path,
12 time::Duration,
13};
14
15use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
16use crossterm::{
17 ExecutableCommand,
18 QueueableCommand, cursor::{Hide, MoveTo, Show},
20 event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
21 style::Attribute,
22 terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
23 tty::IsTty,
24};
25
26use uucore::error::{UResult, USimpleError, UUsageError};
27use uucore::format_usage;
28use uucore::{display::Quotable, show};
29
30use uucore::translate;
31
32#[derive(Debug)]
33enum MoreError {
34 IsDirectory(String),
35 CannotOpenNoSuchFile(String),
36 CannotOpenIOError(String, std::io::ErrorKind),
37 BadUsage,
38}
39
40impl std::fmt::Display for MoreError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 Self::IsDirectory(path) => {
44 write!(
45 f,
46 "{}",
47 translate!(
48 "more-error-is-directory",
49 "path" => path.quote()
50 )
51 )
52 }
53 Self::CannotOpenNoSuchFile(path) => {
54 write!(
55 f,
56 "{}",
57 translate!(
58 "more-error-cannot-open-no-such-file",
59 "path" => path.quote()
60 )
61 )
62 }
63 Self::CannotOpenIOError(path, error) => {
64 write!(
65 f,
66 "{}",
67 translate!(
68 "more-error-cannot-open-io-error",
69 "path" => path.quote(),
70 "error" => error
71 )
72 )
73 }
74 Self::BadUsage => {
75 write!(f, "{}", translate!("more-error-bad-usage"))
76 }
77 }
78 }
79}
80
81impl std::error::Error for MoreError {}
82
83const BELL: char = '\x07'; const MULTI_FILE_TOP_PROMPT: &str = "\r::::::::::::::\n\r{}\n\r::::::::::::::\n";
88
89pub mod options {
90 pub const SILENT: &str = "silent";
91 pub const LOGICAL: &str = "logical";
92 pub const EXIT_ON_EOF: &str = "exit-on-eof";
93 pub const NO_PAUSE: &str = "no-pause";
94 pub const PRINT_OVER: &str = "print-over";
95 pub const CLEAN_PRINT: &str = "clean-print";
96 pub const SQUEEZE: &str = "squeeze";
97 pub const PLAIN: &str = "plain";
98 pub const LINES: &str = "lines";
99 pub const NUMBER: &str = "number";
100 pub const PATTERN: &str = "pattern";
101 pub const FROM_LINE: &str = "from-line";
102 pub const FILES: &str = "files";
103}
104
105struct Options {
106 silent: bool,
107 _logical: bool, _exit_on_eof: bool, _no_pause: bool, print_over: bool,
111 clean_print: bool,
112 squeeze: bool,
113 lines: Option<u16>,
114 from_line: usize,
115 pattern: Option<String>,
116}
117
118impl Options {
119 fn from(matches: &ArgMatches) -> Self {
120 let lines = match (
121 matches.get_one::<u16>(options::LINES).copied(),
122 matches.get_one::<u16>(options::NUMBER).copied(),
123 ) {
124 (Some(n), _) | (None, Some(n)) if n > 0 => Some(n + 1),
127 _ => None, };
129 let from_line = match matches.get_one::<usize>(options::FROM_LINE).copied() {
130 Some(number) => number.saturating_sub(1),
131 _ => 0,
132 };
133 let pattern = matches.get_one::<String>(options::PATTERN).cloned();
134 Self {
135 silent: matches.get_flag(options::SILENT),
136 _logical: matches.get_flag(options::LOGICAL),
137 _exit_on_eof: matches.get_flag(options::EXIT_ON_EOF),
138 _no_pause: matches.get_flag(options::NO_PAUSE),
139 print_over: matches.get_flag(options::PRINT_OVER),
140 clean_print: matches.get_flag(options::CLEAN_PRINT),
141 squeeze: matches.get_flag(options::SQUEEZE),
142 lines,
143 from_line,
144 pattern,
145 }
146 }
147}
148
149#[uucore::main]
150pub fn uumain(args: impl uucore::Args) -> UResult<()> {
151 set_hook(Box::new(|panic_info| {
152 print!("\r");
153 println!("{panic_info}");
154 }));
155 let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
156 let mut options = Options::from(&matches);
157 if let Some(files) = matches.get_many::<OsString>(options::FILES) {
158 let length = files.len();
159
160 let mut files_iter = files.peekable();
161 while let (Some(file_os), next_file) = (files_iter.next(), files_iter.peek()) {
162 let file = Path::new(file_os);
163 if file.is_dir() {
164 show!(UUsageError::new(
165 0,
166 MoreError::IsDirectory(file.to_string_lossy().to_string()).to_string(),
167 ));
168 continue;
169 }
170 if !file.exists() {
171 show!(USimpleError::new(
172 0,
173 MoreError::CannotOpenNoSuchFile(file.to_string_lossy().to_string()).to_string(),
174 ));
175 continue;
176 }
177 let opened_file = match File::open(file) {
178 Err(why) => {
179 show!(USimpleError::new(
180 0,
181 MoreError::CannotOpenIOError(
182 file.to_string_lossy().to_string(),
183 why.kind()
184 )
185 .to_string(),
186 ));
187 continue;
188 }
189 Ok(opened_file) => opened_file,
190 };
191 let next_file_str = next_file.map(|f| f.to_string_lossy().into_owned());
192 more(
193 InputType::File(BufReader::new(opened_file)),
194 length > 1,
195 Some(&file.to_string_lossy()),
196 next_file_str.as_deref(),
197 &mut options,
198 )?;
199 }
200 } else {
201 let stdin = stdin();
202 if stdin.is_tty() {
203 return Err(UUsageError::new(1, MoreError::BadUsage.to_string()));
205 }
206 more(InputType::Stdin(stdin), false, None, None, &mut options)?;
207 }
208
209 Ok(())
210}
211
212pub fn uu_app() -> Command {
213 Command::new(uucore::util_name())
214 .about(translate!("more-about"))
215 .override_usage(format_usage(&translate!("more-usage")))
216 .version(uucore::crate_version!())
217 .help_template(uucore::localized_help_template(uucore::util_name()))
218 .infer_long_args(true)
219 .arg(
220 Arg::new(options::SILENT)
221 .short('d')
222 .long(options::SILENT)
223 .action(ArgAction::SetTrue)
224 .help(translate!("more-help-silent")),
225 )
226 .arg(
227 Arg::new(options::LOGICAL)
228 .short('l')
229 .long(options::LOGICAL)
230 .action(ArgAction::SetTrue)
231 .help(translate!("more-help-logical")),
232 )
233 .arg(
234 Arg::new(options::EXIT_ON_EOF)
235 .short('e')
236 .long(options::EXIT_ON_EOF)
237 .action(ArgAction::SetTrue)
238 .help(translate!("more-help-exit-on-eof")),
239 )
240 .arg(
241 Arg::new(options::NO_PAUSE)
242 .short('f')
243 .long(options::NO_PAUSE)
244 .action(ArgAction::SetTrue)
245 .help(translate!("more-help-no-pause")),
246 )
247 .arg(
248 Arg::new(options::PRINT_OVER)
249 .short('p')
250 .long(options::PRINT_OVER)
251 .action(ArgAction::SetTrue)
252 .help(translate!("more-help-print-over")),
253 )
254 .arg(
255 Arg::new(options::CLEAN_PRINT)
256 .short('c')
257 .long(options::CLEAN_PRINT)
258 .action(ArgAction::SetTrue)
259 .help(translate!("more-help-clean-print")),
260 )
261 .arg(
262 Arg::new(options::SQUEEZE)
263 .short('s')
264 .long(options::SQUEEZE)
265 .action(ArgAction::SetTrue)
266 .help(translate!("more-help-squeeze")),
267 )
268 .arg(
269 Arg::new(options::PLAIN)
270 .short('u')
271 .long(options::PLAIN)
272 .action(ArgAction::SetTrue)
273 .hide(true)
274 .help(translate!("more-help-plain")),
275 )
276 .arg(
277 Arg::new(options::LINES)
278 .short('n')
279 .long(options::LINES)
280 .value_name("number")
281 .num_args(1)
282 .value_parser(value_parser!(u16).range(0..))
283 .help(translate!("more-help-lines")),
284 )
285 .arg(
286 Arg::new(options::NUMBER)
287 .long(options::NUMBER)
288 .num_args(1)
289 .value_parser(value_parser!(u16).range(0..))
290 .help(translate!("more-help-number")),
291 )
292 .arg(
293 Arg::new(options::FROM_LINE)
294 .short('F')
295 .long(options::FROM_LINE)
296 .num_args(1)
297 .value_name("number")
298 .value_parser(value_parser!(usize))
299 .help(translate!("more-help-from-line")),
300 )
301 .arg(
302 Arg::new(options::PATTERN)
303 .short('P')
304 .long(options::PATTERN)
305 .allow_hyphen_values(true)
306 .required(false)
307 .value_name("pattern")
308 .help(translate!("more-help-pattern")),
309 )
310 .arg(
311 Arg::new(options::FILES)
312 .required(false)
313 .action(ArgAction::Append)
314 .help(translate!("more-help-files"))
315 .value_hint(clap::ValueHint::FilePath)
316 .value_parser(clap::value_parser!(OsString)),
317 )
318}
319
320enum InputType {
321 File(BufReader<File>),
322 Stdin(Stdin),
323}
324
325impl InputType {
326 fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> {
327 match self {
328 Self::File(reader) => reader.read_line(buf),
329 Self::Stdin(stdin) => stdin.read_line(buf),
330 }
331 }
332
333 fn len(&self) -> std::io::Result<Option<u64>> {
334 let len = match self {
335 Self::File(reader) => Some(reader.get_ref().metadata()?.len()),
336 Self::Stdin(_) => None,
337 };
338 Ok(len)
339 }
340}
341
342enum OutputType {
343 Tty(Stdout),
344 Pipe(Box<dyn Write>),
345 #[cfg(test)]
346 Test(Vec<u8>),
347}
348
349impl IsTty for OutputType {
350 fn is_tty(&self) -> bool {
351 matches!(self, Self::Tty(_))
352 }
353}
354
355impl Write for OutputType {
356 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
357 match self {
358 Self::Tty(stdout) => stdout.write(buf),
359 Self::Pipe(writer) => writer.write(buf),
360 #[cfg(test)]
361 Self::Test(vec) => vec.write(buf),
362 }
363 }
364
365 fn flush(&mut self) -> std::io::Result<()> {
366 match self {
367 Self::Tty(stdout) => stdout.flush(),
368 Self::Pipe(writer) => writer.flush(),
369 #[cfg(test)]
370 Self::Test(vec) => vec.flush(),
371 }
372 }
373}
374
375fn setup_term() -> UResult<OutputType> {
376 let mut stdout = stdout();
377 if stdout.is_tty() {
378 terminal::enable_raw_mode()?;
379 stdout.execute(EnterAlternateScreen)?.execute(Hide)?;
380 Ok(OutputType::Tty(stdout))
381 } else {
382 Ok(OutputType::Pipe(Box::new(stdout)))
383 }
384}
385
386#[cfg(target_os = "fuchsia")]
387#[inline(always)]
388fn setup_term() -> UResult<OutputType> {
389 Ok(OutputType::Pipe(Box::new(stdout())))
391}
392
393fn reset_term() -> UResult<()> {
394 let mut stdout = stdout();
395 if stdout.is_tty() {
396 stdout.queue(Show)?.queue(LeaveAlternateScreen)?;
397 terminal::disable_raw_mode()?;
398 } else {
399 stdout.queue(Clear(ClearType::CurrentLine))?;
400 write!(stdout, "\r")?;
401 }
402 stdout.flush()?;
403 Ok(())
404}
405
406#[cfg(target_os = "fuchsia")]
407#[inline(always)]
408fn reset_term() -> UResult<()> {
409 Ok(())
410}
411
412struct TerminalGuard;
413
414impl Drop for TerminalGuard {
415 fn drop(&mut self) {
416 let _ = reset_term();
418 }
419}
420
421fn more(
422 input: InputType,
423 multiple_file: bool,
424 file_name: Option<&str>,
425 next_file: Option<&str>,
426 options: &mut Options,
427) -> UResult<()> {
428 let out = setup_term()?;
430 let _guard = TerminalGuard;
432 let (_cols, mut rows) = terminal::size()?;
434 if let Some(number) = options.lines {
435 rows = number;
436 }
437 let mut pager = Pager::new(input, rows, file_name, next_file, options, out)?;
438 pager.handle_from_line()?;
440 pager.handle_pattern_search()?;
442 if multiple_file {
444 pager.display_multi_file_header()?;
445 }
446 pager.draw(None)?;
448 if multiple_file {
450 pager.reset_multi_file_header();
451 options.from_line = 0;
452 }
453 pager.process_events(options)
455}
456
457struct Pager<'a> {
458 input: InputType,
460 file_size: Option<u64>,
462 lines: Vec<String>,
464 cumulative_line_sizes: Vec<u64>,
466 upper_mark: usize,
468 content_rows: usize,
470 lines_squeezed: usize,
472 pattern: Option<String>,
473 file_name: Option<&'a str>,
474 next_file: Option<&'a str>,
475 eof_reached: bool,
476 silent: bool,
477 squeeze: bool,
478 stdout: OutputType,
479}
480
481impl<'a> Pager<'a> {
482 fn new(
483 input: InputType,
484 rows: u16,
485 file_name: Option<&'a str>,
486 next_file: Option<&'a str>,
487 options: &Options,
488 stdout: OutputType,
489 ) -> UResult<Self> {
490 let content_rows = rows.saturating_sub(1).max(1) as usize;
492 let file_size = input.len()?;
493 let pager = Self {
494 input,
495 file_size,
496 lines: Vec::with_capacity(content_rows),
497 cumulative_line_sizes: Vec::new(),
498 upper_mark: options.from_line,
499 content_rows,
500 lines_squeezed: 0,
501 pattern: options.pattern.clone(),
502 file_name,
503 next_file,
504 eof_reached: false,
505 silent: options.silent,
506 squeeze: options.squeeze,
507 stdout,
508 };
509 Ok(pager)
510 }
511
512 fn handle_from_line(&mut self) -> UResult<()> {
513 if !self.read_until_line(self.upper_mark)? {
514 write!(
515 self.stdout,
516 "\r{}{} ({}){}",
517 Attribute::Reverse,
518 translate!(
519 "more-error-cannot-seek-to-line",
520 "line" => (self.upper_mark + 1)
521 ),
522 translate!("more-press-return"),
523 Attribute::Reset,
524 )?;
525 self.stdout.flush()?;
526 self.wait_for_enter_key()?;
527 self.upper_mark = 0;
528 }
529 Ok(())
530 }
531
532 fn read_until_line(&mut self, target_line: usize) -> UResult<bool> {
533 let mut line = String::new();
535 while self.lines.len() <= target_line {
536 let bytes_read = self.input.read_line(&mut line)?;
537 if bytes_read == 0 {
538 return Ok(false); }
540 let last_pos = self.cumulative_line_sizes.last().copied().unwrap_or(0);
542 self.cumulative_line_sizes
543 .push(last_pos + bytes_read as u64);
544 line = line.trim_end().to_string();
546 self.lines.push(std::mem::take(&mut line));
548 }
549 Ok(true)
550 }
551
552 fn wait_for_enter_key(&self) -> UResult<()> {
553 if !self.stdout.is_tty() {
554 return Ok(());
555 }
556 loop {
557 if event::poll(Duration::from_millis(100))? {
558 if let Event::Key(KeyEvent {
559 code: KeyCode::Enter,
560 modifiers: KeyModifiers::NONE,
561 kind: KeyEventKind::Press,
562 ..
563 }) = event::read()?
564 {
565 return Ok(());
566 }
567 }
568 }
569 }
570
571 fn handle_pattern_search(&mut self) -> UResult<()> {
572 if self.pattern.is_none() {
573 return Ok(());
574 }
575 match self.search_pattern_in_file() {
576 Some(line) => self.upper_mark = line,
577 None => {
578 self.pattern = None;
579 write!(
580 self.stdout,
581 "\r{}{} ({}){}",
582 Attribute::Reverse,
583 translate!("more-error-pattern-not-found"),
584 translate!("more-press-return"),
585 Attribute::Reset,
586 )?;
587 self.stdout.flush()?;
588 self.wait_for_enter_key()?;
589 }
590 }
591 Ok(())
592 }
593
594 fn search_pattern_in_file(&mut self) -> Option<usize> {
595 let pattern = self.pattern.clone().expect("pattern should be set");
596 let mut line_num = self.upper_mark;
597 loop {
598 match self.get_line(line_num) {
599 Some(line) if line.contains(&pattern) => return Some(line_num),
600 Some(_) => line_num += 1,
601 None => return None,
602 }
603 }
604 }
605
606 fn get_line(&mut self, index: usize) -> Option<&String> {
607 match self.read_until_line(index) {
608 Ok(true) => self.lines.get(index),
609 _ => None,
610 }
611 }
612
613 fn display_multi_file_header(&mut self) -> UResult<()> {
614 self.stdout.queue(Clear(ClearType::CurrentLine))?;
615 self.stdout.write_all(
616 MULTI_FILE_TOP_PROMPT
617 .replace("{}", self.file_name.unwrap_or_default())
618 .as_bytes(),
619 )?;
620 self.content_rows = self
621 .content_rows
622 .saturating_sub(MULTI_FILE_TOP_PROMPT.lines().count());
623 Ok(())
624 }
625
626 fn reset_multi_file_header(&mut self) {
627 self.content_rows = self
628 .content_rows
629 .saturating_add(MULTI_FILE_TOP_PROMPT.lines().count());
630 }
631
632 fn update_display(&mut self, options: &Options) -> UResult<()> {
633 if options.print_over {
634 self.stdout
635 .execute(MoveTo(0, 0))?
636 .execute(Clear(ClearType::FromCursorDown))?;
637 } else if options.clean_print {
638 self.stdout
639 .execute(Clear(ClearType::All))?
640 .execute(MoveTo(0, 0))?;
641 }
642 Ok(())
643 }
644
645 fn process_events(&mut self, options: &Options) -> UResult<()> {
647 loop {
648 if !event::poll(Duration::from_millis(100))? {
649 continue;
650 }
651 let mut wrong_key = None;
652 match event::read()? {
653 Event::Key(
655 KeyEvent {
656 code: KeyCode::Char('q'),
657 modifiers: KeyModifiers::NONE,
658 kind: KeyEventKind::Press,
659 ..
660 }
661 | KeyEvent {
662 code: KeyCode::Char('c'),
663 modifiers: KeyModifiers::CONTROL,
664 kind: KeyEventKind::Press,
665 ..
666 },
667 ) => {
668 reset_term()?;
669 std::process::exit(0);
670 }
671
672 Event::Key(KeyEvent {
674 code: KeyCode::Down | KeyCode::PageDown | KeyCode::Char(' '),
675 modifiers: KeyModifiers::NONE,
676 ..
677 }) => {
678 if self.eof_reached {
679 return Ok(());
680 }
681 self.page_down();
682 }
683 Event::Key(KeyEvent {
684 code: KeyCode::Enter | KeyCode::Char('j'),
685 modifiers: KeyModifiers::NONE,
686 ..
687 }) => {
688 if self.eof_reached {
689 return Ok(());
690 }
691 self.next_line();
692 }
693
694 Event::Key(KeyEvent {
696 code: KeyCode::Up | KeyCode::PageUp,
697 modifiers: KeyModifiers::NONE,
698 ..
699 }) => {
700 self.page_up();
701 }
702 Event::Key(KeyEvent {
703 code: KeyCode::Char('k'),
704 modifiers: KeyModifiers::NONE,
705 ..
706 }) => {
707 self.prev_line();
708 }
709
710 Event::Resize(col, row) => {
712 self.page_resize(col, row, options.lines);
713 }
714
715 Event::Key(KeyEvent {
717 kind: KeyEventKind::Release,
718 ..
719 }) => continue,
720
721 Event::Key(KeyEvent {
723 code: KeyCode::Char(k),
724 ..
725 }) => wrong_key = Some(k),
726
727 _ => continue,
729 }
730 self.update_display(options)?;
731 self.draw(wrong_key)?;
732 }
733 }
734
735 fn page_down(&mut self) {
736 self.upper_mark = self.upper_mark.saturating_add(self.content_rows);
738 }
739
740 fn next_line(&mut self) {
741 self.upper_mark = self.upper_mark.saturating_add(1);
743 }
744
745 fn page_up(&mut self) {
746 self.eof_reached = false;
747 self.upper_mark = self
749 .upper_mark
750 .saturating_sub(self.content_rows.saturating_add(self.lines_squeezed));
751 if self.squeeze {
752 while self.upper_mark > 0 {
754 let line = self.lines.get(self.upper_mark).expect("line should exist");
755 if !line.trim().is_empty() {
756 break;
757 }
758 self.upper_mark = self.upper_mark.saturating_sub(1);
759 }
760 }
761 }
762
763 fn prev_line(&mut self) {
764 self.eof_reached = false;
765 self.upper_mark = self.upper_mark.saturating_sub(1);
767 }
768
769 fn page_resize(&mut self, _col: u16, row: u16, option_line: Option<u16>) {
771 if option_line.is_none() {
772 self.content_rows = row.saturating_sub(1) as usize;
773 }
774 }
775
776 fn draw(&mut self, wrong_key: Option<char>) -> UResult<()> {
777 self.draw_lines()?;
778 self.draw_status_bar(wrong_key);
779 self.stdout.flush()?;
780 Ok(())
781 }
782
783 fn draw_lines(&mut self) -> UResult<()> {
784 self.stdout.queue(Clear(ClearType::CurrentLine))?;
786 self.lines_squeezed = 0;
788 let mut lines_printed = 0;
790 let mut index = self.upper_mark;
791 while lines_printed < self.content_rows {
792 if !self.read_until_line(index)? {
794 self.eof_reached = true;
795 self.upper_mark = index.saturating_sub(self.content_rows);
796 break;
797 }
798 if self.should_squeeze_line(index) {
800 self.lines_squeezed += 1;
801 index += 1;
802 continue;
803 }
804 let mut line = self.lines[index].clone();
806 if let Some(pattern) = &self.pattern {
807 line = line.replace(
809 pattern,
810 &format!("{}{pattern}{}", Attribute::Reverse, Attribute::Reset),
811 );
812 }
813 self.stdout.write_all(format!("\r{line}\n").as_bytes())?;
814 lines_printed += 1;
815 index += 1;
816 }
817 while lines_printed < self.content_rows {
819 self.stdout.write_all(b"\r~\n")?;
820 lines_printed += 1;
821 }
822 Ok(())
823 }
824
825 fn should_squeeze_line(&self, index: usize) -> bool {
826 if !self.squeeze || index == 0 {
828 return false;
829 }
830 match (self.lines.get(index), self.lines.get(index - 1)) {
832 (Some(current), Some(previous)) => current.is_empty() && previous.is_empty(),
833 _ => false,
834 }
835 }
836
837 fn draw_status_bar(&mut self, wrong_key: Option<char>) {
838 let lower_mark =
840 (self.upper_mark + self.content_rows).min(self.lines.len().saturating_sub(1));
841 let progress_info = if self.eof_reached && self.next_file.is_some() {
845 format!(" (Next file: {})", self.next_file.unwrap())
846 } else if let Some(file_size) = self.file_size {
847 let position = self
849 .cumulative_line_sizes
850 .get(lower_mark)
851 .copied()
852 .unwrap_or_default();
853 if file_size == 0 {
854 " (END)".to_string()
855 } else {
856 let percentage = (position as f64 / file_size as f64 * 100.0).round() as u16;
857 if percentage >= 100 {
858 " (END)".to_string()
859 } else {
860 format!(" ({percentage}%)")
861 }
862 }
863 } else {
864 String::new()
866 };
867 let file_name = self.file_name.unwrap_or(":");
869 let status = format!("{file_name}{progress_info}");
870 let banner = match (self.silent, wrong_key) {
874 (true, Some(key)) => format!(
875 "{status}[{}]",
876 translate!(
877 "more-error-unknown-key",
878 "key" => key,
879 )
880 ),
881 (true, None) => format!("{status}{}", translate!("more-help-message")),
882 (false, Some(_)) => format!("{status}{BELL}"),
883 (false, None) => status,
884 };
885 write!(
887 self.stdout,
888 "\r{}{banner}{}",
889 Attribute::Reverse,
890 Attribute::Reset
891 )
892 .unwrap();
893 }
894}
895
896#[cfg(test)]
897mod tests {
898 use std::{
899 io::Seek,
900 ops::{Deref, DerefMut},
901 };
902
903 use super::*;
904 use tempfile::tempfile;
905
906 impl Deref for OutputType {
907 type Target = Vec<u8>;
908 fn deref(&self) -> &Vec<u8> {
909 match self {
910 Self::Test(buf) => buf,
911 _ => unreachable!(),
912 }
913 }
914 }
915
916 impl DerefMut for OutputType {
917 fn deref_mut(&mut self) -> &mut Vec<u8> {
918 match self {
919 Self::Test(buf) => buf,
920 _ => unreachable!(),
921 }
922 }
923 }
924
925 struct TestPagerBuilder {
926 content: String,
927 options: Options,
928 rows: u16,
929 next_file: Option<&'static str>,
930 }
931
932 impl Default for TestPagerBuilder {
933 fn default() -> Self {
934 Self {
935 content: String::new(),
936 options: Options {
937 silent: false,
938 _logical: false,
939 _exit_on_eof: false,
940 _no_pause: false,
941 print_over: false,
942 clean_print: false,
943 squeeze: false,
944 lines: None,
945 from_line: 0,
946 pattern: None,
947 },
948 rows: 10,
949 next_file: None,
950 }
951 }
952 }
953
954 #[allow(dead_code)]
955 impl TestPagerBuilder {
956 fn new(content: &str) -> Self {
957 Self {
958 content: content.to_string(),
959 ..Default::default()
960 }
961 }
962
963 fn build(mut self) -> Pager<'static> {
964 let mut tmpfile = tempfile().unwrap();
965 tmpfile.write_all(self.content.as_bytes()).unwrap();
966 tmpfile.rewind().unwrap();
967 let out = OutputType::Test(Vec::new());
968 if let Some(rows) = self.options.lines {
969 self.rows = rows;
970 }
971 Pager::new(
972 InputType::File(BufReader::new(tmpfile)),
973 self.rows,
974 None,
975 self.next_file,
976 &self.options,
977 out,
978 )
979 .unwrap()
980 }
981
982 fn silent(mut self) -> Self {
983 self.options.silent = true;
984 self
985 }
986
987 fn print_over(mut self) -> Self {
988 self.options.print_over = true;
989 self
990 }
991
992 fn clean_print(mut self) -> Self {
993 self.options.clean_print = true;
994 self
995 }
996
997 fn squeeze(mut self) -> Self {
998 self.options.squeeze = true;
999 self
1000 }
1001
1002 fn lines(mut self, lines: u16) -> Self {
1003 self.options.lines = Some(lines);
1004 self
1005 }
1006
1007 #[allow(clippy::wrong_self_convention)]
1008 fn from_line(mut self, from_line: usize) -> Self {
1009 self.options.from_line = from_line;
1010 self
1011 }
1012
1013 fn pattern(mut self, pattern: &str) -> Self {
1014 self.options.pattern = Some(pattern.to_owned());
1015 self
1016 }
1017
1018 fn rows(mut self, rows: u16) -> Self {
1019 self.rows = rows;
1020 self
1021 }
1022
1023 fn next_file(mut self, next_file: &'static str) -> Self {
1024 self.next_file = Some(next_file);
1025 self
1026 }
1027 }
1028
1029 #[test]
1030 fn test_get_line_and_len() {
1031 let content = "a\n\tb\nc\n";
1032 let mut pager = TestPagerBuilder::new(content).build();
1033 assert_eq!(pager.get_line(1).unwrap(), "\tb");
1034 assert_eq!(pager.cumulative_line_sizes.len(), 2);
1035 assert_eq!(pager.cumulative_line_sizes[1], 5);
1036 }
1037
1038 #[test]
1039 fn test_navigate_page() {
1040 let content = (0..10).map(|i| i.to_string() + "\n").collect::<String>();
1042
1043 let mut pager = TestPagerBuilder::new(&content).build();
1045 assert_eq!(pager.upper_mark, 0);
1046
1047 pager.page_down();
1048 assert_eq!(pager.upper_mark, pager.content_rows);
1049 pager.draw(None).unwrap();
1050 let mut stdout = String::from_utf8_lossy(&pager.stdout);
1051 assert!(stdout.contains("9\n"));
1052 assert!(!stdout.contains("8\n"));
1053 assert_eq!(pager.upper_mark, 1); pager.page_up();
1056 assert_eq!(pager.upper_mark, 0);
1057
1058 pager.next_line();
1059 assert_eq!(pager.upper_mark, 1);
1060
1061 pager.prev_line();
1062 assert_eq!(pager.upper_mark, 0);
1063 pager.stdout.clear();
1064 pager.draw(None).unwrap();
1065 stdout = String::from_utf8_lossy(&pager.stdout);
1066 assert!(stdout.contains("0\n"));
1067 assert!(!stdout.contains("9\n")); }
1069
1070 #[test]
1071 fn test_silent_mode() {
1072 let content = (0..5).map(|i| i.to_string() + "\n").collect::<String>();
1073 let mut pager = TestPagerBuilder::new(&content)
1074 .from_line(3)
1075 .silent()
1076 .build();
1077 pager.draw_status_bar(None);
1078 let stdout = String::from_utf8_lossy(&pager.stdout);
1079 assert!(stdout.contains(&translate!("more-help-message")));
1080 }
1081
1082 #[test]
1083 fn test_squeeze() {
1084 let content = "Line 0\n\n\n\nLine 4\n\n\nLine 7\n";
1085 let mut pager = TestPagerBuilder::new(content).lines(6).squeeze().build();
1086 assert_eq!(pager.content_rows, 5); assert!(pager.read_until_line(7).unwrap());
1090 assert!(pager.should_squeeze_line(2));
1092 assert!(pager.should_squeeze_line(3));
1093 assert!(pager.should_squeeze_line(6));
1094 assert!(!pager.should_squeeze_line(0));
1096 assert!(!pager.should_squeeze_line(1));
1097 assert!(!pager.should_squeeze_line(4));
1098 assert!(!pager.should_squeeze_line(5));
1099 assert!(!pager.should_squeeze_line(7));
1100
1101 pager.draw(None).unwrap();
1102 let stdout = String::from_utf8_lossy(&pager.stdout);
1103 assert!(stdout.contains("Line 0"));
1104 assert!(stdout.contains("Line 4"));
1105 assert!(stdout.contains("Line 7"));
1106 }
1107
1108 #[test]
1109 fn test_lines_option() {
1110 let content = (0..5).map(|i| i.to_string() + "\n").collect::<String>();
1111
1112 let mut pager = TestPagerBuilder::new(&content).lines(0).build();
1114 pager.draw(None).unwrap();
1115 let mut stdout = String::from_utf8_lossy(&pager.stdout);
1116 assert!(!stdout.is_empty());
1117
1118 let mut pager = TestPagerBuilder::new(&content).lines(3).build();
1120 assert_eq!(pager.content_rows, 3 - 1); pager.draw(None).unwrap();
1122 stdout = String::from_utf8_lossy(&pager.stdout);
1123 assert!(stdout.contains("0\n"));
1124 assert!(stdout.contains("1\n"));
1125 assert!(!stdout.contains("2\n"));
1126 }
1127
1128 #[test]
1129 fn test_from_line_option() {
1130 let content = (0..5).map(|i| i.to_string() + "\n").collect::<String>();
1131
1132 let mut pager = TestPagerBuilder::new(&content).from_line(0).build();
1134 assert!(pager.handle_from_line().is_ok());
1135 pager.draw(None).unwrap();
1136 let stdout = String::from_utf8_lossy(&pager.stdout);
1137 assert!(stdout.contains("0\n"));
1138
1139 pager = TestPagerBuilder::new(&content).from_line(1).build();
1141 assert!(pager.handle_from_line().is_ok());
1142 pager.draw(None).unwrap();
1143 let stdout = String::from_utf8_lossy(&pager.stdout);
1144 assert!(stdout.contains("1\n"));
1145 assert!(!stdout.contains("0\n"));
1146
1147 pager = TestPagerBuilder::new(&content).from_line(99).build();
1149 assert!(pager.handle_from_line().is_ok());
1150 assert_eq!(pager.upper_mark, 0);
1151 let stdout = String::from_utf8_lossy(&pager.stdout);
1152 assert!(stdout.contains(&translate!(
1153 "more-error-cannot-seek-to-line",
1154 "line" => "100"
1155 )));
1156 }
1157
1158 #[test]
1159 fn test_search_pattern_found() {
1160 let content = "foo\nbar\nbaz\n";
1161 let mut pager = TestPagerBuilder::new(content).pattern("bar").build();
1162 assert!(pager.handle_pattern_search().is_ok());
1163 assert_eq!(pager.upper_mark, 1);
1164 pager.draw(None).unwrap();
1165 let stdout = String::from_utf8_lossy(&pager.stdout);
1166 assert!(stdout.contains("bar"));
1167 assert!(!stdout.contains("foo"));
1168 }
1169
1170 #[test]
1171 fn test_search_pattern_not_found() {
1172 let content = "foo\nbar\nbaz\n";
1173 let mut pager = TestPagerBuilder::new(content).pattern("qux").build();
1174 assert!(pager.handle_pattern_search().is_ok());
1175 let stdout = String::from_utf8_lossy(&pager.stdout);
1176 assert!(stdout.contains(&translate!("more-error-pattern-not-found")));
1177 assert_eq!(pager.pattern, None);
1178 assert_eq!(pager.upper_mark, 0);
1179 }
1180
1181 #[test]
1182 fn test_wrong_key() {
1183 let mut pager = TestPagerBuilder::default().silent().build();
1184 pager.draw_status_bar(Some('x'));
1185 let stdout = String::from_utf8_lossy(&pager.stdout);
1186 assert!(stdout.contains(&translate!(
1187 "more-error-unknown-key",
1188 "key" => "x"
1189 )));
1190
1191 pager = TestPagerBuilder::default().build();
1192 pager.draw_status_bar(Some('x'));
1193 let stdout = String::from_utf8_lossy(&pager.stdout);
1194 assert!(stdout.contains(&BELL.to_string()));
1195 }
1196}