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