hyprland workspace, service
This commit is contained in:
Generated
+134
@@ -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"
|
||||||
|
|||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -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,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))),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//! Workspaces module: renders workspaces 1–10 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()]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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};
|
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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user