1use crossterm::{
7 event::{self, Event, KeyCode, KeyEvent},
8 terminal::{disable_raw_mode, enable_raw_mode},
9};
10use std::io::{self, Write};
11
12const DEFAULT_TERM_HEIGHT: usize = 24;
14
15pub fn page_text(content: &str) -> io::Result<()> {
37 let lines: Vec<&str> = content.lines().collect();
38 let line_count = lines.len();
39
40 let term_height = get_terminal_height();
42
43 if line_count <= term_height - 2 {
45 println!("{}", content);
46 return Ok(());
47 }
48
49 page_lines(&lines, term_height)
51}
52
53fn get_terminal_height() -> usize {
55 terminal_size::terminal_size()
56 .map(|(_, terminal_size::Height(h))| h as usize)
57 .unwrap_or(DEFAULT_TERM_HEIGHT)
58}
59
60fn page_lines(lines: &[&str], term_height: usize) -> io::Result<()> {
62 let page_size = term_height - 2; let mut current_line = 0;
64 let total_lines = lines.len();
65
66 let mut stdout = io::stdout();
67
68 enable_raw_mode().map_err(io::Error::other)?;
70
71 let result = (|| -> io::Result<()> {
72 while current_line < total_lines {
73 let end_line = (current_line + page_size).min(total_lines);
75
76 for line in &lines[current_line..end_line] {
78 write!(stdout, "{}\r\n", line)?;
79 }
80 stdout.flush()?;
81
82 current_line = end_line;
83
84 if current_line >= total_lines {
86 break;
87 }
88
89 let remaining = total_lines - current_line;
91 write!(
92 stdout,
93 "\x1b[7m-- More ({} lines remaining) -- Press Space to continue, q to quit \x1b[0m",
94 remaining
95 )?;
96 stdout.flush()?;
97
98 loop {
100 if let Event::Key(KeyEvent { code, .. }) =
101 event::read().map_err(io::Error::other)?
102 {
103 match code {
104 KeyCode::Char(' ') => {
105 break;
107 }
108 KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => {
109 write!(stdout, "\r\x1b[K")?;
111 return Ok(());
112 }
113 _ => {
114 continue;
116 }
117 }
118 }
119 }
120
121 write!(stdout, "\r\x1b[K")?;
123 }
124
125 Ok(())
126 })();
127
128 disable_raw_mode().map_err(io::Error::other)?;
130
131 result
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_get_terminal_height() {
140 let height = get_terminal_height();
141 assert!(height > 0);
143 assert!(height <= 200); }
145
146 #[test]
147 fn test_short_content_no_paging() {
148 let content = "Line 1\nLine 2\nLine 3";
150 let result = page_text(content);
153 assert!(result.is_ok() || result.is_err());
155 }
156
157 #[test]
158 fn test_empty_content() {
159 let result = page_text("");
160 assert!(result.is_ok());
161 }
162
163 #[test]
164 fn test_single_line() {
165 let result = page_text("Single line");
166 assert!(result.is_ok());
167 }
168
169 #[test]
170 fn test_content_with_newlines() {
171 let content = "Line 1\nLine 2\nLine 3\nLine 4\n";
172 let result = page_text(content);
173 assert!(result.is_ok());
174 }
175}