1use crate::config::{Pane, RootSplit, Session, Split, Window};
2use crate::cwd::Cwd;
3use crate::show_warning;
4use std::fmt;
5use std::marker::PhantomData;
6use std::{ffi::OsStr, process::Command};
7
8#[derive(Debug, Clone, Copy)]
9pub enum QueryScope {
10 AllSessions,
11 CurrentSession,
12 CurrentWindow,
13}
14
15#[derive(Debug, Clone, Copy)]
16pub enum SessionSelectMode {
17 Attach,
18 Switch,
19 Detached,
20}
21
22#[derive(Debug)]
23pub struct TmuxCommandBuilder {
24 command: Command,
25 first_command: bool,
26 current_session_name: Option<String>,
27 window_count: u32,
28 active_window_index: Option<u32>,
29}
30
31impl TmuxCommandBuilder {
32 pub fn new(
33 tmux_path: impl AsRef<OsStr>,
34 tmux_args: impl IntoIterator<Item = impl AsRef<OsStr>>,
35 ) -> Self {
36 let mut command = Command::new(tmux_path);
37 command.args(tmux_args);
38
39 Self {
40 command,
41 first_command: true,
42 current_session_name: None,
43 window_count: 0,
44 active_window_index: None,
45 }
46 }
47
48 pub fn into_command(self) -> Command {
49 self.command
50 }
51
52 pub fn query_panes(mut self, format: impl AsRef<OsStr>, scope: QueryScope) -> Self {
53 self.push_new_command("list-panes").push("-F").push(format);
54 self.push_query_scope_arg(scope);
55 self
56 }
57
58 pub fn query_clients(mut self) -> Self {
59 self.push_new_command("list-clients");
60 self
61 }
62
63 pub fn select_session(mut self, name: Option<&str>, mode: SessionSelectMode) -> Self {
64 let select = match mode {
65 SessionSelectMode::Detached => return self,
66 SessionSelectMode::Switch => Self::switch_client,
67 SessionSelectMode::Attach => Self::attach_session,
68 };
69 let target = match name {
70 None => Target::default(),
71 Some(name) => Target::session(name),
72 };
73 select(&mut self, target);
74 self
75 }
76
77 pub fn new_sessions<'a>(self, sessions: impl IntoIterator<Item = &'a Session>) -> Self {
78 sessions
79 .into_iter()
80 .fold(self, |b, session| b.new_session(session))
81 }
82
83 pub fn new_session(mut self, session: &Session) -> Self {
84 if session.windows.is_empty() {
85 return self;
86 }
87
88 self.current_session_name = Some(session.name.clone());
89
90 self.push_new_command("new-session")
91 .push_flag_arg("-s", Some(&session.name))
92 .push_cwd_arg(&session.cwd)
93 .push("-d");
94
95 self.create_initial_window(&session.windows[0], &session.cwd)
96 .new_windows(&session.windows[1..], &session.cwd)
97 }
98
99 pub fn new_windows<'a>(
100 self,
101 windows: impl IntoIterator<Item = &'a Window>,
102 parent_cwd: &Cwd,
103 ) -> Self {
104 let mut builder = windows
105 .into_iter()
106 .fold(self, |b, win| b.new_window(win, parent_cwd, None));
107
108 builder.select_active_window();
109 builder
110 }
111
112 pub fn new_window(
113 mut self,
114 window: &Window,
115 parent_cwd: &Cwd,
116 before_target: Option<&str>,
117 ) -> Self {
118 if window.active {
119 if self.active_window_index.is_none() {
120 self.active_window_index = Some(self.window_count);
121 } else {
122 let session_name = self.current_session_name.as_deref().unwrap_or("(current)");
123 show_warning(&format!(
124 "Multiple active windows in session '{}'",
125 session_name
126 ));
127 }
128 }
129 self.window_count += 1;
130
131 let window_cwd = parent_cwd.joined(&window.cwd);
132 self.push_new_command("new-window")
133 .push_flag_arg("-n", window.name.as_deref())
134 .push_cwd_arg(&window_cwd);
135
136 if let Some(before_target) = before_target {
137 let target = self.session_target().window(before_target);
138 self.push("-b").push_target_arg(target);
139 } else {
140 self.push_target_arg(self.session_target());
141 }
142
143 self.apply_root_split(&window.root_split, &window_cwd);
144 self.select_active_pane(window);
145 self
146 }
147
148 fn create_initial_window(mut self, window: &Window, parent_cwd: &Cwd) -> Self {
149 self.active_window_index = None;
150 self.window_count = 0;
151
152 self = self.new_window(window, parent_cwd, Some("0"));
154
155 let target = self.session_target().window("1");
157 self.push_new_command("kill-window").push_target_arg(target);
158
159 self
160 }
161
162 fn select_active_pane(&mut self, window: &Window) {
163 let active_panes = window
164 .root_split
165 .pane_iter()
166 .enumerate()
167 .filter(|(_, pane)| pane.active)
168 .collect::<Vec<_>>();
169
170 if active_panes.len() > 1 {
171 let session_name = self.current_session_name.as_deref().unwrap_or("(current)");
172 show_warning(&format!(
173 "Multiple active panes in window '{}' of session '{}'",
174 window.name.as_deref().unwrap_or("(unnamed)"),
175 session_name
176 ));
177 }
178
179 if let Some(active_pane) = active_panes.first() {
180 let pane_index = active_pane.0;
181 let target = self
182 .session_target()
183 .current_window()
184 .pane(pane_index.to_string());
185
186 self.push_new_command("select-pane").push_target_arg(target);
187 }
188 }
189
190 fn apply_root_split(&mut self, split: &RootSplit, parent_cwd: &Cwd) -> &mut Self {
191 let first_pane = root_pane(split);
197 let first_pane_cwd = parent_cwd.joined(&first_pane.cwd);
198 self.split_pane(
199 Axis::Horizontal,
200 SplitFlow::Regular,
201 &first_pane_cwd,
202 first_pane.shell_command.as_deref(),
203 None,
204 );
205
206 let first_pane_target = self.session_target().current_window().pane("0");
207 self.push_new_command("kill-pane")
208 .push_target_arg(first_pane_target);
209
210 self.apply_split(split, parent_cwd)
211 }
212
213 fn apply_split(&mut self, split: &Split, parent_cwd: &Cwd) -> &mut Self {
214 let flow = SplitFlow::from(split);
215
216 match split {
217 Split::Pane(pane) => {
218 if let Some(keys) = &pane.send_keys {
219 self.send_keys(keys);
220 }
221 self
222 }
223 Split::H { left, right } => {
224 let (parent, child) = match flow {
225 SplitFlow::Regular => (left, right),
226 SplitFlow::Inverted => (right, left),
227 };
228 let child_pane = root_pane(&child.split);
229 let child_pane_cwd = parent_cwd.joined(&child_pane.cwd);
230
231 self.split_pane(
232 Axis::Horizontal,
233 flow,
234 &child_pane_cwd,
235 child_pane.shell_command.as_deref(),
236 child.width.as_deref(),
237 )
238 .apply_split(&child.split, parent_cwd)
239 .select_pane_at(flow.direction(Axis::Horizontal).inverted())
240 .apply_split(&parent.split, parent_cwd)
241 }
242 Split::V { top, bottom } => {
243 let (parent, child) = match flow {
244 SplitFlow::Regular => (top, bottom),
245 SplitFlow::Inverted => (bottom, top),
246 };
247 let child_pane = root_pane(&child.split);
248 let child_pane_cwd = parent_cwd.joined(&child_pane.cwd);
249
250 self.split_pane(
251 Axis::Vertical,
252 flow,
253 &child_pane_cwd,
254 child_pane.shell_command.as_deref(),
255 child.height.as_deref(),
256 )
257 .apply_split(&child.split, parent_cwd)
258 .select_pane_at(flow.direction(Axis::Vertical).inverted())
259 .apply_split(&parent.split, parent_cwd)
260 }
261 }
262 }
263
264 fn send_keys(&mut self, keys: impl IntoIterator<Item = impl AsRef<OsStr>>) -> &mut Self {
265 let target = self.session_target();
266 self.push_new_command("send-keys").push_target_arg(target);
267 keys.into_iter().fold(self, |b, key| b.push_arg(Some(key)))
268 }
269
270 fn split_pane(
271 &mut self,
272 axis: Axis,
273 flow: SplitFlow,
274 cwd: &Cwd,
275 shell_command: Option<&str>,
276 size: Option<&str>,
277 ) -> &mut Self {
278 let target = self.session_target();
279 self.push_new_command("split-window")
280 .push_target_arg(target)
281 .push_axis_arg(axis)
282 .push_flow_arg(flow)
283 .push_cwd_arg(cwd)
284 .push_flag_arg("-l", size)
285 .push_arg(shell_command)
286 }
287
288 fn select_pane_at(&mut self, direction: Direction) -> &mut Self {
289 let target = self.session_target();
290 self.push_new_command("select-pane")
291 .push_target_arg(target)
292 .push_direction_arg(direction)
293 }
294
295 fn select_window_at(&mut self, direction: Direction) -> &mut Self {
296 let target = self.session_target();
297 self.push_new_command("select-window")
298 .push_target_arg(target)
299 .push_next_prev_arg(direction)
300 }
301
302 fn select_window(&mut self, target: Target<Window>) -> &mut Self {
303 self.push_new_command("select-window")
304 .push_target_arg(target)
305 }
306
307 fn switch_client(&mut self, target: Target<Session>) -> &mut Self {
308 self.push_new_command("switch-client")
309 .push_target_arg(target)
310 }
311
312 fn attach_session(&mut self, target: Target<Session>) -> &mut Self {
313 self.push_new_command("attach-session")
314 .push_target_arg(target)
315 }
316
317 fn select_active_window(&mut self) -> &mut Self {
318 if let Some(index) = self.active_window_index {
319 if let Some(session_name) = self.current_session_name.as_deref() {
320 let target = Target::session(session_name).window(index.to_string());
321 self.select_window(target);
322 } else {
323 let steps = self.window_count - index - 1;
324 for _ in 0..steps {
325 self.select_window_at(Direction::Left);
326 }
327 }
328 }
329 self
330 }
331
332 fn session_target(&self) -> Target<Session> {
333 self.current_session_name
334 .as_ref()
335 .map(|name| Target::session(name.clone()))
336 .unwrap_or_default()
337 }
338
339 fn push_cwd_arg(&mut self, cwd: &Cwd) -> &mut Self {
342 self.push_flag_arg("-c", cwd.to_path())
343 }
344
345 fn push_target_arg<Scope>(&mut self, target: Target<Scope>) -> &mut Self
346 where
347 Target<Scope>: fmt::Display,
348 {
349 self.push_flag_arg("-t", Some(target.to_string()))
350 }
351
352 fn push_axis_arg(&mut self, axis: Axis) -> &mut Self {
353 match axis {
354 Axis::Horizontal => self.push("-h"),
355 Axis::Vertical => self.push("-v"),
356 }
357 }
358
359 fn push_direction_arg(&mut self, direction: Direction) -> &mut Self {
360 match direction {
361 Direction::Left => self.push("-L"),
362 Direction::Right => self.push("-R"),
363 Direction::Up => self.push("-U"),
364 Direction::Down => self.push("-D"),
365 }
366 }
367
368 fn push_next_prev_arg(&mut self, direction: Direction) -> &mut Self {
369 match direction {
370 Direction::Left => self.push("-p"),
371 Direction::Right => self.push("-n"),
372 Direction::Up => self.push("-p"),
373 Direction::Down => self.push("-n"),
374 }
375 }
376
377 fn push_flow_arg(&mut self, flow: SplitFlow) -> &mut Self {
378 match flow {
379 SplitFlow::Regular => self,
380 SplitFlow::Inverted => self.push_arg(Some("-b")),
381 }
382 }
383
384 fn push_query_scope_arg(&mut self, scope: QueryScope) -> &mut Self {
385 match scope {
386 QueryScope::AllSessions => self.push("-a"),
387 QueryScope::CurrentSession => self.push("-s"),
388 QueryScope::CurrentWindow => self,
389 }
390 }
391
392 fn push_flag_arg(
393 &mut self,
394 flag: impl AsRef<OsStr>,
395 arg: Option<impl AsRef<OsStr>>,
396 ) -> &mut Self {
397 if let Some(arg) = arg {
398 self.push(flag).push(arg);
399 }
400 self
401 }
402
403 fn push_arg(&mut self, arg: Option<impl AsRef<OsStr>>) -> &mut Self {
404 if let Some(arg) = arg {
405 self.push(arg);
406 }
407 self
408 }
409
410 fn push_new_command(&mut self, command: &str) -> &mut Self {
411 if self.first_command {
412 self.first_command = false;
413 } else {
414 self.push(";");
415 }
416 self.push(command)
417 }
418
419 fn push(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
420 self.command.arg(arg);
421 self
422 }
423}
424
425#[derive(Debug, Clone, Copy)]
434enum SplitFlow {
435 Regular,
437 Inverted,
439}
440
441impl SplitFlow {
442 fn direction(self, axis: Axis) -> Direction {
443 match (self, axis) {
444 (SplitFlow::Regular, Axis::Horizontal) => Direction::Right,
445 (SplitFlow::Regular, Axis::Vertical) => Direction::Down,
446 (SplitFlow::Inverted, Axis::Horizontal) => Direction::Left,
447 (SplitFlow::Inverted, Axis::Vertical) => Direction::Up,
448 }
449 }
450}
451
452impl From<&'_ Split> for SplitFlow {
453 fn from(split: &'_ Split) -> Self {
454 match split {
455 Split::Pane(_) => SplitFlow::Regular,
456 Split::H { left, .. } => {
457 if left.width.is_some() {
458 SplitFlow::Inverted
459 } else {
460 SplitFlow::Regular
461 }
462 }
463 Split::V { top, .. } => {
464 if top.height.is_some() {
465 SplitFlow::Inverted
466 } else {
467 SplitFlow::Regular
468 }
469 }
470 }
471 }
472}
473
474fn root_pane(split: &Split) -> &Pane {
481 match split {
482 Split::Pane(pane) => pane,
483 Split::H { left, right } => match SplitFlow::from(split) {
484 SplitFlow::Regular => root_pane(&left.split),
485 SplitFlow::Inverted => root_pane(&right.split),
486 },
487 Split::V { top, bottom } => match SplitFlow::from(split) {
488 SplitFlow::Regular => root_pane(&top.split),
489 SplitFlow::Inverted => root_pane(&bottom.split),
490 },
491 }
492}
493
494#[derive(Debug, Clone, Copy)]
495enum Direction {
496 Left,
497 Right,
498 Up,
499 Down,
500}
501
502impl Direction {
503 fn inverted(self) -> Self {
504 match self {
505 Direction::Left => Direction::Right,
506 Direction::Right => Direction::Left,
507 Direction::Up => Direction::Down,
508 Direction::Down => Direction::Up,
509 }
510 }
511}
512
513#[derive(Debug, Clone, Copy)]
514enum Axis {
515 Horizontal,
516 Vertical,
517}
518
519impl From<Direction> for Axis {
520 fn from(direction: Direction) -> Self {
521 match direction {
522 Direction::Left | Direction::Right => Axis::Horizontal,
523 Direction::Up | Direction::Down => Axis::Vertical,
524 }
525 }
526}
527
528#[derive(Debug, Clone)]
529struct Target<Scope> {
530 session: Option<String>,
531 window: Option<String>,
532 pane: Option<String>,
533 _scope: PhantomData<Scope>,
534}
535
536impl Target<Session> {
537 fn session(session: impl Into<String>) -> Self {
538 Self {
539 session: Some(session.into()),
540 window: None,
541 pane: None,
542 _scope: PhantomData,
543 }
544 }
545
546 fn window(self, window: impl Into<String>) -> Target<Window> {
547 Target {
548 session: self.session,
549 window: Some(window.into()),
550 pane: None,
551 _scope: PhantomData,
552 }
553 }
554
555 fn current_window(self) -> Target<Window> {
556 Target {
557 session: self.session,
558 window: None,
559 pane: None,
560 _scope: PhantomData,
561 }
562 }
563}
564
565impl Target<Window> {
566 fn pane(self, pane: impl Into<String>) -> Target<Pane> {
567 Target {
568 session: self.session,
569 window: self.window,
570 pane: Some(pane.into()),
571 _scope: PhantomData,
572 }
573 }
574}
575
576impl fmt::Display for Target<Session> {
577 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578 write!(f, "{}:", self.session.as_deref().unwrap_or(""))
579 }
580}
581
582impl fmt::Display for Target<Window> {
583 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
584 write!(
585 f,
586 "{}:{}.",
587 self.session.as_deref().unwrap_or(""),
588 self.window.as_deref().unwrap_or(""),
589 )
590 }
591}
592
593impl fmt::Display for Target<Pane> {
594 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
595 write!(
596 f,
597 "{}:{}.{}",
598 self.session.as_deref().unwrap_or(""),
599 self.window.as_deref().unwrap_or(""),
600 self.pane.as_deref().unwrap_or("")
601 )
602 }
603}
604
605impl Default for Target<Session> {
606 fn default() -> Self {
607 Self {
608 session: None,
609 window: None,
610 pane: None,
611 _scope: PhantomData,
612 }
613 }
614}