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