use super::architect_pane::PaneProbe;
use super::architect_pane_parse::{Hit, Target, UNPARSED, may_run_tmux, tmux_hits};
use super::architect_pane_probe::Listed;
use super::architect_pane_reach::without_data_bodies;
pub(crate) const TMUX_TARGET_RULE: &str = "tmux-target-unresolved";
pub(super) const OBJECT_FORMAT: &str = "#{session_id}\t#{window_id}\t#{window_index}\t\
#{window_active}\t#{pane_id}\t#{pane_index}\t#{session_name}\t#{window_name}";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TmuxObject {
pub(crate) session: String,
pub(crate) window: String,
pub(crate) window_index: String,
pub(crate) window_active: bool,
pub(crate) pane: String,
pub(crate) pane_index: String,
pub(crate) session_name: String,
pub(crate) window_name: String,
}
type Listing = Result<Vec<TmuxObject>, String>;
const SPECIAL: &[char] = &[
'{', '}', '~', '!', '^', '*', '?', '[', ']', '#', ' ', '\t', '\n',
];
pub(crate) fn evaluate_tmux_exact_target(command: &str, probe: &dyn PaneProbe) -> Option<String> {
let hits = tmux_hits(command);
if hits.is_empty() {
return None;
}
let current = (!command.contains("TMUX"))
.then(|| probe.current_pane())
.flatten();
let mut listed: Vec<(&[String], Listing)> = Vec::new();
for hit in &hits {
if let Some(why) = hit.opaque {
if why != UNPARSED || may_run_tmux(&without_data_bodies(command), 0) {
return Some(unreadable(hit, why));
}
continue;
}
let pane = PANE_VERBS.contains(&hit.verb.as_str());
for target in &hit.targets {
let text = match target {
Target::Literal(text) => text,
Target::Dynamic(text) => {
return Some(refusal(hit, text, "the shell expands it"));
}
Target::Current | Target::Marked | Target::Server | Target::Picked => continue,
};
if !listed.iter().any(|(s, _)| *s == hit.server.as_slice()) {
listed.push((&hit.server, probe.objects(&hit.server)));
}
let Some((_, listing)) = listed.iter().find(|(s, _)| *s == hit.server.as_slice())
else {
return Some(unlisted(hit, text, "no listing"));
};
let objects = match listing {
Ok(objects) => objects,
Err(err) => return Some(unlisted(hit, text, err)),
};
if let Err(why) = resolve_exactly(text, objects, current.as_deref(), pane) {
return Some(refusal(hit, text, why));
}
}
}
None
}
const PANE_VERBS: &[&str] = &[
"send-keys",
"send-prefix",
"paste-buffer",
"pipe-pane",
"kill-pane",
"respawn-pane",
"swap-pane",
"join-pane",
"move-pane",
"break-pane",
"clear-history",
];
const PANE_POSITIONS: &[&str] = &[
"top",
"bottom",
"left",
"right",
"top-left",
"top-right",
"bottom-left",
"bottom-right",
"topleft",
"topright",
"bottomleft",
"bottomright",
"up-of",
"down-of",
"left-of",
"right-of",
];
pub(super) fn resolve_exactly(
text: &str,
rows: &[TmuxObject],
current: Option<&str>,
pane: bool,
) -> Result<(), &'static str> {
if text.is_empty() || text == "=" || text.contains(SPECIAL) {
return Err("it is empty, a special token, a glob or a format");
}
if text.starts_with(['+', '-']) {
return Err("it is a relative token");
}
let Some((session, rest)) = text.split_once(':') else {
if pane && PANE_POSITIONS.iter().any(|w| text.eq_ignore_ascii_case(w)) {
return Err(
"it is a pane position word, which tmux reads as a pane of the \
caller's window before any session",
);
}
return bare(text, rows);
};
let in_session = session_rows(session, rows, current)?;
let (window, pane) = rest.split_once('.').unwrap_or((rest, ""));
let in_window = window_rows(window, &in_session)?;
pane_exists(pane, &in_window)
}
fn is_id(word: &str, sigil: char) -> bool {
word.strip_prefix(sigil)
.is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()))
}
fn is_index(word: &str) -> bool {
!word.is_empty() && word.bytes().all(|b| b.is_ascii_digit())
}
fn bare(text: &str, rows: &[TmuxObject]) -> Result<(), &'static str> {
if let Some(exact) = text.strip_prefix('=') {
let found = rows
.iter()
.any(|r| r.session_name == exact || r.window_name == exact);
return found
.then_some(())
.ok_or("no session or window has that exact name");
}
if text.starts_with(['%', '@', '$']) {
let (id, pane) = text.split_once('.').unwrap_or((text, ""));
let keep: Vec<&TmuxObject> = match id.chars().next() {
Some('%') if is_id(id, '%') && pane.is_empty() => {
rows.iter().filter(|r| r.pane == id).collect()
}
Some('@') if is_id(id, '@') => rows.iter().filter(|r| r.window == id).collect(),
Some('$') if is_id(id, '$') && pane.is_empty() => {
rows.iter().filter(|r| r.session == id).collect()
}
_ => return Err("it is not a whole `%N`, `@N` or `$N` id"),
};
if keep.is_empty() {
return Err("no pane, window or session has that id");
}
return pane_exists(pane, &keep);
}
if text.contains('.') {
return Err("a `window.pane` with no session part reads a session tmux picks");
}
if is_index(text) {
return Err("a bare number is an index in a session tmux picks");
}
if rows.iter().any(|r| r.window_name.starts_with(text)) {
return Err("it also matches a window name, which tmux tries before a session");
}
if rows.iter().any(|r| r.session_name == text) {
return Ok(());
}
Err(missing_session(text, rows))
}
fn missing_session(name: &str, rows: &[TmuxObject]) -> &'static str {
if rows.iter().any(|r| r.session_name.starts_with(name)) {
"it is only a prefix of a session name, and tmux would pick that session"
} else {
"no session has that name"
}
}
fn session_rows<'a>(
part: &str,
rows: &'a [TmuxObject],
current: Option<&str>,
) -> Result<Vec<&'a TmuxObject>, &'static str> {
let first = if part.is_empty() {
let current = current.ok_or("its session is the caller's, which is unknown")?;
rows.iter()
.find(|r| r.pane == current)
.ok_or("its session is the caller's, which is not on this server")?
} else if part.starts_with(['%', '@', '$']) {
if !(is_id(part, '%') || is_id(part, '@') || is_id(part, '$')) {
return Err("it is not a whole `%N`, `@N` or `$N` id");
}
rows.iter()
.find(|r| r.pane == part || r.window == part || r.session == part)
.ok_or("no pane, window or session has that id")?
} else if let Some(exact) = part.strip_prefix('=') {
rows.iter()
.find(|r| r.session_name == exact)
.ok_or("no session has that exact name")?
} else {
rows.iter()
.find(|r| r.session_name == part)
.ok_or_else(|| missing_session(part, rows))?
};
Ok(rows.iter().filter(|r| r.session == first.session).collect())
}
fn window_rows<'a>(
part: &str,
rows: &[&'a TmuxObject],
) -> Result<Vec<&'a TmuxObject>, &'static str> {
let pick = |keep: &dyn Fn(&TmuxObject) -> bool| -> Vec<&'a TmuxObject> {
rows.iter().copied().filter(|r| keep(r)).collect()
};
let keep = if part.is_empty() {
pick(&|r| r.window_active)
} else if part.starts_with(['@', '%']) {
if !(is_id(part, '@') || is_id(part, '%')) {
return Err("it is not a whole `@N` or `%N` id");
}
let window = rows
.iter()
.find(|r| r.window == part || r.pane == part)
.map(|r| r.window.clone());
pick(&|r| Some(&r.window) == window.as_ref())
} else if part.starts_with('$') {
return Err("it is a special window token");
} else if let Some(exact) = part.strip_prefix('=') {
pick(&|r| r.window_name == exact)
} else {
let by_index = if is_index(part) {
pick(&|r| r.window_index == part)
} else {
Vec::new()
};
if by_index.is_empty() {
let by_name = pick(&|r| r.window_name == part);
if by_name.is_empty() && rows.iter().any(|r| r.window_name.starts_with(part)) {
return Err(
"it is only a prefix of a window name, and tmux would pick that window",
);
}
by_name
} else {
by_index
}
};
if keep.is_empty() {
return Err("no window of that session matches it exactly");
}
Ok(keep)
}
fn pane_exists(part: &str, rows: &[&TmuxObject]) -> Result<(), &'static str> {
let found = if part.is_empty() {
true
} else if is_id(part, '%') {
rows.iter().any(|r| r.pane == part)
} else if is_index(part) {
rows.iter().any(|r| r.pane_index == part)
} else {
return Err("its pane part is not a pane index or `%N` id");
};
found
.then_some(())
.ok_or("no pane of that window matches it")
}
pub(super) fn classify_objects(listed: Listed<'_>) -> Result<Vec<TmuxObject>, String> {
match listed {
Listed::NotFound => Err("no tmux binary".into()),
Listed::Failed(err) => Err(err),
Listed::Ran {
ok: false, stderr, ..
} => {
let err = stderr.trim();
let absent = ["(No such file or directory)", "(Connection refused)"]
.iter()
.any(|m| err.contains(m));
if err.contains("no server running") || (err.contains("error connecting to") && absent)
{
return Ok(Vec::new());
}
Err(format!("tmux list-panes failed: {err}"))
}
Listed::Ran {
ok: true, stdout, ..
} => parse_objects(stdout),
}
}
fn parse_objects(text: &str) -> Result<Vec<TmuxObject>, String> {
text.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
let f: Vec<&str> = line.split('\t').collect();
let [
session,
window,
window_index,
active,
pane,
pane_index,
s_name,
w_name,
] = f[..]
else {
return Err(format!("unreadable tmux pane line {line:?}"));
};
Ok(TmuxObject {
session: session.to_owned(),
window: window.to_owned(),
window_index: window_index.to_owned(),
window_active: active == "1",
pane: pane.to_owned(),
pane_index: pane_index.to_owned(),
session_name: s_name.to_owned(),
window_name: w_name.to_owned(),
})
})
.collect()
}
const REMEDY: &str = "Name an existing target exactly: `=session:window.pane`, a window or \
pane index, or an id (`%N`, `@N`, `$N`). Create a session in an earlier call, not in the \
same command. `TRUSTY_MPM_PM_UNRESTRICTED` / `TRUSTY_MPM_DISABLE_HOOKS` do not lift this \
floor.";
fn refusal(hit: &Hit, target: &str, why: &str) -> String {
format!(
"Hard-floor deny (#9001): `tmux {}` targets `{target}`, which does not resolve exactly \
to an existing tmux session, window or pane ({why}). tmux matches an unknown name by \
prefix, so the command could reach another live session (owner ruling 2026-10-01). \
{REMEDY}",
hit.verb
)
}
fn unreadable(hit: &Hit, why: &str) -> String {
let shown = match (&hit.token, hit.verb.as_str()) {
(Some(token), _) => token.clone(),
(None, "") => "tmux".to_owned(),
(None, verb) => format!("tmux {verb}"),
};
format!(
"Hard-floor deny (#9001): the guard cannot read the tmux command at `{shown}` ({why}), \
so it cannot check that its target resolves exactly to an existing tmux session, \
window or pane. A target the guard cannot resolve is refused (owner ruling \
2026-10-01). {REMEDY}"
)
}
fn unlisted(hit: &Hit, target: &str, err: &str) -> String {
format!(
"Hard-floor deny (#9001): `tmux {}` targets `{target}`, and the guard cannot list that \
tmux server to check the target exists ({err}). A target the guard cannot resolve is \
refused (owner ruling 2026-10-01). {REMEDY}",
hit.verb
)
}
#[cfg(test)]
#[path = "tmux_exact_target_tests.rs"]
mod tests;