1use std::io::{self, Write};
35use std::process::{Child, Command, ExitStatus, Stdio};
36use unicode_width::UnicodeWidthChar;
37
38const BUFFER_CAP_BYTES: usize = 1024 * 1024;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum PagerMode {
44 #[default]
46 Auto,
47 Always,
49 Never,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum PagerExitStatus {
56 Success,
58 ExitCode(i32),
60 #[cfg_attr(not(unix), allow(dead_code))]
62 Signal(i32),
63}
64
65impl PagerExitStatus {
66 #[must_use]
72 #[allow(dead_code)]
73 pub fn exit_code(self) -> Option<i32> {
74 match self {
75 Self::Success => None,
76 Self::ExitCode(code) => Some(code),
77 Self::Signal(sig) => Some(128 + sig),
78 }
79 }
80
81 #[must_use]
83 #[allow(dead_code)]
84 pub fn is_success(self) -> bool {
85 matches!(self, Self::Success)
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct PagerConfig {
92 pub command: String,
94 pub enabled: PagerMode,
96 pub threshold: Option<usize>,
98}
99
100impl Default for PagerConfig {
101 fn default() -> Self {
102 Self {
103 command: Self::default_pager_command(),
104 enabled: PagerMode::Auto,
105 threshold: None,
106 }
107 }
108}
109
110impl PagerConfig {
111 #[must_use]
123 pub fn default_pager_command() -> String {
124 std::env::var("SQRY_PAGER")
125 .or_else(|_| std::env::var("PAGER"))
126 .unwrap_or_else(|_| "less -FRX".to_string())
127 }
128
129 #[must_use]
137 pub fn from_cli_flags(pager_flag: bool, no_pager_flag: bool, pager_cmd: Option<&str>) -> Self {
138 let mode = if no_pager_flag {
139 PagerMode::Never
140 } else if pager_flag {
141 PagerMode::Always
142 } else {
143 PagerMode::Auto
144 };
145
146 let command = pager_cmd.map_or_else(Self::default_pager_command, String::from);
147
148 Self {
149 command,
150 enabled: mode,
151 threshold: None,
152 }
153 }
154}
155
156pub struct PagerDecision {
158 config: PagerConfig,
159 is_tty: bool,
160 terminal_height: Option<usize>,
161}
162
163impl PagerDecision {
164 #[must_use]
166 pub fn new(config: PagerConfig) -> Self {
167 use is_terminal::IsTerminal;
168
169 let is_tty = std::io::stdout().is_terminal();
170 let terminal_height = Self::detect_terminal_height();
171
172 Self {
173 config,
174 is_tty,
175 terminal_height,
176 }
177 }
178
179 #[must_use]
181 pub fn is_tty(&self) -> bool {
182 self.is_tty
183 }
184
185 #[must_use]
189 pub fn should_page_rows(&self, displayed_rows: usize) -> bool {
190 match self.config.enabled {
191 PagerMode::Never => false,
192 PagerMode::Always => true,
193 PagerMode::Auto => {
194 if !self.is_tty {
195 return false; }
197
198 let threshold = self.config.threshold.or(self.terminal_height).unwrap_or(24);
199
200 displayed_rows > threshold
201 }
202 }
203 }
204
205 #[must_use]
207 fn detect_terminal_height() -> Option<usize> {
208 use terminal_size::{Height, terminal_size};
209 terminal_size().map(|(_, Height(h))| h as usize)
210 }
211
212 #[must_use]
214 pub fn detect_terminal_width() -> Option<usize> {
215 use terminal_size::{Width, terminal_size};
216 terminal_size().map(|(Width(w), _)| w as usize)
217 }
218}
219
220#[cfg(test)]
222impl PagerDecision {
223 #[must_use]
226 pub fn for_testing(config: PagerConfig, is_tty: bool, terminal_height: Option<usize>) -> Self {
227 Self {
228 config,
229 is_tty,
230 terminal_height,
231 }
232 }
233}
234
235pub struct PagerWriter {
240 child: Child,
241 stdin: std::process::ChildStdin,
242}
243
244impl PagerWriter {
245 pub fn spawn(command: &str) -> io::Result<Self> {
257 let parts = shlex::split(command).ok_or_else(|| {
262 io::Error::new(
263 io::ErrorKind::InvalidInput,
264 format!("Invalid pager command syntax: {command}"),
265 )
266 })?;
267
268 if parts.is_empty() {
269 return Err(io::Error::new(
270 io::ErrorKind::InvalidInput,
271 "Empty pager command",
272 ));
273 }
274
275 let (program, args) = parts.split_first().expect("Already checked non-empty");
276
277 let mut child = Command::new(program)
278 .args(args)
279 .stdin(Stdio::piped())
280 .spawn()?;
281
282 let stdin = child
283 .stdin
284 .take()
285 .ok_or_else(|| io::Error::other("Failed to open pager stdin"))?;
286
287 Ok(Self { child, stdin })
288 }
289
290 pub fn write(&mut self, content: &str) -> io::Result<()> {
296 self.stdin.write_all(content.as_bytes())
297 }
298
299 pub fn wait(mut self) -> io::Result<ExitStatus> {
305 drop(self.stdin); self.child.wait()
307 }
308}
309
310impl Write for PagerWriter {
311 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
312 self.stdin.write(buf)
313 }
314
315 fn flush(&mut self) -> io::Result<()> {
316 self.stdin.flush()
317 }
318}
319
320enum OutputMode {
322 Buffering,
324 Pager(PagerWriter),
326 Direct,
328}
329
330pub struct BufferedOutput {
344 buffer: String,
345 config: PagerConfig,
346 decision: PagerDecision,
347 mode: OutputMode,
348 #[allow(dead_code)]
350 terminal_width: Option<usize>,
351 complete_lines: usize,
353 partial_line_len: usize,
355 spawn_error: Option<io::Error>,
357}
358
359impl BufferedOutput {
360 #[must_use]
362 pub fn new(config: PagerConfig) -> Self {
363 let decision = PagerDecision::new(config.clone());
364 let terminal_width = PagerDecision::detect_terminal_width();
365
366 let (mode, spawn_error) = match config.enabled {
369 PagerMode::Never => (OutputMode::Direct, None),
370 PagerMode::Always => {
371 match PagerWriter::spawn(&config.command) {
373 Ok(pager) => (OutputMode::Pager(pager), None),
374 Err(e) => {
375 let pager_name = config
378 .command
379 .split_whitespace()
380 .next()
381 .unwrap_or(&config.command);
382 if e.kind() == io::ErrorKind::NotFound {
383 eprintln!(
384 "Warning: pager '{pager_name}' not found. Output will not be paged. \
385 To enable paging, install '{pager_name}' or set the SQRY_PAGER environment variable."
386 );
387 (OutputMode::Direct, None)
388 } else {
389 eprintln!(
390 "Error: Failed to start pager '{pager_name}': {e}. \
391 Please check that the binary is correct and executable, \
392 or set a different pager using the SQRY_PAGER environment variable."
393 );
394 (OutputMode::Direct, Some(e))
396 }
397 }
398 }
399 }
400 PagerMode::Auto => {
401 if decision.is_tty() {
404 (OutputMode::Buffering, None)
405 } else {
406 (OutputMode::Direct, None)
407 }
408 }
409 };
410
411 Self {
412 buffer: String::new(),
413 config,
414 decision,
415 mode,
416 terminal_width,
417 complete_lines: 0,
418 partial_line_len: 0,
419 spawn_error,
420 }
421 }
422
423 #[cfg(test)]
428 pub fn new_for_testing(config: PagerConfig) -> Self {
429 let decision = PagerDecision::new(config.clone());
430 let terminal_width = PagerDecision::detect_terminal_width();
431
432 Self {
433 buffer: String::new(),
434 config,
435 decision,
436 mode: OutputMode::Buffering, terminal_width,
438 complete_lines: 0,
439 partial_line_len: 0,
440 spawn_error: None,
441 }
442 }
443
444 fn write_direct(content: &str) -> io::Result<()> {
445 std::io::stdout().write_all(content.as_bytes())
446 }
447
448 fn write_pager(pager: &mut PagerWriter, content: &str) -> io::Result<()> {
449 match pager.write(content) {
450 Ok(()) => Ok(()),
451 Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()),
452 Err(e) => Err(e),
453 }
454 }
455
456 fn update_line_counts(&mut self, content: &str) {
457 let newline_count = content.bytes().filter(|&b| b == b'\n').count();
458 self.complete_lines += newline_count;
459 self.update_partial_line_len(content);
460 }
461
462 fn update_partial_line_len(&mut self, content: &str) {
463 if let Some(last_nl_offset) = content.rfind('\n') {
464 self.partial_line_len = content.len().saturating_sub(last_nl_offset + 1);
465 } else {
466 self.partial_line_len += content.len();
467 }
468 }
469
470 fn displayed_row_estimate(&self) -> usize {
471 self.complete_lines + usize::from(self.partial_line_len > 0)
472 }
473
474 fn should_transition_to_pager(&self, displayed_rows: usize) -> bool {
475 self.decision.should_page_rows(displayed_rows) || self.buffer.len() > BUFFER_CAP_BYTES
476 }
477
478 fn transition_to_pager(&mut self) -> io::Result<()> {
479 match PagerWriter::spawn(&self.config.command) {
480 Ok(mut pager) => {
481 pager.write(&self.buffer)?;
482 self.buffer.clear();
483 self.mode = OutputMode::Pager(pager);
484 Ok(())
485 }
486 Err(e) => self.handle_pager_spawn_error(e),
487 }
488 }
489
490 fn handle_pager_spawn_error(&mut self, err: io::Error) -> io::Result<()> {
491 let pager_name = self
492 .config
493 .command
494 .split_whitespace()
495 .next()
496 .unwrap_or(&self.config.command);
497 if err.kind() == io::ErrorKind::NotFound {
498 eprintln!(
499 "Warning: pager '{pager_name}' not found. Output will not be paged. \
500 To enable paging, install '{pager_name}' or set the SQRY_PAGER environment variable."
501 );
502 } else {
503 eprintln!(
504 "Error: Failed to start pager '{pager_name}': {err}. \
505 Please check that the binary is correct and executable, \
506 or set a different pager using the SQRY_PAGER environment variable."
507 );
508 self.spawn_error = Some(err);
509 }
510
511 Self::write_direct(&self.buffer)?;
512 self.buffer.clear();
513 self.mode = OutputMode::Direct;
514 Ok(())
515 }
516
517 pub fn write(&mut self, content: &str) -> io::Result<()> {
523 match &mut self.mode {
524 OutputMode::Direct => {
525 Self::write_direct(content)
527 }
528 OutputMode::Pager(pager) => {
529 Self::write_pager(pager, content)
531 }
532 OutputMode::Buffering => {
533 self.buffer.push_str(content);
535
536 self.update_line_counts(content);
539
540 let displayed_rows = self.displayed_row_estimate();
546
547 if self.should_transition_to_pager(displayed_rows) {
549 self.transition_to_pager()?;
553 }
554 Ok(())
556 }
557 }
558 }
559
560 pub fn finish(self) -> io::Result<PagerExitStatus> {
572 if let Some(spawn_err) = self.spawn_error {
575 return Err(spawn_err);
576 }
577
578 match self.mode {
579 OutputMode::Direct => Ok(PagerExitStatus::Success),
580 OutputMode::Pager(pager) => {
581 let status = pager.wait()?;
582 Ok(exit_status_to_pager_status(status))
583 }
584 OutputMode::Buffering => {
585 std::io::stdout().write_all(self.buffer.as_bytes())?;
587 Ok(PagerExitStatus::Success)
588 }
589 }
590 }
591}
592
593#[must_use]
595fn is_broken_pipe_exit(status: ExitStatus) -> bool {
596 #[cfg(unix)]
597 {
598 use std::os::unix::process::ExitStatusExt;
599 status.signal() == Some(13)
601 }
602 #[cfg(not(unix))]
603 {
604 matches!(status.code(), Some(0) | Some(1))
606 }
607}
608
609fn exit_status_to_pager_status(status: ExitStatus) -> PagerExitStatus {
615 if status.success() || is_broken_pipe_exit(status) {
617 return PagerExitStatus::Success;
618 }
619
620 #[cfg(unix)]
621 {
622 use std::os::unix::process::ExitStatusExt;
623 if let Some(signal) = status.signal() {
625 return PagerExitStatus::Signal(signal);
626 }
627 }
628
629 if let Some(code) = status.code() {
631 PagerExitStatus::ExitCode(code)
632 } else {
633 PagerExitStatus::ExitCode(1)
636 }
637}
638
639#[allow(dead_code)]
641const TAB_WIDTH: usize = 8;
642
643fn skip_csi_sequence(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
644 while let Some(&next) = chars.peek() {
645 chars.next();
646 if (0x40..=0x7E).contains(&(next as u8)) {
647 break;
648 }
649 }
650}
651
652fn skip_osc_sequence(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
653 while let Some(&next) = chars.peek() {
654 if next == '\x07' {
655 chars.next();
656 break;
657 }
658 if next == '\x1b' {
659 chars.next();
660 if chars.peek() == Some(&'\\') {
661 chars.next();
662 }
663 break;
664 }
665 chars.next();
666 }
667}
668
669#[allow(dead_code)]
674fn strip_ansi(s: &str) -> String {
675 let mut result = String::with_capacity(s.len());
676 let mut chars = s.chars().peekable();
677
678 while let Some(c) = chars.next() {
679 if c == '\x1b' {
680 match chars.peek().copied() {
682 Some('[') => {
683 chars.next();
684 skip_csi_sequence(&mut chars);
685 }
686 Some(']') => {
687 chars.next();
688 skip_osc_sequence(&mut chars);
689 }
690 _ => {}
691 }
692 } else {
693 result.push(c);
694 }
695 }
696 result
697}
698
699#[allow(dead_code)]
703fn displayed_line_width(line: &str) -> usize {
704 let mut width = 0;
705 for c in line.chars() {
706 if c == '\t' {
707 width = (width / TAB_WIDTH + 1) * TAB_WIDTH;
709 } else {
710 width += UnicodeWidthChar::width(c).unwrap_or(0);
711 }
712 }
713 width
714}
715
716#[allow(dead_code)]
722#[must_use]
723pub fn count_displayed_rows(content: &str, terminal_width: Option<usize>) -> usize {
724 let width = terminal_width.unwrap_or(80);
725
726 content
727 .lines()
728 .map(|line| {
729 let clean_line = strip_ansi(line);
731 let line_width = displayed_line_width(&clean_line);
732 if line_width == 0 {
733 1 } else {
735 line_width.div_ceil(width)
737 }
738 })
739 .sum()
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745 use serial_test::serial;
746
747 #[test]
750 fn test_pager_mode_default() {
751 assert_eq!(PagerMode::default(), PagerMode::Auto);
752 }
753
754 #[test]
757 fn test_pager_config_default() {
758 let config = PagerConfig::default();
759 assert_eq!(config.enabled, PagerMode::Auto);
760 assert!(config.threshold.is_none());
761 }
763
764 #[test]
765 #[serial]
766 fn test_pager_config_env_sqry_pager() {
767 unsafe {
769 std::env::set_var("SQRY_PAGER", "bat --style=plain");
770 std::env::remove_var("PAGER");
771 }
772
773 let cmd = PagerConfig::default_pager_command();
774 assert_eq!(cmd, "bat --style=plain");
775
776 unsafe {
777 std::env::remove_var("SQRY_PAGER");
778 }
779 }
780
781 #[test]
782 #[serial]
783 fn test_pager_config_env_pager_fallback() {
784 unsafe {
786 std::env::remove_var("SQRY_PAGER");
787 std::env::set_var("PAGER", "more");
788 }
789
790 let cmd = PagerConfig::default_pager_command();
791 assert_eq!(cmd, "more");
792
793 unsafe {
794 std::env::remove_var("PAGER");
795 }
796 }
797
798 #[test]
799 #[serial]
800 fn test_pager_config_env_sqry_pager_priority() {
801 unsafe {
804 std::env::set_var("SQRY_PAGER", "bat");
805 std::env::set_var("PAGER", "less");
806 }
807
808 let cmd = PagerConfig::default_pager_command();
809 assert_eq!(cmd, "bat");
810
811 unsafe {
812 std::env::remove_var("SQRY_PAGER");
813 std::env::remove_var("PAGER");
814 }
815 }
816
817 #[test]
818 #[serial]
819 fn test_pager_config_env_default_fallback() {
820 unsafe {
823 std::env::remove_var("SQRY_PAGER");
824 std::env::remove_var("PAGER");
825 }
826
827 let cmd = PagerConfig::default_pager_command();
828 assert_eq!(cmd, "less -FRX");
829 }
830
831 #[test]
832 fn test_pager_config_from_cli_flags_no_pager() {
833 let config = PagerConfig::from_cli_flags(false, true, None);
834 assert_eq!(config.enabled, PagerMode::Never);
835 }
836
837 #[test]
838 fn test_pager_config_from_cli_flags_pager() {
839 let config = PagerConfig::from_cli_flags(true, false, None);
840 assert_eq!(config.enabled, PagerMode::Always);
841 }
842
843 #[test]
844 fn test_pager_config_from_cli_flags_auto() {
845 let config = PagerConfig::from_cli_flags(false, false, None);
846 assert_eq!(config.enabled, PagerMode::Auto);
847 }
848
849 #[test]
850 fn test_pager_config_from_cli_flags_custom_cmd() {
851 let config = PagerConfig::from_cli_flags(true, false, Some("bat --color=always"));
852 assert_eq!(config.command, "bat --color=always");
853 }
854
855 #[test]
858 fn test_pager_decision_never_mode() {
859 let config = PagerConfig {
860 enabled: PagerMode::Never,
861 ..Default::default()
862 };
863 let decision = PagerDecision::for_testing(config, true, Some(24));
864 assert!(!decision.should_page_rows(1000));
865 }
866
867 #[test]
868 fn test_pager_decision_always_mode() {
869 let config = PagerConfig {
870 enabled: PagerMode::Always,
871 ..Default::default()
872 };
873 let decision = PagerDecision::for_testing(config, true, Some(24));
874 assert!(decision.should_page_rows(1));
875 }
876
877 #[test]
878 fn test_pager_decision_auto_below_threshold() {
879 let config = PagerConfig {
880 enabled: PagerMode::Auto,
881 threshold: Some(100),
882 ..Default::default()
883 };
884 let decision = PagerDecision::for_testing(config, true, Some(24));
885 assert!(!decision.should_page_rows(50));
886 }
887
888 #[test]
889 fn test_pager_decision_auto_above_threshold() {
890 let config = PagerConfig {
891 enabled: PagerMode::Auto,
892 threshold: Some(100),
893 ..Default::default()
894 };
895 let decision = PagerDecision::for_testing(config, true, Some(24));
896 assert!(decision.should_page_rows(150));
897 }
898
899 #[test]
900 fn test_pager_decision_auto_non_tty() {
901 let config = PagerConfig {
903 enabled: PagerMode::Auto,
904 ..Default::default()
905 };
906 let decision = PagerDecision::for_testing(config, false, Some(24));
907 assert!(!decision.should_page_rows(1000));
908 }
909
910 #[test]
911 fn test_pager_decision_auto_uses_terminal_height() {
912 let config = PagerConfig {
913 enabled: PagerMode::Auto,
914 threshold: None, ..Default::default()
916 };
917 let decision = PagerDecision::for_testing(config, true, Some(30));
918 assert!(!decision.should_page_rows(25)); assert!(decision.should_page_rows(35)); }
921
922 #[test]
923 fn test_pager_decision_auto_default_threshold() {
924 let config = PagerConfig {
926 enabled: PagerMode::Auto,
927 threshold: None,
928 ..Default::default()
929 };
930 let decision = PagerDecision::for_testing(config, true, None);
931 assert!(!decision.should_page_rows(20)); assert!(decision.should_page_rows(30)); }
934
935 #[test]
938 fn test_count_displayed_rows_simple() {
939 let content = "line1\nline2\nline3\n";
940 assert_eq!(count_displayed_rows(content, Some(80)), 3);
941 }
942
943 #[test]
944 fn test_count_displayed_rows_empty_lines() {
945 let content = "line1\n\nline3\n";
946 assert_eq!(count_displayed_rows(content, Some(80)), 3);
947 }
948
949 #[test]
950 fn test_count_displayed_rows_long_line_wraps() {
951 let long_line = "a".repeat(160);
953 assert_eq!(count_displayed_rows(&long_line, Some(80)), 2);
954 }
955
956 #[test]
957 fn test_count_displayed_rows_exactly_width() {
958 let exact_line = "a".repeat(80);
960 assert_eq!(count_displayed_rows(&exact_line, Some(80)), 1);
961 }
962
963 #[test]
964 fn test_count_displayed_rows_unicode() {
965 let cjk = "中文字符"; assert_eq!(count_displayed_rows(cjk, Some(80)), 1);
969
970 assert_eq!(count_displayed_rows(cjk, Some(4)), 2);
972 }
973
974 #[test]
975 fn test_count_displayed_rows_default_width() {
976 let content = "test line\n";
977 assert_eq!(count_displayed_rows(content, None), 1);
979 }
980
981 #[test]
984 fn test_pager_writer_spawn_invalid_syntax() {
985 let result = PagerWriter::spawn("less \"unclosed");
987 assert!(result.is_err());
988 let err = result.err().expect("Should be an error");
991 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
992 }
993
994 #[test]
995 fn test_pager_writer_spawn_empty_command() {
996 let result = PagerWriter::spawn("");
997 assert!(result.is_err());
998 let err = result.err().expect("Should be an error");
999 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1000 }
1001
1002 #[test]
1003 fn test_shlex_parsing_simple() {
1004 let parts = shlex::split("less -R").unwrap();
1005 assert_eq!(parts, vec!["less", "-R"]);
1006 }
1007
1008 #[test]
1009 fn test_shlex_parsing_quoted() {
1010 let parts = shlex::split("\"bat\" --style=plain").unwrap();
1011 assert_eq!(parts, vec!["bat", "--style=plain"]);
1012 }
1013
1014 #[test]
1015 fn test_shlex_parsing_windows_path() {
1016 let parts = shlex::split("\"C:\\Program Files\\Git\\usr\\bin\\less.exe\" -R").unwrap();
1017 assert_eq!(
1018 parts,
1019 vec!["C:\\Program Files\\Git\\usr\\bin\\less.exe", "-R"]
1020 );
1021 }
1022
1023 #[test]
1026 fn test_buffered_output_never_mode_writes_directly() {
1027 let config = PagerConfig {
1030 enabled: PagerMode::Never,
1031 ..Default::default()
1032 };
1033 let output = BufferedOutput::new(config);
1034 assert!(matches!(output.mode, OutputMode::Direct));
1035 }
1036
1037 #[test]
1038 fn test_buffered_output_auto_mode_non_tty_streams_directly() {
1039 let config = PagerConfig {
1042 enabled: PagerMode::Auto,
1043 ..Default::default()
1044 };
1045 let output = BufferedOutput::new(config);
1046 assert!(
1048 matches!(output.mode, OutputMode::Direct)
1049 || matches!(output.mode, OutputMode::Buffering),
1050 "Expected Direct (non-TTY) or Buffering (TTY), got neither"
1051 );
1052 }
1053
1054 #[test]
1057 #[cfg(unix)]
1058 fn test_is_broken_pipe_exit_sigpipe() {
1059 use std::os::unix::process::ExitStatusExt;
1060 let status = ExitStatus::from_raw(13 << 8 | 0x7f); let _ = is_broken_pipe_exit(status);
1064 }
1065
1066 #[test]
1069 fn test_buffer_cap_constant() {
1070 assert_eq!(BUFFER_CAP_BYTES, 1024 * 1024);
1072 }
1073
1074 #[test]
1077 fn test_pager_exit_status_success() {
1078 let status = PagerExitStatus::Success;
1079 assert!(status.is_success());
1080 assert_eq!(status.exit_code(), None);
1081 }
1082
1083 #[test]
1084 fn test_pager_exit_status_exit_code() {
1085 let status = PagerExitStatus::ExitCode(42);
1086 assert!(!status.is_success());
1087 assert_eq!(status.exit_code(), Some(42));
1088 }
1089
1090 #[test]
1091 fn test_pager_exit_status_signal() {
1092 let status = PagerExitStatus::Signal(9);
1094 assert!(!status.is_success());
1095 assert_eq!(status.exit_code(), Some(137));
1096 }
1097
1098 #[test]
1101 fn test_strip_ansi_plain_text() {
1102 assert_eq!(strip_ansi("hello world"), "hello world");
1103 }
1104
1105 #[test]
1106 fn test_strip_ansi_csi_color() {
1107 let colored = "\x1b[31mhello\x1b[0m";
1109 assert_eq!(strip_ansi(colored), "hello");
1110 }
1111
1112 #[test]
1113 fn test_strip_ansi_multiple_codes() {
1114 let colored = "\x1b[1;31mhello\x1b[0m world";
1116 assert_eq!(strip_ansi(colored), "hello world");
1117 }
1118
1119 #[test]
1120 fn test_strip_ansi_osc_sequence() {
1121 let with_osc = "before\x1b]0;window title\x07after";
1123 assert_eq!(strip_ansi(with_osc), "beforeafter");
1124 }
1125
1126 #[test]
1127 fn test_strip_ansi_preserves_unicode() {
1128 let text = "\x1b[32m日本語\x1b[0m";
1129 assert_eq!(strip_ansi(text), "日本語");
1130 }
1131
1132 #[test]
1135 fn test_displayed_line_width_no_tabs() {
1136 assert_eq!(displayed_line_width("hello"), 5);
1137 }
1138
1139 #[test]
1140 fn test_displayed_line_width_single_tab_start() {
1141 assert_eq!(displayed_line_width("\thello"), 8 + 5);
1143 }
1144
1145 #[test]
1146 fn test_displayed_line_width_tab_after_text() {
1147 assert_eq!(displayed_line_width("hi\tworld"), 8 + 5);
1149 }
1150
1151 #[test]
1152 fn test_displayed_line_width_multiple_tabs() {
1153 assert_eq!(displayed_line_width("\t\t"), 16);
1155 }
1156
1157 #[test]
1158 fn test_displayed_line_width_cjk() {
1159 assert_eq!(displayed_line_width("日本"), 4);
1161 }
1162
1163 #[test]
1166 fn test_count_displayed_rows_strips_ansi() {
1167 let colored = "\x1b[31mhello\x1b[0m"; assert_eq!(count_displayed_rows(colored, Some(80)), 1);
1171 }
1172
1173 #[test]
1174 fn test_count_displayed_rows_with_tabs() {
1175 assert_eq!(count_displayed_rows("hi\tworld", Some(80)), 1);
1178
1179 assert_eq!(count_displayed_rows("hi\tworld", Some(10)), 2);
1181 }
1182
1183 #[test]
1184 fn test_count_displayed_rows_ansi_and_tabs_combined() {
1185 let content = "\x1b[32m\tindented\x1b[0m";
1187 assert_eq!(count_displayed_rows(content, Some(80)), 1);
1189 assert_eq!(count_displayed_rows(content, Some(10)), 2);
1190 }
1191
1192 #[test]
1195 fn test_incremental_line_counting_single_write() {
1196 let config = PagerConfig::default();
1198 let mut output = BufferedOutput::new_for_testing(config);
1199
1200 output.write("line1\nline2\nline3\nline4\nline5\n").unwrap();
1202
1203 assert_eq!(output.complete_lines, 5);
1204 assert_eq!(output.partial_line_len, 0);
1205 }
1206
1207 #[test]
1208 fn test_incremental_line_counting_chunked_writes() {
1209 let config = PagerConfig::default();
1212 let mut output = BufferedOutput::new_for_testing(config);
1213
1214 output.write("line1").unwrap();
1216 assert_eq!(output.complete_lines, 0);
1217 assert_eq!(output.partial_line_len, 5);
1218
1219 output.write("\n").unwrap();
1220 assert_eq!(output.complete_lines, 1);
1221 assert_eq!(output.partial_line_len, 0);
1222
1223 output.write("line2").unwrap();
1224 assert_eq!(output.complete_lines, 1);
1225 assert_eq!(output.partial_line_len, 5);
1226
1227 output.write("\n").unwrap();
1228 assert_eq!(output.complete_lines, 2);
1229 assert_eq!(output.partial_line_len, 0);
1230
1231 for i in 3..=10 {
1233 output.write(&format!("line{i}")).unwrap();
1234 output.write("\n").unwrap();
1235 }
1236
1237 assert_eq!(output.complete_lines, 10);
1239 assert_eq!(output.partial_line_len, 0);
1240 }
1241
1242 #[test]
1243 fn test_incremental_line_counting_mixed_writes() {
1244 let config = PagerConfig::default();
1245 let mut output = BufferedOutput::new_for_testing(config);
1246
1247 output.write("line1\nline2\n").unwrap();
1249 assert_eq!(output.complete_lines, 2);
1250 assert_eq!(output.partial_line_len, 0);
1251
1252 output.write("partial").unwrap();
1253 assert_eq!(output.complete_lines, 2);
1254 assert_eq!(output.partial_line_len, 7);
1255
1256 output.write(" more").unwrap();
1257 assert_eq!(output.complete_lines, 2);
1258 assert_eq!(output.partial_line_len, 12);
1259
1260 output.write("\nline4\n").unwrap();
1261 assert_eq!(output.complete_lines, 4);
1262 assert_eq!(output.partial_line_len, 0);
1263 }
1264
1265 #[test]
1266 fn test_incremental_line_counting_multiple_newlines_in_one_write() {
1267 let config = PagerConfig::default();
1268 let mut output = BufferedOutput::new_for_testing(config);
1269
1270 output.write("a\nb\nc\nd\ne").unwrap();
1272 assert_eq!(output.complete_lines, 4); assert_eq!(output.partial_line_len, 1); }
1275}