about summary refs log tree commit diff
path: root/users/tazjin/niri-reap
diff options
context:
space:
mode:
Diffstat (limited to 'users/tazjin/niri-reap')
-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/README.md20
-rw-r--r--users/tazjin/niri-reap/default.nix13
-rw-r--r--users/tazjin/niri-reap/src/main.rs93
6 files changed, 238 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..e7916c5b3acd
--- /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.9"
+source = "git+https://github.com/YaLTeR/niri.git#6a48728ffb1e638839b07f9ab2f06b2adb41dc61"
+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.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
diff --git a/users/tazjin/niri-reap/Cargo.toml b/users/tazjin/niri-reap/Cargo.toml
new file mode 100644
index 000000000000..5f6677196333
--- /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.9" }
diff --git a/users/tazjin/niri-reap/README.md b/users/tazjin/niri-reap/README.md
new file mode 100644
index 000000000000..207a087657fa
--- /dev/null
+++ b/users/tazjin/niri-reap/README.md
@@ -0,0 +1,20 @@
+niri-reap
+=========
+
+Tiny, MIT-licensed companion program for [niri](https://github.com/YaLTeR/niri).
+
+I don't use workspaces in my workflow, but when disconnecting an external
+screen, the workspaces that it was displaying are moved to the remaining screen.
+
+This program "reaps" all windows on workspaces except the currently active one,
+and moves them all to the current workspace.
+
+## Usage
+
+If you have the full TVL monorepo, just `mg run //users/tazjin/niri-reap`. There
+is no configuration, and there are no flags.
+
+If you don't have the TVL monorepo and just want `niri-reap`, do this:
+
+1. Get the code: `git clone https://code.tvl.fyi/depot.git:/users/tazjin/niri-reap.git`
+2. Run the code: `cargo run`
diff --git a/users/tazjin/niri-reap/default.nix b/users/tazjin/niri-reap/default.nix
new file mode 100644
index 000000000000..80c82f475a2f
--- /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.9" = "sha256:1s294bw62mmckq9xyfzgw4p2nvkzday4k276j60m668prhlfp071";
+    };
+  };
+}
diff --git a/users/tazjin/niri-reap/src/main.rs b/users/tazjin/niri-reap/src/main.rs
new file mode 100644
index 000000000000..315a5015d413
--- /dev/null
+++ b/users/tazjin/niri-reap/src/main.rs
@@ -0,0 +1,93 @@
+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 get_active_workspace(workspaces: &[Workspace]) -> &Workspace {
+    workspaces
+        .iter()
+        .filter(|w| w.is_focused)
+        .next()
+        .expect("expected an active workspace")
+}
+
+fn move_workspace_up() {
+    let (result, _) = sock()
+        .send(Request::Action(Action::MoveWorkspaceUp {}))
+        .expect("failed to send workspace move command");
+
+    result.expect("failed to move workspace up");
+}
+
+fn main() {
+    let mut workspaces = list_workspaces();
+    let mut active_workspace = get_active_workspace(&workspaces);
+
+    // Ensure that the current workspace is the first one, to avoid issues with
+    // indices changing during the window moves.
+    while active_workspace.idx > 1 {
+        move_workspace_up();
+        workspaces = list_workspaces();
+        active_workspace = get_active_workspace(&workspaces);
+    }
+
+    let orphan_workspaces = workspaces
+        .iter()
+        .filter(|w| w.output == active_workspace.output)
+        .filter(|w| w.idx > 1)
+        .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);
+    }
+}