use std::io::Write;
use std::process::{Command, Stdio};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PagerError {
#[error("pager I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("pager command not found: {0}")]
NotFound(String),
}
pub struct Pager {
pub command: String,
}
impl Default for Pager {
fn default() -> Self {
Self {
command: "less -r".to_string(),
}
}
}
impl Pager {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_command(mut self, command: &str) -> Self {
self.command = command.to_string();
self
}
fn parse_command(&self) -> Result<(&str, Vec<&str>), PagerError> {
let mut parts = self.command.split_whitespace();
let program = parts
.next()
.ok_or_else(|| PagerError::NotFound("empty pager command".to_string()))?;
let args: Vec<&str> = parts.collect();
Ok((program, args))
}
pub fn show(&self, content: &str) -> Result<(), PagerError> {
let (program, args) = self.parse_command()?;
let mut child = Command::new(program)
.args(&args)
.stdin(Stdio::piped())
.spawn()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
PagerError::NotFound(format!("pager program '{}' not found", program))
} else {
PagerError::Io(e)
}
})?;
if let Some(mut stdin) = child.stdin.take() {
match stdin.write_all(content.as_bytes()) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {}
Err(e) => return Err(PagerError::Io(e)),
}
}
child.wait()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_command() {
let pager = Pager::new();
assert_eq!(pager.command, "less -r");
}
#[test]
fn test_default_trait() {
let pager = Pager::default();
assert_eq!(pager.command, "less -r");
}
#[test]
fn test_with_command() {
let pager = Pager::new().with_command("more");
assert_eq!(pager.command, "more");
}
#[test]
fn test_with_command_chaining() {
let pager = Pager::new()
.with_command("bat --paging=always")
.with_command("less -R");
assert_eq!(pager.command, "less -R");
}
#[test]
fn test_parse_command_simple() {
let pager = Pager::new().with_command("less");
let (program, args) = pager.parse_command().unwrap();
assert_eq!(program, "less");
assert!(args.is_empty());
}
#[test]
fn test_parse_command_with_args() {
let pager = Pager::new().with_command("less -r -X");
let (program, args) = pager.parse_command().unwrap();
assert_eq!(program, "less");
assert_eq!(args, vec!["-r", "-X"]);
}
#[test]
fn test_parse_command_empty() {
let pager = Pager::new().with_command("");
let result = pager.parse_command();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, PagerError::NotFound(_)));
}
#[test]
fn test_parse_command_whitespace_only() {
let pager = Pager::new().with_command(" ");
let result = pager.parse_command();
assert!(result.is_err());
}
#[test]
fn test_show_empty_command() {
let pager = Pager::new().with_command("");
let result = pager.show("hello");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not found"));
}
#[test]
fn test_show_nonexistent_pager() {
let pager = Pager::new().with_command("this_pager_does_not_exist_xyz");
let result = pager.show("hello");
assert!(result.is_err());
}
#[test]
fn test_show_with_echo_cat() {
let pager = Pager::new().with_command("cat");
let result = pager.show("Hello, pager!");
assert!(result.is_ok());
}
#[test]
fn test_show_with_true_command() {
let pager = Pager::new().with_command("true");
let result = pager.show("ignored content");
assert!(result.is_ok());
}
#[test]
fn test_show_empty_content() {
let pager = Pager::new().with_command("cat");
let result = pager.show("");
assert!(result.is_ok());
}
#[test]
fn test_show_large_content() {
let content = "x".repeat(100_000);
let pager = Pager::new().with_command("cat");
let result = pager.show(&content);
assert!(result.is_ok());
}
#[test]
fn test_show_unicode_content() {
let pager = Pager::new().with_command("cat");
let result = pager.show("Hello \u{1F600} world \u{2603} \u{00E9}\u{00E8}\u{00EA}");
assert!(result.is_ok());
}
#[test]
fn test_pager_error_display_io() {
let io_err = std::io::Error::other("disk full");
let pager_err = PagerError::Io(io_err);
assert_eq!(pager_err.to_string(), "pager I/O error: disk full");
}
#[test]
fn test_pager_error_display_not_found() {
let err = PagerError::NotFound("mycommand".to_string());
assert_eq!(err.to_string(), "pager command not found: mycommand");
}
#[test]
fn test_pager_error_is_error_trait() {
let err: Box<dyn std::error::Error> = Box::new(PagerError::NotFound("test".to_string()));
assert!(err.to_string().contains("not found"));
}
#[test]
fn test_pager_error_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no access");
let pager_err: PagerError = io_err.into();
assert!(matches!(pager_err, PagerError::Io(_)));
assert!(pager_err.to_string().contains("no access"));
}
#[test]
fn test_pager_error_debug() {
let err = PagerError::NotFound("test_cmd".to_string());
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("NotFound"));
assert!(debug_str.contains("test_cmd"));
}
#[test]
fn test_pager_command_field_public() {
let mut pager = Pager::new();
pager.command = "more".to_string();
assert_eq!(pager.command, "more");
}
}