use std::process::Command;
use anyhow::{Context, Result};
pub trait PaneSource: Send + Sync {
fn capture(&self, session: &str) -> Result<Vec<String>>;
fn last_activity_secs(&self, _session: &str) -> Option<u64> {
None
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct TmuxPaneSource;
impl PaneSource for TmuxPaneSource {
fn capture(&self, session: &str) -> Result<Vec<String>> {
let output = Command::new("tmux")
.args([
"capture-pane",
"-e",
"-p",
"-J",
"-S",
"-3000",
"-t",
session,
])
.output()
.with_context(|| format!("invoke tmux capture-pane -t {session}"))?;
if !output.status.success() {
return Ok(Vec::new());
}
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.map(|s| s.to_string())
.collect())
}
fn last_activity_secs(&self, session: &str) -> Option<u64> {
let output = Command::new("tmux")
.args(["display-message", "-p", "-t", session, "#{window_activity}"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8_lossy(&output.stdout)
.trim()
.parse::<u64>()
.ok()
}
}
pub fn tail_lines(lines: &[String], n: usize) -> Vec<String> {
let len = lines.len();
let start = len.saturating_sub(n);
lines[start..].to_vec()
}
pub mod test_support {
use super::*;
use std::sync::Mutex;
#[derive(Default)]
pub struct MockPaneSource {
pub lines: Vec<String>,
pub asked: Mutex<Vec<String>>,
pub activity_ts: Option<u64>,
}
impl PaneSource for MockPaneSource {
fn capture(&self, session: &str) -> Result<Vec<String>> {
self.asked.lock().unwrap().push(session.to_string());
Ok(self.lines.clone())
}
fn last_activity_secs(&self, _session: &str) -> Option<u64> {
self.activity_ts
}
}
}
#[cfg(test)]
mod tests {
use super::test_support::MockPaneSource;
use super::*;
use std::sync::Mutex;
#[test]
fn tail_lines_takes_last_n() {
let v: Vec<String> = (0..10).map(|i| format!("line {i}")).collect();
let tail = tail_lines(&v, 3);
assert_eq!(tail, vec!["line 7", "line 8", "line 9"]);
}
#[test]
fn tail_lines_under_n_returns_all() {
let v = vec!["a".to_string(), "b".to_string()];
assert_eq!(tail_lines(&v, 5), v);
}
#[test]
fn tail_lines_empty_returns_empty() {
let v: Vec<String> = Vec::new();
assert!(tail_lines(&v, 5).is_empty());
}
#[test]
fn mock_pane_source_records_session() {
let mock = MockPaneSource {
lines: vec!["hi".into(), "bye".into()],
asked: Mutex::new(Vec::new()),
..Default::default()
};
let lines = mock.capture("t-p-a").unwrap();
assert_eq!(lines, vec!["hi", "bye"]);
assert_eq!(mock.asked.lock().unwrap().clone(), vec!["t-p-a"]);
}
}