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
+3 -1
View File
@@ -3,6 +3,7 @@
mod clock;
mod weather;
mod workspaces;
pub use clock::{Clock, ClockError};
use common::BarModule;
@@ -12,7 +13,8 @@ pub use weather::*;
/// One instance of every enabled module, ready to insert by `id()`.
pub fn all(module_config: Table) -> Vec<Box<dyn BarModule>> {
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(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()]
}
}