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
67pub mod test_support {
71 use super::*;
72 use std::sync::Mutex;
73
74 #[derive(Default)]
78 pub struct MockPaneSource {
79 pub lines: Vec<String>,
80 pub asked: Mutex<Vec<String>>,
81 }
82
83 impl PaneSource for MockPaneSource {
84 fn capture(&self, session: &str) -> Result<Vec<String>> {
85 self.asked.lock().unwrap().push(session.to_string());
86 Ok(self.lines.clone())
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::test_support::MockPaneSource;
94 use super::*;
95 use std::sync::Mutex;
96
97 #[test]
98 fn tail_lines_takes_last_n() {
99 let v: Vec<String> = (0..10).map(|i| format!("line {i}")).collect();
100 let tail = tail_lines(&v, 3);
101 assert_eq!(tail, vec!["line 7", "line 8", "line 9"]);
102 }
103
104 #[test]
105 fn tail_lines_under_n_returns_all() {
106 let v = vec!["a".to_string(), "b".to_string()];
107 assert_eq!(tail_lines(&v, 5), v);
108 }
109
110 #[test]
111 fn tail_lines_empty_returns_empty() {
112 let v: Vec<String> = Vec::new();
113 assert!(tail_lines(&v, 5).is_empty());
114 }
115
116 #[test]
117 fn mock_pane_source_records_session() {
118 let mock = MockPaneSource {
119 lines: vec!["hi".into(), "bye".into()],
120 asked: Mutex::new(Vec::new()),
121 };
122 let lines = mock.capture("t-p-a").unwrap();
123 assert_eq!(lines, vec!["hi", "bye"]);
124 assert_eq!(mock.asked.lock().unwrap().clone(), vec!["t-p-a"]);
125 }
126}