use anyhow::{Context, Result};
use rmux_sdk::{
EnsureSession, Input, Pane, Rmux, RmuxError, Session, SessionName, TerminalSizeSpec,
};
pub(crate) const DEFAULT_COLS: u16 = 200;
pub(crate) const DEFAULT_ROWS: u16 = 50;
#[derive(Debug, Clone)]
pub enum ShellCommand {
Shell(String),
Argv(Vec<String>),
}
#[derive(Debug, Clone)]
pub struct SpawnSpec {
pub name: SessionName,
pub command: ShellCommand,
pub working_directory: Option<String>,
pub environment: Vec<String>,
pub cols: u16,
pub rows: u16,
}
pub async fn spawn_session(rmux: &Rmux, spec: SpawnSpec) -> Result<Session> {
let cols = if spec.cols == 0 {
DEFAULT_COLS
} else {
spec.cols
};
let rows = if spec.rows == 0 {
DEFAULT_ROWS
} else {
spec.rows
};
let mut ensure = EnsureSession::named(spec.name)
.detached(true)
.size(TerminalSizeSpec::new(cols, rows));
ensure = match spec.command {
ShellCommand::Shell(command) => ensure.shell(command),
ShellCommand::Argv(argv) => ensure.argv(argv),
};
if let Some(cwd) = spec.working_directory {
ensure = ensure.working_directory(cwd);
}
if !spec.environment.is_empty() {
ensure = ensure.environment(spec.environment);
}
ensure
.ensure(rmux)
.await
.context("create detached rmux session")
}
pub async fn send_text(session: &Session, text: &str, enter: bool) -> Result<()> {
let pane = session.pane(0, 0);
let payload = if enter {
format!("{text}\n")
} else {
text.to_string()
};
pane.send_text(payload)
.await
.context("send text to rmux pane")
}
pub async fn capture(session: &Session, lines: Option<usize>) -> Result<String> {
let pane = session.pane(0, 0);
let snapshot = pane
.snapshot()
.await
.context("snapshot rmux pane for capture")?;
let mut rows: Vec<String> = snapshot.visible_lines();
while rows.last().is_some_and(|row| row.trim().is_empty()) {
rows.pop();
}
if let Some(tail) = lines {
let start = rows.len().saturating_sub(tail);
rows.drain(..start);
}
Ok(rows.join("\n"))
}
pub async fn list_sessions(rmux: &Rmux) -> Result<Vec<SessionName>> {
rmux.list_sessions().await.context("list rmux sessions")
}
pub async fn broadcast(
rmux: &Rmux,
names: &[SessionName],
text: &str,
enter: bool,
) -> Result<usize> {
if names.is_empty() {
return Ok(0);
}
let mut panes: Vec<Pane> = Vec::with_capacity(names.len());
for name in names {
let session = rmux
.session(name.clone())
.await
.with_context(|| format!("open session {:?} for broadcast", name.as_str()))?;
panes.push(session.pane(0, 0));
}
let payload = if enter {
format!("{text}\n")
} else {
text.to_string()
};
match rmux.broadcast(&panes, Input::text(&payload)).await {
Ok(result) => Ok(result.len()),
Err(RmuxError::PartialBroadcast { source, .. }) => {
let delivered = source.successes().len();
let failed = source.failures().len();
Err(anyhow::anyhow!(
"broadcast partially failed: {delivered} of {} panes accepted the input, \
{failed} rejected it",
delivered + failed
))
}
Err(other) => Err(anyhow::Error::new(other).context("broadcast input to rmux panes")),
}
}
pub async fn kill_session(session: &Session) -> Result<bool> {
session.kill().await.context("kill rmux session")
}