1use std::{path::PathBuf, str::FromStr};
7
8use nom::{
9 IResult, Parser,
10 character::complete::{char, not_line_ending},
11 combinator::all_consuming,
12};
13use serde::{Deserialize, Serialize};
14use smol::process::Command;
15
16use crate::{
17 Result,
18 error::{Error, check_process_success, map_add_intent},
19 pane::Pane,
20 pane_id::{PaneId, parse::pane_id},
21 parse::quoted_nonempty_string,
22 session_id::{SessionId, parse::session_id},
23 window::Window,
24 window_id::{WindowId, parse::window_id},
25};
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Session {
41 pub id: SessionId,
43 pub name: String,
45 pub dirpath: PathBuf,
47}
48
49impl FromStr for Session {
50 type Err = Error;
51
52 fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
75 let desc = "Session";
76 let intent = "##{session_id}:'##{session_name}':##{session_path}";
77
78 let (_, sess) = all_consuming(parse::session)
79 .parse(input)
80 .map_err(|e| map_add_intent(desc, intent, e))?;
81
82 Ok(sess)
83 }
84}
85
86pub(crate) mod parse {
87 use super::*;
88
89 pub(crate) fn session(input: &str) -> IResult<&str, Session> {
90 let (input, (id, _, name, _, dirpath)) = (
91 session_id,
92 char(':'),
93 quoted_nonempty_string,
94 char(':'),
95 not_line_ending,
96 )
97 .parse(input)?;
98
99 Ok((
100 input,
101 Session {
102 id,
103 name: name.to_string(),
104 dirpath: dirpath.into(),
105 },
106 ))
107 }
108}
109
110pub async fn available_sessions() -> Result<Vec<Session>> {
116 let args = vec![
117 "list-sessions",
118 "-F",
119 "#{session_id}:'#{session_name}':#{session_path}",
120 ];
121
122 let output = Command::new("tmux").args(&args).output().await?;
123 let buffer = String::from_utf8(output.stdout)?;
124
125 let result: Result<Vec<Session>> = buffer
128 .trim_end() .split('\n')
130 .map(Session::from_str)
131 .collect();
132
133 result
134}
135
136pub async fn new_session(
144 session: &Session,
145 window: &Window,
146 pane: &Pane,
147 pane_command: Option<&str>,
148) -> Result<(SessionId, WindowId, PaneId)> {
149 let mut args = vec![
150 "new-session",
151 "-d",
152 "-c",
153 pane.dirpath.to_str().unwrap(),
154 "-s",
155 &session.name,
156 "-n",
157 &window.name,
158 "-P",
159 "-F",
160 "#{session_id}:#{window_id}:#{pane_id}",
161 ];
162 if let Some(pane_command) = pane_command {
163 args.push(pane_command);
164 }
165
166 let output = Command::new("tmux").args(&args).output().await?;
167
168 check_process_success(&output, "new-session")?;
171
172 let buffer = String::from_utf8(output.stdout)?;
173 let buffer = buffer.trim_end();
174
175 let desc = "new-session";
176 let intent = "##{session_id}:##{window_id}:##{pane_id}";
177 let (_, (new_session_id, _, new_window_id, _, new_pane_id)) =
178 all_consuming((session_id, char(':'), window_id, char(':'), pane_id))
179 .parse(buffer)
180 .map_err(|e| map_add_intent(desc, intent, e))?;
181
182 Ok((new_session_id, new_window_id, new_pane_id))
183}
184
185#[cfg(test)]
186mod tests {
187 use super::Session;
188 use super::SessionId;
189 use crate::Result;
190 use std::path::PathBuf;
191 use std::str::FromStr;
192
193 #[test]
194 fn parse_list_sessions() {
195 let output = [
196 "$1:'pytorch':/Users/graelo/ml/pytorch",
197 "$2:'rust':/Users/graelo/rust",
198 "$3:'server: $':/Users/graelo/swift",
199 "$4:'tmux-hacking':/Users/graelo/tmux",
200 ];
201 let sessions: Result<Vec<Session>> =
202 output.iter().map(|&line| Session::from_str(line)).collect();
203 let sessions = sessions.expect("Could not parse tmux sessions");
204
205 let expected = vec![
206 Session {
207 id: SessionId::from_str("$1").unwrap(),
208 name: String::from("pytorch"),
209 dirpath: PathBuf::from("/Users/graelo/ml/pytorch"),
210 },
211 Session {
212 id: SessionId::from_str("$2").unwrap(),
213 name: String::from("rust"),
214 dirpath: PathBuf::from("/Users/graelo/rust"),
215 },
216 Session {
217 id: SessionId::from_str("$3").unwrap(),
218 name: String::from("server: $"),
219 dirpath: PathBuf::from("/Users/graelo/swift"),
220 },
221 Session {
222 id: SessionId::from_str("$4").unwrap(),
223 name: String::from("tmux-hacking"),
224 dirpath: PathBuf::from("/Users/graelo/tmux"),
225 },
226 ];
227
228 assert_eq!(sessions, expected);
229 }
230
231 #[test]
232 fn parse_session_with_large_id() {
233 let input = "$999:'large-id-session':/home/user/projects";
234 let session = Session::from_str(input).expect("Should parse session with large id");
235
236 assert_eq!(session.id, SessionId::from_str("$999").unwrap());
237 assert_eq!(session.name, "large-id-session");
238 assert_eq!(session.dirpath, PathBuf::from("/home/user/projects"));
239 }
240
241 #[test]
242 fn parse_session_with_spaces_in_path() {
243 let input = "$5:'dev':/Users/user/My Projects/rust";
244 let session = Session::from_str(input).expect("Should parse session with spaces in path");
245
246 assert_eq!(session.name, "dev");
247 assert_eq!(
248 session.dirpath,
249 PathBuf::from("/Users/user/My Projects/rust")
250 );
251 }
252
253 #[test]
254 fn parse_session_with_unicode_in_name() {
255 let input = "$6:'项目-日本語':/home/user/code";
256 let session = Session::from_str(input).expect("Should parse session with unicode name");
257
258 assert_eq!(session.name, "项目-日本語");
259 }
260
261 #[test]
262 fn parse_session_fails_on_missing_id() {
263 let input = "'session-name':/path/to/dir";
264 let result = Session::from_str(input);
265
266 assert!(result.is_err());
267 }
268
269 #[test]
270 fn parse_session_fails_on_missing_name_quotes() {
271 let input = "$1:session-name:/path/to/dir";
272 let result = Session::from_str(input);
273
274 assert!(result.is_err());
275 }
276
277 #[test]
278 fn parse_session_fails_on_empty_name() {
279 let input = "$1:'':/path/to/dir";
280 let result = Session::from_str(input);
281
282 assert!(result.is_err());
283 }
284
285 #[test]
286 fn parse_session_fails_on_malformed_id() {
287 let input = "@1:'session':/path"; let result = Session::from_str(input);
289
290 assert!(result.is_err());
291 }
292
293 #[test]
294 fn parse_session_with_colon_in_path() {
295 let input = "$7:'test':/path/with:colon/here";
297 let session = Session::from_str(input).expect("Should parse session with colon in path");
298
299 assert_eq!(session.dirpath, PathBuf::from("/path/with:colon/here"));
300 }
301}