use std::path::Path;
use std::process::{Command, Stdio};
use crate::{Error, require};
const TMUX: &str = "tmux";
pub fn validate_session_name(name: &str) -> Result<(), Error> {
if name.is_empty()
|| !name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(Error::Tmux(format!("invalid session name: {name}")));
}
Ok(())
}
pub fn has_session(name: &str) -> bool {
if validate_session_name(name).is_err() {
return false;
}
Command::new(TMUX)
.args(["has-session", "-t", name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn new_session_detached(
name: &str,
cmd: &str,
cwd: Option<&Path>,
env: &[(&str, &str)],
) -> Result<(), Error> {
validate_session_name(name)?;
require(TMUX)?;
if has_session(name) {
return Err(Error::Tmux(format!("session '{name}' already exists")));
}
let mut args: Vec<String> = vec!["new-session".into(), "-d".into(), "-s".into(), name.into()];
if let Some(dir) = cwd {
args.push("-c".into());
args.push(dir.display().to_string());
}
for (k, v) in env {
args.push("-e".into());
args.push(format!("{k}={v}"));
}
args.push(cmd.into());
let status = Command::new(TMUX).args(&args).status()?;
if !status.success() {
return Err(Error::Tmux(format!("failed to create session '{name}'")));
}
let status = Command::new(TMUX)
.args(["set-option", "-t", name, "remain-on-exit", "on"])
.status()?;
if !status.success() {
return Err(Error::Tmux(format!(
"failed to enable remain-on-exit for session '{name}'"
)));
}
Ok(())
}
pub fn session_is_alive(name: &str) -> bool {
if !has_session(name) {
return false;
}
let output = Command::new(TMUX)
.args(["list-panes", "-t", name, "-F", "#{pane_dead}"])
.output();
match output {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
.lines()
.any(|line| line.trim() == "0"),
_ => false,
}
}
pub fn kill_session(name: &str) -> Result<(), Error> {
validate_session_name(name)?;
require(TMUX)?;
if !has_session(name) {
return Ok(());
}
let status = Command::new(TMUX)
.args(["kill-session", "-t", name])
.status()?;
if !status.success() {
return Err(Error::Tmux(format!("failed to kill session '{name}'")));
}
Ok(())
}
pub fn attach(name: &str) -> Result<(), Error> {
validate_session_name(name)?;
require(TMUX)?;
if !has_session(name) {
return Err(Error::Tmux(format!("session '{name}' does not exist")));
}
let status = Command::new(TMUX).args(["attach", "-t", name]).status()?;
if !status.success() {
return Err(Error::Tmux(format!("failed to attach to session '{name}'")));
}
Ok(())
}
pub fn send_keys(name: &str, keys: &str) -> Result<(), Error> {
validate_session_name(name)?;
require(TMUX)?;
if !has_session(name) {
return Err(Error::Tmux(format!("session '{name}' does not exist")));
}
let status = Command::new(TMUX)
.args(["send-keys", "-t", name, keys, "Enter"])
.status()?;
if !status.success() {
return Err(Error::Tmux(format!(
"failed to send keys to session '{name}'"
)));
}
Ok(())
}
pub fn capture_pane(name: &str, lines: Option<usize>) -> Result<String, Error> {
validate_session_name(name)?;
require(TMUX)?;
if !has_session(name) {
return Err(Error::Tmux(format!("session '{name}' does not exist")));
}
let start_arg;
let mut args: Vec<&str> = vec!["capture-pane", "-t", name, "-p"];
if let Some(n) = lines {
start_arg = format!("-{n}");
args.extend(["-S", &start_arg]);
}
let output = Command::new(TMUX).args(&args).output()?;
if !output.status.success() {
return Err(Error::Tmux(format!(
"failed to capture pane from session '{name}'"
)));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
pub fn list_sessions() -> Vec<String> {
let output = Command::new(TMUX)
.args(["list-sessions", "-F", "#{session_name}"])
.output();
match output {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
.lines()
.map(|s| s.to_string())
.collect(),
_ => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_names() {
assert!(validate_session_name("agent0").is_ok());
assert!(validate_session_name("agent-infinity").is_ok());
assert!(validate_session_name("agent_0").is_ok());
assert!(validate_session_name("").is_err());
assert!(validate_session_name("bad name").is_err());
assert!(validate_session_name("bad:name").is_err());
assert!(validate_session_name("bad.name").is_err());
}
#[test]
fn has_session_nonexistent() {
assert!(!has_session("netsky_sh_test_nonexistent_99999"));
assert!(!session_is_alive("netsky_sh_test_nonexistent_99999"));
}
}