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}