1use std::io::{self, BufRead, Write};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4pub const DEFAULT_PAGE_LENGTH: usize = 66;
6pub const DEFAULT_PAGE_WIDTH: usize = 72;
8pub const HEADER_LINES: usize = 5;
10pub const FOOTER_LINES: usize = 5;
12
13#[derive(Clone)]
15pub struct PrConfig {
16 pub first_page: usize,
18 pub last_page: usize,
20 pub columns: usize,
22 pub across: bool,
24 pub show_control_chars: bool,
26 pub double_space: bool,
28 pub date_format: String,
30 pub expand_tabs: Option<(char, usize)>,
32 pub form_feed: bool,
34 pub header: Option<String>,
36 pub output_tabs: Option<(char, usize)>,
38 pub join_lines: bool,
40 pub page_length: usize,
42 pub merge: bool,
44 pub number_lines: Option<(char, usize)>,
46 pub first_line_number: usize,
48 pub indent: usize,
50 pub no_file_warnings: bool,
52 pub separator: Option<char>,
54 pub sep_string: Option<String>,
56 pub omit_header: bool,
58 pub omit_pagination: bool,
60 pub show_nonprinting: bool,
62 pub page_width: usize,
64 pub truncate_lines: bool,
66}
67
68impl Default for PrConfig {
69 fn default() -> Self {
70 Self {
71 first_page: 1,
72 last_page: 0,
73 columns: 1,
74 across: false,
75 show_control_chars: false,
76 double_space: false,
77 date_format: "%Y-%m-%d %H:%M".to_string(),
78 expand_tabs: None,
79 form_feed: false,
80 header: None,
81 output_tabs: None,
82 join_lines: false,
83 page_length: DEFAULT_PAGE_LENGTH,
84 merge: false,
85 number_lines: None,
86 first_line_number: 1,
87 indent: 0,
88 no_file_warnings: false,
89 separator: None,
90 sep_string: None,
91 omit_header: false,
92 omit_pagination: false,
93 show_nonprinting: false,
94 page_width: DEFAULT_PAGE_WIDTH,
95 truncate_lines: false,
96 }
97 }
98}
99
100fn format_header_date(time: &SystemTime, format: &str) -> String {
102 let secs = time
103 .duration_since(UNIX_EPOCH)
104 .unwrap_or_default()
105 .as_secs() as i64;
106 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
107 unsafe {
108 libc::localtime_r(&secs, &mut tm);
109 }
110
111 let c_format = std::ffi::CString::new(format).unwrap_or_default();
113 let mut buf = vec![0u8; 256];
114 let len = unsafe {
115 libc::strftime(
116 buf.as_mut_ptr() as *mut libc::c_char,
117 buf.len(),
118 c_format.as_ptr(),
119 &tm,
120 )
121 };
122 if len == 0 {
123 return String::new();
124 }
125 buf.truncate(len);
126 String::from_utf8_lossy(&buf).into_owned()
127}
128
129fn expand_tabs_in_line(line: &str, tab_char: char, tab_width: usize) -> String {
131 if tab_width == 0 {
132 return line.replace(tab_char, "");
133 }
134 let mut result = String::with_capacity(line.len() + line.len() / 4);
136 let tab_byte = tab_char as u8;
137 let bytes = line.as_bytes();
138 let mut col = 0;
139 let mut seg_start = 0;
140
141 for (i, &b) in bytes.iter().enumerate() {
142 if b == tab_byte {
143 if i > seg_start {
145 result.push_str(&line[seg_start..i]);
146 col += i - seg_start;
147 }
148 let spaces = tab_width - (col % tab_width);
149 let space_buf = " ";
151 let mut remaining = spaces;
152 while remaining > 0 {
153 let chunk = remaining.min(space_buf.len());
154 result.push_str(&space_buf[..chunk]);
155 remaining -= chunk;
156 }
157 col += spaces;
158 seg_start = i + 1;
159 }
160 }
161 if seg_start < bytes.len() {
163 result.push_str(&line[seg_start..]);
164 }
165 result
166}
167
168#[inline]
170fn push_hat_notation(result: &mut String, ch: char) {
171 let b = ch as u32;
172 if b < 32 {
173 result.push('^');
174 result.push((b as u8 + b'@') as char);
175 } else if b == 127 {
176 result.push_str("^?");
177 } else {
178 result.push(ch);
179 }
180}
181
182#[inline]
184fn push_nonprinting(result: &mut String, ch: char) {
185 let b = ch as u32;
186 if b < 32 && b != 9 && b != 10 {
187 result.push('^');
188 result.push((b as u8 + b'@') as char);
189 } else if b == 127 {
190 result.push_str("^?");
191 } else if b >= 128 && b < 160 {
192 result.push_str("M-^");
193 result.push((b as u8 - 128 + b'@') as char);
194 } else if b >= 160 && b < 255 {
195 result.push_str("M-");
196 result.push((b as u8 - 128) as char);
197 } else if b == 255 {
198 result.push_str("M-^?");
199 } else {
200 result.push(ch);
201 }
202}
203
204fn process_control_chars(line: &str, show_control: bool, show_nonprinting: bool) -> String {
206 if !show_control && !show_nonprinting {
207 return line.to_string();
208 }
209 let mut result = String::with_capacity(line.len() + line.len() / 4);
210 for ch in line.chars() {
211 if show_nonprinting {
212 push_nonprinting(&mut result, ch);
213 } else if show_control {
214 push_hat_notation(&mut result, ch);
215 } else {
216 result.push(ch);
217 }
218 }
219 result
220}
221
222fn get_column_separator(config: &PrConfig) -> String {
224 if let Some(ref s) = config.sep_string {
225 s.clone()
226 } else if let Some(c) = config.separator {
227 c.to_string()
228 } else {
229 " ".to_string()
230 }
231}
232
233fn has_explicit_separator(config: &PrConfig) -> bool {
235 config.sep_string.is_some() || config.separator.is_some()
236}
237
238const SPACES: [u8; 256] = [b' '; 256];
244
245#[inline]
247fn write_spaces<W: Write>(output: &mut W, n: usize) -> io::Result<()> {
248 let mut remaining = n;
249 while remaining > 0 {
250 let chunk = remaining.min(SPACES.len());
251 output.write_all(&SPACES[..chunk])?;
252 remaining -= chunk;
253 }
254 Ok(())
255}
256
257fn write_column_padding<W: Write>(
258 output: &mut W,
259 abs_pos: usize,
260 target_abs_pos: usize,
261) -> io::Result<()> {
262 let n = target_abs_pos.saturating_sub(abs_pos);
264 write_spaces(output, n)
265}
266
267pub fn pr_data<W: Write>(
271 data: &[u8],
272 output: &mut W,
273 config: &PrConfig,
274 filename: &str,
275 file_date: Option<SystemTime>,
276) -> io::Result<()> {
277 let needs_transform =
278 config.expand_tabs.is_some() || config.show_control_chars || config.show_nonprinting;
279
280 if needs_transform {
281 let reader = io::Cursor::new(data);
283 return pr_file(reader, output, config, filename, file_date);
284 }
285
286 let is_simple = config.columns <= 1
290 && config.number_lines.is_none()
291 && config.indent == 0
292 && !config.truncate_lines
293 && !config.double_space
294 && !config.across
295 && memchr::memchr(b'\r', data).is_none()
296 && memchr::memchr(b'\x0c', data).is_none();
297
298 if is_simple {
299 if config.omit_pagination && config.first_page == 1 && config.last_page == 0 {
303 return output.write_all(data);
304 }
305 return pr_data_contiguous(data, output, config, filename, file_date);
306 }
307
308 if config.columns <= 1
310 && config.number_lines.is_some()
311 && config.indent == 0
312 && !config.truncate_lines
313 && !config.double_space
314 && memchr::memchr(b'\r', data).is_none()
315 && memchr::memchr(b'\x0c', data).is_none()
316 {
317 return pr_data_numbered(data, output, config, filename, file_date);
318 }
319
320 let mut lines: Vec<&[u8]> = Vec::with_capacity(data.len() / 40 + 64);
322 let mut start = 0;
323 for pos in memchr::memchr_iter(b'\n', data) {
324 let end = if pos > start && data[pos - 1] == b'\r' {
325 pos - 1
326 } else {
327 pos
328 };
329 lines.push(&data[start..end]);
330 start = pos + 1;
331 }
332 if start < data.len() {
334 let end = if data.last() == Some(&b'\r') {
335 data.len() - 1
336 } else {
337 data.len()
338 };
339 lines.push(&data[start..end]);
340 }
341
342 pr_lines_generic(&lines, output, config, filename, file_date)
343}
344
345fn pr_data_contiguous<W: Write>(
349 data: &[u8],
350 output: &mut W,
351 config: &PrConfig,
352 filename: &str,
353 file_date: Option<SystemTime>,
354) -> io::Result<()> {
355 let date = file_date.unwrap_or_else(SystemTime::now);
356 let header_str = config.header.as_deref().unwrap_or(filename);
357 let date_str = format_header_date(&date, &config.date_format);
358
359 let suppress_header = !config.omit_header
360 && !config.omit_pagination
361 && config.page_length <= HEADER_LINES + FOOTER_LINES;
362 let body_lines_per_page = if config.omit_header || config.omit_pagination {
363 if config.page_length > 0 {
364 config.page_length
365 } else {
366 DEFAULT_PAGE_LENGTH
367 }
368 } else if suppress_header {
369 config.page_length
370 } else {
371 config.page_length - HEADER_LINES - FOOTER_LINES
372 };
373 let show_header = !config.omit_header && !config.omit_pagination && !suppress_header;
374
375 if data.is_empty() {
376 if show_header {
377 let mut page_buf: Vec<u8> = Vec::with_capacity(256);
378 write_header(&mut page_buf, &date_str, header_str, 1, config)?;
379 write_footer(&mut page_buf, config)?;
380 output.write_all(&page_buf)?;
381 }
382 return Ok(());
383 }
384
385 let footer: &[u8] = if show_header {
386 if config.form_feed {
387 b"\x0c"
388 } else {
389 b"\n\n\n\n\n"
390 }
391 } else {
392 b""
393 };
394
395 let mut page_buf: Vec<u8> = Vec::with_capacity(128 * 1024);
397 let mut page_num = 1usize;
398 let mut byte_pos = 0usize;
399 loop {
400 if byte_pos >= data.len() {
401 break;
402 }
403
404 let page_start = byte_pos;
406 let mut lines_found = 0usize;
407 let remaining = &data[byte_pos..];
408 let mut page_end = data.len();
409
410 for nl_off in memchr::memchr_iter(b'\n', remaining) {
411 lines_found += 1;
412 if lines_found >= body_lines_per_page {
413 page_end = byte_pos + nl_off + 1;
414 break;
415 }
416 }
417
418 let in_range = page_num >= config.first_page
419 && (config.last_page == 0 || page_num <= config.last_page);
420
421 if in_range {
422 page_buf.clear();
423
424 if show_header {
425 write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
426 }
427
428 page_buf.extend_from_slice(&data[page_start..page_end]);
430
431 if page_buf.last() != Some(&b'\n') {
433 page_buf.push(b'\n');
434 }
435
436 if show_header || (!config.omit_header && !config.omit_pagination) {
438 let pad_lines = body_lines_per_page.saturating_sub(lines_found);
439 page_buf.resize(page_buf.len() + pad_lines, b'\n');
440 }
441
442 page_buf.extend_from_slice(footer);
443
444 output.write_all(&page_buf)?;
445 }
446
447 byte_pos = page_end;
448 page_num += 1;
449
450 if lines_found < body_lines_per_page {
452 break;
453 }
454 }
455
456 Ok(())
457}
458
459fn pr_data_numbered<W: Write>(
463 data: &[u8],
464 output: &mut W,
465 config: &PrConfig,
466 filename: &str,
467 file_date: Option<SystemTime>,
468) -> io::Result<()> {
469 let date = file_date.unwrap_or_else(SystemTime::now);
470 let header_str = config.header.as_deref().unwrap_or(filename);
471 let date_str = format_header_date(&date, &config.date_format);
472
473 let (sep_char, digits) = config.number_lines.unwrap_or(('\t', 5));
474 debug_assert!(sep_char.is_ascii(), "number separator must be ASCII");
475 let sep_byte = sep_char as u8;
476 let suppress_header = !config.omit_header
477 && !config.omit_pagination
478 && config.page_length <= HEADER_LINES + FOOTER_LINES;
479 let body_lines_per_page = if config.omit_header || config.omit_pagination {
480 if config.page_length > 0 {
481 config.page_length
482 } else {
483 DEFAULT_PAGE_LENGTH
484 }
485 } else if suppress_header {
486 config.page_length
487 } else {
488 config.page_length - HEADER_LINES - FOOTER_LINES
489 };
490 let show_header = !config.omit_header && !config.omit_pagination && !suppress_header;
491
492 const BUF_SIZE: usize = 128 * 1024;
494 let mut page_buf: Vec<u8> = Vec::with_capacity(BUF_SIZE + 4096);
495
496 let mut line_number = config.first_line_number;
497 let mut page_num = 1usize;
498
499 let mut line_starts: Vec<usize> = Vec::with_capacity(data.len() / 40 + 64);
501 line_starts.push(0);
502 for pos in memchr::memchr_iter(b'\n', data) {
503 line_starts.push(pos + 1);
504 }
505 let total_lines = if !data.is_empty() && data[data.len() - 1] == b'\n' {
506 line_starts.len() - 1
507 } else {
508 line_starts.len()
509 };
510
511 let mut line_idx = 0;
512
513 while line_idx < total_lines {
514 let page_end = (line_idx + body_lines_per_page).min(total_lines);
515 let in_range = page_num >= config.first_page
516 && (config.last_page == 0 || page_num <= config.last_page);
517
518 if in_range {
519 page_buf.clear();
520
521 if show_header {
522 write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
523 }
524
525 let src = data.as_ptr();
529 for li in line_idx..page_end {
530 let line_start = line_starts[li];
531 let line_end = if li + 1 < line_starts.len() {
532 let end = line_starts[li + 1] - 1;
534 if end > line_start && data[end - 1] == b'\r' {
535 end - 1
536 } else {
537 end
538 }
539 } else {
540 data.len()
541 };
542 let line_len = line_end - line_start;
543
544 let wp = page_buf.len();
545
546 let mut n = line_number;
548 let mut num_pos = 19usize;
549 let mut num_tmp = [0u8; 20];
550 loop {
551 num_tmp[num_pos] = b'0' + (n % 10) as u8;
552 n /= 10;
553 if n == 0 || num_pos == 0 {
554 break;
555 }
556 num_pos -= 1;
557 }
558 let num_digits = 20 - num_pos;
559 let padding = digits.saturating_sub(num_digits);
560 let actual_prefix = padding + num_digits + 1; let needed = actual_prefix + line_len + 1;
566 if page_buf.len() + needed > page_buf.capacity() {
567 page_buf.reserve(needed);
568 }
569 let base = page_buf.as_mut_ptr();
570
571 unsafe {
572 let dst = base.add(wp);
573 std::ptr::write_bytes(dst, b' ', padding);
575 std::ptr::copy_nonoverlapping(
577 num_tmp.as_ptr().add(num_pos),
578 dst.add(padding),
579 num_digits,
580 );
581 *dst.add(padding + num_digits) = sep_byte;
583 if line_len > 0 {
585 std::ptr::copy_nonoverlapping(
586 src.add(line_start),
587 dst.add(actual_prefix),
588 line_len,
589 );
590 }
591 *dst.add(actual_prefix + line_len) = b'\n';
593 page_buf.set_len(wp + actual_prefix + line_len + 1);
594 }
595
596 line_number += 1;
597 }
598
599 if show_header {
601 let body_lines_written = page_end - line_idx;
602 let pad = body_lines_per_page.saturating_sub(body_lines_written);
603 page_buf.resize(page_buf.len() + pad, b'\n');
604 }
605
606 if show_header {
608 write_footer(&mut page_buf, config)?;
609 }
610
611 output.write_all(&page_buf)?;
612 } else {
613 line_number += page_end - line_idx;
615 }
616
617 line_idx = page_end;
618 page_num += 1;
619 }
620
621 Ok(())
622}
623
624pub fn pr_file<R: BufRead, W: Write>(
626 input: R,
627 output: &mut W,
628 config: &PrConfig,
629 filename: &str,
630 file_date: Option<SystemTime>,
631) -> io::Result<()> {
632 let mut all_lines: Vec<String> = Vec::new();
634 for line_result in input.lines() {
635 let line = line_result?;
636 let mut line = line;
637
638 if let Some((tab_char, tab_width)) = config.expand_tabs {
640 line = expand_tabs_in_line(&line, tab_char, tab_width);
641 }
642
643 if config.show_control_chars || config.show_nonprinting {
645 line = process_control_chars(&line, config.show_control_chars, config.show_nonprinting);
646 }
647
648 all_lines.push(line);
649 }
650
651 let refs: Vec<&[u8]> = all_lines.iter().map(|s| s.as_bytes()).collect();
653 pr_lines_generic(&refs, output, config, filename, file_date)
654}
655
656fn pr_lines_generic<W: Write>(
658 all_lines: &[&[u8]],
659 output: &mut W,
660 config: &PrConfig,
661 filename: &str,
662 file_date: Option<SystemTime>,
663) -> io::Result<()> {
664 let date = file_date.unwrap_or_else(SystemTime::now);
665
666 let header_str = config.header.as_deref().unwrap_or(filename);
667 let date_str = format_header_date(&date, &config.date_format);
668
669 let suppress_header = !config.omit_header
673 && !config.omit_pagination
674 && config.page_length <= HEADER_LINES + FOOTER_LINES;
675 let suppressed_config;
678 let effective_config = if suppress_header {
679 suppressed_config = PrConfig {
680 omit_header: true,
681 ..config.clone()
682 };
683 &suppressed_config
684 } else {
685 config
686 };
687 let body_lines_per_page = if config.omit_header || config.omit_pagination {
688 if config.page_length > 0 {
689 config.page_length
690 } else {
691 DEFAULT_PAGE_LENGTH
692 }
693 } else if suppress_header {
694 config.page_length
695 } else {
696 config.page_length - HEADER_LINES - FOOTER_LINES
697 };
698
699 let input_lines_per_page = if config.double_space {
701 (body_lines_per_page + 1) / 2
702 } else {
703 body_lines_per_page
704 };
705
706 let columns = config.columns.max(1);
708
709 let lines_consumed_per_page = if columns > 1 && !config.across {
714 input_lines_per_page * columns
715 } else {
716 input_lines_per_page
717 };
718
719 let total_lines = all_lines.len();
721 let mut line_number = config.first_line_number;
722 let mut page_num = 1usize;
723 let mut line_idx = 0;
724 let mut page_buf: Vec<u8> = Vec::with_capacity(128 * 1024);
726
727 while line_idx < total_lines || (line_idx == 0 && total_lines == 0) {
728 if total_lines == 0 && line_idx == 0 {
730 if page_num >= config.first_page
731 && (config.last_page == 0 || page_num <= config.last_page)
732 {
733 if !config.omit_header && !config.omit_pagination && !suppress_header {
734 write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
735 write_footer(&mut page_buf, config)?;
736 output.write_all(&page_buf)?;
737 }
738 }
739 break;
740 }
741
742 let page_end = (line_idx + lines_consumed_per_page).min(total_lines);
743
744 if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
745 {
746 page_buf.clear();
747
748 if !config.omit_header && !config.omit_pagination && !suppress_header {
750 write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
751 }
752
753 if columns > 1 {
755 write_multicolumn_body(
756 &mut page_buf,
757 &all_lines[line_idx..page_end],
758 effective_config,
759 columns,
760 &mut line_number,
761 body_lines_per_page,
762 )?;
763 } else {
764 write_single_column_body(
765 &mut page_buf,
766 &all_lines[line_idx..page_end],
767 effective_config,
768 &mut line_number,
769 body_lines_per_page,
770 )?;
771 }
772
773 if !config.omit_header && !config.omit_pagination && !suppress_header {
775 write_footer(&mut page_buf, config)?;
776 }
777
778 output.write_all(&page_buf)?;
780 }
781
782 line_idx = page_end;
783 page_num += 1;
784
785 if line_idx >= total_lines {
787 break;
788 }
789 }
790
791 Ok(())
792}
793
794pub fn pr_merge<W: Write>(
796 inputs: &[Vec<String>],
797 output: &mut W,
798 config: &PrConfig,
799 _filenames: &[&str],
800 file_dates: &[SystemTime],
801) -> io::Result<()> {
802 let date = file_dates.first().copied().unwrap_or_else(SystemTime::now);
803 let date_str = format_header_date(&date, &config.date_format);
804 let header_str = config.header.as_deref().unwrap_or("");
805
806 let suppress_header = !config.omit_header
807 && !config.omit_pagination
808 && config.page_length <= HEADER_LINES + FOOTER_LINES;
809 let body_lines_per_page = if config.omit_header || config.omit_pagination {
810 if config.page_length > 0 {
811 config.page_length
812 } else {
813 DEFAULT_PAGE_LENGTH
814 }
815 } else if suppress_header {
816 config.page_length
817 } else {
818 config.page_length - HEADER_LINES - FOOTER_LINES
819 };
820
821 let input_lines_per_page = if config.double_space {
822 (body_lines_per_page + 1) / 2
823 } else {
824 body_lines_per_page
825 };
826
827 let num_files = inputs.len();
828 let explicit_sep = has_explicit_separator(config);
829 let col_sep = get_column_separator(config);
830 let col_width = if explicit_sep {
831 if num_files > 1 {
832 (config
833 .page_width
834 .saturating_sub(col_sep.len() * (num_files - 1)))
835 / num_files
836 } else {
837 config.page_width
838 }
839 } else {
840 config.page_width / num_files
841 };
842
843 let max_lines = inputs.iter().map(|f| f.len()).max().unwrap_or(0);
844 let mut page_num = 1usize;
845 let mut line_idx = 0;
846 let mut line_number = config.first_line_number;
847
848 let col_sep_bytes = col_sep.as_bytes();
849 let mut page_buf: Vec<u8> = Vec::with_capacity(128 * 1024);
850 let mut num_buf = [0u8; 32];
851
852 while line_idx < max_lines {
853 let page_end = (line_idx + input_lines_per_page).min(max_lines);
854
855 if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
856 {
857 page_buf.clear();
858
859 if !config.omit_header && !config.omit_pagination && !suppress_header {
860 write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
861 }
862
863 let indent_str = " ".repeat(config.indent);
864 let mut body_lines_written = 0;
865 for i in line_idx..page_end {
866 if config.double_space && body_lines_written > 0 {
867 page_buf.push(b'\n');
868 body_lines_written += 1;
869 }
870
871 page_buf.extend_from_slice(indent_str.as_bytes());
872 let mut abs_pos = config.indent;
873
874 if let Some((sep, digits)) = config.number_lines {
875 let num_str = format_line_number(line_number, sep, digits, &mut num_buf);
876 page_buf.extend_from_slice(num_str);
877 abs_pos += digits + 1;
878 line_number += 1;
879 }
880
881 for (fi, file_lines) in inputs.iter().enumerate() {
882 let content = if i < file_lines.len() {
883 file_lines[i].as_bytes()
884 } else {
885 b"" as &[u8]
886 };
887 let truncated = if !explicit_sep && content.len() > col_width.saturating_sub(1)
888 {
889 &content[..col_width.saturating_sub(1)]
890 } else if explicit_sep && config.truncate_lines && content.len() > col_width {
891 &content[..col_width]
892 } else {
893 content
894 };
895 if fi < num_files - 1 {
896 if explicit_sep {
897 if fi > 0 {
898 page_buf.extend_from_slice(col_sep_bytes);
899 }
900 page_buf.extend_from_slice(truncated);
901 abs_pos +=
902 truncated.len() + if fi > 0 { col_sep_bytes.len() } else { 0 };
903 } else {
904 page_buf.extend_from_slice(truncated);
905 abs_pos += truncated.len();
906 let target = (fi + 1) * col_width + config.indent;
907 write_column_padding(&mut page_buf, abs_pos, target)?;
908 abs_pos = target;
909 }
910 } else {
911 if explicit_sep && fi > 0 {
912 page_buf.extend_from_slice(col_sep_bytes);
913 }
914 page_buf.extend_from_slice(truncated);
915 }
916 }
917 page_buf.push(b'\n');
918 body_lines_written += 1;
919 }
920
921 while body_lines_written < body_lines_per_page {
923 page_buf.push(b'\n');
924 body_lines_written += 1;
925 }
926
927 if !config.omit_header && !config.omit_pagination && !suppress_header {
928 write_footer(&mut page_buf, config)?;
929 }
930
931 output.write_all(&page_buf)?;
932 }
933
934 line_idx = page_end;
935 page_num += 1;
936 }
937
938 Ok(())
939}
940
941fn write_header<W: Write>(
943 output: &mut W,
944 date_str: &str,
945 header: &str,
946 page_num: usize,
947 config: &PrConfig,
948) -> io::Result<()> {
949 output.write_all(b"\n\n")?;
951
952 let line_width = config.page_width;
954
955 let left = date_str;
956 let center = header;
957 let left_len = left.len();
958 let center_len = center.len();
959
960 let mut page_buf = [0u8; 32];
962 let page_str = format_page_number(page_num, &mut page_buf);
963 let right_len = page_str.len();
964
965 if left_len + center_len + right_len + 2 >= line_width {
967 output.write_all(left.as_bytes())?;
968 output.write_all(b" ")?;
969 output.write_all(center.as_bytes())?;
970 output.write_all(b" ")?;
971 output.write_all(page_str)?;
972 output.write_all(b"\n")?;
973 } else {
974 let total_spaces = line_width - left_len - center_len - right_len;
975 let left_spaces = total_spaces / 2;
976 let right_spaces = total_spaces - left_spaces;
977 output.write_all(left.as_bytes())?;
978 write_spaces(output, left_spaces)?;
979 output.write_all(center.as_bytes())?;
980 write_spaces(output, right_spaces)?;
981 output.write_all(page_str)?;
982 output.write_all(b"\n")?;
983 }
984
985 output.write_all(b"\n\n")?;
987
988 Ok(())
989}
990
991#[inline]
993fn format_page_number(page_num: usize, buf: &mut [u8; 32]) -> &[u8] {
994 const PREFIX: &[u8] = b"Page ";
995 let prefix_len = PREFIX.len();
996 buf[..prefix_len].copy_from_slice(PREFIX);
997 let mut num_buf = [0u8; 20];
999 let mut n = page_num;
1000 let mut pos = 19;
1001 loop {
1002 num_buf[pos] = b'0' + (n % 10) as u8;
1003 n /= 10;
1004 if n == 0 {
1005 break;
1006 }
1007 pos -= 1;
1008 }
1009 let num_len = 20 - pos;
1010 buf[prefix_len..prefix_len + num_len].copy_from_slice(&num_buf[pos..20]);
1011 &buf[..prefix_len + num_len]
1012}
1013
1014fn write_footer<W: Write>(output: &mut W, config: &PrConfig) -> io::Result<()> {
1016 if config.form_feed {
1017 output.write_all(b"\x0c")?;
1018 } else {
1019 output.write_all(b"\n\n\n\n\n")?;
1020 }
1021 Ok(())
1022}
1023
1024fn write_single_column_body<W: Write>(
1026 output: &mut W,
1027 lines: &[&[u8]],
1028 config: &PrConfig,
1029 line_number: &mut usize,
1030 body_lines_per_page: usize,
1031) -> io::Result<()> {
1032 let indent_str = " ".repeat(config.indent);
1033 let content_width = if config.truncate_lines {
1034 compute_content_width(config)
1035 } else {
1036 0
1037 };
1038 let mut body_lines_written = 0;
1039 let mut num_buf = [0u8; 32];
1041
1042 for line in lines.iter() {
1043 output.write_all(indent_str.as_bytes())?;
1044
1045 if let Some((sep, digits)) = config.number_lines {
1046 let num_str = format_line_number(*line_number, sep, digits, &mut num_buf);
1048 output.write_all(num_str)?;
1049 *line_number += 1;
1050 }
1051
1052 let content: &[u8] = if config.truncate_lines {
1053 if line.len() > content_width {
1054 &line[..content_width]
1055 } else {
1056 line
1057 }
1058 } else {
1059 line
1060 };
1061
1062 output.write_all(content)?;
1064 output.write_all(b"\n")?;
1065 body_lines_written += 1;
1066 if body_lines_written >= body_lines_per_page {
1067 break;
1068 }
1069
1070 if config.double_space {
1072 output.write_all(b"\n")?;
1073 body_lines_written += 1;
1074 if body_lines_written >= body_lines_per_page {
1075 break;
1076 }
1077 }
1078 }
1079
1080 if !config.omit_header && !config.omit_pagination {
1082 while body_lines_written < body_lines_per_page {
1083 output.write_all(b"\n")?;
1084 body_lines_written += 1;
1085 }
1086 }
1087
1088 Ok(())
1089}
1090
1091#[inline]
1094fn format_line_number(num: usize, sep: char, digits: usize, buf: &mut [u8; 32]) -> &[u8] {
1095 let mut n = num;
1097 let mut pos = 31;
1098 loop {
1099 buf[pos] = b'0' + (n % 10) as u8;
1100 n /= 10;
1101 if n == 0 || pos == 0 {
1102 break;
1103 }
1104 pos -= 1;
1105 }
1106 let num_digits = 32 - pos;
1107 let padding = if digits > num_digits {
1109 digits - num_digits
1110 } else {
1111 0
1112 };
1113 let total_len = padding + num_digits + sep.len_utf8();
1114 let start = 32 - num_digits;
1117 let sep_byte = sep as u8; let out_start = 32usize.saturating_sub(total_len);
1121 for i in out_start..out_start + padding {
1123 buf[i] = b' ';
1124 }
1125 if out_start + padding != start {
1127 let src = start;
1128 let dst = out_start + padding;
1129 for i in 0..num_digits {
1130 buf[dst + i] = buf[src + i];
1131 }
1132 }
1133 buf[out_start + padding + num_digits] = sep_byte;
1135 &buf[out_start..out_start + total_len]
1136}
1137
1138fn compute_content_width(config: &PrConfig) -> usize {
1140 let mut w = config.page_width;
1141 w = w.saturating_sub(config.indent);
1142 if let Some((_, digits)) = config.number_lines {
1143 w = w.saturating_sub(digits + 1); }
1145 w
1146}
1147
1148fn write_multicolumn_body<W: Write>(
1150 output: &mut W,
1151 lines: &[&[u8]],
1152 config: &PrConfig,
1153 columns: usize,
1154 line_number: &mut usize,
1155 body_lines_per_page: usize,
1156) -> io::Result<()> {
1157 let explicit_sep = has_explicit_separator(config);
1158 let col_sep = get_column_separator(config);
1159 let col_width = if explicit_sep {
1162 if columns > 1 {
1163 (config
1164 .page_width
1165 .saturating_sub(col_sep.len() * (columns - 1)))
1166 / columns
1167 } else {
1168 config.page_width
1169 }
1170 } else {
1171 config.page_width / columns
1172 };
1173 let do_truncate = !config.join_lines;
1176 let content_width = if explicit_sep {
1177 col_width
1178 } else {
1179 col_width.saturating_sub(1)
1180 };
1181
1182 let indent_str = " ".repeat(config.indent);
1183 let col_sep_bytes = col_sep.as_bytes();
1184 let mut body_lines_written = 0;
1185 let mut num_buf = [0u8; 32];
1186
1187 if config.across {
1188 let mut i = 0;
1190 while i < lines.len() {
1191 if config.double_space && body_lines_written > 0 {
1192 output.write_all(b"\n")?;
1193 body_lines_written += 1;
1194 if body_lines_written >= body_lines_per_page {
1195 break;
1196 }
1197 }
1198
1199 output.write_all(indent_str.as_bytes())?;
1200 let mut abs_pos = config.indent;
1201
1202 let mut last_data_col = 0;
1204 for col in 0..columns {
1205 let li = i + col;
1206 if li < lines.len() {
1207 last_data_col = col;
1208 }
1209 }
1210
1211 for col in 0..columns {
1212 let li = i + col;
1213 if li < lines.len() {
1214 if explicit_sep && col > 0 {
1215 output.write_all(col_sep_bytes)?;
1216 abs_pos += col_sep_bytes.len();
1217 }
1218 if let Some((sep, digits)) = config.number_lines {
1219 let num_str = format_line_number(*line_number, sep, digits, &mut num_buf);
1220 output.write_all(num_str)?;
1221 abs_pos += digits + 1;
1222 *line_number += 1;
1223 }
1224 let content: &[u8] = lines[li];
1225 let mut truncated = if do_truncate && content.len() > content_width {
1226 &content[..content_width]
1227 } else {
1228 content
1229 };
1230 if col == last_data_col && !explicit_sep {
1232 while truncated.last() == Some(&b' ') {
1233 truncated = &truncated[..truncated.len() - 1];
1234 }
1235 }
1236 output.write_all(truncated)?;
1237 abs_pos += truncated.len();
1238 if col < last_data_col && !explicit_sep {
1239 let target = (col + 1) * col_width + config.indent;
1240 write_column_padding(output, abs_pos, target)?;
1241 abs_pos = target;
1242 }
1243 }
1244 }
1245 output.write_all(b"\n")?;
1246 body_lines_written += 1;
1247 i += columns;
1248 }
1249 } else {
1250 let n = lines.len();
1254 let base = n / columns;
1255 let extra = n % columns;
1256
1257 let mut col_starts = vec![0usize; columns + 1];
1259 for col in 0..columns {
1260 let col_lines = base + if col < extra { 1 } else { 0 };
1261 col_starts[col + 1] = col_starts[col] + col_lines;
1262 }
1263
1264 let num_rows = if extra > 0 { base + 1 } else { base };
1266
1267 for row in 0..num_rows {
1268 if config.double_space && row > 0 {
1269 output.write_all(b"\n")?;
1270 body_lines_written += 1;
1271 if body_lines_written >= body_lines_per_page {
1272 break;
1273 }
1274 }
1275
1276 output.write_all(indent_str.as_bytes())?;
1277 let mut abs_pos = config.indent;
1278
1279 let mut last_data_col = 0;
1281 for col in 0..columns {
1282 let col_lines = col_starts[col + 1] - col_starts[col];
1283 if row < col_lines {
1284 last_data_col = col;
1285 }
1286 }
1287
1288 for col in 0..columns {
1289 let col_lines = col_starts[col + 1] - col_starts[col];
1290 let li = col_starts[col] + row;
1291 if row < col_lines {
1292 if explicit_sep && col > 0 {
1293 output.write_all(col_sep_bytes)?;
1294 abs_pos += col_sep_bytes.len();
1295 }
1296 if let Some((sep, digits)) = config.number_lines {
1297 let num = config.first_line_number + li;
1298 let num_str = format_line_number(num, sep, digits, &mut num_buf);
1299 output.write_all(num_str)?;
1300 abs_pos += digits + 1;
1301 }
1302 let content: &[u8] = lines[li];
1303 let mut truncated = if do_truncate && content.len() > content_width {
1304 &content[..content_width]
1305 } else {
1306 content
1307 };
1308 if col == last_data_col && !explicit_sep {
1310 while truncated.last() == Some(&b' ') {
1311 truncated = &truncated[..truncated.len() - 1];
1312 }
1313 }
1314 output.write_all(truncated)?;
1315 abs_pos += truncated.len();
1316 if col < last_data_col && !explicit_sep {
1317 let target = (col + 1) * col_width + config.indent;
1319 write_column_padding(output, abs_pos, target)?;
1320 abs_pos = target;
1321 }
1322 } else if col <= last_data_col {
1323 if explicit_sep {
1325 if col > 0 {
1326 output.write_all(col_sep_bytes)?;
1327 abs_pos += col_sep_bytes.len();
1328 }
1329 } else {
1331 let target = (col + 1) * col_width + config.indent;
1332 write_column_padding(output, abs_pos, target)?;
1333 abs_pos = target;
1334 }
1335 }
1336 }
1338 output.write_all(b"\n")?;
1339 body_lines_written += 1;
1340 }
1341 if config.number_lines.is_some() {
1343 *line_number += lines.len();
1344 }
1345 }
1346
1347 if !config.omit_header && !config.omit_pagination {
1349 while body_lines_written < body_lines_per_page {
1350 output.write_all(b"\n")?;
1351 body_lines_written += 1;
1352 }
1353 }
1354
1355 Ok(())
1356}