use std::process::Command;
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Screened {
pub signal: crate::hook::Signal,
pub ask: Option<String>,
pub question: bool,
}
pub(crate) struct Peek {
panes: Option<Vec<(u32, String)>>,
sys: System,
swept_at: std::time::Instant,
}
const SWEEP_STALE: std::time::Duration = std::time::Duration::from_secs(5);
impl Peek {
pub(crate) fn new() -> Peek {
Peek {
panes: None,
sys: System::new(),
swept_at: std::time::Instant::now() - SWEEP_STALE,
}
}
pub(crate) fn read(&mut self, harness: &str, pid: u32) -> Option<Screened> {
if !crate::ui::tabs::screenable(harness) {
return None;
}
if self.swept_at.elapsed() >= SWEEP_STALE {
self.sys.refresh_processes_specifics(
ProcessesToUpdate::All,
false,
ProcessRefreshKind::nothing(),
);
self.panes = crate::inject::list_panes();
self.swept_at = std::time::Instant::now();
}
let rows = self.screen(pid)?;
finish(harness, &rows)
}
fn screen(&mut self, pid: u32) -> Option<Vec<String>> {
if let Some(rows) = shim_screen(pid) {
return Some(rows);
}
let pane = crate::inject::pane_in(&self.sys, self.panes.as_deref()?, pid)?;
capture(&pane)
}
}
pub(crate) fn named(name: &str) -> Option<Screened> {
let capture = crate::rmux::capture(name)?;
let rows = replay(&capture);
finish(crate::ui::tabs::harness_of(name), &rows)
}
fn finish(harness: &str, rows: &[String]) -> Option<Screened> {
if !crate::ui::tabs::screenable(harness) {
return None;
}
let signal = crate::ui::tabs::screen_state(harness, rows)?;
let asking = signal == crate::hook::Signal::NeedsInput;
let ask = asking
.then(|| crate::ui::tabs::screen_ask(harness, rows))
.flatten();
let question = asking && crate::ui::tabs::screen_question(harness, rows);
Some(Screened {
signal,
ask,
question,
})
}
fn shim_screen(pid: u32) -> Option<Vec<String>> {
let mut attach = crate::attach::attach(pid)?;
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(250);
while !attach.pump() {
if attach.closed() || std::time::Instant::now() >= deadline {
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
attach.pump();
let screen = attach.parser.screen();
let (_, cols) = screen.size();
Some(screen.rows(0, cols).collect())
}
fn capture(target: &str) -> Option<Vec<String>> {
let out = Command::new(crate::rmux::BIN)
.args(["capture-pane", "-p", "-J", "-t", target])
.output()
.ok()?;
if !out.status.success() {
return None;
}
Some(
String::from_utf8_lossy(&out.stdout)
.lines()
.map(String::from)
.collect(),
)
}
fn replay(capture: &crate::rmux::Capture) -> Vec<String> {
let mut parser = vt100::Parser::new(capture.rows, capture.cols, 0);
parser.process(&capture.bytes);
parser.screen().rows(0, capture.cols).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rmux::Capture;
fn capture(rows: u16, cols: u16, lines: &[&str]) -> Capture {
Capture {
rows,
cols,
cursor: None,
bytes: lines.join("\r\n").into_bytes(),
}
}
#[test]
fn a_capture_replays_at_the_size_it_was_taken_at() {
let c = capture(24, 80, &["first", "second", "third"]);
let rows = replay(&c);
assert_eq!(rows.len(), 24, "the parser is not the capture's height");
assert_eq!(rows[0], "first".to_string());
assert_eq!(rows[2], "third".to_string());
assert!(
rows.iter().all(|r| r.chars().count() <= 80),
"a row came back wider than the pane"
);
}
#[test]
fn a_screen_is_only_answered_for_a_harness_that_has_one() {
let asking = vec![
String::new(),
String::from("Do you want to make this edit to src/main.rs?"),
String::from("Esc to cancel"),
];
let claude = finish("claude", &asking).expect("a claude screen");
assert_eq!(claude.signal, crate::hook::Signal::NeedsInput);
assert!(finish("bash", &asking).is_none());
let working = finish(
"claude",
&[String::from("Esc to interrupt"), String::from("thinking")],
)
.expect("a working claude screen");
assert_eq!(working.signal, crate::hook::Signal::Busy);
assert!(working.ask.is_none());
}
#[test]
fn a_screen_with_no_footer_on_it_reads_as_nothing() {
let quiet = vec![String::from("the quick brown fox")];
assert!(finish("claude", &quiet).is_none());
}
}