use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub(crate) struct RebindRow {
id: String,
name: String,
outcome: String,
#[serde(default)]
detail: String,
#[serde(default)]
pane_id: Option<String>,
#[serde(default)]
tmux_server: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RebindAll {
results: Vec<RebindRow>,
}
pub(crate) fn render_row(row: &RebindRow) -> String {
let head = format!("{} ({})", row.name, row.id);
match row.outcome.as_str() {
"rebound" => format!(
"{head}: rebound to pane {} on tmux server {}",
row.pane_id.as_deref().unwrap_or("?"),
row.tmux_server.as_deref().unwrap_or("?")
),
"current" if row.detail.is_empty() => format!("{head}: already bound to its live pane"),
"current" => format!("{head}: already bound to its live pane; {}", row.detail),
"skipped" => format!("{head}: skipped — {}; record unchanged", row.detail),
"no_match" => format!("{head}: no match — {}; record unchanged", row.detail),
"ambiguous" => format!("{head}: ambiguous — {}; record unchanged", row.detail),
other => format!("{head}: {other} — {}", row.detail),
}
}
pub(crate) async fn session_rebind(
daemon: &trusty_mpm::client::DaemonClient,
id: Option<String>,
tmux: Option<String>,
) -> anyhow::Result<()> {
let Some(target) = id else {
let resp = daemon
.post("/api/v1/sessions/managed/rebind")
.send()
.await?;
let all: RebindAll = checked(resp).await?.json().await?;
if all.results.is_empty() {
println!("no active or stopped sessions to rebind");
}
for row in &all.results {
println!("{}", render_row(row));
}
let errors = all.results.iter().filter(|r| r.outcome == "error").count();
if errors > 0 {
anyhow::bail!("{errors} session(s) could not be checked");
}
return Ok(());
};
let id = super::managed_route::resolve_managed_match(daemon, &target)
.await
.ok_or_else(|| anyhow::anyhow!("managed session '{target}' not found"))?;
let mut req = daemon.post(format!("/api/v1/sessions/managed/{id}/rebind"));
if let Some(name) = tmux.as_deref() {
req = req.query(&[("tmux", name)]);
}
let row: RebindRow = checked(req.send().await?).await?.json().await?;
let line = render_row(&row);
match row.outcome.as_str() {
"rebound" | "current" => {
println!("{line}");
Ok(())
}
_ => {
eprintln!("error: {line}");
Err(anyhow::anyhow!("session '{target}' was not rebound"))
}
}
}
async fn checked(
resp: trusty_mpm::client::DaemonResponse,
) -> anyhow::Result<trusty_mpm::client::DaemonResponse> {
let status = resp.status();
if status.is_success() {
return Ok(resp);
}
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("daemon returned {status}: {}", body.trim())
}
#[cfg(test)]
mod tests {
use super::*;
fn row(outcome: &str, detail: &str) -> RebindRow {
RebindRow {
id: "id-1".into(),
name: "tm-dogfood".into(),
outcome: outcome.into(),
detail: detail.into(),
pane_id: Some("%1".into()),
tmux_server: Some("66166:1791300000".into()),
}
}
#[test]
fn render_row_names_each_outcome() {
assert_eq!(
render_row(&row("rebound", "")),
"tm-dogfood (id-1): rebound to pane %1 on tmux server 66166:1791300000"
);
assert!(render_row(&row("current", "")).ends_with("already bound to its live pane"));
assert!(
render_row(&row("current", "its runtime is stopped"))
.ends_with("already bound to its live pane; its runtime is stopped")
);
assert!(
render_row(&row("skipped", "on the live server"))
.contains("skipped — on the live server; record unchanged")
);
assert!(render_row(&row("no_match", "gone")).contains("no match — gone; record unchanged"));
assert!(
render_row(&row("ambiguous", "2 panes"))
.contains("ambiguous — 2 panes; record unchanged")
);
assert!(render_row(&row("error", "tmux down")).contains("error — tmux down"));
}
}