Add support to the Hyprland Wayland compositor

This commit is contained in:
Gergő Sályi 2025-04-11 22:04:10 +02:00
parent 94fe622d44
commit 9e2bfaa14e
6 changed files with 185 additions and 4 deletions

2
Cargo.lock generated
View file

@ -699,6 +699,8 @@ dependencies = [
"log", "log",
"mio", "mio",
"niri-ipc", "niri-ipc",
"serde",
"serde_json",
"smithay-client-toolkit", "smithay-client-toolkit",
"swayipc", "swayipc",
] ]

View file

@ -20,6 +20,8 @@ fast_image_resize = "5.0.0"
image = "0.25.0" image = "0.25.0"
log = "0.4.21" log = "0.4.21"
mio = { version = "1.0.2", features = ["os-ext", "os-poll"] } mio = { version = "1.0.2", features = ["os-ext", "os-poll"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
swayipc = "3.0.2" swayipc = "3.0.2"
niri-ipc = "=25.2.0" niri-ipc = "=25.2.0"

View file

@ -9,8 +9,9 @@ license=('MIT' 'Apache')
depends=('gcc-libs' 'glibc') depends=('gcc-libs' 'glibc')
makedepends=('cargo') makedepends=('cargo')
optdepends=( optdepends=(
'sway: supported window manager to set the wallpapers with' 'hyprland: supported window manager to set the wallpapers with'
'niri: supported window manager to set the wallpapers with' 'niri: supported window manager to set the wallpapers with'
'sway: supported window manager to set the wallpapers with'
) )
source=("$pkgname-$pkgver.tar.gz::https://static.crates.io/crates/$pkgname/$pkgname-$pkgver.crate") source=("$pkgname-$pkgver.tar.gz::https://static.crates.io/crates/$pkgname/$pkgname-$pkgver.crate")
sha256sums=('2b087124ea07635e53d411e707f7d22f73c69b40f3986a42c841f9cc19fc2d51') sha256sums=('2b087124ea07635e53d411e707f7d22f73c69b40f3986a42c841f9cc19fc2d51')

View file

@ -72,10 +72,11 @@ 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. multibg-sway supports multiple compositors,
currently only sway, hyprland and niri.
It tries to autodetect the compositor based on environment variables, It tries to autodetect the compositor based on environment variables,
defaulting to sway if that fails. defaulting to sway if that fails.
Pass --compositor niri to ensure it can to talk to niri.")] Pass --compositor {hyprland|niri} to ensure it can to talk to them")]
pub struct Cli { pub struct Cli {
/// adjust contrast, eg. -c=-25 (default: 0) /// adjust contrast, eg. -c=-25 (default: 0)
#[arg(short, long)] #[arg(short, long)]

View file

@ -1,3 +1,4 @@
mod hyprland;
mod niri; mod niri;
mod sway; mod sway;
@ -12,8 +13,9 @@ use std::{
#[derive(Clone, Copy, Debug, clap::ValueEnum)] #[derive(Clone, Copy, Debug, clap::ValueEnum)]
pub enum Compositor { pub enum Compositor {
Sway, Hyprland,
Niri, Niri,
Sway,
} }
impl Compositor { impl Compositor {
@ -28,6 +30,9 @@ impl Compositor {
if xdg_desktop.as_bytes().starts_with(b"sway") { if xdg_desktop.as_bytes().starts_with(b"sway") {
debug!("Selecting compositor Sway based on {xdg_desktop_var}"); debug!("Selecting compositor Sway based on {xdg_desktop_var}");
Some(Compositor::Sway) Some(Compositor::Sway)
} else if xdg_desktop.as_bytes().starts_with(b"Hyprland") {
debug!("Selecting compositor Hyprland based on {xdg_desktop_var}");
Some(Compositor::Hyprland)
} else if xdg_desktop.as_bytes().starts_with(b"niri") { } else if xdg_desktop.as_bytes().starts_with(b"niri") {
debug!("Selecting compositor Niri based on {xdg_desktop_var}"); debug!("Selecting compositor Niri based on {xdg_desktop_var}");
Some(Compositor::Niri) Some(Compositor::Niri)
@ -47,6 +52,10 @@ impl Compositor {
if env::var_os("SWAYSOCK").is_some() { if env::var_os("SWAYSOCK").is_some() {
debug!("Selecting compositor Sway based on SWAYSOCK"); debug!("Selecting compositor Sway based on SWAYSOCK");
Some(Compositor::Sway) Some(Compositor::Sway)
} else if env::var_os("HYPRLAND_INSTANCE_SIGNATURE").is_some() {
debug!("Selecting compositor Hyprland based on \
HYPRLAND_INSTANCE_SIGNATURE");
Some(Compositor::Hyprland)
} else if env::var_os("NIRI_SOCKET").is_some() { } else if env::var_os("NIRI_SOCKET").is_some() {
debug!("Selecting compositor Niri based on NIRI_SOCKET"); debug!("Selecting compositor Niri based on NIRI_SOCKET");
Some(Compositor::Niri) Some(Compositor::Niri)
@ -99,6 +108,9 @@ impl ConnectionTask {
pub fn new(composer: Compositor, tx: Sender<WorkspaceVisible>, waker: Arc<Waker>) -> Self { pub fn new(composer: Compositor, tx: Sender<WorkspaceVisible>, waker: Arc<Waker>) -> Self {
let interface: Box<dyn CompositorInterface> = match composer { let interface: Box<dyn CompositorInterface> = match composer {
Compositor::Sway => Box::new(sway::SwayConnectionTask::new()), Compositor::Sway => Box::new(sway::SwayConnectionTask::new()),
Compositor::Hyprland => Box::new(
hyprland::HyprlandConnectionTask::new()
),
Compositor::Niri => Box::new(niri::NiriConnectionTask::new()), Compositor::Niri => Box::new(niri::NiriConnectionTask::new()),
}; };
@ -120,6 +132,10 @@ impl ConnectionTask {
let composer_interface = sway::SwayConnectionTask::new(); let composer_interface = sway::SwayConnectionTask::new();
composer_interface.subscribe_event_loop(event_sender); composer_interface.subscribe_event_loop(event_sender);
} }
Compositor::Hyprland => {
let composer_interface = hyprland::HyprlandConnectionTask::new();
composer_interface.subscribe_event_loop(event_sender);
}
Compositor::Niri => { Compositor::Niri => {
let composer_interface = niri::NiriConnectionTask::new(); let composer_interface = niri::NiriConnectionTask::new();
composer_interface.subscribe_event_loop(event_sender); composer_interface.subscribe_event_loop(event_sender);

159
src/compositors/hyprland.rs Normal file
View file

@ -0,0 +1,159 @@
// https://wiki.hyprland.org/IPC/
use std::{
env,
io::{Read, Write},
os::unix::net::UnixStream,
path::PathBuf,
};
use log::debug;
use serde::Deserialize;
use super::{CompositorInterface, WorkspaceVisible, EventSender};
pub struct HyprlandConnectionTask {}
impl HyprlandConnectionTask {
pub fn new() -> Self {
HyprlandConnectionTask {}
}
}
impl CompositorInterface for HyprlandConnectionTask {
fn request_visible_workspaces(&mut self) -> Vec<WorkspaceVisible> {
current_state().visible_workspaces
}
fn subscribe_event_loop(self, event_sender: EventSender) {
let mut socket = socket_dir_path();
socket.push(".socket2.sock");
let mut connection = UnixStream::connect(socket)
.expect("Failed to connect to Hyprland events socket");
let initial_state = current_state();
for workspace in initial_state.visible_workspaces {
event_sender.send(workspace);
}
let mut active_monitor = initial_state.active_monitor;
let mut buf = vec![0u8; 2000];
let mut filled = 0usize;
let mut parsed = 0usize;
loop {
let read = connection.read(&mut buf[filled..]).unwrap();
if read == 0 {
panic!("Hyperland events socket disconnected");
}
filled += read;
if filled == buf.len() {
let new_len = buf.len() * 2;
debug!("Growing Hyprland socket read buffer to {new_len}");
buf.resize(new_len, 0u8);
}
loop {
let mut unparsed = &buf[parsed..filled];
let Some(gt_pos) = unparsed.iter().position(|&b| b == b'>')
else { break };
let event_name = &unparsed[..gt_pos];
unparsed = &unparsed[gt_pos+2..];
let Some(lf_pos) = unparsed.iter().position(|&b| b == b'\n')
else { break };
let event_data = &unparsed[..lf_pos];
unparsed = &unparsed[lf_pos+1..];
parsed = filled - unparsed.len();
debug!(
"Hyprland event: {} {}",
String::from_utf8_lossy(event_name),
String::from_utf8_lossy(event_data),
);
if event_name == b"workspace" {
event_sender.send(WorkspaceVisible {
output: active_monitor.clone(),
workspace_name: String::from_utf8(event_data.to_vec())
.unwrap(),
});
} else if event_name == b"focusedmon" {
let comma_pos = event_data.iter()
.position(|&b| b == b',').unwrap();
let monname = &event_data[..comma_pos];
active_monitor = String::from_utf8(monname.to_vec())
.unwrap();
} else if event_name == b"moveworkspace"
|| event_name == b"renameworkspace"
{
let current_state = current_state();
for workspace in current_state.visible_workspaces {
event_sender.send(workspace);
}
active_monitor = current_state.active_monitor;
}
}
if parsed == filled {
filled = 0;
parsed = 0;
} else {
buf.copy_within(parsed..filled, 0);
filled -= parsed;
parsed = 0;
}
}
}
}
// "$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE"
fn socket_dir_path() -> PathBuf {
let xdg_runtime_dir = env::var_os("XDG_RUNTIME_DIR")
.expect("Environment variable XDG_RUNTIME_DIR not set");
let his = env::var_os("HYPRLAND_INSTANCE_SIGNATURE")
.expect("Environment variable HYPRLAND_INSTANCE_SIGNATURE not set");
let mut ret = PathBuf::with_capacity(256);
ret.push(xdg_runtime_dir);
ret.push("hypr");
ret.push(his);
ret
}
fn current_state() -> CurrentState {
let mut socket = socket_dir_path();
socket.push(".socket.sock");
let mut connection = UnixStream::connect(socket)
.expect("Failed to connect to Hyprland requests socket");
connection.write_all(b"j/monitors")
.expect("Failed to send Hyprland monitors requests");
let mut buf = Vec::with_capacity(2000);
// This socket .socket.sock for hyprctl-like requests
// only allows one round trip with a single or batched commands
let read = connection.read_to_end(&mut buf)
.expect("Failed to receive Hyprland monitors response");
let monitors: Vec<Monitor> = serde_json::from_slice(&buf[..read])
.expect("Failed to parse Hyprland monitors response");
let mut active_monitor = String::new();
let mut visible_workspaces = Vec::new();
for monitor in monitors {
if monitor.focused {
active_monitor = monitor.name.clone();
}
visible_workspaces.push(WorkspaceVisible {
output: monitor.name,
workspace_name: monitor.active_workspace.name,
});
}
CurrentState { active_monitor, visible_workspaces }
}
struct CurrentState {
active_monitor: String,
visible_workspaces: Vec<WorkspaceVisible>,
}
#[derive(Deserialize)]
struct Monitor {
name: String,
#[serde(rename = "activeWorkspace")]
active_workspace: ActiveWorkspace,
focused: bool,
}
#[derive(Deserialize)]
struct ActiveWorkspace {
name: String,
}