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",
"mio",
"niri-ipc",
"serde",
"serde_json",
"smithay-client-toolkit",
"swayipc",
]

View file

@ -20,6 +20,8 @@ fast_image_resize = "5.0.0"
image = "0.25.0"
log = "0.4.21"
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"
niri-ipc = "=25.2.0"

View file

@ -9,8 +9,9 @@ license=('MIT' 'Apache')
depends=('gcc-libs' 'glibc')
makedepends=('cargo')
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'
'sway: 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

@ -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.
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,
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 {
/// adjust contrast, eg. -c=-25 (default: 0)
#[arg(short, long)]

View file

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

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,
}