ce6acec299
Installer rewritten around a preflight: distro, package manager, display manager, window manager, terminal, tmux, cargo, git, screen locker, touch device, device permissions and free disk are all checked BEFORE anything is installed, and the total cost is printed once for a single confirmation. Prompts read /dev/tty so they still work when the script is piped from curl, and fall back to defaults with a notice when there is no terminal at all. Several "[ test ] && action" statements were set -e landmines: under set -e an AND-OR list that ends up false aborts the script, so a box with no lightdm, no i3 or nothing to install would have exited silently partway through detection -- which is exactly the fresh-Debian case the installer exists for. Rewritten as if-statements and verified against a stripped PATH with no tmux, cargo, git or package manager present. Also fixed cargo detection reporting blank instead of NOT INSTALLED: the status of `cargo --version | cut` is cut's, and cut succeeds on empty input, so the fallback never fired. Device access now defaults to a udev rule matching touchscreens only, rather than the input group, which grants access to every input device including the keyboard and needs a full logout. README rewritten for someone who has not seen the project: what the photo shows, the hardware, install, then a config built up step by step, each step with the YAML and the resulting map. Every example is verified verbatim against the binary, and every relative link resolves. The mechanism and the reasoning move to notes/: DESIGN.md, HARDWARE-NOTES.md, V1-BASH.md, TODO.md. cad/README.md was a verbatim copy of the one inside geeekpi_rack_adapter_release_v1/, so every path in it -- including the screenshot -- was broken from where it sits. Corrected to its own level, and it now states once that the 9-inch screen, the 10-inch mini-rack mount and the 19-inch rack are three different measurements. The v1 shell implementation is removed; it stays recoverable at tag v1.2 and notes/V1-BASH.md carries the setting-by-setting migration table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
201 lines
5.9 KiB
Rust
201 lines
5.9 KiB
Rust
//! End-to-end against a real tmux server.
|
|
//!
|
|
//! Covers what the unit tests deliberately cannot: that the grid's idea of
|
|
//! "sub-screen 3 of the cell at 0x0" lands on the pane a person would expect,
|
|
//! zoomed. Skipped when tmux is not installed.
|
|
|
|
use std::process::{Command, Stdio};
|
|
|
|
use socktop_swipe::config::Config;
|
|
use socktop_swipe::grid::{Grid, Move};
|
|
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")
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()
|
|
.map(|s| s.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn tmux(args: &[&str]) -> String {
|
|
let out = Command::new("tmux").args(args).output().expect("tmux");
|
|
String::from_utf8_lossy(&out.stdout).trim().to_owned()
|
|
}
|
|
|
|
fn config() -> Config {
|
|
// `true` exits at once, which also exercises remain-on-exit keeping the
|
|
// pane addressable afterwards.
|
|
let yaml = format!(
|
|
r#"
|
|
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:
|
|
- at: "-1x0"
|
|
type: unifly
|
|
- at: "0x0"
|
|
type: socktop
|
|
socktop_group: [alpha, bravo, charlie, delta]
|
|
layout: tiled
|
|
- at: "0x5"
|
|
type: socktop
|
|
socktop_group: "echo1, foxtrot"
|
|
layout: even-vertical
|
|
- at: "1x0"
|
|
type: uptime-kuma-status
|
|
url: https://example.invalid/status
|
|
"#
|
|
);
|
|
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) {
|
|
let s = tmux(&[
|
|
"display-message",
|
|
"-p",
|
|
"-t",
|
|
SESSION,
|
|
"#{window_name}\t#{pane_title}\t#{window_zoomed_flag}",
|
|
]);
|
|
let f: Vec<&str> = s.split('\t').collect();
|
|
(f[0].into(), f[1].into(), f[2] == "1")
|
|
}
|
|
|
|
struct Cleanup;
|
|
impl Drop for Cleanup {
|
|
fn drop(&mut self) {
|
|
let _ = Command::new("tmux")
|
|
.args(["kill-session", "-t", SESSION])
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn session_layout_and_navigation_match_the_grid() {
|
|
if !have_tmux() {
|
|
eprintln!("skipping: tmux is not installed");
|
|
return;
|
|
}
|
|
let _cleanup = Cleanup;
|
|
|
|
let cfg = config();
|
|
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");
|
|
|
|
// One window per cell, in grid order, named by coordinate.
|
|
let windows = tmux(&[
|
|
"list-windows",
|
|
"-t",
|
|
SESSION,
|
|
"-F",
|
|
"#{window_name} #{window_panes}",
|
|
]);
|
|
assert_eq!(
|
|
windows.lines().collect::<Vec<_>>(),
|
|
vec!["rm1c0 1", "r0c0 4", "r0c5 2", "r1c0 1"],
|
|
"windows should be one per cell, in grid order, with one pane per host"
|
|
);
|
|
|
|
// Starts at 0x0's overview: not zoomed, so all four Pis are visible.
|
|
let (win, _, zoomed) = visible();
|
|
assert_eq!(win, "r0c0");
|
|
assert!(!zoomed, "the overview must not be zoomed");
|
|
|
|
// Forward walks the sub-sequence, zooming each host in declaration order.
|
|
for expected in ["alpha", "bravo", "charlie", "delta"] {
|
|
let pos = grid.apply(Move::Forward);
|
|
mux.show(&grid, &pos).unwrap();
|
|
let (win, title, zoomed) = visible();
|
|
assert_eq!(win, "r0c0");
|
|
assert_eq!(title, expected, "wrong host zoomed");
|
|
assert!(zoomed, "{expected} should be zoomed full-screen");
|
|
}
|
|
|
|
// 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();
|
|
assert_eq!(
|
|
win, "r0c5",
|
|
"0x5 follows 0x0 despite the gap in column numbers"
|
|
);
|
|
assert!(!zoomed);
|
|
|
|
// 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();
|
|
assert_eq!(
|
|
(win.as_str(), title.as_str(), zoomed),
|
|
("r0c0", "delta", true)
|
|
);
|
|
|
|
// 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();
|
|
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();
|
|
assert_eq!(
|
|
(win.as_str(), title.as_str(), zoomed),
|
|
("r0c0", "delta", true),
|
|
"returning must restore the host AND its zoom"
|
|
);
|
|
|
|
// Down twice: through row 0 to kuma.
|
|
let pos = grid.apply(Move::Down);
|
|
mux.show(&grid, &pos).unwrap();
|
|
assert_eq!(visible().0, "r1c0");
|
|
}
|
|
|
|
#[test]
|
|
fn pane_commands_are_quoted_not_interpreted() {
|
|
if !have_tmux() {
|
|
return;
|
|
}
|
|
// A profile name containing shell metacharacters must reach the program as
|
|
// one argument, not be re-split or evaluated by the sh tmux runs it under.
|
|
let cfg: Config = serde_yaml::from_str(
|
|
r#"
|
|
session: socktop-swipe-quoting-test
|
|
binaries: { socktop: /bin/echo }
|
|
touch: { device: /dev/null, width: 1, height: 1, grab: false }
|
|
screens:
|
|
- at: "0x0"
|
|
type: socktop
|
|
socktop_group: ["a b; touch /tmp/socktop-swipe-pwned"]
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let cells = monitor::build_cells(&cfg).unwrap();
|
|
let argv = &cells[0].panes[0].command;
|
|
assert_eq!(
|
|
argv,
|
|
&["/bin/echo", "-P", "a b; touch /tmp/socktop-swipe-pwned"]
|
|
);
|
|
|
|
let quoted = shell_words::join(argv.iter().map(String::as_str));
|
|
let reparsed = shell_words::split("ed).unwrap();
|
|
assert_eq!(
|
|
&reparsed, argv,
|
|
"quoting must survive the round trip through sh"
|
|
);
|
|
}
|