Skip to main content

csd/
tmux.rs

1//! Thin wrappers over the `tmux` CLI. Every call shells out to the user's tmux server.
2//!
3//! These are deliberately dumb: build argv, run, map a non-zero exit to [`Error::Tmux`]. Higher
4//! layers ([`crate::commands`]) own the orchestration (readiness retries, sidecar bookkeeping).
5
6use std::process::Command;
7
8use crate::error::{Error, Result};
9
10/// Run a tmux subcommand, returning captured stdout on success.
11fn run(args: &[&str]) -> Result<String> {
12    let output = Command::new("tmux").args(args).output().map_err(|e| {
13        if e.kind() == std::io::ErrorKind::NotFound {
14            Error::TmuxMissing
15        } else {
16            Error::Tmux(e.to_string())
17        }
18    })?;
19
20    if !output.status.success() {
21        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
22        return Err(Error::Tmux(format!("`tmux {}` → {}", args.join(" "), stderr)));
23    }
24
25    Ok(String::from_utf8_lossy(&output.stdout).into_owned())
26}
27
28/// Spawn a detached session named `name`, sized `width`x`height`, with cwd `cwd`, running `command`
29/// (a single shell string the login shell will exec). Returns [`Error::SessionExists`] if taken.
30pub fn new_session(name: &str, width: u16, height: u16, cwd: &str, command: &str) -> Result<()> {
31    if has_session(name)? {
32        return Err(Error::SessionExists(name.to_string()));
33    }
34    run(&[
35        "new-session",
36        "-d",
37        "-s",
38        name,
39        "-x",
40        &width.to_string(),
41        "-y",
42        &height.to_string(),
43        "-c",
44        cwd,
45        command,
46    ])
47    .map(|_| ())
48}
49
50/// Inject literal text into the session's active pane (no key interpretation).
51///
52/// `send-keys -l` is the only injection that landed in the PoC; `paste-buffer` did not.
53pub fn send_literal(name: &str, text: &str) -> Result<()> {
54    require_session(name)?;
55    run(&["send-keys", "-t", name, "-l", text]).map(|_| ())
56}
57
58/// Send a named key (e.g. `Enter`, `Escape`, `C-u`) to the session's active pane.
59pub fn send_key(name: &str, key: &str) -> Result<()> {
60    require_session(name)?;
61    run(&["send-keys", "-t", name, key]).map(|_| ())
62}
63
64/// Capture the visible contents of the session's active pane as plain text.
65pub fn capture_pane(name: &str) -> Result<String> {
66    require_session(name)?;
67    run(&["capture-pane", "-p", "-t", name])
68}
69
70/// Whether a session with this exact name exists on the server.
71pub fn has_session(name: &str) -> Result<bool> {
72    let output = Command::new("tmux")
73        .args(["has-session", "-t", name])
74        .output()
75        .map_err(|e| {
76            if e.kind() == std::io::ErrorKind::NotFound {
77                Error::TmuxMissing
78            } else {
79                Error::Tmux(e.to_string())
80            }
81        })?;
82    // has-session exits non-zero when absent and also when there is no server at all; both mean "no".
83    Ok(output.status.success())
84}
85
86/// Kill a session. Returns [`Error::NoSuchSession`] if it was not running.
87pub fn kill_session(name: &str) -> Result<()> {
88    require_session(name)?;
89    run(&["kill-session", "-t", name]).map(|_| ())
90}
91
92fn require_session(name: &str) -> Result<()> {
93    if !has_session(name)? {
94        return Err(Error::NoSuchSession(name.to_string()));
95    }
96    Ok(())
97}