Skip to main content

wsx_core/tmux/
session.rs

1// tmux session management via CLI
2// ref: tmux(1)
3
4use super::{tmux_cmd, tmux_silent};
5use anyhow::{bail, Result};
6use std::path::{Path, PathBuf};
7use std::process::Stdio;
8
9/// Check if tmux is available.
10pub fn is_available() -> bool {
11    tmux_cmd(&["-V"])
12        .stdout(Stdio::null())
13        .stderr(Stdio::null())
14        .status()
15        .map(|s| s.success())
16        .unwrap_or(false)
17}
18
19/// Returns true when running inside a tmux session.
20pub fn is_inside_tmux() -> bool {
21    std::env::var("TMUX").is_ok()
22}
23
24/// Return (session_name, session_path) pairs for all active sessions.
25pub fn list_sessions_with_paths() -> Vec<(String, PathBuf)> {
26    let Ok(output) = tmux_cmd(&["list-sessions", "-F", "#{session_name}:#{session_path}"]).output()
27    else {
28        return vec![];
29    };
30
31    String::from_utf8_lossy(&output.stdout)
32        .lines()
33        .filter_map(|line| {
34            let mut parts = line.splitn(2, ':');
35            let name = parts.next()?.trim().to_string();
36            let path = parts.next()?.trim().to_string();
37            if name.is_empty() || path.is_empty() {
38                return None;
39            }
40            Some((name, PathBuf::from(path)))
41        })
42        .collect()
43}
44
45/// Return true if a named session exists.
46pub fn session_exists(name: &str) -> bool {
47    tmux_silent(&["has-session", "-t", name])
48        .status()
49        .map(|s| s.success())
50        .unwrap_or(false)
51}
52
53/// Create a new session with starting directory, detached.
54pub fn create_session(name: &str, start_dir: &Path) -> Result<()> {
55    let status = tmux_silent(&[
56        "new-session",
57        "-d",
58        "-s",
59        name,
60        "-c",
61        &start_dir.to_string_lossy(),
62    ])
63    .status()?;
64    if !status.success() {
65        bail!("tmux new-session failed for {}", name);
66    }
67    Ok(())
68}
69
70/// Kill a session by name.
71pub fn kill_session(name: &str) -> Result<()> {
72    tmux_silent(&["kill-session", "-t", name]).status()?;
73    Ok(())
74}
75
76/// Rename a tmux session.
77pub fn rename_session(old_name: &str, new_name: &str) -> Result<()> {
78    let status = tmux_silent(&["rename-session", "-t", old_name, new_name]).status()?;
79    if !status.success() {
80        bail!("tmux rename-session failed");
81    }
82    Ok(())
83}
84
85pub fn attach_session_cmd(name: &str) -> AttachCommand {
86    if is_inside_tmux() {
87        AttachCommand::SwitchClient(name.to_string())
88    } else {
89        AttachCommand::Attach(name.to_string())
90    }
91}
92
93pub enum AttachCommand {
94    SwitchClient(String),
95    Attach(String),
96}
97
98/// Returns true if the user has a tmux config file (~/.tmux.conf or XDG path).
99pub fn user_has_tmux_config() -> bool {
100    let xdg = std::env::var("XDG_CONFIG_HOME")
101        .map(PathBuf::from)
102        .unwrap_or_else(|_| dirs::home_dir().unwrap_or_default().join(".config"));
103    dirs::home_dir()
104        .map(|h| h.join(".tmux.conf").exists())
105        .unwrap_or(false)
106        || xdg.join("tmux/tmux.conf").exists()
107}
108
109/// Apply wsx server-level defaults once at startup. Best-effort, non-fatal.
110pub fn apply_server_defaults() {
111    let _ = tmux_silent(&["set-option", "-g", "extended-keys", "on"]).status();
112}
113
114/// Apply wsx runtime defaults to a session on every attach. Best-effort, non-fatal.
115pub fn apply_session_defaults(session: &str, mobile_detach_key: Option<&str>) {
116    let _ = tmux_silent(&["set-option", "-t", session, "mouse", "on"]).status();
117    let _ = tmux_silent(&["set-option", "-t", session, "focus-events", "on"]).status();
118    if !user_has_tmux_config() {
119        let _ = tmux_silent(&["set-option", "-t", session, "prefix", "C-a"]).status();
120        let _ = tmux_silent(&["bind-key", "-T", "prefix", "a", "send-prefix"]).status();
121    }
122    if let Some(key) = mobile_detach_key {
123        let _ = tmux_silent(&["bind-key", "-n", key, "detach-client"]).status();
124    }
125}
126
127/// switch-client (inside tmux path).
128pub fn switch_client(name: &str) -> Result<()> {
129    let status = tmux_silent(&["switch-client", "-t", name]).status()?;
130    if !status.success() {
131        bail!("tmux switch-client failed for {}", name);
132    }
133    Ok(())
134}
135
136/// attach-session (outside tmux path) — takes over the terminal.
137pub fn attach_foreground(name: &str) -> Result<()> {
138    tmux_cmd(&["attach-session", "-t", name]).status()?;
139    Ok(())
140}
141
142/// Get tmux server PID via `tmux display-message -p "#{pid}"`.
143pub fn server_pid() -> Option<u32> {
144    let output = tmux_cmd(&["display-message", "-p", "#{pid}"])
145        .output()
146        .ok()?;
147    String::from_utf8_lossy(&output.stdout).trim().parse().ok()
148}
149
150/// tmux user option keys for per-session wsx state shared across instances.
151pub const OPT_MUTED: &str = "@wsx-muted";
152
153/// Set a session-local option (readable as #{@key} in status formats).
154pub fn set_session_opt(session: &str, key: &str, value: &str) {
155    let _ = tmux_silent(&["set-option", "-t", session, key, value]).status();
156}
157
158/// Send keys to a session's active pane, followed by Enter.
159pub fn send_keys(session: &str, keys: &str) -> Result<()> {
160    tmux_silent(&["send-keys", "-t", session, keys, "Enter"]).status()?;
161    Ok(())
162}
163
164/// Send keys without appending Enter.
165pub fn send_keys_raw(session: &str, keys: &str) -> Result<()> {
166    tmux_silent(&["send-keys", "-t", session, keys]).status()?;
167    Ok(())
168}
169
170/// Send Ctrl+C to a session's active pane (no Enter).
171pub fn send_ctrl_c(session: &str) -> Result<()> {
172    tmux_silent(&["send-keys", "-t", session, "C-c"]).status()?;
173    Ok(())
174}
175
176/// Generate a unique session name that doesn't conflict with existing sessions.
177pub fn unique_session_name(base: &str) -> String {
178    if !session_exists(base) {
179        return base.to_string();
180    }
181    let mut n = 2;
182    loop {
183        let candidate = format!("{}_{}", base, n);
184        if !session_exists(&candidate) {
185            return candidate;
186        }
187        n += 1;
188    }
189}