tmux_lib/
session.rs

1//! This module provides a few types and functions to handle Tmux sessions.
2//!
3//! The main use cases are running Tmux commands & parsing Tmux session
4//! information.
5
6use std::{path::PathBuf, str::FromStr};
7
8use async_std::process::Command;
9use nom::{
10    character::complete::{char, not_line_ending},
11    combinator::all_consuming,
12    sequence::tuple,
13    IResult,
14};
15use serde::{Deserialize, Serialize};
16
17use crate::{
18    error::{map_add_intent, Error},
19    pane::Pane,
20    pane_id::{parse::pane_id, PaneId},
21    parse::quoted_nonempty_string,
22    session_id::{parse::session_id, SessionId},
23    window::Window,
24    window_id::{parse::window_id, WindowId},
25    Result,
26};
27
28/// A Tmux session.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct Session {
31    /// Session identifier, e.g. `$3`.
32    pub id: SessionId,
33    /// Name of the session.
34    pub name: String,
35    /// Working directory of the session.
36    pub dirpath: PathBuf,
37}
38
39impl FromStr for Session {
40    type Err = Error;
41
42    /// Parse a string containing tmux session status into a new `Session`.
43    ///
44    /// This returns a `Result<Session, Error>` as this call can obviously
45    /// fail if provided an invalid format.
46    ///
47    /// The expected format of the tmux status is
48    ///
49    /// ```text
50    /// $1:'pytorch':/Users/graelo/dl/pytorch
51    /// $2:'rust':/Users/graelo/rust
52    /// $3:'server: $~':/Users/graelo/swift
53    /// $4:'tmux-hacking':/Users/graelo/tmux
54    /// ```
55    ///
56    /// This status line is obtained with
57    ///
58    /// ```text
59    /// tmux list-sessions -F "#{session_id}:'#{session_name}':#{session_path}"
60    /// ```
61    ///
62    /// For definitions, look at `Session` type and the tmux man page for
63    /// definitions.
64    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
65        let desc = "Session";
66        let intent = "#{session_id}:'#{session_name}':#{session_path}";
67
68        let (_, sess) =
69            all_consuming(parse::session)(input).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)) = tuple((
80            session_id,
81            char(':'),
82            quoted_nonempty_string,
83            char(':'),
84            not_line_ending,
85        ))(input)?;
86
87        Ok((
88            input,
89            Session {
90                id,
91                name: name.to_string(),
92                dirpath: dirpath.into(),
93            },
94        ))
95    }
96}
97
98// ------------------------------
99// Ops
100// ------------------------------
101
102/// Return a list of all `Session` from the current tmux session.
103pub async fn available_sessions() -> Result<Vec<Session>> {
104    let args = vec![
105        "list-sessions",
106        "-F",
107        "#{session_id}:'#{session_name}':#{session_path}",
108    ];
109
110    let output = Command::new("tmux").args(&args).output().await?;
111    let buffer = String::from_utf8(output.stdout)?;
112
113    // Each call to `Session::parse` returns a `Result<Session, _>`. All results
114    // are collected into a Result<Vec<Session>, _>, thanks to `collect()`.
115    let result: Result<Vec<Session>> = buffer
116        .trim_end() // trim last '\n' as it would create an empty line
117        .split('\n')
118        .map(Session::from_str)
119        .collect();
120
121    result
122}
123
124/// Create a Tmux session (and thus a window & pane).
125///
126/// The new session attributes:
127///
128/// - the session name is taken from the passed `session`
129/// - the working directory is taken from the pane's working directory.
130///
131pub async fn new_session(
132    session: &Session,
133    window: &Window,
134    pane: &Pane,
135    pane_command: Option<&str>,
136) -> Result<(SessionId, WindowId, PaneId)> {
137    let mut args = vec![
138        "new-session",
139        "-d",
140        "-c",
141        pane.dirpath.to_str().unwrap(),
142        "-s",
143        &session.name,
144        "-n",
145        &window.name,
146        "-P",
147        "-F",
148        "#{session_id}:#{window_id}:#{pane_id}",
149    ];
150    if let Some(pane_command) = pane_command {
151        args.push(pane_command);
152    }
153
154    let output = Command::new("tmux").args(&args).output().await?;
155    let buffer = String::from_utf8(output.stdout)?;
156    let buffer = buffer.trim_end();
157
158    let desc = "new-session";
159    let intent = "#{session_id}:#{window_id}:#{pane_id}";
160    let (_, (new_session_id, _, new_window_id, _, new_pane_id)) = all_consuming(tuple((
161        session_id,
162        char(':'),
163        window_id,
164        char(':'),
165        pane_id,
166    )))(buffer)
167    .map_err(|e| map_add_intent(desc, intent, e))?;
168
169    Ok((new_session_id, new_window_id, new_pane_id))
170}
171
172#[cfg(test)]
173mod tests {
174    use super::Session;
175    use super::SessionId;
176    use crate::Result;
177    use std::path::PathBuf;
178    use std::str::FromStr;
179
180    #[test]
181    fn parse_list_sessions() {
182        let output = vec![
183            "$1:'pytorch':/Users/graelo/ml/pytorch",
184            "$2:'rust':/Users/graelo/rust",
185            "$3:'server: $':/Users/graelo/swift",
186            "$4:'tmux-hacking':/Users/graelo/tmux",
187        ];
188        let sessions: Result<Vec<Session>> =
189            output.iter().map(|&line| Session::from_str(line)).collect();
190        let sessions = sessions.expect("Could not parse tmux sessions");
191
192        let expected = vec![
193            Session {
194                id: SessionId::from_str("$1").unwrap(),
195                name: String::from("pytorch"),
196                dirpath: PathBuf::from("/Users/graelo/ml/pytorch"),
197            },
198            Session {
199                id: SessionId::from_str("$2").unwrap(),
200                name: String::from("rust"),
201                dirpath: PathBuf::from("/Users/graelo/rust"),
202            },
203            Session {
204                id: SessionId::from_str("$3").unwrap(),
205                name: String::from("server: $"),
206                dirpath: PathBuf::from("/Users/graelo/swift"),
207            },
208            Session {
209                id: SessionId::from_str("$4").unwrap(),
210                name: String::from("tmux-hacking"),
211                dirpath: PathBuf::from("/Users/graelo/tmux"),
212            },
213        ];
214
215        assert_eq!(sessions, expected);
216    }
217}