Skip to main content

tmux_lib/
window_id.rs

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