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 <tazjin@tvl.su>
Tested-by: BuildkiteCI
This commit is contained in:
Vincent Ambo 2024-09-07 18:52:25 +03:00 committed by tazjin
parent 8206f68aea
commit 158ba0d607
5 changed files with 201 additions and 0 deletions

1
users/tazjin/niri-reap/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

104
users/tazjin/niri-reap/Cargo.lock generated Normal file
View file

@ -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"

View file

@ -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" }

View file

@ -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";
};
};
}

View file

@ -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);
}
}