tmux_lib/
session_id.rs

1//! Session Id.
2
3use std::str::FromStr;
4
5use nom::{
6    character::complete::{char, digit1},
7    combinator::all_consuming,
8    sequence::preceded,
9    IResult, Parser,
10};
11use serde::{Deserialize, Serialize};
12
13use crate::error::{map_add_intent, Error};
14
15/// The id of a Tmux session.
16///
17/// This wraps the raw tmux representation (`$11`).
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct SessionId(String);
20
21impl FromStr for SessionId {
22    type Err = Error;
23
24    /// Parse into SessionId. The `&str` must start with '$' followed by a
25    /// `u16`.
26    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
27        let desc = "SessionId";
28        let intent = "##{session_id}";
29
30        let (_, sess_id) = all_consuming(parse::session_id)
31            .parse(input)
32            .map_err(|e| map_add_intent(desc, intent, e))?;
33
34        Ok(sess_id)
35    }
36}
37
38impl SessionId {
39    pub fn as_str(&self) -> &str {
40        &self.0
41    }
42}
43
44pub(crate) mod parse {
45    use super::*;
46
47    pub fn session_id(input: &str) -> IResult<&str, SessionId> {
48        let (input, digit) = preceded(char('$'), digit1).parse(input)?;
49        let id = format!("${digit}");
50        Ok((input, SessionId(id)))
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn test_parse_session_id_fn() {
60        let actual = parse::session_id("$43");
61        let expected = Ok(("", SessionId("$43".into())));
62        assert_eq!(actual, expected);
63
64        let actual = parse::session_id("$4");
65        let expected = Ok(("", SessionId("$4".into())));
66        assert_eq!(actual, expected);
67    }
68
69    #[test]
70    fn test_parse_session_id_struct() {
71        let actual = SessionId::from_str("$43");
72        assert!(actual.is_ok());
73        assert_eq!(actual.unwrap(), SessionId("$43".into()));
74
75        let actual = SessionId::from_str("4:38");
76        assert!(matches!(
77            actual,
78            Err(Error::ParseError {
79                desc: "SessionId",
80                intent: "##{session_id}",
81                err: _
82            })
83        ));
84    }
85
86    #[test]
87    fn test_parse_session_id_with_large_number() {
88        let session_id = SessionId::from_str("$99999").unwrap();
89        assert_eq!(session_id, SessionId("$99999".into()));
90    }
91
92    #[test]
93    fn test_parse_session_id_zero() {
94        let session_id = SessionId::from_str("$0").unwrap();
95        assert_eq!(session_id, SessionId("$0".into()));
96    }
97
98    #[test]
99    fn test_parse_session_id_fails_on_wrong_prefix() {
100        // @ is for window, % is for pane
101        assert!(SessionId::from_str("@1").is_err());
102        assert!(SessionId::from_str("%1").is_err());
103    }
104
105    #[test]
106    fn test_parse_session_id_fails_on_no_prefix() {
107        assert!(SessionId::from_str("123").is_err());
108    }
109
110    #[test]
111    fn test_parse_session_id_fails_on_empty() {
112        assert!(SessionId::from_str("").is_err());
113        assert!(SessionId::from_str("$").is_err());
114    }
115
116    #[test]
117    fn test_parse_session_id_fails_on_non_numeric() {
118        assert!(SessionId::from_str("$abc").is_err());
119        assert!(SessionId::from_str("$12abc").is_err());
120    }
121
122    #[test]
123    fn test_parse_session_id_fails_on_extra_content() {
124        // all_consuming should reject trailing content
125        assert!(SessionId::from_str("$12:extra").is_err());
126    }
127
128    #[test]
129    fn test_session_id_leaves_remaining_in_parser() {
130        // The parse function (not FromStr) should leave remaining input
131        let (remaining, session_id) = parse::session_id("$42:rest").unwrap();
132        assert_eq!(remaining, ":rest");
133        assert_eq!(session_id, SessionId("$42".into()));
134    }
135}