diff --git a/README.md b/README.md index 0b3b5b4..f5e71d8 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,12 @@ only sway is tested. sway is also the source of truth for the window list, over its IPC socket, which is the one thing that would need replacing to run elsewhere (`ext-foreign-toplevel-list-v1` already reports app id and title). +The socket is found from `SWAYSOCK`/`I3SOCK` when those point at something that +exists, and otherwise by looking for the running sway's socket in +`$XDG_RUNTIME_DIR`. Inheriting a stale path is easy — any process that outlives +the sway that started it hands one to every shell it spawns — and a picker on a +keybinding should not be the thing that notices. + Known upstream issue: holding per-toplevel capture sessions open makes windows blurry on **fractionally scaled** outputs ([sway#9113](https://github.com/swaywm/sway/issues/9113)). Integer scales are diff --git a/src/main.rs b/src/main.rs index 3556eba..317d1f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,9 +76,7 @@ fn run() -> Result> { // One IPC conversation: the window list, and the displays the grid sizes // itself against. It is closed again before the overlay maps. let (targets, opts) = { - let mut sway = swayipc::Connection::new().map_err(|e| { - format!("cannot reach sway ({e}); wl-pick reads the window list from its IPC socket") - })?; + let mut sway = sway::connect()?; // The displays come first: the grid is sized against the one it will // appear on, so every percentage in the config resolves per monitor. let displays = sway::displays(&mut sway)?; diff --git a/src/sway.rs b/src/sway.rs index 05c7b8d..ae7857a 100644 --- a/src/sway.rs +++ b/src/sway.rs @@ -5,10 +5,100 @@ //! Acting on the choice is deliberately not here: wl-pick reports what was picked //! and the caller decides what that means. +use std::os::unix::net::UnixStream; +use std::path::PathBuf; + use swayipc::{Connection, Node, NodeType}; use crate::target::Target; +/// Open the IPC connection, recovering when the environment lies about where +/// the socket is. +/// +/// swayipc takes the path from `I3SOCK` or `SWAYSOCK` and only falls back to +/// asking sway directly when *neither is set* -- a variable that is set but +/// stale is used as-is, and fails. That happens whenever something in a +/// shell's ancestry outlived the sway that started it: one long-running daemon +/// is enough, and every shell it spawns inherits a path to a socket that no +/// longer exists. Since the running compositor is the one we want either way, +/// go and find its socket instead of failing. +pub fn connect() -> Result { + // The environment still wins when it points at something real. Going + // through swayipc's own lookup instead would spawn `sway + // --get-socketpath` whenever the variables are unset, which prints a + // complaint of its own before we can say anything useful. + if let Some(conn) = env_socket().and_then(|p| UnixStream::connect(p).ok()) { + return Ok(Connection::from(conn)); + } + let live = live_sockets(); + let [path] = live.as_slice() else { + return Err(if live.is_empty() { + "cannot reach sway; wl-pick reads the window list from its IPC \ + socket, and no running sway has one" + .to_string() + } else { + // Several live compositors, so any choice would be a guess: a + // nested sway is a real thing to be running. + format!( + "several sway sockets to choose from ({}); set SWAYSOCK to the one you mean", + live.iter() + .map(|p| p.display().to_string()) + .collect::>() + .join(", ") + ) + }); + }; + UnixStream::connect(path) + .map(Connection::from) + .map_err(|e| format!("cannot reach sway on {} ({e})", path.display())) +} + +/// The pid out of a `sway-ipc...sock` name, and nothing else. +fn socket_pid(name: &str) -> Option<&str> { + name.strip_prefix("sway-ipc.")? + .strip_suffix(".sock")? + .rsplit('.') + .next() + .filter(|pid| !pid.is_empty() && pid.bytes().all(|b| b.is_ascii_digit())) +} + +/// The socket the environment names, if it is actually there. sway's own +/// variable comes second because swayipc reads them in this order. +fn env_socket() -> Option { + ["I3SOCK", "SWAYSOCK"] + .into_iter() + .filter_map(std::env::var_os) + .map(PathBuf::from) + .find(|path| path.exists()) +} + +/// Sockets in the runtime directory whose sway is still running. They are named +/// `sway-ipc...sock`, so the pid says which are worth trying -- and +/// pids get reused, so it has to actually be a sway. +fn live_sockets() -> Vec { + let Some(dir) = std::env::var_os("XDG_RUNTIME_DIR") else { + return Vec::new(); + }; + let Ok(entries) = std::fs::read_dir(dir) else { + return Vec::new(); + }; + let mut found: Vec = entries + .flatten() + .map(|e| e.path()) + .filter(|path| { + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { + return false; + }; + socket_pid(name) + .and_then(|pid| std::fs::read_to_string(format!("/proc/{pid}/comm")).ok()) + .is_some_and(|comm| comm.trim() == "sway") + }) + .collect(); + // Stable order, so the message about several of them does not shuffle. + found.sort(); + found +} + /// Every view in the tree, in tree order (the same traversal the jq filter did, /// so the grid keeps the ordering the muscle memory expects). pub fn windows(conn: &mut Connection) -> Result, swayipc::Error> { @@ -78,3 +168,20 @@ pub fn focused(displays: &[Display]) -> Option<&Display> { .find(|d| d.focused) .or_else(|| displays.first()) } + +#[cfg(test)] +mod tests { + use super::socket_pid; + + #[test] + fn a_socket_name_gives_up_its_pid() { + assert_eq!(socket_pid("sway-ipc.1000.573773.sock"), Some("573773")); + // Anything that is not a live sway's socket must not be tried: the + // runtime directory is full of other people's sockets. + assert_eq!(socket_pid("wayland-1"), None); + assert_eq!(socket_pid("sway-ipc.1000.573773.sock.bak"), None); + assert_eq!(socket_pid("i3-ipc.1000.5.sock"), None); + assert_eq!(socket_pid("sway-ipc.1000..sock"), None); + assert_eq!(socket_pid("sway-ipc.1000.notapid.sock"), None); + } +}