tmux_layout/tmux/
import.rs

1use std::{collections::HashMap, path::Path, process::Stdio};
2use thiserror::Error;
3
4use crate::{
5    config::{self},
6    cwd::Cwd,
7    tmux::{self, TmuxCommandBuilder},
8};
9
10pub use parser::Error as ParseError;
11
12use super::command::QueryScope;
13
14pub fn query_tmux_state(
15    command_builder: TmuxCommandBuilder,
16    scope: QueryScope,
17) -> Result<TmuxState, Error> {
18    let mut command = command_builder
19        .query_panes(parser::TMUX_FORMAT, scope)
20        .into_command();
21
22    let command_out = command.stderr(Stdio::inherit()).output()?;
23    if !command_out.status.success() {
24        return Err(Error::CommandExitCode(
25            command_out.status.code().unwrap_or(1),
26        ));
27    }
28
29    let state_desc = command_out.stdout;
30    let state_desc = std::str::from_utf8(&state_desc)
31        .map_err(|_| Error::ParseError("command output not UTF-8".into()))?;
32
33    Ok(parser::parse_tmux_state(state_desc)?)
34}
35#[derive(Debug, Clone)]
36pub struct TmuxState {
37    pub sessions: HashMap<SessionId, Session>,
38}
39
40impl From<TmuxState> for Vec<config::Session> {
41    fn from(state: TmuxState) -> Self {
42        let mut sessions = state.sessions.into_values().collect::<Vec<_>>();
43        sessions.sort_by_key(|s| s.id);
44        sessions.into_iter().map(Into::into).collect()
45    }
46}
47
48#[derive(Debug, Clone)]
49pub struct Session {
50    pub id: SessionId,
51    pub name: String,
52    pub cwd: String,
53    pub windows: HashMap<WindowId, Window>,
54}
55
56impl From<Session> for config::Session {
57    fn from(session: Session) -> Self {
58        let session_cwd = session.cwd.into();
59
60        let mut windows = session.windows.into_values().collect::<Vec<_>>();
61        windows.sort_by_key(|w| w.index);
62
63        let windows = windows
64            .into_iter()
65            .map(|w| w.into_config_window(&session_cwd))
66            .collect();
67
68        config::Session {
69            name: session.name,
70            cwd: session_cwd,
71            windows,
72        }
73    }
74}
75
76#[derive(Debug, Clone)]
77pub struct Window {
78    pub id: WindowId,
79    pub index: WindowIndex,
80    pub name: String,
81    pub layout: tmux::Layout,
82    pub active: bool,
83    pub panes: HashMap<PaneId, Pane>,
84}
85
86impl Window {
87    fn into_config_window(self, session_cwd: &Cwd) -> config::Window {
88        let session_cwd_path = session_cwd.to_path();
89
90        let mut panes = self.panes.into_values().collect::<Vec<_>>();
91        panes.sort_by_key(|p| p.index);
92
93        let mut root_split = config::Split::from(self.layout).into_root();
94        root_split
95            .pane_iter_mut()
96            .zip(panes)
97            .for_each(|(config_pane, pane)| {
98                config_pane.active = pane.active;
99                config_pane.cwd = session_cwd_path
100                    .and_then(|root| Path::new(&pane.cwd).strip_prefix(root).ok())
101                    .map(|p| p.to_owned().into())
102                    .unwrap_or_else(|| pane.cwd.into());
103            });
104
105        config::Window {
106            name: Some(self.name),
107            cwd: Cwd::new(None),
108            active: self.active,
109            root_split,
110        }
111    }
112}
113
114impl From<Window> for config::Window {
115    fn from(window: Window) -> Self {
116        window.into_config_window(&Cwd::default())
117    }
118}
119
120#[derive(Debug, Clone)]
121pub struct Pane {
122    pub id: PaneId,
123    pub index: PaneIndex,
124    pub active: bool,
125    pub cwd: String,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
129pub struct SessionId(u32);
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
132pub struct WindowId(u32);
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
135pub struct WindowIndex(u32);
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
138pub struct PaneId(u32);
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
141pub struct PaneIndex(u32);
142
143#[derive(Debug, Error)]
144pub enum Error {
145    #[error("error while invoking tmux command: {0}")]
146    CommandIo(#[from] std::io::Error),
147    #[error("non-successful tmux exit code: {0}")]
148    CommandExitCode(i32),
149    #[error("parse error: {0}")]
150    ParseError(#[from] ParseError),
151}
152
153mod parser {
154    use crate::tmux::layout;
155    use nom::Parser;
156    use shellwords::MismatchedQuotes;
157    use std::borrow::Cow;
158    use std::collections::{hash_map::Entry, HashMap};
159    use std::fmt;
160    use std::num::ParseIntError;
161
162    use super::*;
163
164    type Result<A> = std::result::Result<A, Error>;
165
166    pub(super) fn parse_tmux_state(input: &str) -> Result<TmuxState> {
167        let infos = parse_pane_infos(input)?;
168        let mut sessions = HashMap::new();
169
170        for info in infos {
171            let session = match sessions.entry(info.session_id) {
172                Entry::Occupied(o) => o.into_mut(),
173                Entry::Vacant(v) => v.insert(Session {
174                    id: info.session_id,
175                    name: info.session_name,
176                    cwd: info.session_cwd,
177                    windows: Default::default(),
178                }),
179            };
180
181            let window = match session.windows.entry(info.window_id) {
182                Entry::Occupied(o) => o.into_mut(),
183                Entry::Vacant(v) => v.insert(Window {
184                    id: info.window_id,
185                    index: info.window_index,
186                    name: info.window_name,
187                    layout: info.window_layout,
188                    active: info.window_active,
189                    panes: Default::default(),
190                }),
191            };
192
193            window.panes.insert(
194                info.pane_id,
195                Pane {
196                    id: info.pane_id,
197                    index: info.pane_index,
198                    active: info.pane_active,
199                    cwd: info.pane_cwd,
200                },
201            );
202        }
203
204        Ok(TmuxState { sessions })
205    }
206
207    #[derive(Debug, Clone)]
208    struct PaneInfo {
209        session_id: SessionId,
210        window_id: WindowId,
211        pane_id: PaneId,
212        session_name: String,
213        session_cwd: String,
214        window_index: WindowIndex,
215        window_name: String,
216        window_active: bool,
217        window_layout: tmux::Layout,
218        pane_index: PaneIndex,
219        pane_active: bool,
220        pane_cwd: String,
221    }
222
223    fn parse_pane_infos(input: &str) -> Result<Vec<PaneInfo>> {
224        input.lines().map(parse_line).collect()
225    }
226
227    pub(super) const TMUX_FORMAT: &str = "#{q:session_id} #{q:window_id} #{q:pane_id} \
228        #{q:session_name} #{q:session_path} #{q:window_index} #{q:window_name} \
229        #{q:window_active} #{q:window_layout} #{q:pane_index} #{q:pane_active} \
230        #{q:pane_current_path}";
231
232    fn parse_line(line: &str) -> Result<PaneInfo> {
233        let mut words = shellwords::split(line)?.into_iter();
234        let mut next_word = || words.next().ok_or_else(|| Error::from("missing word"));
235
236        let session_id_desc = next_word()?;
237        let session_id = all_consuming(session_id).parse(&session_id_desc)?.1;
238        let window_id_desc = next_word()?;
239        let window_id = all_consuming(window_id).parse(&window_id_desc)?.1;
240        let pane_id_desc = next_word()?;
241        let pane_id = all_consuming(pane_id).parse(&pane_id_desc)?.1;
242        let session_name = next_word()?;
243        let session_cwd = next_word()?;
244        let window_index = WindowIndex(next_word()?.parse()?);
245        let window_name = next_word()?;
246        let window_active = next_word()?.parse::<u8>()? != 0;
247        let window_layout_desc = next_word()?;
248        let window_layout = tmux::Layout::parse(&window_layout_desc)?;
249        let pane_index = PaneIndex(next_word()?.parse()?);
250        let pane_active = next_word()?.parse::<u8>()? != 0;
251        let pane_cwd = next_word().unwrap_or_default();
252
253        Ok(PaneInfo {
254            session_id,
255            window_id,
256            pane_id,
257            session_name,
258            session_cwd,
259            window_index,
260            window_name,
261            window_active,
262            window_layout,
263            pane_index,
264            pane_active,
265            pane_cwd,
266        })
267    }
268
269    use nom::{
270        bytes::complete::tag,
271        character::complete::u32,
272        combinator::{all_consuming, map},
273        sequence::preceded,
274        IResult,
275    };
276
277    type I<'a> = &'a str;
278    type NomResult<'a, A> = IResult<I<'a>, A>;
279
280    fn session_id(i: I) -> NomResult<SessionId> {
281        map(preceded(tag("$"), u32), SessionId).parse(i)
282    }
283
284    fn window_id(i: I) -> NomResult<WindowId> {
285        map(preceded(tag("@"), u32), WindowId).parse(i)
286    }
287
288    fn pane_id(i: I) -> NomResult<PaneId> {
289        map(preceded(tag("%"), u32), PaneId).parse(i)
290    }
291
292    #[derive(Debug)]
293    pub struct Error {
294        pub message: Cow<'static, str>,
295    }
296
297    impl From<String> for Error {
298        fn from(message: String) -> Self {
299            Error {
300                message: Cow::Owned(message),
301            }
302        }
303    }
304
305    impl From<&'static str> for Error {
306        fn from(message: &'static str) -> Self {
307            Error {
308                message: Cow::Borrowed(message),
309            }
310        }
311    }
312
313    impl From<MismatchedQuotes> for Error {
314        fn from(_: MismatchedQuotes) -> Self {
315            Error::from("missing quotes")
316        }
317    }
318
319    impl<E: std::error::Error> From<nom::Err<E>> for Error {
320        fn from(err: nom::Err<E>) -> Self {
321            Error::from(format!("{}", err))
322        }
323    }
324
325    impl From<ParseIntError> for Error {
326        fn from(err: ParseIntError) -> Self {
327            Error::from(format!("{}", err))
328        }
329    }
330
331    impl From<layout::Error> for Error {
332        fn from(err: layout::Error) -> Self {
333            Error::from(format!("{}", err))
334        }
335    }
336
337    impl fmt::Display for Error {
338        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339            write!(f, "{}", self.message)
340        }
341    }
342
343    impl std::error::Error for Error {}
344}