use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::OnceLock;
use std::time::Duration;
use wait_timeout::ChildExt;
use worktrunk::config::UserConfig;
use worktrunk::shell::extract_filename_from_path;
use crate::pager::{git_config_pager, parse_pager_value};
static CACHED_PAGER: OnceLock<Option<String>> = OnceLock::new();
pub(super) const PAGER_TIMEOUT: Duration = Duration::from_millis(2000);
fn needs_paging_disabled(pager_cmd: &str) -> bool {
pager_cmd
.split_whitespace()
.next()
.and_then(extract_filename_from_path)
.is_some_and(|basename| {
basename.eq_ignore_ascii_case("delta")
|| basename.eq_ignore_ascii_case("bat")
|| basename.eq_ignore_ascii_case("batcat")
})
}
pub(super) fn diff_pager() -> Option<&'static String> {
CACHED_PAGER
.get_or_init(|| {
if let Ok(config) = UserConfig::load()
&& let Some(pager) = config.switch_picker(None).pager
&& !pager.trim().is_empty()
{
return Some(pager);
}
let pager = if let Ok(p) = std::env::var("GIT_PAGER") {
parse_pager_value(&p)
} else {
git_config_pager()
};
pager.map(|p| {
if needs_paging_disabled(&p) {
format!("{} --paging=never", p)
} else {
p
}
})
})
.as_ref()
}
pub(super) fn pipe_through_pager(text: &str, pager_cmd: &str, width: usize) -> String {
log::debug!("Piping through pager: {}", pager_cmd);
let mut child = match Command::new("sh")
.arg("-c")
.arg(pager_cmd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.env("COLUMNS", width.to_string())
.env_remove(worktrunk::shell_exec::DIRECTIVE_FILE_ENV_VAR)
.spawn()
{
Ok(child) => child,
Err(e) => {
log::debug!("Failed to spawn pager: {}", e);
return text.to_string();
}
};
let stdin = child.stdin.take();
let input = text.to_string();
let writer_thread = std::thread::spawn(move || {
if let Some(mut stdin) = stdin {
use std::io::Write;
let _ = stdin.write_all(input.as_bytes());
}
});
let stdout = child.stdout.take();
let reader_thread = std::thread::spawn(move || {
stdout.map(|mut stdout| {
let mut output = Vec::new();
let _ = stdout.read_to_end(&mut output);
output
})
});
match child.wait_timeout(PAGER_TIMEOUT) {
Ok(Some(status)) => {
let _ = writer_thread.join();
if let Ok(Some(output)) = reader_thread.join()
&& status.success()
&& let Ok(s) = String::from_utf8(output)
{
return s;
}
log::debug!("Pager exited with status: {}", status);
}
Ok(None) => {
log::debug!("Pager timed out after {:?}", PAGER_TIMEOUT);
let _ = child.kill();
let _ = child.wait();
let _ = reader_thread.join();
}
Err(e) => {
log::debug!("Failed to wait for pager: {}", e);
let _ = child.kill();
let _ = child.wait();
let _ = reader_thread.join();
}
}
text.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_needs_paging_disabled() {
assert!(needs_paging_disabled("delta"));
assert!(needs_paging_disabled("delta --side-by-side"));
assert!(needs_paging_disabled("delta --paging=always"));
assert!(needs_paging_disabled("/usr/bin/delta"));
assert!(needs_paging_disabled(
"/opt/homebrew/bin/delta --line-numbers"
));
assert!(needs_paging_disabled("bat"));
assert!(needs_paging_disabled("/usr/bin/bat"));
assert!(needs_paging_disabled("bat --style=plain"));
assert!(!needs_paging_disabled("less"));
assert!(!needs_paging_disabled("diff-so-fancy"));
assert!(!needs_paging_disabled("colordiff"));
assert!(!needs_paging_disabled("delta-preview"));
assert!(!needs_paging_disabled("/path/to/delta-preview"));
assert!(needs_paging_disabled("batcat"));
assert!(needs_paging_disabled("Delta"));
assert!(needs_paging_disabled("DELTA"));
assert!(needs_paging_disabled("BAT"));
assert!(needs_paging_disabled("Bat"));
assert!(needs_paging_disabled("BatCat"));
assert!(needs_paging_disabled("delta.exe"));
assert!(needs_paging_disabled("Delta.EXE"));
}
#[test]
fn test_get_diff_pager_initializes() {
let _ = diff_pager();
}
#[test]
fn test_pipe_through_pager_passthrough() {
let input = "line 1\nline 2\nline 3";
let result = pipe_through_pager(input, "cat", 80);
assert_eq!(result, input);
}
#[test]
fn test_pipe_through_pager_with_transform() {
let input = "hello world";
let result = pipe_through_pager(input, "tr 'a-z' 'A-Z'", 80);
assert_eq!(result, "HELLO WORLD");
}
#[test]
fn test_pipe_through_pager_invalid_command() {
let input = "original text";
let result = pipe_through_pager(input, "nonexistent-command-xyz", 80);
assert_eq!(result, input);
}
#[test]
fn test_pipe_through_pager_failing_command() {
let input = "original text";
let result = pipe_through_pager(input, "false", 80);
assert_eq!(result, input);
}
}