tmux_layout/tmux/
command.rs

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        // Create our first window at index 0 (pushing the intial window to index 1).
153        self = self.new_window(window, parent_cwd, Some("0"));
154
155        // Kill the initial window.
156        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        // We now have a fresh window with a single, unconfigured pane.
192        // To apply our options to the pane, we created a horizontal split
193        // with our designated first pane on the right. Afterwards we kill
194        // the initial placeholder pane.
195
196        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    // Primitives
340
341    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/// When splitting the parent pane, the split direction depens on the
426/// location of size information. If we are, for instance, supposed
427/// to give the right pane a width of 25%, we create a new pane on the
428/// right. This way, we can always supply the size information to the
429/// `-l` option of the `split-window` command.
430///
431/// Splitting to the left (or top for vertical splits) is considered
432/// an "inverted" flow.
433#[derive(Debug, Clone, Copy)]
434enum SplitFlow {
435    /// Flowing to the right/bottom
436    Regular,
437    /// Flowing to the left/top
438    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
474/// Finds the root pane for the given split (i.e. the pane all
475/// rescursive splits are created on).
476///
477/// The path to the root pane depends on the flows of the
478/// intermediate splits, which themselves depend on the splits'
479/// size information.
480fn 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}