Set window options per window; label panes where programs cannot clobber them
Two bugs found deploying to the LattePanda, both of which v1 also had. Window options do not propagate from the session and new windows do not inherit them. `set-option -t <session> pane-border-status` quietly applies to whichever window happens to be current, so only ONE window ever got pane borders -- v1's rack display has had unlabeled borders on two of its three windows this whole time and nobody noticed, because the window that got them was the one usually on screen. This is the same trap that makes remain-on-exit useless here. pane-border-status, pane-border-format and allow-rename are now set per window in place(), and a test asserts every window has them. Pane labels no longer use `select-pane -T`. The pane *title* is writable by whatever runs in the pane: unifly probes for Kitty graphics support on startup and tmux consumed part of that probe as a title change, so the border read "Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA" instead of "unifly". Labels now live in a pane-scoped user option, @socktop_label, which no escape sequence can reach; pane-border-format falls back to the title if it is somehow unset. Also: `terminal:` now expands a leading ~/ like `binaries:` already did. A window manager's PATH rarely includes ~/.cargo/bin, so a full path is the usual answer there and should not have to be spelled out longhand. Build cost corrected from guesses to measurements on the LattePanda (Atom x5-Z8350, 4 cores, 1.9 GB, no swap, toolchain already present): 108 seconds, peak 1.1 GB, 103 MB target directory, 946 KB binary. The README said twenty minutes and the installer budgeted 600 MB; both were wrong. Integration tests now use one tmux session name each -- cargo runs them in parallel and they were tearing down each other's server state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+71
-19
@@ -12,8 +12,6 @@ use socktop_swipe::monitor;
|
||||
use socktop_swipe::session::tmux::Tmux;
|
||||
use socktop_swipe::session::Multiplexer;
|
||||
|
||||
const SESSION: &str = "socktop-swipe-selftest";
|
||||
|
||||
fn have_tmux() -> bool {
|
||||
Command::new("tmux")
|
||||
.arg("-V")
|
||||
@@ -29,12 +27,12 @@ fn tmux(args: &[&str]) -> String {
|
||||
String::from_utf8_lossy(&out.stdout).trim().to_owned()
|
||||
}
|
||||
|
||||
fn config() -> Config {
|
||||
fn config(session: &str) -> Config {
|
||||
// `true` exits at once, which also exercises remain-on-exit keeping the
|
||||
// pane addressable afterwards.
|
||||
let yaml = format!(
|
||||
r#"
|
||||
session: {SESSION}
|
||||
session: {session}
|
||||
binaries: {{ socktop: /bin/echo, unifly: /bin/echo, uptime-kuma-status: /bin/echo }}
|
||||
touch: {{ device: /dev/null, width: 1280, height: 720, grab: false }}
|
||||
screens:
|
||||
@@ -56,24 +54,26 @@ screens:
|
||||
serde_yaml::from_str(&yaml).expect("test config should parse")
|
||||
}
|
||||
|
||||
/// (active window name, active pane title, is the window zoomed)
|
||||
fn visible() -> (String, String, bool) {
|
||||
/// (active window name, active pane label, is the window zoomed)
|
||||
fn visible(session: &str) -> (String, String, bool) {
|
||||
let s = tmux(&[
|
||||
"display-message",
|
||||
"-p",
|
||||
"-t",
|
||||
SESSION,
|
||||
"#{window_name}\t#{pane_title}\t#{window_zoomed_flag}",
|
||||
session,
|
||||
"#{window_name}\t#{@socktop_label}\t#{window_zoomed_flag}",
|
||||
]);
|
||||
let f: Vec<&str> = s.split('\t').collect();
|
||||
(f[0].into(), f[1].into(), f[2] == "1")
|
||||
}
|
||||
|
||||
struct Cleanup;
|
||||
/// Each test uses its own session name: cargo runs tests in parallel and they
|
||||
/// would otherwise tear down each other's tmux server state.
|
||||
struct Cleanup(&'static str);
|
||||
impl Drop for Cleanup {
|
||||
fn drop(&mut self) {
|
||||
let _ = Command::new("tmux")
|
||||
.args(["kill-session", "-t", SESSION])
|
||||
.args(["kill-session", "-t", self.0])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
@@ -86,9 +86,10 @@ fn session_layout_and_navigation_match_the_grid() {
|
||||
eprintln!("skipping: tmux is not installed");
|
||||
return;
|
||||
}
|
||||
let _cleanup = Cleanup;
|
||||
const SESSION: &str = "socktop-swipe-selftest-nav";
|
||||
let _cleanup = Cleanup(SESSION);
|
||||
|
||||
let cfg = config();
|
||||
let cfg = config(SESSION);
|
||||
let mut grid = Grid::new(monitor::build_cells(&cfg).unwrap()).unwrap();
|
||||
let mux = Tmux::new(&cfg.session, false);
|
||||
mux.build(&grid).expect("session should build");
|
||||
@@ -108,7 +109,7 @@ fn session_layout_and_navigation_match_the_grid() {
|
||||
);
|
||||
|
||||
// Starts at 0x0's overview: not zoomed, so all four Pis are visible.
|
||||
let (win, _, zoomed) = visible();
|
||||
let (win, _, zoomed) = visible(SESSION);
|
||||
assert_eq!(win, "r0c0");
|
||||
assert!(!zoomed, "the overview must not be zoomed");
|
||||
|
||||
@@ -116,7 +117,7 @@ fn session_layout_and_navigation_match_the_grid() {
|
||||
for expected in ["alpha", "bravo", "charlie", "delta"] {
|
||||
let pos = grid.apply(Move::Forward);
|
||||
mux.show(&grid, &pos).unwrap();
|
||||
let (win, title, zoomed) = visible();
|
||||
let (win, title, zoomed) = visible(SESSION);
|
||||
assert_eq!(win, "r0c0");
|
||||
assert_eq!(title, expected, "wrong host zoomed");
|
||||
assert!(zoomed, "{expected} should be zoomed full-screen");
|
||||
@@ -125,7 +126,7 @@ fn session_layout_and_navigation_match_the_grid() {
|
||||
// Past the last host, on to the next cell's overview.
|
||||
let pos = grid.apply(Move::Forward);
|
||||
mux.show(&grid, &pos).unwrap();
|
||||
let (win, _, zoomed) = visible();
|
||||
let (win, _, zoomed) = visible(SESSION);
|
||||
assert_eq!(
|
||||
win, "r0c5",
|
||||
"0x5 follows 0x0 despite the gap in column numbers"
|
||||
@@ -135,7 +136,7 @@ fn session_layout_and_navigation_match_the_grid() {
|
||||
// Back must land on 0x0's LAST host, not its overview.
|
||||
let pos = grid.apply(Move::Back);
|
||||
mux.show(&grid, &pos).unwrap();
|
||||
let (win, title, zoomed) = visible();
|
||||
let (win, title, zoomed) = visible(SESSION);
|
||||
assert_eq!(
|
||||
(win.as_str(), title.as_str(), zoomed),
|
||||
("r0c0", "delta", true)
|
||||
@@ -144,14 +145,14 @@ fn session_layout_and_navigation_match_the_grid() {
|
||||
// Up to unifly: a single-pane cell, so nothing to zoom.
|
||||
let pos = grid.apply(Move::Up);
|
||||
mux.show(&grid, &pos).unwrap();
|
||||
let (win, _, zoomed) = visible();
|
||||
let (win, _, zoomed) = visible(SESSION);
|
||||
assert_eq!(win, "rm1c0");
|
||||
assert!(!zoomed, "a one-pane cell has nothing to zoom into");
|
||||
|
||||
// And back down to exactly the host we left.
|
||||
let pos = grid.apply(Move::Down);
|
||||
mux.show(&grid, &pos).unwrap();
|
||||
let (win, title, zoomed) = visible();
|
||||
let (win, title, zoomed) = visible(SESSION);
|
||||
assert_eq!(
|
||||
(win.as_str(), title.as_str(), zoomed),
|
||||
("r0c0", "delta", true),
|
||||
@@ -161,7 +162,58 @@ fn session_layout_and_navigation_match_the_grid() {
|
||||
// Down twice: through row 0 to kuma.
|
||||
let pos = grid.apply(Move::Down);
|
||||
mux.show(&grid, &pos).unwrap();
|
||||
assert_eq!(visible().0, "r1c0");
|
||||
assert_eq!(visible(SESSION).0, "r1c0");
|
||||
}
|
||||
|
||||
/// Window options do not propagate from the session, and new windows do not
|
||||
/// inherit them. Setting one with `set-option -t <session>` quietly applies it
|
||||
/// to whichever window is current, which is how v1 ended up with pane borders
|
||||
/// on only one of its three windows. Assert EVERY window got them.
|
||||
#[test]
|
||||
fn window_options_are_set_on_every_window() {
|
||||
if !have_tmux() {
|
||||
return;
|
||||
}
|
||||
const SESSION: &str = "socktop-swipe-selftest-opts";
|
||||
let _cleanup = Cleanup(SESSION);
|
||||
|
||||
let cfg = config(SESSION);
|
||||
let grid = Grid::new(monitor::build_cells(&cfg).unwrap()).unwrap();
|
||||
Tmux::new(&cfg.session, false).build(&grid).unwrap();
|
||||
|
||||
for (window, want_labels) in [
|
||||
("rm1c0", vec!["unifly"]),
|
||||
("r0c0", vec!["alpha", "bravo", "charlie", "delta"]),
|
||||
("r0c5", vec!["echo1", "foxtrot"]),
|
||||
("r1c0", vec!["uptime kuma"]),
|
||||
] {
|
||||
let target = format!("{SESSION}:{window}");
|
||||
for (opt, want) in [("pane-border-status", "top"), ("allow-rename", "off")] {
|
||||
let got = tmux(&["show-options", "-w", "-t", &target, "-v", opt]);
|
||||
assert_eq!(got, want, "{window} is missing the {opt} window option");
|
||||
}
|
||||
assert!(
|
||||
tmux(&[
|
||||
"show-options",
|
||||
"-w",
|
||||
"-t",
|
||||
&target,
|
||||
"-v",
|
||||
"pane-border-format"
|
||||
])
|
||||
.contains("@socktop_label"),
|
||||
"{window} is missing the pane-border-format"
|
||||
);
|
||||
|
||||
// Labels live in a pane-scoped user option precisely so the program in
|
||||
// the pane cannot overwrite them with a title escape sequence.
|
||||
let labels = tmux(&["list-panes", "-t", &target, "-F", "#{@socktop_label}"]);
|
||||
assert_eq!(
|
||||
labels.lines().collect::<Vec<_>>(),
|
||||
want_labels,
|
||||
"{window} has the wrong pane labels"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user