1use std::process::Command;
12
13use anyhow::{Context, Result};
14
15pub trait PaneSource: Send + Sync {
20 fn capture(&self, session: &str) -> Result<Vec<String>>;
21}
22
23#[derive(Debug, Default, Clone, Copy)]
31pub struct TmuxPaneSource;
32
33impl PaneSource for TmuxPaneSource {
34 fn capture(&self, session: &str) -> Result<Vec<String>> {
35 let output = Command::new("tmux")
36 .args([
37 "capture-pane",
38 "-e",
39 "-p",
40 "-J",
41 "-S",
42 "-3000",
43 "-t",
44 session,
45 ])
46 .output()
47 .with_context(|| format!("invoke tmux capture-pane -t {session}"))?;
48 if !output.status.success() {
49 return Ok(Vec::new());
50 }
51 Ok(String::from_utf8_lossy(&output.stdout)
52 .lines()
53 .map(|s| s.to_string())
54 .collect())
55 }
56}
57
58pub fn tail_lines(lines: &[String], n: usize) -> Vec<String> {
62 let len = lines.len();
63 let start = len.saturating_sub(n);
64 lines[start..].to_vec()
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use std::sync::Mutex;
71
72 #[derive(Default)]
76 pub struct MockPaneSource {
77 pub lines: Vec<String>,
78 pub asked: Mutex<Vec<String>>,
79 }
80
81 impl PaneSource for MockPaneSource {
82 fn capture(&self, session: &str) -> Result<Vec<String>> {
83 self.asked.lock().unwrap().push(session.to_string());
84 Ok(self.lines.clone())
85 }
86 }
87
88 #[test]
89 fn tail_lines_takes_last_n() {
90 let v: Vec<String> = (0..10).map(|i| format!("line {i}")).collect();
91 let tail = tail_lines(&v, 3);
92 assert_eq!(tail, vec!["line 7", "line 8", "line 9"]);
93 }
94
95 #[test]
96 fn tail_lines_under_n_returns_all() {
97 let v = vec!["a".to_string(), "b".to_string()];
98 assert_eq!(tail_lines(&v, 5), v);
99 }
100
101 #[test]
102 fn tail_lines_empty_returns_empty() {
103 let v: Vec<String> = Vec::new();
104 assert!(tail_lines(&v, 5).is_empty());
105 }
106
107 #[test]
108 fn mock_pane_source_records_session() {
109 let mock = MockPaneSource {
110 lines: vec!["hi".into(), "bye".into()],
111 asked: Mutex::new(Vec::new()),
112 };
113 let lines = mock.capture("t-p-a").unwrap();
114 assert_eq!(lines, vec!["hi", "bye"]);
115 assert_eq!(mock.asked.lock().unwrap().clone(), vec!["t-p-a"]);
116 }
117}