From 158ba0d607c3b9cae23d2407587afa1c3e679376 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 7 Sep 2024 18:52:25 +0300 Subject: [PATCH] feat(tazjin/niri-reap): add a workspace compacting tool I don't use workspaces and don't have them bound to anything in my Niri configuration. However, when an external screen is unplugged, its workspace (and windows) move to one of the remaining outputs. This adds a tool that makes the windows available again by "reaping" them from the other workspaces and moving them to the current one. For starters I'll bind this to a key and see how it works in practice. Change-Id: I18b2d60e93c8397dd637cdc426b4e46af5725558 Reviewed-on: https://cl.tvl.fyi/c/depot/+/12451 Reviewed-by: tazjin Tested-by: BuildkiteCI --- users/tazjin/niri-reap/.gitignore | 1 + users/tazjin/niri-reap/Cargo.lock | 104 +++++++++++++++++++++++++++++ users/tazjin/niri-reap/Cargo.toml | 7 ++ users/tazjin/niri-reap/default.nix | 13 ++++ users/tazjin/niri-reap/src/main.rs | 76 +++++++++++++++++++++ 5 files changed, 201 insertions(+) create mode 100644 users/tazjin/niri-reap/.gitignore create mode 100644 users/tazjin/niri-reap/Cargo.lock create mode 100644 users/tazjin/niri-reap/Cargo.toml create mode 100644 users/tazjin/niri-reap/default.nix create mode 100644 users/tazjin/niri-reap/src/main.rs diff --git a/users/tazjin/niri-reap/.gitignore b/users/tazjin/niri-reap/.gitignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/users/tazjin/niri-reap/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/users/tazjin/niri-reap/Cargo.lock b/users/tazjin/niri-reap/Cargo.lock new file mode 100644 index 000000000..d2f5c5307 --- /dev/null +++ b/users/tazjin/niri-reap/Cargo.lock @@ -0,0 +1,104 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "niri-ipc" +version = "0.1.8" +source = "git+https://github.com/YaLTeR/niri.git#370fd4e172ec3daf9dc9c75dc0555fe91182f731" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "niri-reap" +version = "0.1.0" +dependencies = [ + "niri-ipc", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/users/tazjin/niri-reap/Cargo.toml b/users/tazjin/niri-reap/Cargo.toml new file mode 100644 index 000000000..20512685b --- /dev/null +++ b/users/tazjin/niri-reap/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "niri-reap" +version = "0.1.0" +edition = "2021" + +[dependencies] +niri-ipc = { git = "https://github.com/YaLTeR/niri.git", version = "0.1.8" } diff --git a/users/tazjin/niri-reap/default.nix b/users/tazjin/niri-reap/default.nix new file mode 100644 index 000000000..b2a0594b7 --- /dev/null +++ b/users/tazjin/niri-reap/default.nix @@ -0,0 +1,13 @@ +{ depot, pkgs, ... }: + +pkgs.rustPlatform.buildRustPackage { + name = "niri-reap"; + src = depot.third_party.gitignoreSource ./.; + + cargoLock = { + lockFile = ./Cargo.lock; + outputHashes = { + "niri-ipc-0.1.8" = "sha256:0wyl0mpk9hg67bvj7q120wanrdqn3ls9zv9vjv9yxp11kan5pi1q"; + }; + }; +} diff --git a/users/tazjin/niri-reap/src/main.rs b/users/tazjin/niri-reap/src/main.rs new file mode 100644 index 000000000..d89b18fc5 --- /dev/null +++ b/users/tazjin/niri-reap/src/main.rs @@ -0,0 +1,76 @@ +use niri_ipc::socket::Socket; +use niri_ipc::{Action, Reply, Request, Response, Window, Workspace}; + +fn sock() -> Socket { + Socket::connect().expect("could not connect to Niri socket") +} + +fn list_workspaces() -> Vec { + let (reply, _) = sock() + .send(Request::Workspaces) + .expect("failed to send workspace request"); + + match reply { + Reply::Err(err) => panic!("failed to list workspaces: {}", err), + Reply::Ok(Response::Workspaces(w)) => w, + Reply::Ok(other) => panic!("unexpected reply from Niri: {:#?}", other), + } +} + +fn list_windows() -> Vec { + let (reply, _) = sock() + .send(Request::Windows) + .expect("failed to send window request"); + + match reply { + Reply::Err(err) => panic!("failed to list windows: {}", err), + Reply::Ok(Response::Windows(w)) => w, + Reply::Ok(other) => panic!("unexpected reply from Niri: {:#?}", other), + } +} + +fn reap_window(window: u64, workspace: u64) { + let (reply, _) = sock() + .send(Request::Action(Action::MoveWindowToWorkspace { + window_id: Some(window), + reference: niri_ipc::WorkspaceReferenceArg::Id(workspace), + })) + .expect("failed to send window move request"); + + reply.expect("failed to move window to workspace"); +} + +fn main() { + let workspaces = list_workspaces(); + + let active_workspace = workspaces + .iter() + .filter(|w| w.is_focused) + .next() + .expect("expected an active workspace"); + + let orphan_workspaces = workspaces + .iter() + .filter(|w| w.output == active_workspace.output) + // Only select workspaces that are further down, to avoid issues with + // indices changing during the operation. + .filter(|w| w.idx > active_workspace.idx) + .map(|w| w.id) + .collect::>(); + + if orphan_workspaces.is_empty() { + return; + } + + let reapable = list_windows() + .into_iter() + .filter(|w| match w.workspace_id { + Some(id) => orphan_workspaces.contains(&id), + None => true, + }) + .collect::>(); + + for window in reapable.iter().rev() { + reap_window(window.id, active_workspace.id); + } +}