tmux_tui/
tmux.rs

1//! tmux process interaction: data models and command wrappers.
2//!
3//! Everything that shells out to `tmux` lives here, so the rest of the crate
4//! deals in plain data and `Result`s rather than raw `Command`s.
5
6use std::io;
7use std::process::Command;
8
9/// A pane as reported by tmux, with its cell geometry within its window.
10#[derive(Clone)]
11pub struct Pane {
12    pub id: String,
13    pub index: String,
14    pub left: u16,
15    pub top: u16,
16    pub width: u16,
17    pub height: u16,
18    pub active: bool,
19    pub cmd: String,
20}
21
22/// A window and the panes it contains.
23#[derive(Clone)]
24pub struct Win {
25    pub id: String,
26    pub index: String,
27    pub name: String,
28    pub active: bool,
29    pub w: u16,
30    pub h: u16,
31    pub panes: Vec<Pane>,
32}
33
34fn out(args: &[&str]) -> io::Result<String> {
35    let o = Command::new("tmux").args(args).output()?;
36    Ok(String::from_utf8_lossy(&o.stdout).to_string())
37}
38
39fn fire(args: &[&str]) -> io::Result<()> {
40    Command::new("tmux").args(args).status()?;
41    Ok(())
42}
43
44/// Run a tmux command, returning `Ok(stdout)` on success or `Err(stderr)` on
45/// a non-zero exit (the outer `io::Result` is for spawn failures).
46fn capture(args: &[&str]) -> io::Result<Result<String, String>> {
47    let o = Command::new("tmux").args(args).output()?;
48    Ok(if o.status.success() {
49        Ok(String::from_utf8_lossy(&o.stdout).trim().to_string())
50    } else {
51        Err(String::from_utf8_lossy(&o.stderr).trim().to_string())
52    })
53}
54
55fn parse_pane(f: &[&str]) -> Pane {
56    Pane {
57        id: f[0].to_string(),
58        index: f[1].to_string(),
59        left: f[2].parse().unwrap_or(0),
60        top: f[3].parse().unwrap_or(0),
61        width: f[4].parse().unwrap_or(1),
62        height: f[5].parse().unwrap_or(1),
63        active: f[6].trim() == "1",
64        cmd: f[7].to_string(),
65    }
66}
67
68const PANE_FMT: &str = "#{pane_id}\t#{pane_index}\t#{pane_left}\t#{pane_top}\t#{pane_width}\t#{pane_height}\t#{pane_active}\t#{pane_current_command}";
69
70/// Current window dimensions and the geometry of each of its panes.
71pub fn query() -> io::Result<(u16, u16, Vec<Pane>)> {
72    let dims = out(&["display-message", "-p", "#{window_width} #{window_height}"])?;
73    let mut it = dims.split_whitespace();
74    let win_w = it.next().and_then(|s| s.parse().ok()).unwrap_or(80u16);
75    let win_h = it.next().and_then(|s| s.parse().ok()).unwrap_or(24u16);
76    let raw = out(&["list-panes", "-F", PANE_FMT])?;
77    let mut panes = Vec::new();
78    for line in raw.lines() {
79        let f: Vec<&str> = line.split('\t').collect();
80        if f.len() >= 8 {
81            panes.push(parse_pane(&f));
82        }
83    }
84    Ok((win_w, win_h, panes))
85}
86
87/// Every window in the session, each with its panes, ordered by index.
88pub fn query_windows() -> io::Result<Vec<Win>> {
89    let fmt = "#{window_id}\t#{window_index}\t#{window_name}\t#{window_active}\t#{window_width}\t#{window_height}\t#{pane_id}\t#{pane_index}\t#{pane_left}\t#{pane_top}\t#{pane_width}\t#{pane_height}\t#{pane_active}\t#{pane_current_command}";
90    let raw = out(&["list-panes", "-s", "-F", fmt])?;
91    let mut wins: Vec<Win> = Vec::new();
92    for line in raw.lines() {
93        let f: Vec<&str> = line.split('\t').collect();
94        if f.len() < 14 {
95            continue;
96        }
97        let pane = parse_pane(&f[6..14]);
98        let wid = f[0].to_string();
99        if let Some(w) = wins.iter_mut().find(|w| w.id == wid) {
100            w.panes.push(pane);
101        } else {
102            wins.push(Win {
103                id: wid,
104                index: f[1].to_string(),
105                name: f[2].to_string(),
106                active: f[3].trim() == "1",
107                w: f[4].parse().unwrap_or(80),
108                h: f[5].parse().unwrap_or(24),
109                panes: vec![pane],
110            });
111        }
112    }
113    wins.sort_by_key(|w| w.index.parse::<i64>().unwrap_or(0));
114    Ok(wins)
115}
116
117/// Split `target` and return the new pane id. `flag` is "-h"/"-v"; `before`
118/// puts the new pane left/above instead of right/below.
119pub fn split(target: &str, flag: &str, before: bool) -> io::Result<Result<String, String>> {
120    let mut args = vec!["split-window", flag];
121    if before {
122        args.push("-b");
123    }
124    args.extend(["-t", target, "-P", "-F", "#{pane_id}"]);
125    capture(&args)
126}
127
128pub fn select_layout(name: &str) -> io::Result<Result<String, String>> {
129    capture(&["select-layout", name])
130}
131
132pub fn join_pane(pane: &str, win: &str) -> io::Result<Result<String, String>> {
133    capture(&["join-pane", "-s", pane, "-t", win])
134}
135
136pub fn swap_pane(s: &str, t: &str) -> io::Result<()> {
137    fire(&["swap-pane", "-s", s, "-t", t])
138}
139
140pub fn kill_pane(id: &str) -> io::Result<()> {
141    fire(&["kill-pane", "-t", id])
142}
143
144pub fn kill_window(id: &str) -> io::Result<()> {
145    fire(&["kill-window", "-t", id])
146}
147
148pub fn next_layout() -> io::Result<()> {
149    fire(&["next-layout"])
150}
151
152pub fn select_window(id: &str) -> io::Result<()> {
153    fire(&["select-window", "-t", id])
154}
155
156pub fn swap_window(a: &str, b: &str) -> io::Result<()> {
157    fire(&["swap-window", "-d", "-s", a, "-t", b])
158}
159
160pub fn rename_window(id: &str, name: &str) -> io::Result<()> {
161    fire(&["rename-window", "-t", id, name])
162}
163
164pub fn new_window() -> io::Result<()> {
165    fire(&["new-window", "-a"])
166}
167
168/// Focus a specific pane by id.
169pub fn select_pane(id: &str) -> io::Result<()> {
170    fire(&["select-pane", "-t", id])
171}
172
173/// Move focus to the neighbouring pane; `dir` is "-L"/"-R"/"-U"/"-D".
174pub fn select_pane_dir(dir: &str) -> io::Result<()> {
175    fire(&["select-pane", dir])
176}