This commit is contained in:
2026-09-07 11:41:59 +01:00
parent bc5a26b728
commit 3490af52e4
12 changed files with 274 additions and 170 deletions
Generated
+39 -10
View File
@@ -98,16 +98,6 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "barbar-core"
version = "0.1.0"
dependencies = [
"chrono",
"iced",
"iced_layershell",
"tokio",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.8.0" version = "0.8.0"
@@ -340,6 +330,13 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "common"
version = "0.1.0"
dependencies = [
"iced",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -349,6 +346,19 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "core"
version = "0.1.0"
dependencies = [
"chrono",
"common",
"iced",
"iced_layershell",
"module-clock",
"service-datatime",
"tokio",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -1702,6 +1712,15 @@ dependencies = [
"paste", "paste",
] ]
[[package]]
name = "module-clock"
version = "0.1.0"
dependencies = [
"common",
"iced",
"service-datatime",
]
[[package]] [[package]]
name = "naga" name = "naga"
version = "27.0.3" version = "27.0.3"
@@ -2499,6 +2518,16 @@ dependencies = [
"syn 3.0.5", "syn 3.0.5",
] ]
[[package]]
name = "service-datatime"
version = "0.1.0"
dependencies = [
"chrono",
"common",
"iced",
"tokio",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "2.0.1" version = "2.0.1"
+2 -1
View File
@@ -1,5 +1,6 @@
[workspace] [workspace]
members = ["crates/*"] members = ["crates/core", "crates/modules/clock", "crates/services/datatime", "crates/common"]
resolver = "2"
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "common"
version.workspace = true
edition.workspace = true
[lib]
name = "common"
path = "src/lib.rs"
[dependencies]
iced = { workspace = true }
+127
View File
@@ -0,0 +1,127 @@
//! Barbar's protocol crate: the types that cross crate boundaries.
//!
//! Nothing here may reference app-level types (`Message`, `Cmd`, `Msg`,
//! layer-shell effects). `common` is the hub every other crate depends on,
//! so it must stay acyclic and pure.
use std::any::Any;
use std::fmt;
use std::sync::Arc;
use iced::window;
use iced::{Element, Task};
// ---------------------------------------------------------------------------
// Service model
// ---------------------------------------------------------------------------
/// What a clock subscription wants: the tick interval and payload selector.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum ClockKind {
Mins,
Seconds,
}
/// Clock tick payload: the value plus the kind that produced it.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ClockPayload {
pub kind: ClockKind,
pub value: String,
}
/// Route key a module subscribes with; `Clock(kind)` selects the ticker.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Service {
Clock(ClockKind),
}
/// A service event with a typed payload. The app only reads
/// `key()` for fan-out; the consuming module downcasts the payload.
#[derive(Clone, Debug)]
pub enum ServicePayloadKind {
Clock(ClockPayload),
}
impl ServicePayloadKind {
/// Route key for this event. One arm per kind — adding a service
/// touches only this match, never the routing code.
pub fn key(&self) -> Service {
match self {
Self::Clock(payload) => Service::Clock(payload.kind),
}
}
/// Erases the typed payload for the module to downcast.
pub fn into_payload(self) -> Arc<dyn Any + Send + Sync> {
match self {
Self::Clock(payload) => Arc::new(payload),
}
}
}
// ---------------------------------------------------------------------------
// Module protocol
// ---------------------------------------------------------------------------
/// A module-local message, boxed with its owner's `id`. The app routes by
/// `id` and never names a module's message type; the module downcasts.
#[derive(Clone)]
pub struct ModuleMsg {
pub id: &'static str,
pub payload: Arc<dyn Any + Send + Sync>,
}
impl ModuleMsg {
/// Wraps a module's own message; `id` must match its `BarModule::id`.
pub fn new<T: Any + Send + Sync>(id: &'static str, msg: T) -> Self {
Self {
id,
payload: Arc::new(msg),
}
}
/// Downcasts to the module's own message type.
pub fn downcast<T: Any + Send + Sync>(&self) -> Option<&T> {
self.payload.downcast_ref()
}
}
impl fmt::Debug for ModuleMsg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ModuleMsg")
.field("id", &self.id)
.field("payload", &self.payload.type_id())
.finish()
}
}
/// What a module asks the app to do. Modules never name app types; the app
/// maps these into its own effects (`Cmd`).
#[derive(Debug, Clone)]
pub enum ModuleEffect {
/// Toggle a popup for the given module id (element id anchors it).
RequestPopup(String, String),
/// Request removal of the popup surface.
ClosePopup(window::Id),
}
/// A bar module. Object-safe; modules live in a `BTreeMap` keyed by id. The
/// `ModuleMsg` boundary keeps the app ignorant of each module's message enum.
pub trait BarModule: Send {
fn id(&self) -> &'static str;
/// Bar contents. When `window_id` is `Some`, this is the module's popup
/// surface and the module returns its popup contents instead.
fn view(&self, window_id: Option<window::Id>) -> Element<'_, ModuleMsg>;
/// Handles a routed message; may return app-level tasks (e.g. popups).
fn update(&mut self, msg: ModuleMsg) -> Task<ModuleEffect>;
/// What services this module wants.
fn services(&self) -> Vec<Service>;
/// Requested popup size when it's not the generic small menu.
fn popup_size(&self) -> Option<(u32, u32)> {
None
}
}
+7 -2
View File
@@ -1,13 +1,18 @@
[package] [package]
name = "barbar-core" name = "core"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
[[bin]] [[bin]]
name = "barbar-core" name = "core"
[dependencies] [dependencies]
common = { path = "../common" }
iced = { workspace = true } iced = { workspace = true }
iced_layershell = { workspace = true } iced_layershell = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
# Modules
module_clock = { package = "module-clock", path = "../modules/clock" }
# Services
service_datatime = { package = "service-datatime", path = "../services/datatime" }
+28 -10
View File
@@ -13,13 +13,9 @@ use iced_layershell::reexport::Anchor;
use iced_layershell::settings::{LayerShellSettings, Settings, StartMode}; use iced_layershell::settings::{LayerShellSettings, Settings, StartMode};
use iced_layershell::to_layer_message; use iced_layershell::to_layer_message;
use crate::module::{BarModule, ModuleMsg}; use common::{BarModule, ModuleEffect, ModuleMsg, Service as ModuleService, ServicePayloadKind};
use crate::services::Service as ModuleService;
use crate::services::ServicePayloadKind;
mod module;
mod popup; mod popup;
mod services;
/// Height of the bar in logical pixels. /// Height of the bar in logical pixels.
const BAR_HEIGHT: u32 = 36; const BAR_HEIGHT: u32 = 36;
@@ -43,7 +39,7 @@ struct Bar {
impl Bar { impl Bar {
fn new() -> Self { fn new() -> Self {
let mut modules: BTreeMap<&'static str, Box<dyn BarModule>> = BTreeMap::new(); let mut modules: BTreeMap<&'static str, Box<dyn BarModule>> = BTreeMap::new();
modules.insert("clock", Box::new(crate::module::clock::Clock::new())); modules.insert("clock", Box::new(module_clock::Clock::new()));
let routes = modules let routes = modules
.iter() .iter()
.map(|(id, module)| (*id, module.services())) .map(|(id, module)| (*id, module.services()))
@@ -100,6 +96,26 @@ fn namespace() -> String {
String::from("barbar") String::from("barbar")
} }
/// Maps a module-requested effect into an app message. Modules never name
/// app types; this is the only place the two meet.
fn module_effect_to_message(effect: ModuleEffect) -> Message {
match effect {
ModuleEffect::RequestPopup(module_id, element_id) => {
Message::Effect(Cmd::RequestPopup(module_id, element_id))
}
ModuleEffect::ClosePopup(id) => Message::Effect(Cmd::ClosePopup(id)),
}
}
/// One arm per service kind: turns a route key into its subscription.
/// Adding a service touches only this match (plus the leaf crate).
fn service_subscription(service: &ModuleService) -> Subscription<Message> {
match service {
ModuleService::Clock(_) => service_datatime::ClockTicker::run()
.map(|event| Message::Event(Msg::Subscription(event))),
}
}
fn main() -> Result<(), iced_layershell::Error> { fn main() -> Result<(), iced_layershell::Error> {
let start_mode = match std::env::args().nth(1) { let start_mode = match std::env::args().nth(1) {
Some(output) => StartMode::TargetScreen(output), Some(output) => StartMode::TargetScreen(output),
@@ -127,7 +143,7 @@ fn gather_subscriptions(bar: &Bar) -> Subscription<Message> {
let route_sub = Subscription::batch( let route_sub = Subscription::batch(
bar.routes bar.routes
.values() .values()
.flat_map(|x| x.iter().map(ModuleService::subscription)) .flat_map(|x| x.iter().map(service_subscription))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
let close_events = window::close_events().map(|id| Message::Event(Msg::WindowClosed(id))); let close_events = window::close_events().map(|id| Message::Event(Msg::WindowClosed(id)));
@@ -151,8 +167,8 @@ fn update(bar: &mut Bar, msg: Message) -> Task<Message> {
fn handle_event(bar: &mut Bar, event: Msg) -> Task<Message> { fn handle_event(bar: &mut Bar, event: Msg) -> Task<Message> {
match event { match event {
Msg::Module(m) => match bar.modules.get_mut(m.id) { Msg::Module(m) => match bar.modules.get_mut(m.id) {
// Modules return their own app-level tasks (e.g. popups). // Modules return protocol tasks; map effects into `Cmd`s.
Some(module) => module.update(m), Some(module) => module.update(m).map(module_effect_to_message),
None => Task::none(), None => Task::none(),
}, },
@@ -169,10 +185,12 @@ fn handle_event(bar: &mut Bar, event: Msg) -> Task<Message> {
.is_some_and(|kinds| kinds.contains(&key)) .is_some_and(|kinds| kinds.contains(&key))
}) })
.map(|(_, module)| { .map(|(_, module)| {
module.update(ModuleMsg { module
.update(ModuleMsg {
id: module.id(), id: module.id(),
payload: payload.clone(), payload: payload.clone(),
}) })
.map(module_effect_to_message)
}) })
.fold(Task::none(), |acc, t| acc.chain(t)) .fold(Task::none(), |acc, t| acc.chain(t))
} }
-63
View File
@@ -1,63 +0,0 @@
use std::any::Any;
use std::fmt;
use std::sync::Arc;
use iced::{Element, Task};
use crate::services::Service;
use crate::Message;
pub mod clock;
/// A module-local message, boxed with its owner's `id`. The app routes by
/// `id` and never names a module's message type; the module downcasts.
#[derive(Clone)]
pub struct ModuleMsg {
pub id: &'static str,
pub payload: Arc<dyn Any + Send + Sync>,
}
impl ModuleMsg {
/// Wraps a module's own message; `id` must match its `BarModule::id`.
pub fn new<T: Any + Send + Sync>(id: &'static str, msg: T) -> Self {
Self {
id,
payload: Arc::new(msg),
}
}
/// Downcasts to the module's own message type.
pub fn downcast<T: Any + Send + Sync>(&self) -> Option<&T> {
self.payload.downcast_ref()
}
}
impl fmt::Debug for ModuleMsg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ModuleMsg")
.field("id", &self.id)
.field("payload", &self.payload.type_id())
.finish()
}
}
/// A bar module. Object-safe; modules live in a `BTreeMap` keyed by id. The
/// `ModuleMsg` boundary keeps the app ignorant of each module's message enum.
pub trait BarModule: Send {
fn id(&self) -> &'static str;
/// Bar contents. When `window_id` is `Some`, this is the module's popup
/// surface and the module returns its popup contents instead.
fn view(&self, window_id: Option<iced::window::Id>) -> Element<'_, ModuleMsg>;
/// Handles a routed message; may return app-level tasks (e.g. popups).
fn update(&mut self, msg: ModuleMsg) -> Task<Message>;
/// What services this module wants.
fn services(&self) -> Vec<Service>;
/// Requested popup size when it's not the generic small menu.
fn popup_size(&self) -> Option<(u32, u32)> {
None
}
}
-47
View File
@@ -1,47 +0,0 @@
use std::any::Any;
use std::sync::Arc;
use crate::{
services::clock::{ClockKind, ClockPayload, ClockTicker},
Message,
};
pub mod clock;
/// Route key a module subscribes with; `Clock(kind)` selects the ticker.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Service {
Clock(ClockKind),
}
impl Service {
pub fn subscription(&self) -> iced::Subscription<Message> {
match self {
Self::Clock(_) => ClockTicker::run(),
}
}
}
/// A service event with a typed payload. The app only reads
/// `key()` for fan-out; the consuming module downcasts the payload.
#[derive(Clone, Debug)]
pub enum ServicePayloadKind {
Clock(ClockPayload),
}
impl ServicePayloadKind {
/// Route key for this event. One arm per kind — adding a service
/// touches only this match, never the routing code.
pub fn key(&self) -> Service {
match self {
Self::Clock(payload) => Service::Clock(payload.kind),
}
}
/// Erases the typed payload for the module to downcast.
pub fn into_payload(self) -> Arc<dyn Any + Send + Sync> {
match self {
Self::Clock(payload) => Arc::new(payload),
}
}
}
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "module-clock"
version.workspace = true
edition.workspace = true
[lib]
name = "module_clock"
path = "src/lib.rs"
[dependencies]
common = { path = "../../common" }
service_datatime = { package = "service-datatime", path = "../../services/datatime" }
iced = { workspace = true }
@@ -1,14 +1,17 @@
//! Clock module: subscribes to second ticks, renders the time in the bar.
//! Left-click toggles the seconds suffix; right-click opens a popup with
//! the current time in large text.
use iced::widget::{container, mouse_area, text}; use iced::widget::{container, mouse_area, text};
use iced::{Element, Task}; use iced::{Element, Task};
use crate::module::{BarModule, ModuleMsg}; use common::{BarModule, ClockKind, ClockPayload, ModuleEffect, ModuleMsg, Service};
use crate::services::clock::{ClockKind, ClockPayload};
use crate::services::Service;
use crate::{Cmd, Message};
/// Clock module: subscribes to second ticks, renders the time in the bar. /// Big-text popup size (logical px).
/// Left-click toggles the seconds suffix; right-click opens a popup with const POPUP_W: u32 = 360;
/// the current time in large text. const POPUP_H: u32 = 160;
/// A module-local message for the clock.
#[derive(Clone)] #[derive(Clone)]
pub enum ClockMsg { pub enum ClockMsg {
/// Toggle the seconds suffix. /// Toggle the seconds suffix.
@@ -17,10 +20,6 @@ pub enum ClockMsg {
OpenPopup, OpenPopup,
} }
/// Big-text popup size (logical px).
const POPUP_W: u32 = 360;
const POPUP_H: u32 = 160;
pub struct Clock { pub struct Clock {
value: String, value: String,
show_seconds: bool, show_seconds: bool,
@@ -48,6 +47,12 @@ impl Clock {
} }
} }
impl Default for Clock {
fn default() -> Self {
Self::new()
}
}
impl BarModule for Clock { impl BarModule for Clock {
fn id(&self) -> &'static str { fn id(&self) -> &'static str {
"clock" "clock"
@@ -69,7 +74,7 @@ impl BarModule for Clock {
} }
} }
fn update(&mut self, msg: ModuleMsg) -> Task<Message> { fn update(&mut self, msg: ModuleMsg) -> Task<ModuleEffect> {
// Subscription fan-out delivers a raw `ClockPayload`, not `ClockMsg`. // Subscription fan-out delivers a raw `ClockPayload`, not `ClockMsg`.
if let Some(payload) = msg.downcast::<ClockPayload>() { if let Some(payload) = msg.downcast::<ClockPayload>() {
self.value = payload.value.clone(); self.value = payload.value.clone();
@@ -80,10 +85,10 @@ impl BarModule for Clock {
self.show_seconds = !self.show_seconds; self.show_seconds = !self.show_seconds;
Task::none() Task::none()
} }
Some(ClockMsg::OpenPopup) => Task::done(Message::Effect(Cmd::RequestPopup( Some(ClockMsg::OpenPopup) => Task::done(ModuleEffect::RequestPopup(
self.id().to_string(), self.id().to_string(),
self.id().to_string(), self.id().to_string(),
))), )),
None => Task::none(), None => Task::none(),
} }
} }
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "service-datatime"
version.workspace = true
edition.workspace = true
[lib]
name = "service_datatime"
path = "src/lib.rs"
[dependencies]
common = { path = "../../common" }
iced = { workspace = true }
tokio = { workspace = true }
chrono = { workspace = true }
@@ -1,22 +1,14 @@
//! Clock service: emits a `ServicePayloadKind` event whenever a clock kind's
//! displayed value changes (minute or second rollover).
//!
//! The ticker knows nothing about the app's `Message` type; it produces
//! protocol events from `common` that core routes to subscribed modules.
use std::collections::HashMap; use std::collections::HashMap;
use iced::{futures::SinkExt, Subscription}; use iced::{futures::SinkExt, Subscription};
use crate::{services::ServicePayloadKind, Message, Msg}; use common::{ClockKind, ClockPayload, ServicePayloadKind};
/// What a clock subscription wants: the tick interval and payload selector.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum ClockKind {
Mins,
Seconds,
}
/// Clock tick payload: the value plus the kind that produced it.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ClockPayload {
pub kind: ClockKind,
pub value: String,
}
/// Emits a payload only when a kind's displayed value changes (minute or /// Emits a payload only when a kind's displayed value changes (minute or
/// second rollover). Drive `tick` once per second. /// second rollover). Drive `tick` once per second.
@@ -30,20 +22,19 @@ impl ClockTicker {
Self::default() Self::default()
} }
pub fn run() -> Subscription<Message> { /// Subscription that emits `ServicePayloadKind::Clock` on every change.
pub fn run() -> Subscription<ServicePayloadKind> {
Subscription::run_with((), |_| { Subscription::run_with((), |_| {
iced::stream::channel( iced::stream::channel(
0, 0,
|mut sender: iced::futures::channel::mpsc::Sender<Message>| async move { |mut sender: iced::futures::channel::mpsc::Sender<ServicePayloadKind>| async move {
let mut clock = ClockTicker::new(); let mut clock = ClockTicker::new();
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1)); let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
loop { loop {
interval.tick().await; interval.tick().await;
for payload in clock.tick() { for payload in clock.tick() {
if sender if sender
.send(Message::Event(Msg::Subscription( .send(ServicePayloadKind::Clock(payload))
ServicePayloadKind::Clock(payload),
)))
.await .await
.is_err() .is_err()
{ {