hyprland workspace, service

This commit is contained in:
2026-09-13 18:15:22 +01:00
parent 57201bc0ff
commit a14b6f4967
12 changed files with 551 additions and 7 deletions
Generated
+134
View File
@@ -145,6 +145,28 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -362,6 +384,17 @@ dependencies = [
"rand_core", "rand_core",
] ]
[[package]]
name = "chardetng"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
dependencies = [
"cfg-if",
"encoding_rs",
"memchr",
]
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.45" version = "0.4.45"
@@ -510,6 +543,15 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "convert_case"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "core" name = "core"
version = "0.1.0" version = "0.1.0"
@@ -702,6 +744,29 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "derive_more"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.119",
"unicode-xid",
]
[[package]] [[package]]
name = "dispatch" name = "dispatch"
version = "0.2.0" version = "0.2.0"
@@ -765,6 +830,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.41" version = "0.8.41"
@@ -1002,6 +1073,16 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[package]]
name = "futures-lite"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.34" version = "0.3.34"
@@ -1376,6 +1457,35 @@ dependencies = [
"windows-registry", "windows-registry",
] ]
[[package]]
name = "hyprland"
version = "0.4.0-beta.3"
source = "git+https://github.com/hyprland-community/hyprland-rs?branch=master#c7037d4ce5c64e7f376a58802b806471792b0993"
dependencies = [
"async-stream",
"chardetng",
"derive_more",
"either",
"encoding_rs",
"futures-lite",
"hyprland-macros",
"pastey",
"serde",
"serde_json",
"serde_repr",
"tokio",
]
[[package]]
name = "hyprland-macros"
version = "0.4.0-beta.3"
source = "git+https://github.com/hyprland-community/hyprland-rs?branch=master#c7037d4ce5c64e7f376a58802b806471792b0993"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.65" version = "0.1.65"
@@ -2610,6 +2720,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pastey"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -3251,6 +3367,17 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "serde_repr"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.5",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.1.1" version = "1.1.1"
@@ -3266,6 +3393,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"common", "common",
"hyprland",
"iced", "iced",
"reqwest", "reqwest",
"serde", "serde",
@@ -3977,6 +4105,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
+2
View File
@@ -15,6 +15,7 @@ pub struct BarbarConfig {
pub enum Modules { pub enum Modules {
Clock, Clock,
Weather, Weather,
Workspaces,
} }
impl Modules { impl Modules {
@@ -22,6 +23,7 @@ impl Modules {
match self { match self {
Modules::Clock => "clock", Modules::Clock => "clock",
Modules::Weather => "weather", Modules::Weather => "weather",
Modules::Workspaces => "workspaces",
} }
} }
} }
@@ -0,0 +1,33 @@
//! Hyprland compositor messages.
use crate::Service;
/// Route key a module subscribes under to receive compositor events. One
/// stream carries every event; the [`HyprlandEvent`] payload tags which one,
/// so modules filter on it.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum HyprlandMsg {
Events,
}
impl HyprlandMsg {
/// Route key a module subscribes under to receive this service's events.
pub fn key(self) -> Service {
Service("hyprland.events")
}
}
/// A hyprland compositor event, published to subscribers of the events key.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum HyprlandEvent {
/// Focused workspace changed.
WorkspaceChanged { id: i32, name: String },
/// A workspace was created.
WorkspaceAdded { id: i32, name: String },
/// A workspace was destroyed.
WorkspaceRemoved { id: i32, name: String },
/// Focused window changed; empty class/title mean no window is focused.
ActiveWindow { class: String, title: String },
/// Focused monitor changed.
Monitor { name: String },
}
@@ -1,7 +1,9 @@
//! Messages owned by services. //! Messages owned by services.
mod datetime; mod datetime;
mod hyprland;
mod weather; mod weather;
pub use datetime::*; pub use datetime::*;
pub use hyprland::*;
pub use weather::*; pub use weather::*;
+9 -4
View File
@@ -34,7 +34,8 @@ fn bar_view(bar: &Bar) -> Element<'_, Message> {
.map(|w| Message::Event(Msg::Wire(w))) .map(|w| Message::Event(Msg::Wire(w)))
}) })
.collect::<Vec<Element<Message>>>()) .collect::<Vec<Element<Message>>>())
.spacing(8); .spacing(8)
.align_y(Alignment::Center);
let middle = row(bar let middle = row(bar
.order .order
@@ -48,7 +49,8 @@ fn bar_view(bar: &Bar) -> Element<'_, Message> {
.map(|w| Message::Event(Msg::Wire(w))) .map(|w| Message::Event(Msg::Wire(w)))
}) })
.collect::<Vec<Element<Message>>>()) .collect::<Vec<Element<Message>>>())
.spacing(8); .spacing(8)
.align_y(Alignment::Center);
let right = row(bar let right = row(bar
.order .order
.2 .2
@@ -61,7 +63,8 @@ fn bar_view(bar: &Bar) -> Element<'_, Message> {
.map(|w| Message::Event(Msg::Wire(w))) .map(|w| Message::Event(Msg::Wire(w)))
}) })
.collect::<Vec<Element<Message>>>()) .collect::<Vec<Element<Message>>>())
.spacing(8); .spacing(8)
.align_y(Alignment::Center);
let row = row![ let row = row![
left, left,
@@ -70,11 +73,13 @@ fn bar_view(bar: &Bar) -> Element<'_, Message> {
space::horizontal(), space::horizontal(),
right, right,
] ]
.width(Length::Fill); .width(Length::Fill)
.align_y(Alignment::Center);
container(row) container(row)
.padding(8) .padding(8)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill)
.align_y(Alignment::Center) .align_y(Alignment::Center)
.into() .into()
} }
+3 -1
View File
@@ -3,6 +3,7 @@
mod clock; mod clock;
mod weather; mod weather;
mod workspaces;
pub use clock::{Clock, ClockError}; pub use clock::{Clock, ClockError};
use common::BarModule; use common::BarModule;
@@ -12,7 +13,8 @@ pub use weather::*;
/// One instance of every enabled module, ready to insert by `id()`. /// One instance of every enabled module, ready to insert by `id()`.
pub fn all(module_config: Table) -> Vec<Box<dyn BarModule>> { pub fn all(module_config: Table) -> Vec<Box<dyn BarModule>> {
vec![ vec![
Box::new(clock::Clock::new(Some(module_config))), Box::new(clock::Clock::new(Some(module_config.clone()))),
Box::new(weather::WeatherModule::default()), Box::new(weather::WeatherModule::default()),
Box::new(workspaces::Workspaces::new(Some(module_config))),
] ]
} }
+140
View File
@@ -0,0 +1,140 @@
//! Workspaces module: renders workspaces 110 as cubes, coloured and scaled by
//! state — focused largest, existing mid, non-existent smallest. State arrives
//! as `HyprlandEvent`s from the hyprland service.
//!
//! Ported from the Quickshell `Workspaces.qml`: constant 10×15 slot per
//! workspace, corner radius 2.5, and the inactive cubes scaled about their
//! centre (0.85 existing / 0.75 absent) so the shrink never shifts the row.
use std::collections::BTreeSet;
use common::{BarModule, HyprlandEvent, HyprlandMsg, ModuleEffect, Service, Wire};
use iced::border;
use iced::widget::{container, row, space};
use iced::{window, Color, Element, Length, Task};
use serde::Deserialize;
use toml::Table;
/// Focused workspace (gruvbox base10 / `Colors.primaryText`).
const ACTIVE: Color = Color::from_rgb(0.9216, 0.8588, 0.6980);
/// Workspace exists but is not focused (base06 / `Colors.textSecondary`).
const EXISTS: Color = Color::from_rgb(0.4863, 0.4353, 0.3922);
/// Workspace does not exist (base04 / `Colors.textDisabled`).
const ABSENT: Color = Color::from_rgb(0.3137, 0.2863, 0.2706);
/// Look of the workspace row, from `[modules.workspaces]`; every field
/// defaults to the value Quickshell's `Workspaces.qml` hardcodes, so an
/// absent table (or absent key) renders identically to before.
#[derive(Deserialize)]
#[serde(default)]
struct WorkspacesConfig {
/// Number of slots to render (workspaces `1..=count`).
count: i32,
/// Horizontal gap between slots in logical px.
gap: f32,
/// Slot width in logical px — every workspace keeps this slot, so the
/// unfocused shrink never moves the row.
slot_w: f32,
/// Slot height in logical px.
slot_h: f32,
/// Corner radius.
radius: f32,
/// Visual scale per state.
scale_active: f32,
scale_exists: f32,
scale_absent: f32,
}
impl Default for WorkspacesConfig {
fn default() -> Self {
Self {
count: 10,
gap: 6.0,
slot_w: 10.0,
slot_h: 15.0,
radius: 2.5,
scale_active: 1.0,
scale_exists: 0.85,
scale_absent: 0.75,
}
}
}
fn config_workspaces(table: Option<Table>) -> Option<WorkspacesConfig> {
let ws = table?.get("workspaces")?.clone();
ws.try_into().ok()
}
pub struct Workspaces {
cfg: WorkspacesConfig,
active: Option<i32>,
existing: BTreeSet<i32>,
}
impl Workspaces {
pub fn new(config: Option<Table>) -> Self {
Self {
cfg: config_workspaces(config).unwrap_or_default(),
active: None,
existing: BTreeSet::new(),
}
}
fn cube(&self, id: i32) -> Element<'_, Wire> {
let (color, scale) = if self.active == Some(id) {
(ACTIVE, self.cfg.scale_active)
} else if self.existing.contains(&id) {
(EXISTS, self.cfg.scale_exists)
} else {
(ABSENT, self.cfg.scale_absent)
};
let radius = self.cfg.radius;
let bar = container(space())
.width(Length::Fixed(self.cfg.slot_w * scale))
.height(Length::Fixed(self.cfg.slot_h * scale))
.style(move |_| container::Style {
background: Some(color.into()),
border: border::rounded(radius),
..Default::default()
});
container(bar)
.center_x(Length::Fixed(self.cfg.slot_w))
.center_y(Length::Fixed(self.cfg.slot_h))
.into()
}
}
impl BarModule for Workspaces {
fn id(&self) -> &'static str {
"workspaces"
}
fn view(&self, _window_id: Option<window::Id>) -> Element<'_, Wire> {
row((1..=self.cfg.count).map(|id| self.cube(id)))
.spacing(self.cfg.gap)
.into()
}
fn update(&mut self, msg: Wire) -> Task<ModuleEffect> {
if let Some(event) = msg.downcast::<HyprlandEvent>() {
match event {
HyprlandEvent::WorkspaceChanged { id, .. } => self.active = Some(*id),
HyprlandEvent::WorkspaceAdded { id, .. } => {
self.existing.insert(*id);
}
HyprlandEvent::WorkspaceRemoved { id, .. } => {
self.existing.remove(id);
if self.active == Some(*id) {
self.active = None;
}
}
HyprlandEvent::ActiveWindow { .. } | HyprlandEvent::Monitor { .. } => {}
}
}
Task::none()
}
fn services(&self) -> Vec<Service> {
vec![HyprlandMsg::Events.key()]
}
}
+2 -1
View File
@@ -10,9 +10,10 @@ path = "src/lib.rs"
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
iced = { workspace = true } iced = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true, features = ["net", "io-util"] }
tracing = { workspace = true } tracing = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
reqwest = { version = "0.13.5", features = ["json"] } reqwest = { version = "0.13.5", features = ["json"] }
hyprland = { git = "https://github.com/hyprland-community/hyprland-rs", branch = "master", default-features = false, features = ["listener", "tokio"] }
+205
View File
@@ -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(),
}
}
+1
View File
@@ -0,0 +1 @@
pub mod hyprland;
+4
View File
@@ -6,6 +6,7 @@ use iced::Subscription;
use common::{Inbox, Service, Wire}; use common::{Inbox, Service, Wire};
pub mod compositors;
pub mod datetime; pub mod datetime;
pub mod weather; pub mod weather;
@@ -23,6 +24,9 @@ impl IntoSubscription for Service {
let key = self.0; let key = self.0;
match key { match key {
k if k.starts_with(datetime::NAMESPACE) => datetime::ClockTicker::run(inbox), 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), k if k.starts_with(weather::NAMESPACE) => weather::WeatherService::run(inbox),
_ => { _ => {
tracing::warn!(service = key, "no service impl for route key"); tracing::warn!(service = key, "no service impl for route key");
+16 -1
View File
@@ -1,9 +1,24 @@
monitor = "DP-2" monitor = "DP-2"
[order] [order]
left = ["weather"] left = ["workspaces", "weather"]
middle = [] middle = []
right = ["clock"] right = ["clock"]
[modules.clock] [modules.clock]
show_seconds = false show_seconds = false
[modules.workspaces]
# Slots rendered, left to right (workspaces 1..=count).
count = 10
# Horizontal gap between slots, logical px.
gap = 3.0
# Slot size, logical px.
slot_w = 10.0
slot_h = 17.0
# Corner radius.
radius = 2.5
# Visual scale per state (focused / exists / absent).
scale_active = 1.0
scale_exists = 0.85
scale_absent = 0.65