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