diff --git a/Cargo.lock b/Cargo.lock index a11ad90..e887983 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -698,6 +698,7 @@ dependencies = [ "image", "log", "mio", + "niri-ipc", "smithay-client-toolkit", "swayipc", ] @@ -708,6 +709,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "niri-ipc" +version = "25.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01515d0a7e73f1f3bd0347100542c4c3f6ebc280688add12e7ed2af4c35af4fb" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "nom" version = "7.1.3" @@ -1037,18 +1048,18 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -1057,9 +1068,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 25c2ad9..1a68e9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ image = "0.25.0" log = "0.4.21" mio = { version = "1.0.2", features = ["os-ext", "os-poll"] } swayipc = "3.0.2" +niri-ipc = "=25.2.0" [dependencies.smithay-client-toolkit] version = "0.19.2" diff --git a/PKGBUILD b/PKGBUILD index 600f1f9..2d28504 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -8,7 +8,10 @@ url="https://github.com/gergo-salyi/multibg-sway" license=('MIT' 'Apache') depends=('gcc-libs' 'glibc') makedepends=('cargo') -optdepends=('sway: window manager to set the wallpapers with') +optdepends=( + 'sway: supported window manager to set the wallpapers with' + 'niri: supported window manager to set the wallpapers with' +) source=("$pkgname-$pkgver.tar.gz::https://static.crates.io/crates/$pkgname/$pkgname-$pkgver.crate") sha256sums=('2b087124ea07635e53d411e707f7d22f73c69b40f3986a42c841f9cc19fc2d51') diff --git a/src/cli.rs b/src/cli.rs index f603cb1..a8cdc4f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,7 +4,7 @@ use clap::{Parser, ValueEnum}; #[command(author, version, long_about = None, about = "\ Set a different wallpaper for the background of each Sway workspace - $ multibg-sway + $ multibg-sway [--compositor ] Wallpapers should be arranged in the following directory structure: @@ -69,7 +69,13 @@ Nevertheless the contrast and brightness might be adjusted here: $ multibg-sway --contrast=-25 --brightness=-60 ~/my_wallpapers In case of errors multibg-sway logs to stderr and tries to continue. -One may wish to redirect stderr if multibg-sway is being run as a daemon.")] +One may wish to redirect stderr if multibg-sway is being run as a daemon. + + +multibg-sway supports multiple compositors, currently only sway and niri. +It tries to autodetect the compositor based on environment variables, +defaulting to sway if that fails. +Pass --compositor niri to ensure it can to talk to niri.")] pub struct Cli { /// adjust contrast, eg. -c=-25 (default: 0) #[arg(short, long)] @@ -80,6 +86,9 @@ pub struct Cli { /// wl_buffer pixel format (default: auto) #[arg(long)] pub pixelformat: Option, + /// Wayland compositor to connect (autodetect by default) + #[arg(long)] + pub compositor: Option, /// directory with: wallpaper_dir/output/workspace_name.{jpg|png|...} pub wallpaper_dir: String, } diff --git a/src/compositors.rs b/src/compositors.rs new file mode 100644 index 0000000..e9a5226 --- /dev/null +++ b/src/compositors.rs @@ -0,0 +1,132 @@ +mod niri; +mod sway; + +use std::{ + env, + os::unix::ffi::OsStrExt, +}; + +use log::{debug, warn}; +use mio::Waker; +use std::{ + sync::{mpsc::Sender, Arc}, thread::spawn +}; + +#[derive(Clone, Copy, Debug, clap::ValueEnum)] +pub enum Compositor { + Sway, + Niri, +} + +impl Compositor { + pub fn from_env() -> Option { + Compositor::from_xdg_desktop_var("XDG_SESSION_DESKTOP") + .or_else(|| Compositor::from_xdg_desktop_var("XDG_CURRENT_DESKTOP")) + .or_else(Compositor::from_ipc_socket_var) + } + + fn from_xdg_desktop_var(xdg_desktop_var: &str) -> Option { + if let Some(xdg_desktop) = env::var_os(xdg_desktop_var) { + if xdg_desktop.as_bytes().starts_with(b"sway") { + debug!("Selecting compositor Sway based on {xdg_desktop_var}"); + Some(Compositor::Sway) + } else if xdg_desktop.as_bytes().starts_with(b"niri") { + debug!("Selecting compositor Niri based on {xdg_desktop_var}"); + Some(Compositor::Niri) + } else { + warn!("Unrecognized compositor from {xdg_desktop_var} \ + environment variable: {xdg_desktop:?}"); + None + } + } else { + None + } + } + + fn from_ipc_socket_var() -> Option { + if env::var_os("SWAYSOCK").is_some() { + debug!("Selecting compositor Sway based on SWAYSOCK"); + Some(Compositor::Sway) + } else if env::var_os("NIRI_SOCKET").is_some() { + debug!("Selecting compositor Niri based on NIRI_SOCKET"); + Some(Compositor::Niri) + } else { + None + } + } +} + +// impl From<&str> for Compositor { +// fn from(s: &str) -> Self { +// match s { +// "sway" => Compositor::Sway, +// "niri" => Compositor::Niri, +// _ => panic!("Unknown compositor"), +// } +// } +// } + +pub trait CompositorInterface: Send + Sync { + fn request_visible_workspace( + &mut self, + output: &str, + tx: Sender, + waker: Arc, + ); + fn request_visible_workspaces(&mut self, tx: Sender, waker: Arc); + fn subscribe_event_loop(self, tx: Sender, waker: Arc); +} + +pub struct ConnectionTask { + tx: Sender, + waker: Arc, + interface: Box, +} + +impl ConnectionTask { + pub fn new(composer: Compositor, tx: Sender, waker: Arc) -> Self { + let interface: Box = match composer { + Compositor::Sway => Box::new(sway::SwayConnectionTask::new()), + Compositor::Niri => Box::new(niri::NiriConnectionTask::new()), + }; + + ConnectionTask { + tx, + waker, + interface, + } + } + + pub fn spawn_subscribe_event_loop( + composer: Compositor, + tx: Sender, + waker: Arc, + ) { + spawn(move || match composer { + Compositor::Sway => { + let composer_interface = sway::SwayConnectionTask::new(); + composer_interface.subscribe_event_loop(tx, waker); + } + Compositor::Niri => { + let composer_interface = niri::NiriConnectionTask::new(); + composer_interface.subscribe_event_loop(tx, waker); + } + }); + } + + pub fn request_visible_workspace(&mut self, output: &str) { + self.interface + .request_visible_workspace(output, self.tx.clone(), self.waker.clone()); + } + + pub fn request_visible_workspaces(&mut self) { + self.interface + .request_visible_workspaces(self.tx.clone(), self.waker.clone()); + } +} + +#[derive(Debug)] +pub struct WorkspaceVisible { + pub output: String, + pub workspace_name: String, +} diff --git a/src/compositors/niri.rs b/src/compositors/niri.rs new file mode 100644 index 0000000..79558c9 --- /dev/null +++ b/src/compositors/niri.rs @@ -0,0 +1,97 @@ +use std::sync::{mpsc::Sender, Arc}; + +use super::{CompositorInterface, WorkspaceVisible}; +use mio::Waker; +use niri_ipc::{socket::Socket, Event, Request, Response}; + +pub struct NiriConnectionTask {} + +impl NiriConnectionTask { + pub fn new() -> Self { + NiriConnectionTask {} + } + + fn query_workspace(&self, id: u64) -> (String, String) { + if let Ok((Ok(Response::Workspaces(workspaces)), _)) = Socket::connect() + .expect("failed to connect to niri socket") + .send(Request::Workspaces) + { + if let Some(workspace) = workspaces.into_iter().find(|w| w.id == id) { + return ( + workspace.name.unwrap_or_else(String::new), + workspace.output.unwrap_or_else(String::new), + ); + } + panic!("unknown workspace id"); + } else { + panic!("niri workspace query failed"); + } + } +} +impl CompositorInterface for NiriConnectionTask { + fn request_visible_workspace( + &mut self, + output: &str, + tx: Sender, + waker: Arc, + ) { + if let Ok((Ok(Response::Workspaces(workspaces)), _)) = Socket::connect() + .expect("failed to connect to niri socket") + .send(Request::Workspaces) + { + if let Some(workspace) = workspaces + .into_iter() + .filter(|w| w.is_focused) + .find(|w| w.output.as_ref().map_or("", |v| v) == output) + { + tx.send(WorkspaceVisible { + output: workspace.output.unwrap_or_else(String::new), + workspace_name: workspace.name.unwrap_or_else(String::new), + }) + .unwrap(); + + waker.wake().unwrap(); + } + } + } + + fn request_visible_workspaces(&mut self, tx: Sender, waker: Arc) { + if let Ok((Ok(Response::Workspaces(workspaces)), _)) = Socket::connect() + .expect("failed to connect to niri socket") + .send(Request::Workspaces) + { + for workspace in workspaces.into_iter().filter(|w| w.is_active) { + tx.send(WorkspaceVisible { + output: workspace.output.unwrap_or_else(String::new), + workspace_name: workspace.name.unwrap_or_else(String::new), + }) + .unwrap(); + + waker.wake().unwrap(); + } + } + } + + fn subscribe_event_loop(self, tx: Sender, waker: Arc) { + if let Ok((Ok(Response::Handled), mut callback)) = Socket::connect() + .expect("failed to connect to niri socket") + .send(Request::EventStream) + { + while let Ok(event) = callback() { + if let Event::WorkspaceActivated { id, focused: _ } = event { + let (workspace_name, output) = self.query_workspace(id); + + tx.send(WorkspaceVisible { + output, + workspace_name, + }) + .unwrap(); + + waker.wake().unwrap(); + } + } + } else { + panic!("failed to subscribe to event stream"); + } + } +} diff --git a/src/compositors/sway.rs b/src/compositors/sway.rs new file mode 100644 index 0000000..c4312b3 --- /dev/null +++ b/src/compositors/sway.rs @@ -0,0 +1,86 @@ +use std::{ + sync::{mpsc::Sender, Arc}, +}; + +use super::{CompositorInterface, WorkspaceVisible}; +use mio::Waker; +use swayipc::{Connection, Event, EventType, WorkspaceChange}; + +pub struct SwayConnectionTask { + sway_conn: Connection, +} + +impl SwayConnectionTask { + pub fn new() -> Self { + SwayConnectionTask { + sway_conn: Connection::new().expect("Failed to connect to sway socket. If you're not using sway, pass the correct --compositor argument. Original cause"), + } + } +} + +impl CompositorInterface for SwayConnectionTask { + fn request_visible_workspace( + &mut self, + output: &str, + tx: Sender, + waker: Arc, + ) { + if let Some(workspace) = self + .sway_conn + .get_workspaces() + .unwrap() + .into_iter() + .filter(|w| w.visible) + .find(|w| w.output == output) + { + tx + .send(WorkspaceVisible { + output: workspace.output, + workspace_name: workspace.name, + }) + .unwrap(); + + waker.wake().unwrap(); + } + } + + fn request_visible_workspaces(&mut self, tx: Sender, waker: Arc) { + for workspace in self + .sway_conn + .get_workspaces() + .unwrap() + .into_iter() + .filter(|w| w.visible) + { + tx + .send(WorkspaceVisible { + output: workspace.output, + workspace_name: workspace.name, + }) + .unwrap(); + } + waker.wake().unwrap(); + } + + fn subscribe_event_loop(self, tx: Sender, waker: Arc) { + let event_stream = self.sway_conn.subscribe([EventType::Workspace]).unwrap(); + for event_result in event_stream { + let event = event_result.unwrap(); + let Event::Workspace(workspace_event) = event else { + continue; + }; + if let WorkspaceChange::Focus = workspace_event.change { + let current_workspace = workspace_event.current.unwrap(); + + tx + .send(WorkspaceVisible { + output: current_workspace.output.unwrap(), + workspace_name: current_workspace.name.unwrap(), + }) + .unwrap(); + + waker.wake().unwrap(); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 4402d9f..c4c37e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ mod cli; mod image; -mod sway; +mod compositors; mod wayland; use std::{ @@ -36,7 +36,7 @@ use smithay_client_toolkit::reexports::protocols use crate::{ cli::{Cli, PixelFormat}, - sway::{SwayConnectionTask, WorkspaceVisible}, + compositors::{Compositor, ConnectionTask, WorkspaceVisible}, wayland::State, }; @@ -79,6 +79,10 @@ fn main() let waker = Arc::new(Waker::new(poll.registry(), SWAY).unwrap()); let (tx, rx) = channel(); + let compositor = cli.compositor + .or_else(Compositor::from_env) + .unwrap_or(Compositor::Sway); + let mut state = State { compositor_state, registry_state, @@ -91,7 +95,8 @@ fn main() .is_some_and(|p| p == PixelFormat::Baseline), pixel_format: None, background_layers: Vec::new(), - sway_connection_task: SwayConnectionTask::new( + compositor_connection_task: ConnectionTask::new( + compositor, tx.clone(), Arc::clone(&waker) ), brightness: cli.brightness.unwrap_or(0), @@ -119,7 +124,7 @@ fn main() drop(read_guard); const SWAY: Token = Token(1); - SwayConnectionTask::new(tx, waker).spawn_subscribe_event_loop(); + ConnectionTask::spawn_subscribe_event_loop(compositor, tx, waker); loop { event_queue.flush().unwrap(); diff --git a/src/sway.rs b/src/sway.rs deleted file mode 100644 index 4e5fb36..0000000 --- a/src/sway.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::{ - sync::{Arc, mpsc::Sender}, - thread::spawn, -}; - -use mio::Waker; -use swayipc::{Connection, Event, EventType, WorkspaceChange}; - -#[derive(Debug)] -pub struct WorkspaceVisible { - pub output: String, - pub workspace_name: String -} - -pub struct SwayConnectionTask { - sway_conn: Connection, - tx: Sender, - waker: Arc, -} -impl SwayConnectionTask -{ - pub fn new(tx: Sender, waker: Arc) -> Self { - SwayConnectionTask { - sway_conn: Connection::new() - .expect("Failed to connect to sway socket"), - tx, - waker - } - } - - pub fn request_visible_workspace(&mut self, output: &str) { - if let Some(workspace) = self.sway_conn.get_workspaces().unwrap() - .into_iter() - .filter(|w| w.visible) - .find(|w| w.output == output) - { - self.tx.send(WorkspaceVisible { - output: workspace.output, - workspace_name: workspace.name, - }).unwrap(); - - self.waker.wake().unwrap(); - } - } - - pub fn request_visible_workspaces(&mut self) { - for workspace in self.sway_conn.get_workspaces().unwrap() - .into_iter().filter(|w| w.visible) - { - self.tx.send(WorkspaceVisible { - output: workspace.output, - workspace_name: workspace.name, - }).unwrap(); - } - self.waker.wake().unwrap(); - } - - pub fn spawn_subscribe_event_loop(self) { - spawn(|| self.subscribe_event_loop()); - } - - fn subscribe_event_loop(self) { - let event_stream = self.sway_conn.subscribe([EventType::Workspace]) - .unwrap(); - for event_result in event_stream { - let event = event_result.unwrap(); - let Event::Workspace(workspace_event) = event else {continue}; - if let WorkspaceChange::Focus = workspace_event.change { - let current_workspace = workspace_event.current.unwrap(); - - self.tx.send(WorkspaceVisible { - output: current_workspace.output.unwrap(), - workspace_name: current_workspace.name.unwrap(), - }).unwrap(); - - self.waker.wake().unwrap(); - } - } - } -} diff --git a/src/wayland.rs b/src/wayland.rs index 638b674..1e4572b 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -35,7 +35,7 @@ use smithay_client_toolkit::reexports::protocols::wp::viewporter::client::{ use crate::{ image::workspace_bgs_from_output_image_dir, - sway::SwayConnectionTask, + compositors::ConnectionTask, }; pub struct State { @@ -49,7 +49,7 @@ pub struct State { pub force_xrgb8888: bool, pub pixel_format: Option, pub background_layers: Vec, - pub sway_connection_task: SwayConnectionTask, + pub compositor_connection_task: ConnectionTask, pub brightness: i32, pub contrast: f32, } @@ -150,7 +150,7 @@ impl LayerShellHandler for State if !bg_layer.configured { bg_layer.configured = true; - self.sway_connection_task + self.compositor_connection_task .request_visible_workspace(&bg_layer.output_name); debug!( @@ -529,7 +529,7 @@ Restart multibg-sway or expect broken wallpapers or low quality due to scaling" // Workspaces on the destroyed output may have been moved anywhere // so reset the wallpaper on all the visible workspaces - self.sway_connection_task.request_visible_workspaces(); + self.compositor_connection_task.request_visible_workspaces(); debug!( "Dropping {} wallpapers on destroyed output for workspaces: {}",