about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVincent Ambo <tazjin@tvl.su>2024-09-07T15·52+0300
committertazjin <tazjin@tvl.su>2024-09-13T10·52+0000
commit158ba0d607c3b9cae23d2407587afa1c3e679376 (patch)
tree1a9c40af8f00e64d86a1fe0f19c3400ffbf24a21
parent8206f68aeaa701647211ba9b32e903e42364670c (diff)
feat(tazjin/niri-reap): add a workspace compacting tool r/8672
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 <tazjin@tvl.su>
Tested-by: BuildkiteCI
-rw-r--r--users/tazjin/niri-reap/.gitignore1
-rw-r--r--users/tazjin/niri-reap/Cargo.lock104
-rw-r--r--users/tazjin/niri-reap/Cargo.toml7
-rw-r--r--users/tazjin/niri-reap/default.nix13
-rw-r--r--users/tazjin/niri-reap/src/main.rs76
5 files changed, 201 insertions, 0 deletions
diff --git a/users/tazjin/niri-reap/.gitignore b/users/tazjin/niri-reap/.gitignore
new file mode 100644
index 000000000000..2f7896d1d136
--- /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 000000000000..d2f5c53075f2
--- /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 000000000000..20512685b48d
--- /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 000000000000..b2a0594b7748
--- /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 000000000000..d89b18fc57cf
--- /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<Workspace> {
+    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<Window> {
+    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::<Vec<_>>();
+
+    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::<Vec<_>>();
+
+    for window in reapable.iter().rev() {
+        reap_window(window.id, active_workspace.id);
+    }
+}