use std::env;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::io::Result;
#[cfg(unix)]
use std::fs::File;
#[cfg(unix)]
use std::io::Error;
#[cfg(unix)]
use std::io::IsTerminal;
#[cfg(unix)]
use std::io::Write;
#[cfg(unix)]
use std::io::stdout;
#[cfg(unix)]
use std::os::fd::AsRawFd;
#[cfg(unix)]
use std::os::fd::RawFd;
#[cfg(unix)]
use std::process::Child;
#[cfg(unix)]
use std::process::Command;
#[cfg(unix)]
use std::process::Stdio;
pub const DEFAULT_PAGER_ENV: &str = "PAGER";
pub const NOPAGER_ENV: &str = "NOPAGER";
pub const DEFAULT_PAGER: &str = "less";
#[derive(Debug, Default)]
pub struct Pager {
command: Option<OsString>,
default_command: Option<OsString>,
envs: Vec<(OsString, OsString)>,
}
impl Pager {
pub fn new() -> Self {
Self::default()
}
pub fn command<S: Into<OsString>>(mut self, command: S) -> Self {
self.command = Some(command.into());
self
}
pub fn default_command<S: Into<OsString>>(mut self, command: S) -> Self {
self.default_command = Some(command.into());
self
}
pub fn envs<I, K, V>(mut self, envs: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<OsString>,
V: Into<OsString>,
{
self.envs = envs.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
self
}
#[cfg(unix)]
fn determine_command(&self) -> Option<OsString> {
if env::var_os(NOPAGER_ENV).is_some() {
return None;
}
self.command
.clone()
.or_else(|| env::var_os(DEFAULT_PAGER_ENV))
.or_else(|| self.default_command.clone())
.or_else(|| Some(OsStr::new(DEFAULT_PAGER).into()))
}
pub fn spawn(self) -> Result<PagerSession> {
#[cfg(not(unix))]
{
Ok(PagerSession {})
}
#[cfg(unix)]
{
if !stdout().is_terminal() {
return Ok(PagerSession::new_inactive());
}
let Some(pager_command) = self.determine_command() else {
return Ok(PagerSession::new_inactive());
};
let pager_str =
pager_command.to_str().ok_or_else(|| Error::other("pager command contains non-UTF8 characters"))?;
let args = shell_words::split(pager_str).map_err(Error::other)?;
let Some((program, args)) = args.split_first() else {
return Err(Error::other("pager command is empty"));
};
let pager_process = Command::new(program).args(args).envs(self.envs).stdin(Stdio::piped()).spawn()?;
let pager_stdin_fd = pager_process.stdin.as_ref().expect("stdin must be piped as configured").as_raw_fd();
let original_stdout_fd;
unsafe {
original_stdout_fd = libc::dup(libc::STDOUT_FILENO);
if original_stdout_fd < 0 {
return Err(Error::last_os_error());
}
if libc::dup2(pager_stdin_fd, libc::STDOUT_FILENO) != libc::STDOUT_FILENO {
libc::close(original_stdout_fd);
return Err(Error::last_os_error());
}
}
Ok(PagerSession::new_active(pager_process, original_stdout_fd))
}
}
pub fn page<T, F>(self, action: F) -> Result<T>
where
F: FnOnce(bool) -> T,
{
let session = self.spawn()?;
let result = action(session.is_active());
drop(session);
Ok(result)
}
}
#[must_use = "if the pager session is not bound, it will be dropped immediately, closing the pager"]
pub struct PagerSession {
#[cfg(unix)]
process: Option<Child>,
#[cfg(unix)]
original_stdout_fd: Option<RawFd>,
}
#[cfg(unix)]
impl PagerSession {
fn new_active(process: Child, original_stdout_fd: RawFd) -> Self {
Self { process: Some(process), original_stdout_fd: Some(original_stdout_fd) }
}
fn new_inactive() -> Self {
Self { process: None, original_stdout_fd: None }
}
}
impl PagerSession {
pub fn is_active(&self) -> bool {
#[cfg(unix)]
{
self.process.is_some()
}
#[cfg(not(unix))]
{
false
}
}
}
#[cfg(unix)]
impl Drop for PagerSession {
fn drop(&mut self) {
let Some(mut process) = self.process.take() else {
return;
};
let Some(original_stdout_fd) = self.original_stdout_fd.take() else {
return;
};
unsafe {
if let Ok(null_device) = File::create("/dev/null") {
libc::dup2(null_device.as_raw_fd(), libc::STDOUT_FILENO);
}
let _ = stdout().flush();
libc::dup2(original_stdout_fd, libc::STDOUT_FILENO);
libc::close(original_stdout_fd);
}
let _ = process.wait();
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvGuard {
key: OsString,
old_value: Option<OsString>,
}
impl EnvGuard {
fn set(key: &str, value: &str) -> Self {
let key = OsString::from(key);
let old_value = env::var_os(&key);
unsafe {
env::set_var(&key, value);
}
Self { key, old_value }
}
fn remove(key: &str) -> Self {
let key = OsString::from(key);
let old_value = env::var_os(&key);
unsafe {
env::remove_var(&key);
}
Self { key, old_value }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
if let Some(old_value) = &self.old_value {
env::set_var(&self.key, old_value);
} else {
env::remove_var(&self.key);
}
}
}
}
fn assert_command(pager: &Pager, expected: &str) {
assert_eq!(pager.determine_command(), Some(OsStr::new(expected).into()));
}
#[test]
fn test_pager_disabled_by_nopager_env() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = EnvGuard::set(NOPAGER_ENV, "1");
let pager = Pager::new();
assert!(pager.determine_command().is_none());
}
#[test]
fn test_default_fallback_pager_is_more() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard1 = EnvGuard::remove(NOPAGER_ENV);
let _guard2 = EnvGuard::remove(DEFAULT_PAGER_ENV);
let pager = Pager::new();
assert_command(&pager, DEFAULT_PAGER); }
#[test]
fn test_pager_reads_from_mago_pager_env() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = EnvGuard::set(DEFAULT_PAGER_ENV, "less -R");
let pager = Pager::new();
assert_command(&pager, "less -R");
}
#[test]
fn test_explicit_command_overrides_env() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = EnvGuard::set(DEFAULT_PAGER_ENV, "less");
let pager = Pager::new().command("bat");
assert_command(&pager, "bat");
}
#[test]
fn test_explicit_default_command_is_used_as_fallback() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = EnvGuard::remove(DEFAULT_PAGER_ENV);
let pager = Pager::new().default_command("less");
assert_command(&pager, "less");
}
#[test]
fn test_env_overrides_explicit_default_command() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = EnvGuard::set(DEFAULT_PAGER_ENV, "bat");
let pager = Pager::new().default_command("less");
assert_command(&pager, "bat");
}
}