use anyhow::{anyhow, Context, Result};
use serde::Serialize;
use tokio::process::Command;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Session {
pub name: String,
pub windows: i32,
pub attached: i32,
pub created_unix: i64,
}
pub async fn list_sessions() -> Result<Vec<Session>> {
let output = Command::new("tmux")
.args([
"list-sessions",
"-F",
"#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}",
])
.output()
.await
.context("failed to execute tmux")?;
if !output.status.success() {
let msg = String::from_utf8_lossy(&output.stderr).trim().to_string();
if msg.contains("failed to connect to server") || msg.contains("no server running") {
return Ok(vec![]);
}
return Err(anyhow!("tmux list-sessions failed: {}", msg));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut out = vec![];
for line in stdout.lines() {
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() != 4 {
continue;
}
out.push(Session {
name: parts[0].to_string(),
windows: parts[1].parse().unwrap_or(0),
attached: parts[2].parse().unwrap_or(0),
created_unix: parts[3].parse().unwrap_or(0),
});
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
}
pub async fn new_session(name: &str) -> Result<()> {
let output = Command::new("tmux")
.args(["new-session", "-d", "-s", name])
.output()
.await
.context("failed to execute tmux")?;
if !output.status.success() {
let msg = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(anyhow!("tmux new-session failed: {}", msg));
}
Ok(())
}
pub async fn kill_session(name: &str) -> Result<()> {
let output = Command::new("tmux")
.args(["kill-session", "-t", name])
.output()
.await
.context("failed to execute tmux")?;
if !output.status.success() {
let msg = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(anyhow!("tmux kill-session failed: {}", msg));
}
Ok(())
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Pane {
pub index: String,
pub title: String,
pub active: bool,
}
pub async fn list_panes(session: &str) -> Result<Vec<Pane>> {
let output = Command::new("tmux")
.args([
"list-windows",
"-t", session,
"-F",
"#{window_index}\t#{window_name}\t#{window_active}",
])
.output()
.await
.context("failed to execute tmux")?;
if !output.status.success() {
let msg = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(anyhow!("tmux list-windows failed: {}", msg));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut out = vec![];
for line in stdout.lines() {
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() != 3 {
continue;
}
out.push(Pane {
index: parts[0].to_string(),
title: parts[1].to_string(),
active: parts[2] == "1",
});
}
Ok(out)
}
pub async fn select_pane(session: &str, window_index: &str) -> Result<()> {
let target = format!("{}:{}", session, window_index);
let output = Command::new("tmux")
.args(["select-window", "-t", &target])
.output()
.await
.context("failed to execute tmux")?;
if !output.status.success() {
let msg = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(anyhow!("tmux select-window failed: {}", msg));
}
Ok(())
}
pub async fn send_line(session: &str, text: &str) -> Result<()> {
let set = Command::new("tmux")
.args(["set-buffer", "--", text])
.output()
.await
.context("failed to execute tmux set-buffer")?;
if !set.status.success() {
let msg = String::from_utf8_lossy(&set.stderr).trim().to_string();
return Err(anyhow!("tmux set-buffer failed: {}", msg));
}
let paste = Command::new("tmux")
.args(["paste-buffer", "-t", session])
.output()
.await
.context("failed to execute tmux paste-buffer")?;
if !paste.status.success() {
let msg = String::from_utf8_lossy(&paste.stderr).trim().to_string();
return Err(anyhow!("tmux paste-buffer failed: {}", msg));
}
let enter = Command::new("tmux")
.args(["send-keys", "-t", session, "Enter"])
.output()
.await
.context("failed to execute tmux send-keys")?;
if !enter.status.success() {
let msg = String::from_utf8_lossy(&enter.stderr).trim().to_string();
return Err(anyhow!("tmux send-keys failed: {}", msg));
}
Ok(())
}
pub async fn run_command(session: &str, command: &str) -> Result<String> {
let next_pane = format!("{}:.+", session);
let prev_pane = format!("{}:.-", session);
let args: Vec<&str> = match command {
"new-window" => vec!["new-window", "-t", session],
"kill-window" => vec!["kill-window", "-t", session],
"split-h" => vec!["split-window", "-h", "-t", session],
"split-v" => vec!["split-window", "-v", "-t", session],
"next-window" => vec!["next-window", "-t", session],
"prev-window" => vec!["previous-window", "-t", session],
"next-pane" => vec!["select-pane", "-t", &next_pane],
"prev-pane" => vec!["select-pane", "-t", &prev_pane],
"kill-pane" => vec!["kill-pane", "-t", session],
"zoom-pane" => vec!["resize-pane", "-Z", "-t", session],
_ => return Err(anyhow!("unknown command: {}", command)),
};
let output = Command::new("tmux")
.args(&args)
.output()
.await
.context("failed to execute tmux")?;
if !output.status.success() {
let msg = String::from_utf8_lossy(&output.stderr).trim().to_string();
if msg.contains("no remaining") || msg.contains("session not found") {
return Ok(msg);
}
return Err(anyhow!("tmux {} failed: {}", command, msg));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
pub async fn capture_history(session: &str, lines: i32) -> Result<String> {
let start = format!("-{}", lines);
let output = Command::new("tmux")
.args([
"capture-pane",
"-p", "-e", "-S", &start, "-t", session,
])
.output()
.await
.context("failed to execute tmux capture-pane")?;
if !output.status.success() {
let msg = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(anyhow!("tmux capture-pane failed: {}", msg));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}