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());
135 let mut col = 0;
136 for ch in line.chars() {
137 if ch == tab_char {
138 let spaces = tab_width - (col % tab_width);
139 for _ in 0..spaces {
140 result.push(' ');
141 }
142 col += spaces;
143 } else {
144 result.push(ch);
145 col += 1;
146 }
147 }
148 result
149}
150
151fn to_hat_notation(ch: char) -> String {
153 let b = ch as u32;
154 if b < 32 {
155 format!("^{}", (b as u8 + b'@') as char)
156 } else if b == 127 {
157 "^?".to_string()
158 } else {
159 ch.to_string()
160 }
161}
162
163fn to_nonprinting(ch: char) -> String {
165 let b = ch as u32;
166 if b < 32 && b != 9 && b != 10 {
167 format!("^{}", (b as u8 + b'@') as char)
169 } else if b == 127 {
170 "^?".to_string()
171 } else if b >= 128 && b < 160 {
172 format!("M-^{}", (b as u8 - 128 + b'@') as char)
173 } else if b >= 160 && b < 255 {
174 format!("M-{}", (b as u8 - 128) as char)
175 } else if b == 255 {
176 "M-^?".to_string()
177 } else {
178 ch.to_string()
179 }
180}
181
182fn process_control_chars(line: &str, show_control: bool, show_nonprinting: bool) -> String {
184 if !show_control && !show_nonprinting {
185 return line.to_string();
186 }
187 let mut result = String::with_capacity(line.len());
188 for ch in line.chars() {
189 if show_nonprinting {
190 result.push_str(&to_nonprinting(ch));
191 } else if show_control {
192 result.push_str(&to_hat_notation(ch));
193 } else {
194 result.push(ch);
195 }
196 }
197 result
198}
199
200fn get_column_separator(config: &PrConfig) -> String {
202 if let Some(ref s) = config.sep_string {
203 s.clone()
204 } else if let Some(c) = config.separator {
205 c.to_string()
206 } else {
207 " ".to_string()
208 }
209}
210
211pub fn pr_file<R: BufRead, W: Write>(
213 input: R,
214 output: &mut W,
215 config: &PrConfig,
216 filename: &str,
217 file_date: Option<SystemTime>,
218) -> io::Result<()> {
219 let date = file_date.unwrap_or_else(SystemTime::now);
220
221 let mut all_lines: Vec<String> = Vec::new();
223 for line_result in input.lines() {
224 let line = line_result?;
225 let mut line = line;
226
227 if let Some((tab_char, tab_width)) = config.expand_tabs {
229 line = expand_tabs_in_line(&line, tab_char, tab_width);
230 }
231
232 line = process_control_chars(&line, config.show_control_chars, config.show_nonprinting);
234
235 all_lines.push(line);
236 }
237
238 let header_str = config.header.as_deref().unwrap_or(filename);
239 let date_str = format_header_date(&date, &config.date_format);
240
241 let body_lines_per_page = if config.omit_header || config.omit_pagination {
243 if config.page_length > 0 {
244 config.page_length
245 } else {
246 DEFAULT_PAGE_LENGTH
247 }
248 } else if config.page_length <= HEADER_LINES + FOOTER_LINES {
249 1
251 } else {
252 config.page_length - HEADER_LINES - FOOTER_LINES
253 };
254
255 let input_lines_per_page = if config.double_space {
257 (body_lines_per_page + 1) / 2
258 } else {
259 body_lines_per_page
260 };
261
262 let columns = config.columns.max(1);
264 let lines_per_column = if columns > 1 && !config.across {
265 input_lines_per_page / columns
266 } else {
267 input_lines_per_page
268 };
269
270 let lines_consumed_per_page = if columns > 1 && !config.across {
271 lines_per_column * columns
272 } else {
273 input_lines_per_page
274 };
275
276 let total_lines = all_lines.len();
278 let mut line_number = config.first_line_number;
279 let mut page_num = 1usize;
280 let mut line_idx = 0;
281
282 while line_idx < total_lines || (line_idx == 0 && total_lines == 0) {
283 if total_lines == 0 && line_idx == 0 {
285 if page_num >= config.first_page
286 && (config.last_page == 0 || page_num <= config.last_page)
287 {
288 if !config.omit_header && !config.omit_pagination {
289 write_header(output, &date_str, header_str, page_num, config)?;
290 }
291 if !config.omit_header && !config.omit_pagination {
292 write_footer(output, config)?;
293 }
294 }
295 break;
296 }
297
298 let page_end = (line_idx + lines_consumed_per_page).min(total_lines);
299
300 if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
301 {
302 if !config.omit_header && !config.omit_pagination {
304 write_header(output, &date_str, header_str, page_num, config)?;
305 }
306
307 if columns > 1 {
309 write_multicolumn_body(
310 output,
311 &all_lines[line_idx..page_end],
312 config,
313 columns,
314 lines_per_column,
315 &mut line_number,
316 body_lines_per_page,
317 )?;
318 } else {
319 write_single_column_body(
320 output,
321 &all_lines[line_idx..page_end],
322 config,
323 &mut line_number,
324 body_lines_per_page,
325 )?;
326 }
327
328 if !config.omit_header && !config.omit_pagination {
330 write_footer(output, config)?;
331 }
332 }
333
334 line_idx = page_end;
335 page_num += 1;
336
337 if line_idx >= total_lines {
339 break;
340 }
341 }
342
343 Ok(())
344}
345
346pub fn pr_merge<W: Write>(
348 inputs: &[Vec<String>],
349 output: &mut W,
350 config: &PrConfig,
351 filenames: &[&str],
352 file_dates: &[SystemTime],
353) -> io::Result<()> {
354 let date = file_dates.first().copied().unwrap_or_else(SystemTime::now);
355 let date_str = format_header_date(&date, &config.date_format);
356 let header_str = config
357 .header
358 .as_deref()
359 .unwrap_or_else(|| filenames.first().copied().unwrap_or(""));
360
361 let body_lines_per_page = if config.omit_header || config.omit_pagination {
362 if config.page_length > 0 {
363 config.page_length
364 } else {
365 DEFAULT_PAGE_LENGTH
366 }
367 } else if config.page_length <= HEADER_LINES + FOOTER_LINES {
368 1
369 } else {
370 config.page_length - HEADER_LINES - FOOTER_LINES
371 };
372
373 let input_lines_per_page = if config.double_space {
374 (body_lines_per_page + 1) / 2
375 } else {
376 body_lines_per_page
377 };
378
379 let num_files = inputs.len();
380 let col_sep = get_column_separator(config);
381 let col_width = if num_files > 1 {
382 (config
383 .page_width
384 .saturating_sub(col_sep.len() * (num_files - 1)))
385 / num_files
386 } else {
387 config.page_width
388 };
389
390 let max_lines = inputs.iter().map(|f| f.len()).max().unwrap_or(0);
391 let mut page_num = 1usize;
392 let mut line_idx = 0;
393 let mut line_number = config.first_line_number;
394
395 while line_idx < max_lines {
396 let page_end = (line_idx + input_lines_per_page).min(max_lines);
397
398 if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
399 {
400 if !config.omit_header && !config.omit_pagination {
401 write_header(output, &date_str, header_str, page_num, config)?;
402 }
403
404 let mut body_lines_written = 0;
405 for i in line_idx..page_end {
406 if config.double_space && body_lines_written > 0 {
407 writeln!(output)?;
408 body_lines_written += 1;
409 }
410
411 let indent_str = " ".repeat(config.indent);
412 write!(output, "{}", indent_str)?;
413
414 if let Some((sep, digits)) = config.number_lines {
415 write!(output, "{:>width$}{}", line_number, sep, width = digits)?;
416 line_number += 1;
417 }
418
419 for (fi, file_lines) in inputs.iter().enumerate() {
420 let content = if i < file_lines.len() {
421 &file_lines[i]
422 } else {
423 ""
424 };
425 let truncated = if config.truncate_lines && content.len() > col_width {
426 &content[..col_width]
427 } else {
428 content
429 };
430 if fi > 0 {
431 write!(output, "{}", col_sep)?;
432 }
433 write!(output, "{:<width$}", truncated, width = col_width)?;
434 }
435 writeln!(output)?;
436 body_lines_written += 1;
437 }
438
439 while body_lines_written < body_lines_per_page {
441 writeln!(output)?;
442 body_lines_written += 1;
443 }
444
445 if !config.omit_header && !config.omit_pagination {
446 write_footer(output, config)?;
447 }
448 }
449
450 line_idx = page_end;
451 page_num += 1;
452 }
453
454 Ok(())
455}
456
457fn write_header<W: Write>(
459 output: &mut W,
460 date_str: &str,
461 header: &str,
462 page_num: usize,
463 config: &PrConfig,
464) -> io::Result<()> {
465 writeln!(output)?;
467 writeln!(output)?;
468
469 let page_str = format!("Page {}", page_num);
472 let line_width = config.page_width;
473
474 let left = date_str;
475 let right = &page_str;
476 let center = header;
477
478 let left_len = left.len();
480 let right_len = right.len();
481 let center_len = center.len();
482
483 if left_len + center_len + right_len + 2 >= line_width {
487 writeln!(output, "{} {} {}", left, center, right)?;
489 } else {
490 let total_spaces = line_width - left_len - center_len - right_len;
491 let left_spaces = total_spaces / 2;
493 let right_spaces = total_spaces - left_spaces;
494 writeln!(
495 output,
496 "{}{}{}{}{}",
497 left,
498 " ".repeat(left_spaces),
499 center,
500 " ".repeat(right_spaces),
501 right
502 )?;
503 }
504
505 writeln!(output)?;
507 writeln!(output)?;
508
509 Ok(())
510}
511
512fn write_footer<W: Write>(output: &mut W, config: &PrConfig) -> io::Result<()> {
514 if config.form_feed {
515 write!(output, "\x0c")?;
516 } else {
517 for _ in 0..FOOTER_LINES {
518 writeln!(output)?;
519 }
520 }
521 Ok(())
522}
523
524fn write_single_column_body<W: Write>(
526 output: &mut W,
527 lines: &[String],
528 config: &PrConfig,
529 line_number: &mut usize,
530 body_lines_per_page: usize,
531) -> io::Result<()> {
532 let indent_str = " ".repeat(config.indent);
533 let mut body_lines_written = 0;
534
535 for (i, line) in lines.iter().enumerate() {
536 if config.double_space && i > 0 {
537 writeln!(output)?;
538 body_lines_written += 1;
539 if body_lines_written >= body_lines_per_page {
540 break;
541 }
542 }
543
544 write!(output, "{}", indent_str)?;
545
546 if let Some((sep, digits)) = config.number_lines {
547 write!(output, "{:>width$}{}", line_number, sep, width = digits)?;
548 *line_number += 1;
549 }
550
551 let content = if config.truncate_lines {
552 let max_w = compute_content_width(config);
553 if line.len() > max_w {
554 &line[..max_w]
555 } else {
556 line.as_str()
557 }
558 } else {
559 line.as_str()
560 };
561
562 writeln!(output, "{}", content)?;
563 body_lines_written += 1;
564 }
565
566 if !config.omit_header && !config.omit_pagination {
568 while body_lines_written < body_lines_per_page {
569 writeln!(output)?;
570 body_lines_written += 1;
571 }
572 }
573
574 Ok(())
575}
576
577fn compute_content_width(config: &PrConfig) -> usize {
579 let mut w = config.page_width;
580 w = w.saturating_sub(config.indent);
581 if let Some((_, digits)) = config.number_lines {
582 w = w.saturating_sub(digits + 1); }
584 w
585}
586
587fn write_multicolumn_body<W: Write>(
589 output: &mut W,
590 lines: &[String],
591 config: &PrConfig,
592 columns: usize,
593 lines_per_column: usize,
594 line_number: &mut usize,
595 body_lines_per_page: usize,
596) -> io::Result<()> {
597 let col_sep = get_column_separator(config);
598 let col_width = if columns > 1 {
599 (config
600 .page_width
601 .saturating_sub(col_sep.len() * (columns - 1)))
602 / columns
603 } else {
604 config.page_width
605 };
606
607 let indent_str = " ".repeat(config.indent);
608 let mut body_lines_written = 0;
609
610 if config.across {
611 let mut i = 0;
613 while i < lines.len() {
614 if config.double_space && body_lines_written > 0 {
615 writeln!(output)?;
616 body_lines_written += 1;
617 if body_lines_written >= body_lines_per_page {
618 break;
619 }
620 }
621
622 write!(output, "{}", indent_str)?;
623
624 for col in 0..columns {
625 let li = i + col;
626 if col > 0 {
627 write!(output, "{}", col_sep)?;
628 }
629 if li < lines.len() {
630 if let Some((sep, digits)) = config.number_lines {
631 write!(output, "{:>width$}{}", line_number, sep, width = digits)?;
632 *line_number += 1;
633 }
634 let content = &lines[li];
635 let truncated = if config.truncate_lines && content.len() > col_width {
636 &content[..col_width]
637 } else {
638 content.as_str()
639 };
640 if col < columns - 1 {
641 write!(output, "{:<width$}", truncated, width = col_width)?;
642 } else {
643 write!(output, "{}", truncated)?;
644 }
645 }
646 }
647 writeln!(output)?;
648 body_lines_written += 1;
649 i += columns;
650 }
651 } else {
652 for row in 0..lines_per_column {
654 if config.double_space && row > 0 {
655 writeln!(output)?;
656 body_lines_written += 1;
657 if body_lines_written >= body_lines_per_page {
658 break;
659 }
660 }
661
662 write!(output, "{}", indent_str)?;
663
664 for col in 0..columns {
665 let li = col * lines_per_column + row;
666 if col > 0 {
667 write!(output, "{}", col_sep)?;
668 }
669 if li < lines.len() {
670 if let Some((sep, digits)) = config.number_lines {
671 let num = config.first_line_number + li;
672 write!(output, "{:>width$}{}", num, sep, width = digits)?;
673 }
674 let content = &lines[li];
675 let truncated = if config.truncate_lines && content.len() > col_width {
676 &content[..col_width]
677 } else {
678 content.as_str()
679 };
680 if col < columns - 1 {
681 write!(output, "{:<width$}", truncated, width = col_width)?;
682 } else {
683 write!(output, "{}", truncated)?;
684 }
685 } else if col < columns - 1 {
686 write!(output, "{:<width$}", "", width = col_width)?;
687 }
688 }
689 writeln!(output)?;
690 body_lines_written += 1;
691 }
692 if config.number_lines.is_some() {
694 *line_number += lines.len();
695 }
696 }
697
698 if !config.omit_header && !config.omit_pagination {
700 while body_lines_written < body_lines_per_page {
701 writeln!(output)?;
702 body_lines_written += 1;
703 }
704 }
705
706 Ok(())
707}