gwm/multiplexer.rs
1//! Terminal-multiplexer integration. Builds the argv vectors for
2//! `tmux new-window/split-window` and `zellij action new-tab/new-pane` so
3//! `gwm tmux <pattern>` / `gwm zellij <pattern>` can open a worktree in
4//! one keystroke from inside an already-running multiplexer session.
5//!
6//! The command builders are pure functions returning `Vec<String>` so the
7//! integration tests can pin the exact incantation without spawning tmux
8//! or zellij on every test runner. The actual `std::process::Command`
9//! spawn lives in `cli.rs`, matching the lazygit-launch pattern in
10//! `tui/mod.rs::run_lazygit`.
11
12use std::path::Path;
13
14/// Multiplexer the user opted into via `gwm tmux …` / `gwm zellij …`.
15/// Carried through the CLI dispatch so the not-running error and the
16/// argv builder share one source of truth for the binary name.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Multiplexer {
19 Tmux,
20 Zellij,
21}
22
23impl Multiplexer {
24 /// Binary name as it appears on `$PATH`. Used both for the spawn and
25 /// for the `<bin> session not running` error string.
26 pub fn binary(self) -> &'static str {
27 match self {
28 Multiplexer::Tmux => "tmux",
29 Multiplexer::Zellij => "zellij",
30 }
31 }
32}
33
34/// How to open the worktree inside the multiplexer.
35/// `Window` = new tmux window / zellij tab (the default — full screen real estate).
36/// `Split` = split the current pane (the `-p` flag — keeps both views visible).
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum SpawnMode {
39 Window,
40 Split,
41}
42
43/// Build `tmux new-window -n <name> -c <path>` (Window) or
44/// `tmux split-window -c <path>` (Split). `<name>` is the worktree's
45/// short name so it shows up legibly in tmux's status bar; tmux panes
46/// don't carry a name attribute, so Split intentionally omits `-n`.
47pub fn build_tmux_command(name: &str, path: &Path, mode: SpawnMode) -> Vec<String> {
48 let path_str = path.display().to_string();
49 match mode {
50 SpawnMode::Window => vec![
51 "tmux".into(),
52 "new-window".into(),
53 "-n".into(),
54 name.into(),
55 "-c".into(),
56 path_str,
57 ],
58 SpawnMode::Split => vec!["tmux".into(), "split-window".into(), "-c".into(), path_str],
59 }
60}
61
62/// Build `zellij action new-tab --name <name> --cwd <path>` (Window) or
63/// `zellij action new-pane --cwd <path>` (Split). `--cwd` on `new-tab`
64/// requires zellij ≥ 0.40 — older versions surface their own error,
65/// which is preferable to silently ignoring the cwd.
66pub fn build_zellij_command(name: &str, path: &Path, mode: SpawnMode) -> Vec<String> {
67 let path_str = path.display().to_string();
68 match mode {
69 SpawnMode::Window => vec![
70 "zellij".into(),
71 "action".into(),
72 "new-tab".into(),
73 "--name".into(),
74 name.into(),
75 "--cwd".into(),
76 path_str,
77 ],
78 SpawnMode::Split => vec![
79 "zellij".into(),
80 "action".into(),
81 "new-pane".into(),
82 "--cwd".into(),
83 path_str,
84 ],
85 }
86}
87
88/// `true` when `$TMUX` is set to a non-empty value — tmux exports the
89/// socket path to every process spawned inside a session, so its
90/// presence is the canonical "am I inside tmux?" probe.
91///
92/// Takes the env value as a parameter (rather than reading it directly)
93/// so the unit tests can exercise both branches without mutating the
94/// process environment. The CLI dispatcher calls
95/// `detect_tmux(std::env::var("TMUX").ok())`.
96pub fn detect_tmux(env: Option<String>) -> bool {
97 match env {
98 Some(s) => !s.is_empty(),
99 None => false,
100 }
101}
102
103/// `true` when `$ZELLIJ` is set to a non-empty value. Zellij exports the
104/// variable to every command spawned inside a session, similar to tmux.
105pub fn detect_zellij(env: Option<String>) -> bool {
106 match env {
107 Some(s) => !s.is_empty(),
108 None => false,
109 }
110}