use std::sync::Arc;
use axum::{
Router,
extract::{Path as AxumPath, Query, State},
response::IntoResponse,
routing::post,
};
use serde::{Deserialize, Serialize};
use super::summary::parse_id;
use crate::daemon::rpc::managed::outcome::RouteOutcome;
use crate::daemon::state::DaemonState;
use crate::session_manager::pane_rebind::RebindOutcome;
use crate::session_manager::{ManagedError, ManagedSessionState, SessionManager};
pub fn router() -> Router<Arc<DaemonState>> {
Router::new()
.route("/api/v1/sessions/managed/{id}/rebind", post(rebind_route))
.route("/api/v1/sessions/managed/rebind", post(rebind_all_route))
}
#[derive(Debug, Default, Deserialize)]
pub struct RebindQuery {
#[serde(default)]
pub tmux: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct RebindResponse {
pub id: String,
pub name: String,
pub outcome: String,
pub detail: String,
pub pane_id: Option<String>,
pub tmux_server: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct RebindAllResponse {
pub results: Vec<RebindResponse>,
}
fn response(id: String, name: &str, outcome: &RebindOutcome) -> RebindResponse {
let (detail, pane_id, tmux_server, name) = match outcome {
RebindOutcome::Rebound {
tmux_name,
pane_id,
tmux_server,
} => (
String::new(),
Some(pane_id.clone()),
Some(tmux_server.clone()),
tmux_name.clone(),
),
RebindOutcome::Current { stopped: false } => (String::new(), None, None, name.to_owned()),
RebindOutcome::Current { stopped: true } => (
format!("its runtime is stopped; `tm sessions resume {id}` starts it"),
None,
None,
name.to_owned(),
),
RebindOutcome::NoMatch(why)
| RebindOutcome::Ambiguous(why)
| RebindOutcome::Skipped(why) => (why.clone(), None, None, name.to_owned()),
};
RebindResponse {
id,
name,
outcome: outcome.label().to_owned(),
detail,
pane_id,
tmux_server,
}
}
pub async fn rebind_route(
State(state): State<Arc<DaemonState>>,
AxumPath(id_str): AxumPath<String>,
Query(query): Query<RebindQuery>,
) -> impl IntoResponse {
rebind_core(&state, &id_str, query.tmux.as_deref()).await
}
pub(crate) async fn rebind_core(
state: &Arc<DaemonState>,
id_str: &str,
tmux: Option<&str>,
) -> RouteOutcome {
let id = match parse_id(id_str) {
Ok(id) => id,
Err((code, msg)) => return RouteOutcome::text(code.as_u16(), msg),
};
let mgr = state.session_manager().await;
match mgr.rebind_session(&id, tmux).await {
Ok(outcome) => {
let name = mgr.get(&id).await.map(|r| r.tmux_name).unwrap_or_default();
RouteOutcome::ok(&response(id.to_string(), &name, &outcome))
}
Err(ManagedError::SessionNotFound(_)) => {
RouteOutcome::text(404, format!("session {id_str} not found"))
}
Err(ManagedError::InvalidState(_, why)) => RouteOutcome::text(409, why),
Err(e @ ManagedError::TmuxUnavailable(_)) => {
RouteOutcome::text(503, format!("{e}; the record was not changed"))
}
Err(e) => RouteOutcome::text(500, format!("{e}; the record was not changed")),
}
}
pub async fn rebind_all_route(State(state): State<Arc<DaemonState>>) -> impl IntoResponse {
rebind_all_core(&state).await
}
pub(crate) async fn rebind_all_core(state: &Arc<DaemonState>) -> RouteOutcome {
let mgr = state.session_manager().await;
RouteOutcome::ok(&RebindAllResponse {
results: rebind_each(&mgr).await,
})
}
async fn rebind_each(mgr: &SessionManager) -> Vec<RebindResponse> {
let mut results = Vec::new();
for record in mgr.list().await {
if !matches!(
record.state,
ManagedSessionState::Active | ManagedSessionState::Stopped
) {
continue;
}
let id = record.id.to_string();
let row = match mgr.rebind_if_server_replaced(&record.id).await {
Ok(outcome) => response(id, &record.tmux_name, &outcome),
Err(e) => RebindResponse {
id,
name: record.tmux_name.clone(),
outcome: "error".to_owned(),
detail: format!("{e}; the record was not changed"),
pane_id: None,
tmux_server: None,
},
};
results.push(row);
}
results
}