Add support to the niri Wayland compositor

Implement niri support
Try to autodetect running compositor based on environment variables
XDG_SESSION_DESKTOP, XDG_CURRENT_DESKTOP, SWAYSOCK, NIRI_SOCKET in this order
Add the --compositor command line option
Add niri as optional dependency to the Arch Linux PKGBUILD
Update README and --help to include niri support

Merges the changes from
https://github.com/gergo-salyi/multibg-sway/pull/12
Thanks to the co-author for the contribution

Co-authored-by: Florian Finkernagel <finkernagel@imt.uni-marburg.de>
This commit is contained in:
Gergő Sályi 2025-04-08 19:09:00 +02:00
parent 0771c0586a
commit 1fdbd7ffb0
10 changed files with 361 additions and 97 deletions

23
Cargo.lock generated
View file

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

View file

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

View file

@ -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')

View file

@ -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 <WALLPAPER_DIR>
$ multibg-sway <WALLPAPER_DIR> [--compositor <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<PixelFormat>,
/// Wayland compositor to connect (autodetect by default)
#[arg(long)]
pub compositor: Option<crate::compositors::Compositor>,
/// directory with: wallpaper_dir/output/workspace_name.{jpg|png|...}
pub wallpaper_dir: String,
}

132
src/compositors.rs Normal file
View file

@ -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> {
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<Compositor> {
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<Compositor> {
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<WorkspaceVisible>,
waker: Arc<Waker>,
);
fn request_visible_workspaces(&mut self, tx: Sender<WorkspaceVisible>, waker: Arc<Waker>);
fn subscribe_event_loop(self, tx: Sender<WorkspaceVisible>, waker: Arc<Waker>);
}
pub struct ConnectionTask {
tx: Sender<WorkspaceVisible>,
waker: Arc<Waker>,
interface: Box<dyn CompositorInterface>,
}
impl ConnectionTask {
pub fn new(composer: Compositor, tx: Sender<WorkspaceVisible>, waker: Arc<Waker>) -> Self {
let interface: Box<dyn CompositorInterface> = 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<WorkspaceVisible>,
waker: Arc<Waker>,
) {
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,
}

97
src/compositors/niri.rs Normal file
View file

@ -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<WorkspaceVisible>,
waker: Arc<Waker>,
) {
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<WorkspaceVisible>, waker: Arc<Waker>) {
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<WorkspaceVisible>, waker: Arc<Waker>) {
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");
}
}
}

86
src/compositors/sway.rs Normal file
View file

@ -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<WorkspaceVisible>,
waker: Arc<Waker>,
) {
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<WorkspaceVisible>, waker: Arc<Waker>) {
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<WorkspaceVisible>, waker: Arc<Waker>) {
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();
}
}
}
}

View file

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

View file

@ -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<WorkspaceVisible>,
waker: Arc<Waker>,
}
impl SwayConnectionTask
{
pub fn new(tx: Sender<WorkspaceVisible>, waker: Arc<Waker>) -> 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();
}
}
}
}

View file

@ -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<wl_shm::Format>,
pub background_layers: Vec<BackgroundLayer>,
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: {}",