use super::tmux_query::{Snapshot, Window, poll, ztmux_cmd};
const LAYOUTS: &[&str] = &[
"even-horizontal",
"even-vertical",
"main-horizontal",
"main-vertical",
"tiled",
];
struct Args {
layout: String,
session: Option<String>,
force: bool,
}
struct Target {
target: String,
location: String,
panes: i64,
}
pub(crate) fn run(socket: &str) -> i32 {
let Some(args) = parse_args() else {
eprintln!(
"usage: ztmux equalize [layout] [-s session] [-f]\n layout: {}",
LAYOUTS.join(" | ")
);
return 2;
};
let snap = poll(socket);
if let Some(e) = &snap.error {
eprintln!("ztmux equalize: {e}");
return 1;
}
let targets = select(&snap, args.session.as_deref());
if targets.is_empty() {
eprintln!("ztmux equalize: no multi-pane windows matched");
return 1;
}
if !args.force {
println!(
"would set layout {:?} on {} window(s):",
args.layout,
targets.len()
);
for t in &targets {
println!(" {} ({} panes)", t.location, t.panes);
}
println!("(dry-run; pass -f to apply)");
return 0;
}
let mut done = 0;
for t in &targets {
if ztmux_cmd(socket, &["select-layout", "-t", &t.target, &args.layout])
.status()
.is_ok_and(|s| s.success())
{
done += 1;
}
}
println!(
"re-laid {done}/{} window(s) as {}",
targets.len(),
args.layout
);
i32::from(done != targets.len())
}
fn parse_args() -> Option<Args> {
let argv: Vec<String> = std::env::args().collect();
let start = argv.iter().position(|a| a == "equalize")? + 1;
let rest = &argv[start..];
let session = rest.windows(2).find(|w| w[0] == "-s").map(|w| w[1].clone());
let force = rest.iter().any(|a| a == "-f" || a == "--force");
let mut layout = None;
let mut i = 0;
while i < rest.len() {
if rest[i] == "-s" {
i += 2; continue;
}
if !rest[i].starts_with('-') {
layout = Some(rest[i].clone());
break;
}
i += 1;
}
let layout = layout.unwrap_or_else(|| "tiled".to_string());
if !LAYOUTS.contains(&layout.as_str()) {
return None;
}
Some(Args {
layout,
session,
force,
})
}
fn select(snap: &Snapshot, session: Option<&str>) -> Vec<Target> {
let mut sel: Vec<&Window> = snap
.windows
.iter()
.filter(|w| w.panes > 1)
.filter(|w| session.is_none_or(|s| w.session == s))
.collect();
sel.sort_by(|a, b| a.session.cmp(&b.session).then(a.index.cmp(&b.index)));
sel.into_iter()
.map(|w| Target {
target: format!("{}:{}", w.session, w.index),
location: format!("{}:{} ({})", w.session, w.index, w.name),
panes: w.panes,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn win(session: &str, index: i64, name: &str, panes: i64) -> Window {
Window {
session: session.into(),
index,
name: name.into(),
panes,
..Default::default()
}
}
fn snap() -> Snapshot {
Snapshot {
windows: vec![
win("work", 0, "edit", 3),
win("work", 1, "solo", 1), win("ops", 0, "logs", 2),
],
..Default::default()
}
}
#[test]
fn single_pane_windows_are_skipped() {
let t = select(&snap(), None);
let locs: Vec<&str> = t.iter().map(|t| t.target.as_str()).collect();
assert_eq!(locs, vec!["ops:0", "work:0"]); }
#[test]
fn session_filter_restricts_targets() {
let t = select(&snap(), Some("work"));
assert_eq!(t.len(), 1);
assert_eq!(t[0].target, "work:0");
assert_eq!(t[0].panes, 3);
}
#[test]
fn no_multipane_windows_yields_empty() {
let s = Snapshot {
windows: vec![win("a", 0, "w", 1)],
..Default::default()
};
assert!(select(&s, None).is_empty());
}
#[test]
fn targets_are_session_colon_index() {
let t = select(&snap(), Some("ops"));
assert_eq!(t[0].target, "ops:0");
assert!(t[0].location.contains("logs"));
}
}