use std::{path::PathBuf, str::FromStr};
use nom::{
IResult, Parser,
character::complete::{char, not_line_ending},
combinator::all_consuming,
};
use serde::{Deserialize, Serialize};
use smol::process::Command;
use crate::{
Result,
error::{Error, check_process_success, map_add_intent},
pane::Pane,
pane_id::{PaneId, parse::pane_id},
parse::quoted_nonempty_string,
session_id::{SessionId, parse::session_id},
window::Window,
window_id::{WindowId, parse::window_id},
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Session {
pub id: SessionId,
pub name: String,
pub dirpath: PathBuf,
}
impl FromStr for Session {
type Err = Error;
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
let desc = "Session";
let intent = "##{session_id}:'##{session_name}':##{session_path}";
let (_, sess) = all_consuming(parse::session)
.parse(input)
.map_err(|e| map_add_intent(desc, intent, e))?;
Ok(sess)
}
}
pub(crate) mod parse {
use super::*;
pub(crate) fn session(input: &str) -> IResult<&str, Session> {
let (input, (id, _, name, _, dirpath)) = (
session_id,
char(':'),
quoted_nonempty_string,
char(':'),
not_line_ending,
)
.parse(input)?;
Ok((
input,
Session {
id,
name: name.to_string(),
dirpath: dirpath.into(),
},
))
}
}
pub async fn available_sessions() -> Result<Vec<Session>> {
let args = vec![
"list-sessions",
"-F",
"#{session_id}:'#{session_name}':#{session_path}",
];
let output = Command::new("tmux").args(&args).output().await?;
let buffer = String::from_utf8(output.stdout)?;
let result: Result<Vec<Session>> = buffer
.trim_end() .split('\n')
.map(Session::from_str)
.collect();
result
}
pub async fn new_session(
session: &Session,
window: &Window,
pane: &Pane,
pane_command: Option<&str>,
) -> Result<(SessionId, WindowId, PaneId)> {
let mut args = vec![
"new-session",
"-d",
"-c",
pane.dirpath.to_str().unwrap(),
"-s",
&session.name,
"-n",
&window.name,
"-P",
"-F",
"#{session_id}:#{window_id}:#{pane_id}",
];
if let Some(pane_command) = pane_command {
args.push(pane_command);
}
let output = Command::new("tmux").args(&args).output().await?;
check_process_success(&output, "new-session")?;
let buffer = String::from_utf8(output.stdout)?;
let buffer = buffer.trim_end();
let desc = "new-session";
let intent = "##{session_id}:##{window_id}:##{pane_id}";
let (_, (new_session_id, _, new_window_id, _, new_pane_id)) =
all_consuming((session_id, char(':'), window_id, char(':'), pane_id))
.parse(buffer)
.map_err(|e| map_add_intent(desc, intent, e))?;
Ok((new_session_id, new_window_id, new_pane_id))
}
#[cfg(test)]
mod tests {
use super::Session;
use super::SessionId;
use crate::Result;
use std::path::PathBuf;
use std::str::FromStr;
#[test]
fn parse_list_sessions() {
let output = [
"$1:'pytorch':/Users/graelo/ml/pytorch",
"$2:'rust':/Users/graelo/rust",
"$3:'server: $':/Users/graelo/swift",
"$4:'tmux-hacking':/Users/graelo/tmux",
];
let sessions: Result<Vec<Session>> =
output.iter().map(|&line| Session::from_str(line)).collect();
let sessions = sessions.expect("Could not parse tmux sessions");
let expected = vec![
Session {
id: SessionId::from_str("$1").unwrap(),
name: String::from("pytorch"),
dirpath: PathBuf::from("/Users/graelo/ml/pytorch"),
},
Session {
id: SessionId::from_str("$2").unwrap(),
name: String::from("rust"),
dirpath: PathBuf::from("/Users/graelo/rust"),
},
Session {
id: SessionId::from_str("$3").unwrap(),
name: String::from("server: $"),
dirpath: PathBuf::from("/Users/graelo/swift"),
},
Session {
id: SessionId::from_str("$4").unwrap(),
name: String::from("tmux-hacking"),
dirpath: PathBuf::from("/Users/graelo/tmux"),
},
];
assert_eq!(sessions, expected);
}
#[test]
fn parse_session_with_large_id() {
let input = "$999:'large-id-session':/home/user/projects";
let session = Session::from_str(input).expect("Should parse session with large id");
assert_eq!(session.id, SessionId::from_str("$999").unwrap());
assert_eq!(session.name, "large-id-session");
assert_eq!(session.dirpath, PathBuf::from("/home/user/projects"));
}
#[test]
fn parse_session_with_spaces_in_path() {
let input = "$5:'dev':/Users/user/My Projects/rust";
let session = Session::from_str(input).expect("Should parse session with spaces in path");
assert_eq!(session.name, "dev");
assert_eq!(
session.dirpath,
PathBuf::from("/Users/user/My Projects/rust")
);
}
#[test]
fn parse_session_with_unicode_in_name() {
let input = "$6:'项目-日本語':/home/user/code";
let session = Session::from_str(input).expect("Should parse session with unicode name");
assert_eq!(session.name, "项目-日本語");
}
#[test]
fn parse_session_fails_on_missing_id() {
let input = "'session-name':/path/to/dir";
let result = Session::from_str(input);
assert!(result.is_err());
}
#[test]
fn parse_session_fails_on_missing_name_quotes() {
let input = "$1:session-name:/path/to/dir";
let result = Session::from_str(input);
assert!(result.is_err());
}
#[test]
fn parse_session_fails_on_empty_name() {
let input = "$1:'':/path/to/dir";
let result = Session::from_str(input);
assert!(result.is_err());
}
#[test]
fn parse_session_fails_on_malformed_id() {
let input = "@1:'session':/path"; let result = Session::from_str(input);
assert!(result.is_err());
}
#[test]
fn parse_session_with_colon_in_path() {
let input = "$7:'test':/path/with:colon/here";
let session = Session::from_str(input).expect("Should parse session with colon in path");
assert_eq!(session.dirpath, PathBuf::from("/path/with:colon/here"));
}
}