use std::process::Command;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub const FRESH: Duration = Duration::from_secs(120);
pub const FRESH_FOR_OPTION_P: Duration = Duration::from_secs(15);
pub fn inside() -> bool {
std::env::var_os("TMUX").is_some_and(|v| !v.is_empty())
}
const OPTION_P_KEYS: [&str; 2] = ["M-p", "π"];
const COPY_TABLES: [&str; 2] = ["copy-mode", "copy-mode-vi"];
pub fn prepare_copy_mode() {
let Ok(pane) = std::env::var("TMUX_PANE") else {
return;
};
run(&["set-option", "-p", "-t", &pane, "@peekme", "1"]);
let action = "if-shell -F '#{selection_present}' \
'send-keys -X copy-selection-and-cancel' 'send-keys -X cancel' ; \
send-keys M-p";
for table in COPY_TABLES {
for key in OPTION_P_KEYS {
let bound = Command::new(tmux_bin())
.args(["list-keys", "-T", table, key])
.output()
.is_ok_and(|o| o.status.success() && !o.stdout.is_empty());
if !bound {
run(&[
"bind-key",
"-T",
table,
key,
"if-shell",
"-F",
"#{@peekme}",
action,
]);
}
}
}
}
pub fn release_copy_mode() {
if let Ok(pane) = std::env::var("TMUX_PANE") {
run(&["set-option", "-p", "-u", "-t", &pane, "@peekme"]);
}
}
fn run(args: &[&str]) {
let _ = Command::new(tmux_bin())
.args(args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
fn tmux_bin() -> String {
std::env::var("PEEKME_TMUX_BIN").unwrap_or_else(|_| "tmux".into())
}
fn unix(t: SystemTime) -> u64 {
t.duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs())
}
pub fn newest_buffer() -> Option<String> {
newest(&list_buffers()?, 0).map(|(_, name)| name)
}
pub fn fresh_buffer(baseline: Option<&str>, max_age: Duration) -> Option<String> {
let oldest = unix(SystemTime::now().checked_sub(max_age).unwrap_or(UNIX_EPOCH));
let (_, name) = newest(&list_buffers()?, oldest)?;
if Some(name.as_str()) == baseline {
return None; }
let text = Command::new(tmux_bin())
.args(["show-buffer", "-b", &name])
.output()
.ok()?;
let text = String::from_utf8_lossy(&text.stdout).into_owned();
(!text.trim().is_empty()).then_some(text)
}
pub fn mouse() -> Option<bool> {
let mut args = vec!["display-message", "-p"];
let pane = std::env::var("TMUX_PANE").ok();
if let Some(p) = &pane {
args.extend(["-t", p]);
}
args.push("#{mouse}");
let out = Command::new(tmux_bin()).args(&args).output().ok()?;
match String::from_utf8_lossy(&out.stdout).trim() {
"1" | "on" => Some(true),
"0" | "off" => Some(false),
_ => None,
}
}
fn list_buffers() -> Option<String> {
let out = Command::new(tmux_bin())
.args(["list-buffers", "-F", "#{buffer_created} #{buffer_name}"])
.output()
.ok()?;
Some(String::from_utf8_lossy(&out.stdout).into_owned())
}
fn newest(list: &str, oldest: u64) -> Option<(u64, String)> {
list.lines()
.filter_map(|l| {
let (created, name) = l.split_once(' ')?;
Some((created.parse::<u64>().ok()?, name.to_string()))
})
.find(|(created, _)| *created >= oldest)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn picks_the_newest_fresh_buffer() {
let list = "1790000200 buffer5\n1790000150 buffer3\n1790000100 buffer2\n";
assert_eq!(
newest(list, 1790000000).map(|b| b.1).as_deref(),
Some("buffer5")
);
assert_eq!(newest(list, 1790000201), None, "all too old");
assert_eq!(newest("", 0), None);
}
}