tmux_lib/
pane.rs

1//! This module provides a few types and functions to handle Tmux Panes.
2//!
3//! The main use cases are running Tmux commands & parsing Tmux panes
4//! information.
5
6use std::path::PathBuf;
7use std::str::FromStr;
8
9use async_std::process::Command;
10use nom::{
11    character::complete::{char, digit1, not_line_ending},
12    combinator::{all_consuming, map_res},
13    sequence::tuple,
14    IResult,
15};
16use serde::{Deserialize, Serialize};
17
18use crate::{
19    error::{check_empty_process_output, map_add_intent, Error},
20    pane_id::{parse::pane_id, PaneId},
21    parse::{boolean, quoted_nonempty_string},
22    window_id::WindowId,
23    Result,
24};
25
26/// A Tmux pane.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Pane {
29    /// Pane identifier, e.g. `%37`.
30    pub id: PaneId,
31    /// Describes the Pane index in the Window
32    pub index: u16,
33    /// Describes if the pane is currently active (focused).
34    pub is_active: bool,
35    /// Title of the Pane (usually defaults to the hostname)
36    pub title: String,
37    /// Current dirpath of the Pane
38    pub dirpath: PathBuf,
39    /// Current command executed in the Pane
40    pub command: String,
41}
42
43impl FromStr for Pane {
44    type Err = Error;
45
46    /// Parse a string containing tmux panes status into a new `Pane`.
47    ///
48    /// This returns a `Result<Pane, Error>` as this call can obviously
49    /// fail if provided an invalid format.
50    ///
51    /// The expected format of the tmux status is
52    ///
53    /// ```text
54    /// %20:0:false:'rmbp':'nvim':/Users/graelo/code/rust/tmux-backup
55    /// %21:1:true:'rmbp':'tmux':/Users/graelo/code/rust/tmux-backup
56    /// %27:2:false:'rmbp':'man man':/Users/graelo/code/rust/tmux-backup
57    /// ```
58    ///
59    /// This status line is obtained with
60    ///
61    /// ```text
62    /// tmux list-panes -F "#{pane_id}:#{pane_index}:#{?pane_active,true,false}:'#{pane_title}':'#{pane_current_command}':#{pane_current_path}"
63    /// ```
64    ///
65    /// For definitions, look at `Pane` type and the tmux man page for
66    /// definitions.
67    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
68        let desc = "Pane";
69        let intent = "#{pane_id}:#{pane_index}:#{?pane_active,true,false}:'#{pane_title}':'#{pane_current_command}':#{pane_current_path}";
70
71        let (_, pane) =
72            all_consuming(parse::pane)(input).map_err(|e| map_add_intent(desc, intent, e))?;
73
74        Ok(pane)
75    }
76}
77
78impl Pane {
79    /// Return the entire Pane content as a `Vec<u8>`.
80    ///
81    /// # Note
82    ///
83    /// The output contains the escape codes, joined lines with trailing spaces. This output is
84    /// processed by the function `tmux_lib::utils::cleanup_captured_buffer`.
85    ///
86    pub async fn capture(&self) -> Result<Vec<u8>> {
87        let args = vec![
88            "capture-pane",
89            "-t",
90            self.id.as_str(),
91            "-J", // preserves trailing spaces & joins any wrapped lines
92            "-e", // include escape sequences for text & background
93            "-p", // output goes to stdout
94            "-S", // starting line number
95            "-",  // start of history
96            "-E", // ending line number
97            "-",  // end of history
98        ];
99
100        let output = Command::new("tmux").args(&args).output().await?;
101
102        Ok(output.stdout)
103    }
104}
105
106pub(crate) mod parse {
107    use super::*;
108
109    pub(crate) fn pane(input: &str) -> IResult<&str, Pane> {
110        let (input, (id, _, index, _, is_active, _, title, _, command, _, dirpath)) =
111            tuple((
112                pane_id,
113                char(':'),
114                map_res(digit1, str::parse),
115                char(':'),
116                boolean,
117                char(':'),
118                quoted_nonempty_string,
119                char(':'),
120                quoted_nonempty_string,
121                char(':'),
122                not_line_ending,
123            ))(input)?;
124
125        Ok((
126            input,
127            Pane {
128                id,
129                index,
130                is_active,
131                title: title.into(),
132                dirpath: dirpath.into(),
133                command: command.into(),
134            },
135        ))
136    }
137}
138
139// ------------------------------
140// Ops
141// ------------------------------
142
143/// Return a list of all `Pane` from all sessions.
144pub async fn available_panes() -> Result<Vec<Pane>> {
145    let args = vec![
146        "list-panes",
147        "-a",
148        "-F",
149        "#{pane_id}\
150        :#{pane_index}\
151        :#{?pane_active,true,false}\
152        :'#{pane_title}'\
153        :'#{pane_current_command}'\
154        :#{pane_current_path}",
155    ];
156
157    let output = Command::new("tmux").args(&args).output().await?;
158    let buffer = String::from_utf8(output.stdout)?;
159
160    // Each call to `Pane::parse` returns a `Result<Pane, _>`. All results
161    // are collected into a Result<Vec<Pane>, _>, thanks to `collect()`.
162    let result: Result<Vec<Pane>> = buffer
163        .trim_end() // trim last '\n' as it would create an empty line
164        .split('\n')
165        .map(Pane::from_str)
166        .collect();
167
168    result
169}
170
171/// Create a new pane (horizontal split) in the window with `window_id`, and return the new
172/// pane id.
173pub async fn new_pane(
174    reference_pane: &Pane,
175    pane_command: Option<&str>,
176    window_id: &WindowId,
177) -> Result<PaneId> {
178    let mut args = vec![
179        "split-window",
180        "-h",
181        "-c",
182        reference_pane.dirpath.to_str().unwrap(),
183        "-t",
184        window_id.as_str(),
185        "-P",
186        "-F",
187        "#{pane_id}",
188    ];
189    if let Some(pane_command) = pane_command {
190        args.push(pane_command);
191    }
192
193    let output = Command::new("tmux").args(&args).output().await?;
194    let buffer = String::from_utf8(output.stdout)?;
195
196    let new_id = PaneId::from_str(buffer.trim_end())?;
197    Ok(new_id)
198}
199
200/// Select (make active) the pane with `pane_id`.
201pub async fn select_pane(pane_id: &PaneId) -> Result<()> {
202    let args = vec!["select-pane", "-t", pane_id.as_str()];
203
204    let output = Command::new("tmux").args(&args).output().await?;
205    check_empty_process_output(&output, "select-pane")
206}
207
208#[cfg(test)]
209mod tests {
210    use super::Pane;
211    use super::PaneId;
212    use crate::Result;
213    use std::path::PathBuf;
214    use std::str::FromStr;
215
216    #[test]
217    fn parse_list_panes() {
218        let output = vec![
219            "%20:0:false:'rmbp':'nvim':/Users/graelo/code/rust/tmux-backup",
220            "%21:1:true:'graelo@server: ~':'tmux':/Users/graelo/code/rust/tmux-backup",
221            "%27:2:false:'rmbp':'man man':/Users/graelo/code/rust/tmux-backup",
222        ];
223        let panes: Result<Vec<Pane>> = output.iter().map(|&line| Pane::from_str(line)).collect();
224        let panes = panes.expect("Could not parse tmux panes");
225
226        let expected = vec![
227            Pane {
228                id: PaneId::from_str("%20").unwrap(),
229                index: 0,
230                is_active: false,
231                title: String::from("rmbp"),
232                dirpath: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
233                command: String::from("nvim"),
234            },
235            Pane {
236                id: PaneId(String::from("%21")),
237                index: 1,
238                is_active: true,
239                title: String::from("graelo@server: ~"),
240                dirpath: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
241                command: String::from("tmux"),
242            },
243            Pane {
244                id: PaneId(String::from("%27")),
245                index: 2,
246                is_active: false,
247                title: String::from("rmbp"),
248                dirpath: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
249                command: String::from("man man"),
250            },
251        ];
252
253        assert_eq!(panes, expected);
254    }
255}