|
|
|
@@ -0,0 +1,205 @@
|
|
|
|
|
//! Hyprland compositor service: listens on the compositor's event socket and
|
|
|
|
|
//! republishes focus changes (`HyprlandEvent`) to modules subscribed to
|
|
|
|
|
//! `hyprland.events`.
|
|
|
|
|
//!
|
|
|
|
|
//! Seeds the workspace list and current focus on startup so subscribers have
|
|
|
|
|
//! state before the first event — the event socket only reports changes.
|
|
|
|
|
//!
|
|
|
|
|
//! The startup snapshot is queried over Hyprland's command socket directly:
|
|
|
|
|
//! the `hyprland` crate's `data` types cannot deserialize 0.56 responses
|
|
|
|
|
//! (`Workspace.id` was dropped), though its event stream still works.
|
|
|
|
|
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
use common::{Endpoint, HyprlandEvent, Inbox, Wire};
|
|
|
|
|
use hyprland::event_listener::{Event, EventStream};
|
|
|
|
|
use iced::futures::{channel::mpsc, SinkExt, StreamExt};
|
|
|
|
|
use iced::Subscription;
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
|
use tokio::net::UnixStream;
|
|
|
|
|
|
|
|
|
|
/// Route-key namespace owned by this service; the key is `"hyprland.events"`.
|
|
|
|
|
pub const NAMESPACE: &str = "hyprland.";
|
|
|
|
|
|
|
|
|
|
/// A workspace as far as the bar cares — only the name, which for numbered
|
|
|
|
|
/// workspaces is also the id.
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct WorkspaceState {
|
|
|
|
|
name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct ClientState {
|
|
|
|
|
class: String,
|
|
|
|
|
title: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct MonitorState {
|
|
|
|
|
name: String,
|
|
|
|
|
focused: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct HyprlandService;
|
|
|
|
|
|
|
|
|
|
impl HyprlandService {
|
|
|
|
|
pub fn run(inbox: Inbox) -> Subscription<Wire> {
|
|
|
|
|
Subscription::run_with(inbox, |inbox| {
|
|
|
|
|
let key = inbox.key();
|
|
|
|
|
let mut _rx = inbox.take().expect("inbox receiver is taken once");
|
|
|
|
|
iced::stream::channel(0, move |mut sender: mpsc::Sender<Wire>| async move {
|
|
|
|
|
tracing::info!(key, "hyprland service started");
|
|
|
|
|
|
|
|
|
|
if !seed(&mut sender, key).await {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut stream = EventStream::new();
|
|
|
|
|
while let Some(event) = stream.next().await {
|
|
|
|
|
let event = match event {
|
|
|
|
|
Ok(event) => event,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!(error = %e, "hyprland event stream error");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
tracing::debug!(?event, "hyprland raw event");
|
|
|
|
|
for event in translate(event) {
|
|
|
|
|
if !publish(&mut sender, key, event).await {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tracing::warn!(key, "hyprland event stream ended");
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Publishes the current compositor state before live events start; `false`
|
|
|
|
|
/// if the subscriber channel is gone.
|
|
|
|
|
async fn seed(sender: &mut mpsc::Sender<Wire>, key: &'static str) -> bool {
|
|
|
|
|
if let Some(list) = query::<Vec<WorkspaceState>>("workspaces").await {
|
|
|
|
|
for ws in list {
|
|
|
|
|
let event = HyprlandEvent::WorkspaceAdded {
|
|
|
|
|
id: workspace_id(&ws.name, -1),
|
|
|
|
|
name: ws.name,
|
|
|
|
|
};
|
|
|
|
|
if !publish(sender, key, event).await {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(ws) = query::<WorkspaceState>("activeworkspace").await {
|
|
|
|
|
let event = HyprlandEvent::WorkspaceChanged {
|
|
|
|
|
id: workspace_id(&ws.name, -1),
|
|
|
|
|
name: ws.name,
|
|
|
|
|
};
|
|
|
|
|
if !publish(sender, key, event).await {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(m) = query::<Vec<MonitorState>>("monitors")
|
|
|
|
|
.await
|
|
|
|
|
.and_then(|list| list.into_iter().find(|m| m.focused))
|
|
|
|
|
{
|
|
|
|
|
if !publish(sender, key, HyprlandEvent::Monitor { name: m.name }).await {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(c) = query::<ClientState>("activewindow").await {
|
|
|
|
|
let event = HyprlandEvent::ActiveWindow {
|
|
|
|
|
class: c.class,
|
|
|
|
|
title: c.title,
|
|
|
|
|
};
|
|
|
|
|
if !publish(sender, key, event).await {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Runs `j/<cmd>` against Hyprland's command socket and parses the reply.
|
|
|
|
|
/// `None` if Hyprland isn't running, the query fails, or the shape is unknown.
|
|
|
|
|
async fn query<T: for<'de> Deserialize<'de>>(cmd: &str) -> Option<T> {
|
|
|
|
|
let runtime = std::env::var_os("XDG_RUNTIME_DIR")?;
|
|
|
|
|
let signature = std::env::var_os("HYPRLAND_INSTANCE_SIGNATURE")?;
|
|
|
|
|
let path = PathBuf::from(runtime)
|
|
|
|
|
.join("hypr")
|
|
|
|
|
.join(signature)
|
|
|
|
|
.join(".socket.sock");
|
|
|
|
|
|
|
|
|
|
let mut stream = UnixStream::connect(path).await.ok()?;
|
|
|
|
|
stream.write_all(format!("j/{cmd}").as_bytes()).await.ok()?;
|
|
|
|
|
let mut reply = String::new();
|
|
|
|
|
stream.read_to_string(&mut reply).await.ok()?;
|
|
|
|
|
serde_json::from_str(&reply).ok()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Workspace id for the bar: the numeric name when it is one (numbered
|
|
|
|
|
/// workspaces), else `fallback` (the event's own id, `-1` when unknown).
|
|
|
|
|
fn workspace_id(name: &str, fallback: i32) -> i32 {
|
|
|
|
|
name.parse().unwrap_or(fallback)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Publishes one event to subscribers; `false` if the channel is gone.
|
|
|
|
|
async fn publish(sender: &mut mpsc::Sender<Wire>, key: &'static str, event: HyprlandEvent) -> bool {
|
|
|
|
|
tracing::debug!(?event, "hyprland event");
|
|
|
|
|
let wire = Wire::topic(Endpoint::service(key), key, event);
|
|
|
|
|
if sender.send(wire).await.is_err() {
|
|
|
|
|
tracing::warn!(key, "hyprland send failed; stopping service");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Maps one crate event onto the protocol payload(s); empty for events the
|
|
|
|
|
/// bar does not publish. `focusedmon` yields two: the monitor change *and* the
|
|
|
|
|
/// workspace that monitor just focused.
|
|
|
|
|
fn translate(event: Event) -> Vec<HyprlandEvent> {
|
|
|
|
|
match event {
|
|
|
|
|
Event::WorkspaceChanged(w) => {
|
|
|
|
|
let name = w.name.to_string();
|
|
|
|
|
vec![HyprlandEvent::WorkspaceChanged {
|
|
|
|
|
id: workspace_id(&name, w.id),
|
|
|
|
|
name,
|
|
|
|
|
}]
|
|
|
|
|
}
|
|
|
|
|
Event::WorkspaceAdded(w) => {
|
|
|
|
|
let name = w.name.to_string();
|
|
|
|
|
vec![HyprlandEvent::WorkspaceAdded {
|
|
|
|
|
id: workspace_id(&name, w.id),
|
|
|
|
|
name,
|
|
|
|
|
}]
|
|
|
|
|
}
|
|
|
|
|
Event::WorkspaceDeleted(w) => {
|
|
|
|
|
let name = w.name.to_string();
|
|
|
|
|
vec![HyprlandEvent::WorkspaceRemoved {
|
|
|
|
|
id: workspace_id(&name, w.id),
|
|
|
|
|
name,
|
|
|
|
|
}]
|
|
|
|
|
}
|
|
|
|
|
Event::ActiveWindowChanged(w) => vec![HyprlandEvent::ActiveWindow {
|
|
|
|
|
class: w.as_ref().map(|w| w.class.clone()).unwrap_or_default(),
|
|
|
|
|
title: w.map(|w| w.title).unwrap_or_default(),
|
|
|
|
|
}],
|
|
|
|
|
Event::ActiveMonitorChanged(m) => {
|
|
|
|
|
let mut events = vec![HyprlandEvent::Monitor {
|
|
|
|
|
name: m.monitor_name,
|
|
|
|
|
}];
|
|
|
|
|
// Focusing a monitor also focuses its workspace; switching to a
|
|
|
|
|
// workspace on another monitor fires only this event, so without
|
|
|
|
|
// it the bar's active workspace never updates.
|
|
|
|
|
if let Some(ws) = m.workspace_name {
|
|
|
|
|
let name = ws.to_string();
|
|
|
|
|
events.push(HyprlandEvent::WorkspaceChanged {
|
|
|
|
|
id: workspace_id(&name, -1),
|
|
|
|
|
name,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
events
|
|
|
|
|
}
|
|
|
|
|
_ => Vec::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|