tmux_lib/
window.rs

1//! This module provides a few types and functions to handle Tmux windows.
2//!
3//! The main use cases are running Tmux commands & parsing Tmux window information.
4
5use std::str::FromStr;
6
7use async_std::process::Command;
8
9use nom::{
10    character::complete::{char, digit1},
11    combinator::{all_consuming, map_res, recognize},
12    sequence::tuple,
13    IResult,
14};
15use serde::{Deserialize, Serialize};
16
17use crate::{
18    error::{check_empty_process_output, map_add_intent, Error},
19    layout::{self, window_layout},
20    pane::Pane,
21    pane_id::{parse::pane_id, PaneId},
22    parse::{boolean, quoted_nonempty_string},
23    session::Session,
24    window_id::{parse::window_id, WindowId},
25    Result,
26};
27
28/// A Tmux window.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct Window {
31    /// Window identifier, e.g. `@3`.
32    pub id: WindowId,
33    /// Index of the Window in the Session.
34    pub index: u16,
35    /// Describes whether the Window is active.
36    pub is_active: bool,
37    /// Describes how panes are laid out in the Window.
38    pub layout: String,
39    /// Name of the Window.
40    pub name: String,
41    /// Name of Sessions to which this Window is attached.
42    pub sessions: Vec<String>,
43}
44
45impl FromStr for Window {
46    type Err = Error;
47
48    /// Parse a string containing the tmux window status into a new `Window`.
49    ///
50    /// This returns a `Result<Window, Error>` as this call can obviously
51    /// fail if provided an invalid format.
52    ///
53    /// The expected format of the tmux status is
54    ///
55    /// ```text
56    /// @1:0:true:035d,334x85,0,0{167x85,0,0,1,166x85,168,0[166x48,168,0,2,166x36,168,49,3]}:'ignite':'pytorch'
57    /// @2:1:false:4438,334x85,0,0[334x41,0,0{167x41,0,0,4,166x41,168,0,5},334x43,0,42{167x43,0,42,6,166x43,168,42,7}]:'dates-attn':'pytorch'
58    /// @3:2:false:9e8b,334x85,0,0{167x85,0,0,8,166x85,168,0,9}:'th-bits':'pytorch'
59    /// @4:3:false:64ef,334x85,0,0,10:'docker-pytorch':'pytorch'
60    /// @5:0:true:64f0,334x85,0,0,11:'ben':'rust'
61    /// @6:1:false:64f1,334x85,0,0,12:'pyo3':'rust'
62    /// @7:2:false:64f2,334x85,0,0,13:'mdns-repeater':'rust'
63    /// @8:0:true:64f3,334x85,0,0,14:'combine':'swift'
64    /// @9:0:false:64f4,334x85,0,0,15:'copyrat':'tmux-hacking'
65    /// @10:1:false:ae3a,334x85,0,0[334x48,0,0,17,334x36,0,49{175x36,0,49,18,158x36,176,49,19}]:'mytui-app':'tmux-hacking'
66    /// @11:2:true:e2e2,334x85,0,0{175x85,0,0,20,158x85,176,0[158x42,176,0,21,158x42,176,43,27]}:'tmux-backup':'tmux-hacking'
67    /// ```
68    ///
69    /// This status line is obtained with
70    ///
71    /// ```text
72    /// tmux list-windows -a -F "#{window_id}:#{window_index}:#{?window_active,true,false}:#{window_layout}:'#{window_name}':'#{window_linked_sessions_list}'"
73    /// ```
74    ///
75    /// For definitions, look at `Window` type and the tmux man page for
76    /// definitions.
77    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
78        let desc = "Window";
79        let intent = "#{window_id}:#{window_index}:#{?window_active,true,false}:#{window_layout}:'#{window_name}':'#{window_linked_sessions_list}'";
80
81        let (_, window) =
82            all_consuming(parse::window)(input).map_err(|e| map_add_intent(desc, intent, e))?;
83
84        Ok(window)
85    }
86}
87
88impl Window {
89    /// Return all `PaneId` in this window.
90    pub fn pane_ids(&self) -> Vec<PaneId> {
91        let layout = layout::parse_window_layout(&self.layout).unwrap();
92        layout.pane_ids().iter().map(PaneId::from).collect()
93    }
94}
95
96pub(crate) mod parse {
97    use super::*;
98
99    pub(crate) fn window(input: &str) -> IResult<&str, Window> {
100        let (input, (id, _, index, _, is_active, _, layout, _, name, _, session_names)) =
101            tuple((
102                window_id,
103                char(':'),
104                map_res(digit1, str::parse),
105                char(':'),
106                boolean,
107                char(':'),
108                recognize(window_layout),
109                char(':'),
110                quoted_nonempty_string,
111                char(':'),
112                quoted_nonempty_string,
113            ))(input)?;
114
115        Ok((
116            input,
117            Window {
118                id,
119                index,
120                is_active,
121                layout: layout.to_string(),
122                name: name.to_string(),
123                sessions: vec![session_names.to_string()],
124            },
125        ))
126    }
127}
128
129// ------------------------------
130// Ops
131// ------------------------------
132
133/// Return a list of all `Window` from all sessions.
134pub async fn available_windows() -> Result<Vec<Window>> {
135    let args = vec![
136        "list-windows",
137        "-a",
138        "-F",
139        "#{window_id}\
140        :#{window_index}\
141        :#{?window_active,true,false}\
142        :#{window_layout}\
143        :'#{window_name}'\
144        :'#{window_linked_sessions_list}'",
145    ];
146
147    let output = Command::new("tmux").args(&args).output().await?;
148    let buffer = String::from_utf8(output.stdout)?;
149
150    // Note: each call to the `Window::from_str` returns a `Result<Window, _>`.
151    // All results are then collected into a Result<Vec<Window>, _>, via
152    // `collect()`.
153    let result: Result<Vec<Window>> = buffer
154        .trim_end() // trim last '\n' as it would create an empty line
155        .split('\n')
156        .map(Window::from_str)
157        .collect();
158
159    result
160}
161
162/// Create a Tmux window in a session exactly named as the passed `session`.
163///
164/// The new window attributes:
165///
166/// - created in the `session`
167/// - the window name is taken from the passed `window`
168/// - the working directory is the pane's working directory.
169///
170pub async fn new_window(
171    session: &Session,
172    window: &Window,
173    pane: &Pane,
174    pane_command: Option<&str>,
175) -> Result<(WindowId, PaneId)> {
176    let exact_session_name = format!("={}", session.name);
177
178    let mut args = vec![
179        "new-window",
180        "-d",
181        "-c",
182        pane.dirpath.to_str().unwrap(),
183        "-n",
184        &window.name,
185        "-t",
186        &exact_session_name,
187        "-P",
188        "-F",
189        "#{window_id}:#{pane_id}",
190    ];
191    if let Some(pane_command) = pane_command {
192        args.push(pane_command);
193    }
194
195    let output = Command::new("tmux").args(&args).output().await?;
196    let buffer = String::from_utf8(output.stdout)?;
197    let buffer = buffer.trim_end();
198
199    let desc = "new-window";
200    let intent = "#{window_id}:#{pane_id}";
201
202    let (_, (new_window_id, _, new_pane_id)) =
203        all_consuming(tuple((window_id, char(':'), pane_id)))(buffer)
204            .map_err(|e| map_add_intent(desc, intent, e))?;
205
206    Ok((new_window_id, new_pane_id))
207}
208
209/// Apply the provided `layout` to the window with `window_id`.
210pub async fn set_layout(layout: &str, window_id: &WindowId) -> Result<()> {
211    let args = vec!["select-layout", "-t", window_id.as_str(), layout];
212
213    let output = Command::new("tmux").args(&args).output().await?;
214    check_empty_process_output(&output, "select-layout")
215}
216
217/// Select (make active) the window with `window_id`.
218pub async fn select_window(window_id: &WindowId) -> Result<()> {
219    let args = vec!["select-window", "-t", window_id.as_str()];
220
221    let output = Command::new("tmux").args(&args).output().await?;
222    check_empty_process_output(&output, "select-window")
223}
224
225#[cfg(test)]
226mod tests {
227    use super::Window;
228    use super::WindowId;
229    use crate::Result;
230    use std::str::FromStr;
231
232    #[test]
233    fn parse_list_sessions() {
234        let output = vec![
235            "@1:0:true:035d,334x85,0,0{167x85,0,0,1,166x85,168,0[166x48,168,0,2,166x36,168,49,3]}:'ignite':'pytorch'",
236            "@2:1:false:4438,334x85,0,0[334x41,0,0{167x41,0,0,4,166x41,168,0,5},334x43,0,42{167x43,0,42,6,166x43,168,42,7}]:'dates-attn':'pytorch'",
237            "@3:2:false:9e8b,334x85,0,0{167x85,0,0,8,166x85,168,0,9}:'th-bits':'pytorch'",
238            "@4:3:false:64ef,334x85,0,0,10:'docker-pytorch':'pytorch'",
239            "@5:0:true:64f0,334x85,0,0,11:'ben':'rust'",
240            "@6:1:false:64f1,334x85,0,0,12:'pyo3':'rust'",
241            "@7:2:false:64f2,334x85,0,0,13:'mdns-repeater':'rust'",
242            "@8:0:true:64f3,334x85,0,0,14:'combine':'swift'",
243            "@9:0:false:64f4,334x85,0,0,15:'copyrat':'tmux-hacking'",
244            "@10:1:false:ae3a,334x85,0,0[334x48,0,0,17,334x36,0,49{175x36,0,49,18,158x36,176,49,19}]:'mytui-app':'tmux-hacking'",
245            "@11:2:true:e2e2,334x85,0,0{175x85,0,0,20,158x85,176,0[158x42,176,0,21,158x42,176,43,27]}:'tmux-backup':'tmux-hacking'",
246        ];
247        let sessions: Result<Vec<Window>> =
248            output.iter().map(|&line| Window::from_str(line)).collect();
249        let windows = sessions.expect("Could not parse tmux sessions");
250
251        let expected = vec![
252            Window {
253                id: WindowId::from_str("@1").unwrap(),
254                index: 0,
255                is_active: true,
256                layout: String::from(
257                    "035d,334x85,0,0{167x85,0,0,1,166x85,168,0[166x48,168,0,2,166x36,168,49,3]}",
258                ),
259                name: String::from("ignite"),
260                sessions: vec![String::from("pytorch")],
261            },
262            Window {
263                id: WindowId::from_str("@2").unwrap(),
264                index: 1,
265                is_active: false,
266                layout: String::from(
267                    "4438,334x85,0,0[334x41,0,0{167x41,0,0,4,166x41,168,0,5},334x43,0,42{167x43,0,42,6,166x43,168,42,7}]",
268                ),
269                name: String::from("dates-attn"),
270                sessions: vec![String::from("pytorch")],
271            },
272            Window {
273                id: WindowId::from_str("@3").unwrap(),
274                index: 2,
275                is_active: false,
276                layout: String::from(
277                    "9e8b,334x85,0,0{167x85,0,0,8,166x85,168,0,9}",
278                ),
279                name: String::from("th-bits"),
280                sessions: vec![String::from("pytorch")],
281            },
282            Window {
283                id: WindowId::from_str("@4").unwrap(),
284                index: 3,
285                is_active: false,
286                layout: String::from(
287                    "64ef,334x85,0,0,10",
288                ),
289                name: String::from("docker-pytorch"),
290                sessions: vec![String::from("pytorch")],
291            },
292            Window {
293                id: WindowId::from_str("@5").unwrap(),
294                index: 0,
295                is_active: true,
296                layout: String::from(
297                    "64f0,334x85,0,0,11",
298                ),
299                name: String::from("ben"),
300                sessions: vec![String::from("rust")],
301            },
302            Window {
303                id: WindowId::from_str("@6").unwrap(),
304                index: 1,
305                is_active: false,
306                layout: String::from(
307                    "64f1,334x85,0,0,12",
308                ),
309                name: String::from("pyo3"),
310                sessions: vec![String::from("rust")],
311            },
312            Window {
313                id: WindowId::from_str("@7").unwrap(),
314                index: 2,
315                is_active: false,
316                layout: String::from(
317                    "64f2,334x85,0,0,13",
318                ),
319                name: String::from("mdns-repeater"),
320                sessions: vec![String::from("rust")],
321            },
322            Window {
323                id: WindowId::from_str("@8").unwrap(),
324                index: 0,
325                is_active: true,
326                layout: String::from(
327                    "64f3,334x85,0,0,14",
328                ),
329                name: String::from("combine"),
330                sessions: vec![String::from("swift")],
331            },
332            Window {
333                id: WindowId::from_str("@9").unwrap(),
334                index: 0,
335                is_active: false,
336                layout: String::from(
337                    "64f4,334x85,0,0,15",
338                ),
339                name: String::from("copyrat"),
340                sessions: vec![String::from("tmux-hacking")],
341            },
342            Window {
343                id: WindowId::from_str("@10").unwrap(),
344                index: 1,
345                is_active: false,
346                layout: String::from(
347                    "ae3a,334x85,0,0[334x48,0,0,17,334x36,0,49{175x36,0,49,18,158x36,176,49,19}]",
348                ),
349                name: String::from("mytui-app"),
350                sessions: vec![String::from("tmux-hacking")],
351            },
352            Window {
353                id: WindowId::from_str("@11").unwrap(),
354                index: 2,
355                is_active: true,
356                layout: String::from(
357                    "e2e2,334x85,0,0{175x85,0,0,20,158x85,176,0[158x42,176,0,21,158x42,176,43,27]}",
358                ),
359                name: String::from("tmux-backup"),
360                sessions: vec![String::from("tmux-hacking")],
361            },
362        ];
363
364        assert_eq!(windows, expected);
365    }
366}