axl_lib/multiplexer/
tmux.rs

1use anyhow::Result;
2use colored::Colorize;
3use std::{
4    env,
5    path::{Path, PathBuf},
6    process::{Command, Output},
7};
8use tracing::{debug, error, info, instrument, trace, warn};
9
10use crate::{config::config_env::ConfigEnvKey, error::AxlError, helper::wrap_command};
11
12pub struct Tmux;
13
14impl Tmux {
15    #[instrument(err)]
16    pub fn open(path: &Path, name: &str) -> Result<()> {
17        info!(
18            "Attempting to open Tmux session with path: {:?}, name: {:?}!",
19            path, name,
20        );
21
22        if !path.exists() {
23            return Err(
24                AxlError::ProjectPathDoesNotExist(path.to_string_lossy().to_string()).into(),
25            );
26        }
27
28        if !Self::in_session() {
29            Self::create_new_attached_attach_if_exists(name, path)?;
30        } else if Self::has_session(name) {
31            info!("Session '{name}' already exists, opening.");
32            Self::switch(name)?;
33        } else {
34            info!("Session '{name}' does not already exist, creating and opening.",);
35
36            if Self::create_new_detached(name, path).is_ok_and(|o| o.status.success()) {
37                Self::switch(name)?;
38            } else {
39                eprintln!("{}", "Session failed to open.".red().bold());
40            }
41        }
42
43        Ok(())
44    }
45
46    #[instrument(err)]
47    pub fn open_existing(name: &str) -> Result<()> {
48        info!(
49            "Attempting to open existing Tmux session with name: {:?}!",
50            name,
51        );
52
53        if !Self::in_session() {
54            trace!("Not currently in session, attempting to attach to tmux session",);
55            Self::attach()?;
56        }
57
58        Self::switch(name)?;
59
60        Ok(())
61    }
62
63    #[instrument]
64    pub fn list_sessions() -> Result<Vec<String>> {
65        Ok(
66            String::from_utf8_lossy(&wrap_command(Command::new("tmux").arg("ls"))?.stdout)
67                .trim_end()
68                .split('\n')
69                .map(|s| s.split(':').collect::<Vec<_>>()[0].to_string())
70                .filter(|s| !s.is_empty())
71                .collect(),
72        )
73    }
74
75    #[instrument]
76    pub fn get_current_session() -> String {
77        String::from_utf8_lossy(
78            &wrap_command(
79                Command::new("tmux")
80                    .arg("display-message")
81                    .arg("-p")
82                    .arg("#S"),
83            )
84            .expect("tmux should be able to show current session")
85            .stdout,
86        )
87        .trim_end()
88        .to_string()
89    }
90
91    #[instrument(err)]
92    pub fn kill_sessions(sessions: &[String], current_session: &str) -> Result<()> {
93        sessions
94            .iter()
95            .filter(|s| *s != current_session)
96            .for_each(|s| {
97                if Self::kill_session(s).is_ok() {
98                    if s.is_empty() {
99                        warn!("No session picked");
100                    } else {
101                        info!("Killed {}.", s);
102                    }
103                } else {
104                    error!("Error while killing {}.", s)
105                }
106            });
107
108        if sessions.contains(&current_session.to_string()) {
109            debug!("current session [{current_session}] was included to be killed.");
110
111            if Self::kill_session(current_session).is_ok() {
112                if current_session.is_empty() {
113                    warn!("No session picked");
114                } else {
115                    info!("Killed {current_session}.");
116                }
117            } else {
118                error!("Error while killing {current_session}.")
119            }
120        }
121
122        Ok(())
123    }
124
125    #[instrument(err)]
126    pub fn unique_session() -> Result<()> {
127        for i in 0..10 {
128            let name = &i.to_string();
129            if !Self::has_session(name) {
130                if Self::create_new_detached(name, &PathBuf::try_from(ConfigEnvKey::Home)?)
131                    .is_ok_and(|o| o.status.success())
132                {
133                    Self::switch(name)?;
134                    break;
135                } else {
136                    eprintln!("{}", "Session failed to open.".red().bold());
137                }
138            }
139        }
140        Ok(())
141    }
142}
143
144impl Tmux {
145    #[allow(dead_code)] // This will likely be needed eventually.
146    #[instrument(err)]
147    fn create_new_detached_attach_if_exists(name: &str, path: &Path) -> Result<Output> {
148        wrap_command(Command::new("tmux").args([
149            "new-session",
150            "-Ad",
151            "-s",
152            name,
153            "-c",
154            path.to_str().unwrap_or_default(),
155        ]))
156    }
157
158    #[instrument(err)]
159    fn create_new_attached_attach_if_exists(name: &str, path: &Path) -> Result<Output> {
160        wrap_command(Command::new("tmux").args([
161            "new-session",
162            "-A",
163            "-s",
164            name,
165            "-c",
166            path.to_str().unwrap_or_default(),
167        ]))
168    }
169
170    #[instrument(err)]
171    fn create_new_detached(name: &str, path: &Path) -> Result<Output> {
172        wrap_command(Command::new("tmux").args([
173            "new-session",
174            "-d",
175            "-s",
176            name,
177            "-c",
178            path.to_str().unwrap_or_default(),
179        ]))
180    }
181
182    #[instrument(err)]
183    fn switch(to_name: &str) -> Result<Output> {
184        wrap_command(Command::new("tmux").args(["switch-client", "-t", to_name]))
185    }
186
187    #[instrument(err)]
188    fn attach() -> Result<Output> {
189        wrap_command(Command::new("tmux").args(["attach"]))
190    }
191
192    #[instrument]
193    fn has_session(project_name: &str) -> bool {
194        let output = wrap_command(Command::new("tmux").args([
195            "has-session",
196            "-t",
197            &format!("={}", project_name),
198        ]));
199
200        output.is_ok_and(|o| o.status.success())
201    }
202
203    #[instrument(err)]
204    fn kill_session(project_name: &str) -> Result<()> {
205        wrap_command(Command::new("tmux").args([
206            "kill-session",
207            "-t",
208            &format!("={}", project_name),
209        ]))?;
210        Ok(())
211    }
212
213    #[instrument]
214    fn in_session() -> bool {
215        env::var("TMUX").is_ok()
216    }
217}