axl_lib/multiplexer/
tmux.rs1use 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(¤t_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)] #[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}