diff --git a/Cargo.lock b/Cargo.lock index b08ca93..b8e1e80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index c726e73..234c6c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/README.md b/README.md index b576131..0e15ee5 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/deps/README.md b/deps/README.md new file mode 100644 index 0000000..b8a7a2d --- /dev/null +++ b/deps/README.md @@ -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 +``` diff --git a/deps/niri-ipc-25.2.0/Cargo.lock b/deps/niri-ipc-25.2.0/Cargo.lock new file mode 100644 index 0000000..3f72dff --- /dev/null +++ b/deps/niri-ipc-25.2.0/Cargo.lock @@ -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" diff --git a/deps/niri-ipc-25.2.0/Cargo.toml b/deps/niri-ipc-25.2.0/Cargo.toml new file mode 100644 index 0000000..77c1df8 --- /dev/null +++ b/deps/niri-ipc-25.2.0/Cargo.toml @@ -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 "] +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" diff --git a/deps/niri-ipc-25.2.0/README.md b/deps/niri-ipc-25.2.0/README.md new file mode 100644 index 0000000..3c311ce --- /dev/null +++ b/deps/niri-ipc-25.2.0/README.md @@ -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" +``` diff --git a/deps/niri-ipc-25.2.0/src/lib.rs b/deps/niri-ipc-25.2.0/src/lib.rs new file mode 100644 index 0000000..e0a213e --- /dev/null +++ b/deps/niri-ipc-25.2.0/src/lib.rs @@ -0,0 +1,1308 @@ +//! Types for communicating with niri via IPC. +//! +//! After connecting to the niri socket, you can send a single [`Request`] and receive a single +//! [`Reply`], which is a `Result` wrapping a [`Response`]. If you requested an event stream, you +//! can keep reading [`Event`]s from the socket after the response. +//! +//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However, +//! it is a fairly simple helper, so if you need async, or if you're using a different language, +//! you are encouraged to communicate with the socket manually. +//! +//! 1. Read the socket filesystem path from [`socket::SOCKET_PATH_ENV`] (`$NIRI_SOCKET`). +//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow +//! up with a line break and a flush, or just flush and shutdown the write end of the socket. +//! 3. Niri will respond with a single line JSON-formatted [`Reply`]. +//! 4. If you requested an event stream, niri will keep responding with JSON-formatted [`Event`]s, +//! on a single line each. +//! +//! ## 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" +//! ``` +//! +//! ## Features +//! +//! This crate defines the following features: +//! - `json-schema`: derives the [schemars](https://lib.rs/crates/schemars) `JsonSchema` trait for +//! the types. +//! - `clap`: derives the clap CLI parsing traits for some types. Used internally by niri itself. +#![warn(missing_docs)] + +use std::collections::HashMap; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +pub mod socket; +pub mod state; + +/// Request from client to niri. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum Request { + /// Request the version string for the running niri instance. + Version, + /// Request information about connected outputs. + Outputs, + /// Request information about workspaces. + Workspaces, + /// Request information about open windows. + Windows, + /// Request information about layer-shell surfaces. + Layers, + /// Request information about the configured keyboard layouts. + KeyboardLayouts, + /// Request information about the focused output. + FocusedOutput, + /// Request information about the focused window. + FocusedWindow, + /// Perform an action. + Action(Action), + /// Change output configuration temporarily. + /// + /// The configuration is changed temporarily and not saved into the config file. If the output + /// configuration subsequently changes in the config file, these temporary changes will be + /// forgotten. + Output { + /// Output name. + output: String, + /// Configuration to apply. + action: OutputAction, + }, + /// Start continuously receiving events from the compositor. + /// + /// The compositor should reply with `Reply::Ok(Response::Handled)`, then continuously send + /// [`Event`]s, one per line. + /// + /// The event stream will always give you the full current state up-front. For example, the + /// first workspace-related event you will receive will be [`Event::WorkspacesChanged`] + /// containing the full current workspaces state. You *do not* need to separately send + /// [`Request::Workspaces`] when using the event stream. + /// + /// Where reasonable, event stream state updates are atomic, though this is not always the + /// case. For example, a window may end up with a workspace id for a workspace that had already + /// been removed. This can happen if the corresponding [`Event::WorkspacesChanged`] arrives + /// before the corresponding [`Event::WindowOpenedOrChanged`]. + EventStream, + /// Respond with an error (for testing error handling). + ReturnError, +} + +/// Reply from niri to client. +/// +/// Every request gets one reply. +/// +/// * If an error had occurred, it will be an `Reply::Err`. +/// * If the request does not need any particular response, it will be +/// `Reply::Ok(Response::Handled)`. Kind of like an `Ok(())`. +/// * Otherwise, it will be `Reply::Ok(response)` with one of the other [`Response`] variants. +pub type Reply = Result; + +/// Successful response from niri to client. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum Response { + /// A request that does not need a response was handled successfully. + Handled, + /// The version string for the running niri instance. + Version(String), + /// Information about connected outputs. + /// + /// Map from output name to output info. + Outputs(HashMap), + /// Information about workspaces. + Workspaces(Vec), + /// Information about open windows. + Windows(Vec), + /// Information about layer-shell surfaces. + Layers(Vec), + /// Information about the keyboard layout. + KeyboardLayouts(KeyboardLayouts), + /// Information about the focused output. + FocusedOutput(Option), + /// Information about the focused window. + FocusedWindow(Option), + /// Output configuration change result. + OutputConfigChanged(OutputConfigChanged), +} + +/// Actions that niri can perform. +// Variants in this enum should match the spelling of the ones in niri-config. Most, but not all, +// variants from niri-config should be present here. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "clap", derive(clap::Parser))] +#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))] +#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum Action { + /// Exit niri. + Quit { + /// Skip the "Press Enter to confirm" prompt. + #[cfg_attr(feature = "clap", arg(short, long))] + skip_confirmation: bool, + }, + /// Power off all monitors via DPMS. + PowerOffMonitors {}, + /// Power on all monitors via DPMS. + PowerOnMonitors {}, + /// Spawn a command. + Spawn { + /// Command to spawn. + #[cfg_attr(feature = "clap", arg(last = true, required = true))] + command: Vec, + }, + /// Do a screen transition. + DoScreenTransition { + /// Delay in milliseconds for the screen to freeze before starting the transition. + #[cfg_attr(feature = "clap", arg(short, long))] + delay_ms: Option, + }, + /// Open the screenshot UI. + Screenshot {}, + /// Screenshot the focused screen. + ScreenshotScreen { + /// Write the screenshot to disk in addition to putting it in your clipboard. + /// + /// The screenshot is saved according to the `screenshot-path` config setting. + #[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))] + write_to_disk: bool, + }, + /// Screenshot a window. + #[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))] + ScreenshotWindow { + /// Id of the window to screenshot. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + /// Write the screenshot to disk in addition to putting it in your clipboard. + /// + /// The screenshot is saved according to the `screenshot-path` config setting. + #[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))] + write_to_disk: bool, + }, + /// Close a window. + #[cfg_attr(feature = "clap", clap(about = "Close the focused window"))] + CloseWindow { + /// Id of the window to close. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Toggle fullscreen on a window. + #[cfg_attr( + feature = "clap", + clap(about = "Toggle fullscreen on the focused window") + )] + FullscreenWindow { + /// Id of the window to toggle fullscreen of. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Focus a window by id. + FocusWindow { + /// Id of the window to focus. + #[cfg_attr(feature = "clap", arg(long))] + id: u64, + }, + /// Focus a window in the focused column by index. + FocusWindowInColumn { + /// Index of the window in the column. + /// + /// The index starts from 1 for the topmost window. + #[cfg_attr(feature = "clap", arg())] + index: u8, + }, + /// Focus the previously focused window. + FocusWindowPrevious {}, + /// Focus the column to the left. + FocusColumnLeft {}, + /// Focus the column to the right. + FocusColumnRight {}, + /// Focus the first column. + FocusColumnFirst {}, + /// Focus the last column. + FocusColumnLast {}, + /// Focus the next column to the right, looping if at end. + FocusColumnRightOrFirst {}, + /// Focus the next column to the left, looping if at start. + FocusColumnLeftOrLast {}, + /// Focus the window or the monitor above. + FocusWindowOrMonitorUp {}, + /// Focus the window or the monitor below. + FocusWindowOrMonitorDown {}, + /// Focus the column or the monitor to the left. + FocusColumnOrMonitorLeft {}, + /// Focus the column or the monitor to the right. + FocusColumnOrMonitorRight {}, + /// Focus the window below. + FocusWindowDown {}, + /// Focus the window above. + FocusWindowUp {}, + /// Focus the window below or the column to the left. + FocusWindowDownOrColumnLeft {}, + /// Focus the window below or the column to the right. + FocusWindowDownOrColumnRight {}, + /// Focus the window above or the column to the left. + FocusWindowUpOrColumnLeft {}, + /// Focus the window above or the column to the right. + FocusWindowUpOrColumnRight {}, + /// Focus the window or the workspace above. + FocusWindowOrWorkspaceDown {}, + /// Focus the window or the workspace above. + FocusWindowOrWorkspaceUp {}, + /// Focus the topmost window. + FocusWindowTop {}, + /// Focus the bottommost window. + FocusWindowBottom {}, + /// Focus the window below or the topmost window. + FocusWindowDownOrTop {}, + /// Focus the window above or the bottommost window. + FocusWindowUpOrBottom {}, + /// Move the focused column to the left. + MoveColumnLeft {}, + /// Move the focused column to the right. + MoveColumnRight {}, + /// Move the focused column to the start of the workspace. + MoveColumnToFirst {}, + /// Move the focused column to the end of the workspace. + MoveColumnToLast {}, + /// Move the focused column to the left or to the monitor to the left. + MoveColumnLeftOrToMonitorLeft {}, + /// Move the focused column to the right or to the monitor to the right. + MoveColumnRightOrToMonitorRight {}, + /// Move the focused window down in a column. + MoveWindowDown {}, + /// Move the focused window up in a column. + MoveWindowUp {}, + /// Move the focused window down in a column or to the workspace below. + MoveWindowDownOrToWorkspaceDown {}, + /// Move the focused window up in a column or to the workspace above. + MoveWindowUpOrToWorkspaceUp {}, + /// Consume or expel a window left. + #[cfg_attr( + feature = "clap", + clap(about = "Consume or expel the focused window left") + )] + ConsumeOrExpelWindowLeft { + /// Id of the window to consume or expel. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Consume or expel a window right. + #[cfg_attr( + feature = "clap", + clap(about = "Consume or expel the focused window right") + )] + ConsumeOrExpelWindowRight { + /// Id of the window to consume or expel. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Consume the window to the right into the focused column. + ConsumeWindowIntoColumn {}, + /// Expel the focused window from the column. + ExpelWindowFromColumn {}, + /// Swap focused window with one to the right. + SwapWindowRight {}, + /// Swap focused window with one to the left. + SwapWindowLeft {}, + /// Toggle the focused column between normal and tabbed display. + ToggleColumnTabbedDisplay {}, + /// Set the display mode of the focused column. + SetColumnDisplay { + /// Display mode to set. + #[cfg_attr(feature = "clap", arg())] + display: ColumnDisplay, + }, + /// Center the focused column on the screen. + CenterColumn {}, + /// Center a window on the screen. + #[cfg_attr( + feature = "clap", + clap(about = "Center the focused window on the screen") + )] + CenterWindow { + /// Id of the window to center. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Focus the workspace below. + FocusWorkspaceDown {}, + /// Focus the workspace above. + FocusWorkspaceUp {}, + /// Focus a workspace by reference (index or name). + FocusWorkspace { + /// Reference (index or name) of the workspace to focus. + #[cfg_attr(feature = "clap", arg())] + reference: WorkspaceReferenceArg, + }, + /// Focus the previous workspace. + FocusWorkspacePrevious {}, + /// Move the focused window to the workspace below. + MoveWindowToWorkspaceDown {}, + /// Move the focused window to the workspace above. + MoveWindowToWorkspaceUp {}, + /// Move a window to a workspace. + #[cfg_attr( + feature = "clap", + clap(about = "Move the focused window to a workspace by reference (index or name)") + )] + MoveWindowToWorkspace { + /// Id of the window to move. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + window_id: Option, + + /// Reference (index or name) of the workspace to move the window to. + #[cfg_attr(feature = "clap", arg())] + reference: WorkspaceReferenceArg, + }, + /// Move the focused column to the workspace below. + MoveColumnToWorkspaceDown {}, + /// Move the focused column to the workspace above. + MoveColumnToWorkspaceUp {}, + /// Move the focused column to a workspace by reference (index or name). + MoveColumnToWorkspace { + /// Reference (index or name) of the workspace to move the column to. + #[cfg_attr(feature = "clap", arg())] + reference: WorkspaceReferenceArg, + }, + /// Move the focused workspace down. + MoveWorkspaceDown {}, + /// Move the focused workspace up. + MoveWorkspaceUp {}, + /// Move a workspace to a specific index on its monitor. + #[cfg_attr( + feature = "clap", + clap(about = "Move the focused workspace to a specific index on its monitor") + )] + MoveWorkspaceToIndex { + /// New index for the workspace. + #[cfg_attr(feature = "clap", arg())] + index: usize, + + /// Reference (index or name) of the workspace to move. + /// + /// If `None`, uses the focused workspace. + #[cfg_attr(feature = "clap", arg(long))] + reference: Option, + }, + /// Set the name of a workspace. + #[cfg_attr( + feature = "clap", + clap(about = "Set the name of the focused workspace") + )] + SetWorkspaceName { + /// New name for the workspace. + #[cfg_attr(feature = "clap", arg())] + name: String, + + /// Reference (index or name) of the workspace to name. + /// + /// If `None`, uses the focused workspace. + #[cfg_attr(feature = "clap", arg(long))] + workspace: Option, + }, + /// Unset the name of a workspace. + #[cfg_attr( + feature = "clap", + clap(about = "Unset the name of the focused workspace") + )] + UnsetWorkspaceName { + /// Reference (index or name) of the workspace to unname. + /// + /// If `None`, uses the focused workspace. + #[cfg_attr(feature = "clap", arg())] + reference: Option, + }, + /// Focus the monitor to the left. + FocusMonitorLeft {}, + /// Focus the monitor to the right. + FocusMonitorRight {}, + /// Focus the monitor below. + FocusMonitorDown {}, + /// Focus the monitor above. + FocusMonitorUp {}, + /// Focus the previous monitor. + FocusMonitorPrevious {}, + /// Focus the next monitor. + FocusMonitorNext {}, + /// Move the focused window to the monitor to the left. + MoveWindowToMonitorLeft {}, + /// Move the focused window to the monitor to the right. + MoveWindowToMonitorRight {}, + /// Move the focused window to the monitor below. + MoveWindowToMonitorDown {}, + /// Move the focused window to the monitor above. + MoveWindowToMonitorUp {}, + /// Move the focused window to the previous monitor. + MoveWindowToMonitorPrevious {}, + /// Move the focused window to the next monitor. + MoveWindowToMonitorNext {}, + /// Move the focused column to the monitor to the left. + MoveColumnToMonitorLeft {}, + /// Move the focused column to the monitor to the right. + MoveColumnToMonitorRight {}, + /// Move the focused column to the monitor below. + MoveColumnToMonitorDown {}, + /// Move the focused column to the monitor above. + MoveColumnToMonitorUp {}, + /// Move the focused column to the previous monitor. + MoveColumnToMonitorPrevious {}, + /// Move the focused column to the next monitor. + MoveColumnToMonitorNext {}, + /// Change the width of a window. + #[cfg_attr( + feature = "clap", + clap(about = "Change the width of the focused window") + )] + SetWindowWidth { + /// Id of the window whose width to set. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + + /// How to change the width. + #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))] + change: SizeChange, + }, + /// Change the height of a window. + #[cfg_attr( + feature = "clap", + clap(about = "Change the height of the focused window") + )] + SetWindowHeight { + /// Id of the window whose height to set. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + + /// How to change the height. + #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))] + change: SizeChange, + }, + /// Reset the height of a window back to automatic. + #[cfg_attr( + feature = "clap", + clap(about = "Reset the height of the focused window back to automatic") + )] + ResetWindowHeight { + /// Id of the window whose height to reset. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Switch between preset column widths. + SwitchPresetColumnWidth {}, + /// Switch between preset window widths. + SwitchPresetWindowWidth { + /// Id of the window whose width to switch. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Switch between preset window heights. + SwitchPresetWindowHeight { + /// Id of the window whose height to switch. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Toggle the maximized state of the focused column. + MaximizeColumn {}, + /// Change the width of the focused column. + SetColumnWidth { + /// How to change the width. + #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))] + change: SizeChange, + }, + /// Expand the focused column to space not taken up by other fully visible columns. + ExpandColumnToAvailableWidth {}, + /// Switch between keyboard layouts. + SwitchLayout { + /// Layout to switch to. + #[cfg_attr(feature = "clap", arg())] + layout: LayoutSwitchTarget, + }, + /// Show the hotkey overlay. + ShowHotkeyOverlay {}, + /// Move the focused workspace to the monitor to the left. + MoveWorkspaceToMonitorLeft {}, + /// Move the focused workspace to the monitor to the right. + MoveWorkspaceToMonitorRight {}, + /// Move the focused workspace to the monitor below. + MoveWorkspaceToMonitorDown {}, + /// Move the focused workspace to the monitor above. + MoveWorkspaceToMonitorUp {}, + /// Move the focused workspace to the previous monitor. + MoveWorkspaceToMonitorPrevious {}, + /// Move the focused workspace to the next monitor. + MoveWorkspaceToMonitorNext {}, + /// Move a workspace to a specific monitor. + #[cfg_attr( + feature = "clap", + clap(about = "Move the focused workspace to a specific monitor") + )] + MoveWorkspaceToMonitor { + /// The target output name. + #[cfg_attr(feature = "clap", arg())] + output: String, + + // Reference (index or name) of the workspace to move. + /// + /// If `None`, uses the focused workspace. + #[cfg_attr(feature = "clap", arg(long))] + reference: Option, + }, + /// Toggle a debug tint on windows. + ToggleDebugTint {}, + /// Toggle visualization of render element opaque regions. + DebugToggleOpaqueRegions {}, + /// Toggle visualization of output damage. + DebugToggleDamage {}, + /// Move the focused window between the floating and the tiling layout. + ToggleWindowFloating { + /// Id of the window to move. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Move the focused window to the floating layout. + MoveWindowToFloating { + /// Id of the window to move. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Move the focused window to the tiling layout. + MoveWindowToTiling { + /// Id of the window to move. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, + /// Switches focus to the floating layout. + FocusFloating {}, + /// Switches focus to the tiling layout. + FocusTiling {}, + /// Toggles the focus between the floating and the tiling layout. + SwitchFocusBetweenFloatingAndTiling {}, + /// Move a floating window on screen. + #[cfg_attr(feature = "clap", clap(about = "Move the floating window on screen"))] + MoveFloatingWindow { + /// Id of the window to move. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + + /// How to change the X position. + #[cfg_attr( + feature = "clap", + arg(short, long, default_value = "+0", allow_negative_numbers = true) + )] + x: PositionChange, + + /// How to change the Y position. + #[cfg_attr( + feature = "clap", + arg(short, long, default_value = "+0", allow_negative_numbers = true) + )] + y: PositionChange, + }, + /// Toggle the opacity of a window. + #[cfg_attr( + feature = "clap", + clap(about = "Toggle the opacity of the focused window") + )] + ToggleWindowRuleOpacity { + /// Id of the window. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, +} + +/// Change in window or column size. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum SizeChange { + /// Set the size in logical pixels. + SetFixed(i32), + /// Set the size as a proportion of the working area. + SetProportion(f64), + /// Add or subtract to the current size in logical pixels. + AdjustFixed(i32), + /// Add or subtract to the current size as a proportion of the working area. + AdjustProportion(f64), +} + +/// Change in floating window position. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum PositionChange { + /// Set the position in logical pixels. + SetFixed(f64), + /// Add or subtract to the current position in logical pixels. + AdjustFixed(f64), +} + +/// Workspace reference (id, index or name) to operate on. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum WorkspaceReferenceArg { + /// Id of the workspace. + Id(u64), + /// Index of the workspace. + Index(u8), + /// Name of the workspace. + Name(String), +} + +/// Layout to switch to. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum LayoutSwitchTarget { + /// The next configured layout. + Next, + /// The previous configured layout. + Prev, + /// The specific layout by index. + Index(u8), +} + +/// How windows display in a column. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum ColumnDisplay { + /// Windows are tiled vertically across the working area height. + Normal, + /// Windows are in tabs. + Tabbed, +} + +/// Output actions that niri can perform. +// Variants in this enum should match the spelling of the ones in niri-config. Most thigs from +// niri-config should be present here. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "clap", derive(clap::Parser))] +#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))] +#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum OutputAction { + /// Turn off the output. + Off, + /// Turn on the output. + On, + /// Set the output mode. + Mode { + /// Mode to set, or "auto" for automatic selection. + /// + /// Run `niri msg outputs` to see the available modes. + #[cfg_attr(feature = "clap", arg())] + mode: ModeToSet, + }, + /// Set the output scale. + Scale { + /// Scale factor to set, or "auto" for automatic selection. + #[cfg_attr(feature = "clap", arg())] + scale: ScaleToSet, + }, + /// Set the output transform. + Transform { + /// Transform to set, counter-clockwise. + #[cfg_attr(feature = "clap", arg())] + transform: Transform, + }, + /// Set the output position. + Position { + /// Position to set, or "auto" for automatic selection. + #[cfg_attr(feature = "clap", command(subcommand))] + position: PositionToSet, + }, + /// Set the variable refresh rate mode. + Vrr { + /// Variable refresh rate mode to set. + #[cfg_attr(feature = "clap", command(flatten))] + vrr: VrrToSet, + }, +} + +/// Output mode to set. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum ModeToSet { + /// Niri will pick the mode automatically. + Automatic, + /// Specific mode. + Specific(ConfiguredMode), +} + +/// Output mode as set in the config file. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub struct ConfiguredMode { + /// Width in physical pixels. + pub width: u16, + /// Height in physical pixels. + pub height: u16, + /// Refresh rate. + pub refresh: Option, +} + +/// Output scale to set. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum ScaleToSet { + /// Niri will pick the scale automatically. + Automatic, + /// Specific scale. + Specific(f64), +} + +/// Output position to set. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "clap", derive(clap::Subcommand))] +#[cfg_attr(feature = "clap", command(subcommand_value_name = "POSITION"))] +#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Position Values"))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum PositionToSet { + /// Position the output automatically. + #[cfg_attr(feature = "clap", command(name = "auto"))] + Automatic, + /// Set a specific position. + #[cfg_attr(feature = "clap", command(name = "set"))] + Specific(ConfiguredPosition), +} + +/// Output position as set in the config file. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "clap", derive(clap::Args))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub struct ConfiguredPosition { + /// Logical X position. + pub x: i32, + /// Logical Y position. + pub y: i32, +} + +/// Output VRR to set. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "clap", derive(clap::Args))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub struct VrrToSet { + /// Whether to enable variable refresh rate. + #[cfg_attr( + feature = "clap", + arg( + value_name = "ON|OFF", + action = clap::ArgAction::Set, + value_parser = clap::builder::BoolishValueParser::new(), + hide_possible_values = true, + ), + )] + pub vrr: bool, + /// Only enable when the output shows a window matching the variable-refresh-rate window rule. + #[cfg_attr(feature = "clap", arg(long))] + pub on_demand: bool, +} + +/// Connected output. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub struct Output { + /// Name of the output. + pub name: String, + /// Textual description of the manufacturer. + pub make: String, + /// Textual description of the model. + pub model: String, + /// Serial of the output, if known. + pub serial: Option, + /// Physical width and height of the output in millimeters, if known. + pub physical_size: Option<(u32, u32)>, + /// Available modes for the output. + pub modes: Vec, + /// Index of the current mode in [`Self::modes`]. + /// + /// `None` if the output is disabled. + pub current_mode: Option, + /// Whether the output supports variable refresh rate. + pub vrr_supported: bool, + /// Whether variable refresh rate is enabled on the output. + pub vrr_enabled: bool, + /// Logical output information. + /// + /// `None` if the output is not mapped to any logical output (for example, if it is disabled). + pub logical: Option, +} + +/// Output mode. +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub struct Mode { + /// Width in physical pixels. + pub width: u16, + /// Height in physical pixels. + pub height: u16, + /// Refresh rate in millihertz. + pub refresh_rate: u32, + /// Whether this mode is preferred by the monitor. + pub is_preferred: bool, +} + +/// Logical output in the compositor's coordinate space. +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub struct LogicalOutput { + /// Logical X position. + pub x: i32, + /// Logical Y position. + pub y: i32, + /// Width in logical pixels. + pub width: u32, + /// Height in logical pixels. + pub height: u32, + /// Scale factor. + pub scale: f64, + /// Transform. + pub transform: Transform, +} + +/// Output transform, which goes counter-clockwise. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum Transform { + /// Untransformed. + Normal, + /// Rotated by 90°. + #[serde(rename = "90")] + _90, + /// Rotated by 180°. + #[serde(rename = "180")] + _180, + /// Rotated by 270°. + #[serde(rename = "270")] + _270, + /// Flipped horizontally. + Flipped, + /// Rotated by 90° and flipped horizontally. + #[cfg_attr(feature = "clap", value(name("flipped-90")))] + Flipped90, + /// Flipped vertically. + #[cfg_attr(feature = "clap", value(name("flipped-180")))] + Flipped180, + /// Rotated by 270° and flipped horizontally. + #[cfg_attr(feature = "clap", value(name("flipped-270")))] + Flipped270, +} + +/// Toplevel window. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub struct Window { + /// Unique id of this window. + /// + /// This id remains constant while this window is open. + /// + /// Do not assume that window ids will always increase without wrapping, or start at 1. That is + /// an implementation detail subject to change. For example, ids may change to be randomly + /// generated for each new window. + pub id: u64, + /// Title, if set. + pub title: Option, + /// Application ID, if set. + pub app_id: Option, + /// Process ID that created the Wayland connection for this window, if known. + /// + /// Currently, windows created by xdg-desktop-portal-gnome will have a `None` PID, but this may + /// change in the future. + pub pid: Option, + /// Id of the workspace this window is on, if any. + pub workspace_id: Option, + /// Whether this window is currently focused. + /// + /// There can be either one focused window or zero (e.g. when a layer-shell surface has focus). + pub is_focused: bool, + /// Whether this window is currently floating. + /// + /// If the window isn't floating then it is in the tiling layout. + pub is_floating: bool, +} + +/// Output configuration change result. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum OutputConfigChanged { + /// The target output was connected and the change was applied. + Applied, + /// The target output was not found, the change will be applied when it is connected. + OutputWasMissing, +} + +/// A workspace. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub struct Workspace { + /// Unique id of this workspace. + /// + /// This id remains constant regardless of the workspace moving around and across monitors. + /// + /// Do not assume that workspace ids will always increase without wrapping, or start at 1. That + /// is an implementation detail subject to change. For example, ids may change to be randomly + /// generated for each new workspace. + pub id: u64, + /// Index of the workspace on its monitor. + /// + /// This is the same index you can use for requests like `niri msg action focus-workspace`. + /// + /// This index *will change* as you move and re-order workspace. It is merely the workspace's + /// current position on its monitor. Workspaces on different monitors can have the same index. + /// + /// If you need a unique workspace id that doesn't change, see [`Self::id`]. + pub idx: u8, + /// Optional name of the workspace. + pub name: Option, + /// Name of the output that the workspace is on. + /// + /// Can be `None` if no outputs are currently connected. + pub output: Option, + /// Whether the workspace is currently active on its output. + /// + /// Every output has one active workspace, the one that is currently visible on that output. + pub is_active: bool, + /// Whether the workspace is currently focused. + /// + /// There's only one focused workspace across all outputs. + pub is_focused: bool, + /// Id of the active window on this workspace, if any. + pub active_window_id: Option, +} + +/// Configured keyboard layouts. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub struct KeyboardLayouts { + /// XKB names of the configured layouts. + pub names: Vec, + /// Index of the currently active layout in `names`. + pub current_idx: u8, +} + +/// A layer-shell layer. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum Layer { + /// The background layer. + Background, + /// The bottom layer. + Bottom, + /// The top layer. + Top, + /// The overlay layer. + Overlay, +} + +/// Keyboard interactivity modes for a layer-shell surface. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum LayerSurfaceKeyboardInteractivity { + /// Surface cannot receive keyboard focus. + None, + /// Surface receives keyboard focus whenever possible. + Exclusive, + /// Surface receives keyboard focus on demand, e.g. when clicked. + OnDemand, +} + +/// A layer-shell surface. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub struct LayerSurface { + /// Namespace provided by the layer-shell client. + pub namespace: String, + /// Name of the output the surface is on. + pub output: String, + /// Layer that the surface is on. + pub layer: Layer, + /// The surface's keyboard interactivity mode. + pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity, +} + +/// A compositor event. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum Event { + /// The workspace configuration has changed. + WorkspacesChanged { + /// The new workspace configuration. + /// + /// This configuration completely replaces the previous configuration. I.e. if any + /// workspaces are missing from here, then they were deleted. + workspaces: Vec, + }, + /// A workspace was activated on an output. + /// + /// This doesn't always mean the workspace became focused, just that it's now the active + /// workspace on its output. All other workspaces on the same output become inactive. + WorkspaceActivated { + /// Id of the newly active workspace. + id: u64, + /// Whether this workspace also became focused. + /// + /// If `true`, this is now the single focused workspace. All other workspaces are no longer + /// focused, but they may remain active on their respective outputs. + focused: bool, + }, + /// An active window changed on a workspace. + WorkspaceActiveWindowChanged { + /// Id of the workspace on which the active window changed. + workspace_id: u64, + /// Id of the new active window, if any. + active_window_id: Option, + }, + /// The window configuration has changed. + WindowsChanged { + /// The new window configuration. + /// + /// This configuration completely replaces the previous configuration. I.e. if any windows + /// are missing from here, then they were closed. + windows: Vec, + }, + /// A new toplevel window was opened, or an existing toplevel window changed. + WindowOpenedOrChanged { + /// The new or updated window. + /// + /// If the window is focused, all other windows are no longer focused. + window: Window, + }, + /// A toplevel window was closed. + WindowClosed { + /// Id of the removed window. + id: u64, + }, + /// Window focus changed. + /// + /// All other windows are no longer focused. + WindowFocusChanged { + /// Id of the newly focused window, or `None` if no window is now focused. + id: Option, + }, + /// The configured keyboard layouts have changed. + KeyboardLayoutsChanged { + /// The new keyboard layout configuration. + keyboard_layouts: KeyboardLayouts, + }, + /// The keyboard layout switched. + KeyboardLayoutSwitched { + /// Index of the newly active layout. + idx: u8, + }, +} + +impl FromStr for WorkspaceReferenceArg { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let reference = if let Ok(index) = s.parse::() { + if let Ok(idx) = u8::try_from(index) { + Self::Index(idx) + } else { + return Err("workspace index must be between 0 and 255"); + } + } else { + Self::Name(s.to_string()) + }; + + Ok(reference) + } +} + +impl FromStr for SizeChange { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.split_once('%') { + Some((value, empty)) => { + if !empty.is_empty() { + return Err("trailing characters after '%' are not allowed"); + } + + match value.bytes().next() { + Some(b'-' | b'+') => { + let value = value.parse().map_err(|_| "error parsing value")?; + Ok(Self::AdjustProportion(value)) + } + Some(_) => { + let value = value.parse().map_err(|_| "error parsing value")?; + Ok(Self::SetProportion(value)) + } + None => Err("value is missing"), + } + } + None => { + let value = s; + match value.bytes().next() { + Some(b'-' | b'+') => { + let value = value.parse().map_err(|_| "error parsing value")?; + Ok(Self::AdjustFixed(value)) + } + Some(_) => { + let value = value.parse().map_err(|_| "error parsing value")?; + Ok(Self::SetFixed(value)) + } + None => Err("value is missing"), + } + } + } + } +} + +impl FromStr for PositionChange { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let value = s; + match value.bytes().next() { + Some(b'-' | b'+') => { + let value = value.parse().map_err(|_| "error parsing value")?; + Ok(Self::AdjustFixed(value)) + } + Some(_) => { + let value = value.parse().map_err(|_| "error parsing value")?; + Ok(Self::SetFixed(value)) + } + None => Err("value is missing"), + } + } +} + +impl FromStr for LayoutSwitchTarget { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "next" => Ok(Self::Next), + "prev" => Ok(Self::Prev), + other => match other.parse() { + Ok(layout) => Ok(Self::Index(layout)), + _ => Err(r#"invalid layout action, can be "next", "prev" or a layout index"#), + }, + } + } +} + +impl FromStr for ColumnDisplay { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "normal" => Ok(Self::Normal), + "tabbed" => Ok(Self::Tabbed), + _ => Err(r#"invalid column display, can be "normal" or "tabbed""#), + } + } +} + +impl FromStr for Transform { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "normal" => Ok(Self::Normal), + "90" => Ok(Self::_90), + "180" => Ok(Self::_180), + "270" => Ok(Self::_270), + "flipped" => Ok(Self::Flipped), + "flipped-90" => Ok(Self::Flipped90), + "flipped-180" => Ok(Self::Flipped180), + "flipped-270" => Ok(Self::Flipped270), + _ => Err(concat!( + r#"invalid transform, can be "90", "180", "270", "#, + r#""flipped", "flipped-90", "flipped-180" or "flipped-270""# + )), + } + } +} + +impl FromStr for ModeToSet { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("auto") { + return Ok(Self::Automatic); + } + + let mode = s.parse()?; + Ok(Self::Specific(mode)) + } +} + +impl FromStr for ConfiguredMode { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let Some((width, rest)) = s.split_once('x') else { + return Err("no 'x' separator found"); + }; + + let (height, refresh) = match rest.split_once('@') { + Some((height, refresh)) => (height, Some(refresh)), + None => (rest, None), + }; + + let width = width.parse().map_err(|_| "error parsing width")?; + let height = height.parse().map_err(|_| "error parsing height")?; + let refresh = refresh + .map(str::parse) + .transpose() + .map_err(|_| "error parsing refresh rate")?; + + Ok(Self { + width, + height, + refresh, + }) + } +} + +impl FromStr for ScaleToSet { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("auto") { + return Ok(Self::Automatic); + } + + let scale = s.parse().map_err(|_| "error parsing scale")?; + Ok(Self::Specific(scale)) + } +} diff --git a/deps/niri-ipc-25.2.0/src/socket.rs b/deps/niri-ipc-25.2.0/src/socket.rs new file mode 100644 index 0000000..d629f1a --- /dev/null +++ b/deps/niri-ipc-25.2.0/src/socket.rs @@ -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 { + 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) -> io::Result { + 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)> { + 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)) + } +} diff --git a/deps/niri-ipc-25.2.0/src/state.rs b/deps/niri-ipc-25.2.0/src/state.rs new file mode 100644 index 0000000..2ab58fc --- /dev/null +++ b/deps/niri-ipc-25.2.0/src/state.rs @@ -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; + + /// 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; +} + +/// 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, +} + +/// 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, +} + +/// The keyboard layout state communicated over the event stream. +#[derive(Debug, Default)] +pub struct KeyboardLayoutsState { + /// Configured keyboard layouts. + pub keyboard_layouts: Option, +} + +impl EventStreamStatePart for EventStreamState { + fn replicate(&self) -> Vec { + 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 { + 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 { + let workspaces = self.workspaces.values().cloned().collect(); + vec![Event::WorkspacesChanged { workspaces }] + } + + fn apply(&mut self, event: Event) -> Option { + 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 { + let windows = self.windows.values().cloned().collect(); + vec![Event::WindowsChanged { windows }] + } + + fn apply(&mut self, event: Event) -> Option { + 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 { + if let Some(keyboard_layouts) = self.keyboard_layouts.clone() { + vec![Event::KeyboardLayoutsChanged { keyboard_layouts }] + } else { + vec![] + } + } + + fn apply(&mut self, event: Event) -> Option { + 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 + } +} diff --git a/src/compositors.rs b/src/compositors.rs index e6425bd..6d8bbf7 100644 --- a/src/compositors.rs +++ b/src/compositors.rs @@ -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 { + 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 { + // Example: "25.02 (unknown commit)" + let mut iter = version_str.split(|c: char| !c.is_ascii_digit()); + let major = iter.next()?.parse::().ok()?; + let minor = iter.next()?.parse::().ok()?; + Some(niri_ver(major, minor)) +} + +fn niri_ver(major: u32, minor: u32) -> u64 { + ((major as u64) << 32) | (minor as u64) +} diff --git a/src/compositors/niri.rs b/src/compositors/niri2502.rs similarity index 97% rename from src/compositors/niri.rs rename to src/compositors/niri2502.rs index 496fd5e..f4c28ea 100644 --- a/src/compositors/niri.rs +++ b/src/compositors/niri2502.rs @@ -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}; diff --git a/src/compositors/niri2505.rs b/src/compositors/niri2505.rs new file mode 100644 index 0000000..03520f2 --- /dev/null +++ b/src/compositors/niri2505.rs @@ -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 { + 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 { + 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 { + 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 +}