use anyhow::Result;
pub(crate) fn action(args: &[&str]) -> Result<String> {
let out = std::process::Command::new("zellij").arg("action").args(args).stdin(std::process::Stdio::null()).output()?;
anyhow::ensure!(out.status.success(), "zellij {} failed ({})", args[0], out.status);
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
pub(crate) fn find(panes: &[serde_json::Value], pane: u64) -> Option<&serde_json::Value> {
panes.iter().find(|p| p["id"].as_u64() == Some(pane) && p["is_plugin"] != true)
}
pub(crate) fn alive(id: &str) -> bool {
state(id) == Some(true)
}
pub(crate) fn state(id: &str) -> Option<bool> {
let n = id.strip_prefix("terminal_").and_then(|n| n.parse::<u64>().ok())?;
Some(find(&list(&[])?, n).is_some_and(|p| p["exited"] != true))
}
pub(crate) fn list(more: &[&str]) -> Option<Vec<serde_json::Value>> {
let out = action(&[&["list-panes", "-j", "-s"], more].concat()).ok()?;
serde_json::from_str::<serde_json::Value>(&out).ok()?.as_array().cloned()
}
pub(crate) fn offer(pane: u64, prompt: &str) -> Result<bool> {
let id = format!("terminal_{pane}");
if asking(&action(&["dump-screen", "-p", &id])?) {
return Ok(false);
}
anyhow::ensure!(other_tab(pane).is_none(), "it's in another zellij tab (an older dv's): close it there, and Space a starts one here");
action(&["paste", "-p", &id, prompt])?;
show(pane)?;
Ok(true)
}
pub(crate) fn close_if_done(pane: u64, session: &str, since: i64) -> bool {
let Some(last) = crate::claude::turn_ended(session, since) else { return false };
if last.trim_end().ends_with('?') {
return false;
}
let id = format!("terminal_{pane}");
let Ok(screen) = action(&["dump-screen", "-a", "-p", &id]) else { return false };
if asking(&strip_ansi(&screen)) || !input_empty(&screen) {
return false;
}
let panes = list(&["-t"]).unwrap_or_default();
let Some(me) = find(&panes, pane) else { return false };
let tab = me["tab_id"].as_u64().map(|t| t.to_string()).unwrap_or_default();
let shown = || action(&["are-floating-panes-visible", "--tab-id", &tab]).is_ok_and(|o| o.trim() == "true");
if me["is_focused"] == true && (me["is_floating"] != true || shown()) {
return false;
}
action(&["close-pane", "-p", &id]).is_ok()
}
pub(crate) fn input_empty(screen: &str) -> bool {
let lines: Vec<&str> = screen.lines().collect();
let rule = |l: &str| {
let t = strip_ansi(l);
t.trim().chars().count() >= 10 && t.trim().chars().all(|c| c == '─')
};
let rules: Vec<usize> = (0..lines.len()).filter(|&i| rule(lines[i])).collect();
let [.., a, b] = rules[..] else { return false };
!lines[a + 1..b].iter().any(|l| typed(l))
}
fn typed(line: &str) -> bool {
let (mut dim, mut grey) = (false, false);
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next();
let mut params = String::new();
for d in chars.by_ref() {
if d.is_ascii_alphabetic() {
if d == 'm' {
sgr(¶ms, &mut dim, &mut grey);
}
break;
}
params.push(d);
}
}
continue;
}
if !dim && !grey && !c.is_whitespace() && !matches!(c, '❯' | '>' | '\u{a0}' | '│') {
return true;
}
}
false
}
fn sgr(params: &str, dim: &mut bool, grey: &mut bool) {
let n: Vec<u32> = params.split(';').map(|p| p.parse().unwrap_or(0)).collect();
let mut i = 0;
while i < n.len() {
match n[i] {
0 => (*dim, *grey) = (false, false),
2 => *dim = true,
22 => *dim = false,
30..=37 | 39 | 91..=97 => *grey = false,
90 => *grey = true,
38 if n.get(i + 1) == Some(&2) && i + 4 < n.len() => {
let (r, g, b) = (n[i + 2], n[i + 3], n[i + 4]);
*grey = r == g && g == b && r < 200;
i += 4;
}
38 if n.get(i + 1) == Some(&5) => {
*grey = n.get(i + 2).is_some_and(|&c| c == 8 || (232..=250).contains(&c));
i += 2;
}
_ => {}
}
i += 1;
}
}
pub(crate) fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' && chars.peek() == Some(&'[') {
chars.next();
for d in chars.by_ref() {
if d.is_ascii_alphabetic() {
break;
}
}
continue;
}
out.push(c);
}
out
}
pub(crate) fn screen(pane: u64) -> String {
action(&["dump-screen", "-p", &format!("terminal_{pane}")]).unwrap_or_default()
}
pub(crate) fn show(pane: u64) -> Result<()> {
match other_tab(pane) {
Some(t) => action(&["go-to-tab-by-id", &t.to_string()]),
None => action(&["focus-pane-id", &format!("terminal_{pane}")]),
}
.map(drop)
}
pub(crate) fn working(screen: &str) -> bool {
screen.contains("esc to interrupt") || screen.lines().any(|l| l.split_once("… (").is_some_and(|(_, t)| t.starts_with(|c: char| c.is_ascii_digit())))
}
pub(crate) fn asking(screen: &str) -> bool {
["Do you want", "Would you like", "❯ 1.", "trust this folder"].iter().any(|p| screen.contains(p))
}
fn other_tab(pane: u64) -> Option<u64> {
let panes = list(&["-t"])?;
let tab = |n: u64| find(&panes, n).and_then(|p| p["tab_id"].as_u64());
let own = std::env::var("ZELLIJ_PANE_ID").ok().and_then(|id| id.parse().ok()).and_then(tab)?;
tab(pane).filter(|&t| t != own)
}