hyprland workspace, service
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod hyprland;
|
||||
@@ -6,6 +6,7 @@ use iced::Subscription;
|
||||
|
||||
use common::{Inbox, Service, Wire};
|
||||
|
||||
pub mod compositors;
|
||||
pub mod datetime;
|
||||
pub mod weather;
|
||||
|
||||
@@ -23,6 +24,9 @@ impl IntoSubscription for Service {
|
||||
let key = self.0;
|
||||
match key {
|
||||
k if k.starts_with(datetime::NAMESPACE) => datetime::ClockTicker::run(inbox),
|
||||
k if k.starts_with(compositors::hyprland::NAMESPACE) => {
|
||||
compositors::hyprland::HyprlandService::run(inbox)
|
||||
}
|
||||
k if k.starts_with(weather::NAMESPACE) => weather::WeatherService::run(inbox),
|
||||
_ => {
|
||||
tracing::warn!(service = key, "no service impl for route key");
|
||||
|
||||
Reference in New Issue
Block a user