Add support to the Hyprland Wayland compositor
This commit is contained in:
parent
94fe622d44
commit
9e2bfaa14e
6 changed files with 185 additions and 4 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -699,6 +699,8 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"mio",
|
"mio",
|
||||||
"niri-ipc",
|
"niri-ipc",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"smithay-client-toolkit",
|
"smithay-client-toolkit",
|
||||||
"swayipc",
|
"swayipc",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
3
PKGBUILD
3
PKGBUILD
|
@ -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')
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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
159
src/compositors/hyprland.rs
Normal 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,
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue