use std::cell::OnceCell;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use trusty_mpm::core::architect_launch::live_architect_lineage;
use trusty_mpm::core::architect_session::recorded_session_names;
use super::architect_pane::{Pane, PaneProbe};
use super::tmux_exact_target::{OBJECT_FORMAT, TmuxObject, classify_objects};
use crate::commands::fleet::launch::ARCHITECT_SESSION;
const PANE_FORMAT: &str =
"#{pane_id}\t#{window_id}\t#{session_id}\t#{pane_pid}\t#{pane_marked}\t#{session_name}";
pub(crate) struct LivePanes {
root: Option<PathBuf>,
lineage: OnceCell<Result<Vec<u32>, String>>,
marks: OnceCell<Result<ArchitectMarks, String>>,
deadline: OnceCell<Instant>,
}
pub(super) const LISTING_BUDGET: Duration = Duration::from_secs(2);
#[derive(Debug, Default)]
pub(super) struct ArchitectMarks {
pub(super) lineage: Vec<u32>,
pub(super) sessions: Vec<String>,
}
pub(super) fn architect_marks(
lineage: &Result<Vec<u32>, String>,
sidecars: impl FnOnce() -> Result<Vec<String>, String>,
) -> Result<ArchitectMarks, String> {
match lineage {
Ok(lineage) => Ok(ArchitectMarks {
lineage: lineage.clone(),
sessions: Vec::new(),
}),
Err(err) => sidecars()
.map(|sessions| ArchitectMarks {
lineage: Vec::new(),
sessions,
})
.map_err(|why| format!("the Architect launch records do not read ({err}; {why})")),
}
}
impl LivePanes {
pub(crate) fn ambient() -> Self {
Self {
root: dirs::home_dir()
.filter(|home| home.is_absolute())
.map(|home| home.join(".trusty-mpm")),
lineage: OnceCell::new(),
marks: OnceCell::new(),
deadline: OnceCell::new(),
}
}
fn budget(&self) -> Duration {
let end = *self
.deadline
.get_or_init(|| Instant::now() + LISTING_BUDGET);
end.saturating_duration_since(Instant::now())
}
fn list<T>(
&self,
server: &[String],
format: &str,
classify: impl FnOnce(Listed<'_>) -> T,
) -> T {
let budget = self.budget();
list_panes(server, format, classify, |argv| {
if budget.is_zero() {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"the tmux listing budget is spent",
));
}
trusty_mpm::core::tmux::run_tmux_argv_bounded(argv, budget)
})
}
fn lineage(&self) -> &Result<Vec<u32>, String> {
self.lineage.get_or_init(|| match &self.root {
Some(root) => live_architect_lineage(root),
None => Err("no home directory".into()),
})
}
fn marks(&self) -> &Result<ArchitectMarks, String> {
self.marks.get_or_init(|| {
architect_marks(self.lineage(), || match &self.root {
Some(root) => recorded_session_names(root),
None => Err("no home directory".into()),
})
})
}
}
impl PaneProbe for LivePanes {
fn architect_live(&self) -> Result<bool, String> {
self.lineage()
.as_ref()
.map(|l| !l.is_empty())
.map_err(Clone::clone)
}
fn panes(&self, server: &[String]) -> Result<Vec<Pane>, String> {
self.list(server, PANE_FORMAT, |listed| {
classify_listing(listed, self.marks())
})
}
fn current_pane(&self) -> Option<String> {
std::env::var("TMUX_PANE").ok().filter(|p| !p.is_empty())
}
fn objects(&self, server: &[String]) -> Result<Vec<TmuxObject>, String> {
self.list(server, OBJECT_FORMAT, classify_objects)
}
}
pub(super) fn list_panes<T>(
server: &[String],
format: &str,
classify: impl FnOnce(Listed<'_>) -> T,
run: impl FnOnce(&[String]) -> std::io::Result<std::process::Output>,
) -> T {
let mut argv = server.to_vec();
argv.extend(["list-panes", "-a", "-F", format].map(str::to_owned));
let run = run(&argv);
let (stdout, stderr) = match &run {
Ok(out) => (
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
),
Err(_) => Default::default(),
};
let listed = match &run {
Ok(out) => Listed::Ran {
ok: out.status.success(),
stdout: &stdout,
stderr: &stderr,
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Listed::NotFound,
Err(err) => Listed::Failed(err.to_string()),
};
classify(listed)
}
#[derive(Debug)]
pub(super) enum Listed<'a> {
NotFound,
Failed(String),
Ran {
ok: bool,
stdout: &'a str,
stderr: &'a str,
},
}
pub(super) fn classify_listing(
listed: Listed<'_>,
marks: &Result<ArchitectMarks, String>,
) -> Result<Vec<Pane>, String> {
match listed {
Listed::NotFound => Ok(Vec::new()),
Listed::Failed(err) => Err(err),
Listed::Ran {
ok: false, stderr, ..
} => {
let err = stderr.trim();
if ["no server running", "error connecting to"]
.iter()
.any(|m| err.contains(m))
{
return Ok(Vec::new());
}
Err(err.to_owned())
}
Listed::Ran {
ok: true, stdout, ..
} => {
let marks = marks.as_ref().map_err(Clone::clone)?;
parse_panes(stdout, &marks.lineage, &marks.sessions)
}
}
}
pub(super) fn parse_panes(
text: &str,
lineage: &[u32],
sessions: &[String],
) -> Result<Vec<Pane>, String> {
text.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
let f: Vec<&str> = line.splitn(6, '\t').collect();
let [pane, window, session, pid, marked, name] = f[..] else {
return Err(format!("unreadable tmux pane line {line:?}"));
};
let pid: u32 = pid
.parse()
.map_err(|_| format!("unreadable pane pid in {line:?}"))?;
Ok(Pane {
pane: pane.to_owned(),
window: window.to_owned(),
session: session.to_owned(),
name: name.to_owned(),
architect: name == ARCHITECT_SESSION
|| sessions.iter().any(|s| s == name)
|| lineage.contains(&pid),
marked: marked == "1",
})
})
.collect()
}