Fix niri compatibility, add versioned niri-ipc dependencies

Using multiple niri-ipc versions need an ugly workaround
where we vendor and re-publish old versions to crates.io
See deps/README.md

Fixes issue:
https://github.com/gergo-salyi/multibg-wayland/issues/16
This commit is contained in:
Gergő Sályi 2025-06-01 13:39:50 +02:00
parent 620219e18e
commit 9005a99289
13 changed files with 2192 additions and 12 deletions

17
Cargo.lock generated
View file

@ -640,6 +640,7 @@ dependencies = [
"image",
"libc",
"log",
"multibg-wayland-niri-ipc",
"niri-ipc",
"rustix",
"scopeguard",
@ -650,10 +651,20 @@ dependencies = [
]
[[package]]
name = "niri-ipc"
version = "25.2.0"
name = "multibg-wayland-niri-ipc"
version = "0.250200.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01515d0a7e73f1f3bd0347100542c4c3f6ebc280688add12e7ed2af4c35af4fb"
checksum = "57a296f0c9e420f45cc2671130f99e9291c99c644934ad948c8a2f34e75ba368"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "niri-ipc"
version = "25.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc3e165f7854b2f83054a2e8f7024baa49666ad25cdb95b8fb9fd17c48045605"
dependencies = [
"serde",
"serde_json",

View file

@ -11,7 +11,7 @@ repository = "https://github.com/gergo-salyi/multibg-wayland"
license = "MIT OR Apache-2.0"
keywords = ["wallpaper", "background", "desktop", "wayland", "sway"]
categories = ["command-line-utilities", "multimedia::images"]
exclude = ["/PKGBUILD", "/PKGBUILD.in", "/scripts/"]
exclude = ["/PKGBUILD", "/PKGBUILD.in", "/deps/", "/scripts/"]
[dependencies]
anyhow = "1.0.97"
@ -21,7 +21,8 @@ env_logger = "0.11.3"
fast_image_resize = "5.0.0"
libc = "0.2.171"
log = "0.4.21"
niri-ipc = "=25.2.0"
niri-ipc-25-2-0 = { package = "multibg-wayland-niri-ipc", version = "=0.250200.0" }
niri-ipc-25-5-1 = { package = "niri-ipc", version = "=25.5.1" }
rustix = { version = "0.38.44", features = ["event", "fs", "pipe"] }
scopeguard = "1.2.0"
serde = { version = "1.0.219", features = ["derive"] }

View file

@ -119,6 +119,8 @@ If using the --gpu option also consider installing Vulkan validation layers from
## License
Source files in this project are distributed under MIT OR Apache-2.0
Source files in this project (except vendored dependencies under `deps/`) are distributed under MIT OR Apache-2.0.
Vendored dependencies under `deps/` are distributed under their respective licenses.
Objects resulting from building this project might be under GPL-3.0-or-later due to licenses of statically linked dependencies. Open an issue if you need compile time features gating such dependencies.

41
deps/README.md vendored Normal file
View file

@ -0,0 +1,41 @@
# Vendored dependencies
We need to use multiple versions of the `niri-ipc` crate dependency to maintain compatibility with multiple version of niri. Ideally we would do this:
```toml
# Cargo.toml
[dependencies]
niri-ipc-25-2-0 = { package = "niri-ipc", version = "=25.2.0" }
niri-ipc-25-5-1 = { package = "niri-ipc", version = "=25.5.1" }
```
However for some braindamaged reasons `cargo` refuses to allow this if any of the multiple versions are semver compatible (see [cargo issue](https://github.com/rust-lang/cargo/issues/12787))
So we do a disgusting workaround here where we vendor older versions of the `niri-ipc` crate and re-publish them on crates.io under the name `multibg-wayland-niri-ipc` with semver incompatible versions e.g. `"25.2.0"` => `"0.250200.0"`
## License
Vendored dependencies are included here under their respective licenses
## Workflow
Example:
Download the crate:
```sh
curl --fail --proto '=https' --tlsv1.2 https://static.crates.io/crates/niri-ipc/niri-ipc-25.2.0.crate | tar -xz
```
Remove crates.io artifacts:
```sh
rm -f .cargo_vcs_info.json Cargo.toml.orig
```
Edit `Cargo.toml`:
- `name = "niri-ipc"` => `name = "multibg-wayland-niri-ipc"`
- `version = "25.2.0"` => `version = "0.250200.0"`
Re-publish:
```sh
cargo publish
```

338
deps/niri-ipc-25.2.0/Cargo.lock generated vendored Normal file
View file

@ -0,0 +1,338 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys",
]
[[package]]
name = "clap"
version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "dyn-clone"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "multibg-wayland-niri-ipc"
version = "0.250200.0"
dependencies = [
"clap",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "once_cell"
version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "proc-macro2"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "schemars"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
dependencies = [
"dyn-clone",
"schemars_derive",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn",
]
[[package]]
name = "serde"
version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

55
deps/niri-ipc-25.2.0/Cargo.toml vendored Normal file
View file

@ -0,0 +1,55 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "multibg-wayland-niri-ipc" # name = "niri-ipc"
version = "0.250200.0" # version = "25.2.0"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Types and helpers for interfacing with the niri Wayland compositor."
readme = "README.md"
keywords = ["wayland"]
categories = [
"api-bindings",
"os",
]
license = "GPL-3.0-or-later"
repository = "https://github.com/YaLTeR/niri"
[features]
clap = ["dep:clap"]
json-schema = ["dep:schemars"]
[lib]
name = "niri_ipc"
path = "src/lib.rs"
[dependencies.clap]
version = "4.5.30"
features = ["derive"]
optional = true
[dependencies.schemars]
version = "0.8.21"
optional = true
[dependencies.serde]
version = "1.0.218"
features = ["derive"]
[dependencies.serde_json]
version = "1.0.139"

16
deps/niri-ipc-25.2.0/README.md vendored Normal file
View file

@ -0,0 +1,16 @@
# niri-ipc
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
## Backwards compatibility
This crate follows the niri version.
It is **not** API-stable in terms of the Rust semver.
In particular, expect new struct fields and enum variants to be added in patch version bumps.
Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=25.2.0"
```

1308
deps/niri-ipc-25.2.0/src/lib.rs vendored Normal file

File diff suppressed because it is too large Load diff

77
deps/niri-ipc-25.2.0/src/socket.rs vendored Normal file
View file

@ -0,0 +1,77 @@
//! Helper for blocking communication over the niri socket.
use std::env;
use std::io::{self, BufRead, BufReader, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use std::path::Path;
use crate::{Event, Reply, Request};
/// Name of the environment variable containing the niri IPC socket path.
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
/// Helper for blocking communication over the niri socket.
///
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
/// and serialization/deserialization of messages.
pub struct Socket {
stream: UnixStream,
}
impl Socket {
/// Connects to the default niri IPC socket.
///
/// This is equivalent to calling [`Self::connect_to`] with the path taken from the
/// [`SOCKET_PATH_ENV`] environment variable.
pub fn connect() -> io::Result<Self> {
let socket_path = env::var_os(SOCKET_PATH_ENV).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("{SOCKET_PATH_ENV} is not set, are you running this within niri?"),
)
})?;
Self::connect_to(socket_path)
}
/// Connects to the niri IPC socket at the given path.
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
let stream = UnixStream::connect(path.as_ref())?;
Ok(Self { stream })
}
/// Sends a request to niri and returns the response.
///
/// Return values:
///
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
/// * `Ok(Err(message))`: error message from niri
/// * `Err(error)`: error communicating with niri
///
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
/// otherwise.
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
let Self { mut stream } = self;
let mut buf = serde_json::to_string(&request).unwrap();
stream.write_all(buf.as_bytes())?;
stream.shutdown(Shutdown::Write)?;
let mut reader = BufReader::new(stream);
buf.clear();
reader.read_line(&mut buf)?;
let reply = serde_json::from_str(&buf)?;
let events = move || {
buf.clear();
reader.read_line(&mut buf)?;
let event = serde_json::from_str(&buf)?;
Ok(event)
};
Ok((reply, events))
}
}

194
deps/niri-ipc-25.2.0/src/state.rs vendored Normal file
View file

@ -0,0 +1,194 @@
//! Helpers for keeping track of the event stream state.
//!
//! 1. Create an [`EventStreamState`] using `Default::default()`, or any individual state part if
//! you only care about part of the state.
//! 2. Connect to the niri socket and request an event stream.
//! 3. Pass every [`Event`] to [`EventStreamStatePart::apply`] on your state.
//! 4. Read the fields of the state as needed.
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use crate::{Event, KeyboardLayouts, Window, Workspace};
/// Part of the state communicated via the event stream.
pub trait EventStreamStatePart {
/// Returns a sequence of events that replicates this state from default initialization.
fn replicate(&self) -> Vec<Event>;
/// Applies the event to this state.
///
/// Returns `None` after applying the event, and `Some(event)` if the event is ignored by this
/// part of the state.
fn apply(&mut self, event: Event) -> Option<Event>;
}
/// The full state communicated over the event stream.
///
/// Different parts of the state are not guaranteed to be consistent across every single event
/// sent by niri. For example, you may receive the first [`Event::WindowOpenedOrChanged`] for a
/// just-opened window *after* an [`Event::WorkspaceActiveWindowChanged`] for that window. Between
/// these two events, the workspace active window id refers to a window that does not yet exist in
/// the windows state part.
#[derive(Debug, Default)]
pub struct EventStreamState {
/// State of workspaces.
pub workspaces: WorkspacesState,
/// State of workspaces.
pub windows: WindowsState,
/// State of the keyboard layouts.
pub keyboard_layouts: KeyboardLayoutsState,
}
/// The workspaces state communicated over the event stream.
#[derive(Debug, Default)]
pub struct WorkspacesState {
/// Map from a workspace id to the workspace.
pub workspaces: HashMap<u64, Workspace>,
}
/// The windows state communicated over the event stream.
#[derive(Debug, Default)]
pub struct WindowsState {
/// Map from a window id to the window.
pub windows: HashMap<u64, Window>,
}
/// The keyboard layout state communicated over the event stream.
#[derive(Debug, Default)]
pub struct KeyboardLayoutsState {
/// Configured keyboard layouts.
pub keyboard_layouts: Option<KeyboardLayouts>,
}
impl EventStreamStatePart for EventStreamState {
fn replicate(&self) -> Vec<Event> {
let mut events = Vec::new();
events.extend(self.workspaces.replicate());
events.extend(self.windows.replicate());
events.extend(self.keyboard_layouts.replicate());
events
}
fn apply(&mut self, event: Event) -> Option<Event> {
let event = self.workspaces.apply(event)?;
let event = self.windows.apply(event)?;
let event = self.keyboard_layouts.apply(event)?;
Some(event)
}
}
impl EventStreamStatePart for WorkspacesState {
fn replicate(&self) -> Vec<Event> {
let workspaces = self.workspaces.values().cloned().collect();
vec![Event::WorkspacesChanged { workspaces }]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::WorkspacesChanged { workspaces } => {
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
}
Event::WorkspaceActivated { id, focused } => {
let ws = self.workspaces.get(&id);
let ws = ws.expect("activated workspace was missing from the map");
let output = ws.output.clone();
for ws in self.workspaces.values_mut() {
let got_activated = ws.id == id;
if ws.output == output {
ws.is_active = got_activated;
}
if focused {
ws.is_focused = got_activated;
}
}
}
Event::WorkspaceActiveWindowChanged {
workspace_id,
active_window_id,
} => {
let ws = self.workspaces.get_mut(&workspace_id);
let ws = ws.expect("changed workspace was missing from the map");
ws.active_window_id = active_window_id;
}
event => return Some(event),
}
None
}
}
impl EventStreamStatePart for WindowsState {
fn replicate(&self) -> Vec<Event> {
let windows = self.windows.values().cloned().collect();
vec![Event::WindowsChanged { windows }]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::WindowsChanged { windows } => {
self.windows = windows.into_iter().map(|win| (win.id, win)).collect();
}
Event::WindowOpenedOrChanged { window } => {
let (id, is_focused) = match self.windows.entry(window.id) {
Entry::Occupied(mut entry) => {
let entry = entry.get_mut();
*entry = window;
(entry.id, entry.is_focused)
}
Entry::Vacant(entry) => {
let entry = entry.insert(window);
(entry.id, entry.is_focused)
}
};
if is_focused {
for win in self.windows.values_mut() {
if win.id != id {
win.is_focused = false;
}
}
}
}
Event::WindowClosed { id } => {
let win = self.windows.remove(&id);
win.expect("closed window was missing from the map");
}
Event::WindowFocusChanged { id } => {
for win in self.windows.values_mut() {
win.is_focused = Some(win.id) == id;
}
}
event => return Some(event),
}
None
}
}
impl EventStreamStatePart for KeyboardLayoutsState {
fn replicate(&self) -> Vec<Event> {
if let Some(keyboard_layouts) = self.keyboard_layouts.clone() {
vec![Event::KeyboardLayoutsChanged { keyboard_layouts }]
} else {
vec![]
}
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
self.keyboard_layouts = Some(keyboard_layouts);
}
Event::KeyboardLayoutSwitched { idx } => {
let kb = self.keyboard_layouts.as_mut();
let kb = kb.expect("keyboard layouts must be set before a layout can be switched");
kb.current_idx = idx;
}
event => return Some(event),
}
None
}
}

View file

@ -1,14 +1,18 @@
mod hyprland;
mod niri;
mod niri2502;
mod niri2505;
mod sway;
use std::{
env,
os::unix::ffi::OsStrExt,
process::Command,
sync::{mpsc::Sender, Arc},
thread,
};
use anyhow::{bail, Context};
use serde::Deserialize;
use log::{debug, warn};
use crate::poll::Waker;
@ -116,7 +120,17 @@ impl ConnectionTask {
Compositor::Hyprland => Box::new(
hyprland::HyprlandConnectionTask::new()
),
Compositor::Niri => Box::new(niri::NiriConnectionTask::new()),
Compositor::Niri => match get_niri_version() {
Ok(niri_verison) => if niri_verison >= niri_ver(25, 5) {
Box::new(niri2505::NiriConnectionTask::new())
} else {
Box::new(niri2502::NiriConnectionTask::new())
},
Err(e) => {
warn!("Failed to get niri version: {e:#}");
Box::new(niri2505::NiriConnectionTask::new())
}
}
};
ConnectionTask {
@ -144,9 +158,19 @@ impl ConnectionTask {
hyprland::HyprlandConnectionTask::new();
composer_interface.subscribe_event_loop(event_sender);
}
Compositor::Niri => {
let composer_interface = niri::NiriConnectionTask::new();
composer_interface.subscribe_event_loop(event_sender);
Compositor::Niri => match get_niri_version() {
Ok(niri_verison) => if niri_verison >= niri_ver(25, 5) {
niri2505::NiriConnectionTask::new()
.subscribe_event_loop(event_sender)
} else {
niri2502::NiriConnectionTask::new()
.subscribe_event_loop(event_sender)
},
Err(e) => {
warn!("Failed to get niri version: {e:#}");
niri2505::NiriConnectionTask::new()
.subscribe_event_loop(event_sender)
}
}
})
.unwrap();
@ -191,3 +215,39 @@ pub struct WorkspaceVisible {
pub output: String,
pub workspace_name: String,
}
#[derive(Deserialize)]
struct NiriVersionJson {
compositor: String,
}
// Example:
// $ niri msg --json version
// {"cli":"25.02 (unknown commit)","compositor":"25.02 (unknown commit)"}
fn get_niri_version() -> anyhow::Result<u64> {
let out = Command::new("niri")
.args(["msg", "--json", "version"])
.output().context("Command niri msg version failed")?;
if !out.status.success() {
bail!("Command niri msg version exited with {}: {}",
out.status, String::from_utf8_lossy(&out.stderr));
}
let version_json: NiriVersionJson = serde_json::from_slice(&out.stdout)
.context("Failed to deserialize niri msg version json")?;
debug!("Niri version: {}", version_json.compositor);
let version = parse_niri_version(&version_json.compositor)
.context("Failed to parse niri version")?;
Ok(version)
}
fn parse_niri_version(version_str: &str) -> Option<u64> {
// Example: "25.02 (unknown commit)"
let mut iter = version_str.split(|c: char| !c.is_ascii_digit());
let major = iter.next()?.parse::<u32>().ok()?;
let minor = iter.next()?.parse::<u32>().ok()?;
Some(niri_ver(major, minor))
}
fn niri_ver(major: u32, minor: u32) -> u64 {
((major as u64) << 32) | (minor as u64)
}

View file

@ -1,7 +1,7 @@
use std::io;
use log::debug;
use niri_ipc::{socket::Socket, Event, Request, Response, Workspace};
use niri_ipc_25_2_0::{socket::Socket, Event, Request, Response, Workspace};
use super::{CompositorInterface, EventSender, WorkspaceVisible};

View file

@ -0,0 +1,77 @@
use std::io;
use log::debug;
use niri_ipc_25_5_1::{socket::Socket, Event, Request, Response, Workspace};
use super::{CompositorInterface, EventSender, WorkspaceVisible};
pub struct NiriConnectionTask {}
impl NiriConnectionTask {
pub fn new() -> Self {
NiriConnectionTask {}
}
}
impl CompositorInterface for NiriConnectionTask {
fn request_visible_workspaces(&mut self) -> Vec<WorkspaceVisible> {
request_workspaces().into_iter()
.filter(|w| w.is_active)
.map(|workspace| WorkspaceVisible {
output: workspace.output.unwrap_or_default(),
workspace_name: workspace.name
.unwrap_or_else(|| format!("{}", workspace.idx)),
})
.collect()
}
fn subscribe_event_loop(self, event_sender: EventSender) {
let mut workspaces_state = request_workspaces();
let mut callback = request_event_stream();
while let Ok(event) = callback() {
match event {
Event::WorkspaceActivated { id, focused: _ } => {
debug!("Niri event: workspace id {id} activated");
let visible_workspace =
find_workspace(&workspaces_state, id);
event_sender.send(visible_workspace);
},
Event::WorkspacesChanged { workspaces } => {
debug!("Niri event: workspaces changed: {workspaces:?}");
workspaces_state = workspaces
},
_ => {},
}
}
}
}
fn find_workspace(workspaces: &[Workspace], id: u64) -> WorkspaceVisible {
let workspace = workspaces.iter()
.find(|workspace| workspace.id == id)
.unwrap_or_else(|| panic!("Unknown niri workspace id {id}"));
let workspace_name = workspace.name.clone()
.unwrap_or_else(|| format!("{}", workspace.idx));
let output = workspace.output.clone().unwrap_or_default();
WorkspaceVisible { output, workspace_name }
}
fn request_event_stream() -> impl FnMut() -> Result<Event, io::Error> {
let mut socket = Socket::connect().expect("failed to connect to niri socket");
let Ok(Ok(Response::Handled)) = socket.send(Request::EventStream) else {
panic!("failed to subscribe to event stream");
};
socket.read_events()
}
fn request_workspaces() -> Vec<Workspace> {
let response = Socket::connect()
.expect("failed to connect to niri socket")
.send(Request::Workspaces)
.expect("failed to send niri ipc request")
.expect("niri workspace query failed");
let Response::Workspaces(workspaces) = response else {
panic!("unexpected response from niri");
};
workspaces
}