From 3490af52e4de68e6001178029eb15ff1ef543a30 Mon Sep 17 00:00:00 2001 From: Doloro1978 Date: Mon, 7 Sep 2026 11:41:59 +0100 Subject: [PATCH] refec --- Cargo.lock | 49 +++++-- Cargo.toml | 3 +- crates/common/Cargo.toml | 11 ++ crates/common/src/lib.rs | 127 ++++++++++++++++++ crates/core/Cargo.toml | 9 +- crates/core/src/main.rs | 44 ++++-- crates/core/src/module/mod.rs | 63 --------- crates/core/src/services/mod.rs | 47 ------- crates/modules/clock/Cargo.toml | 13 ++ .../clock.rs => modules/clock/src/lib.rs} | 33 +++-- crates/services/datatime/Cargo.toml | 14 ++ .../clock.rs => services/datatime/src/lib.rs} | 31 ++--- 12 files changed, 274 insertions(+), 170 deletions(-) create mode 100644 crates/common/Cargo.toml create mode 100644 crates/common/src/lib.rs delete mode 100644 crates/core/src/module/mod.rs delete mode 100644 crates/core/src/services/mod.rs create mode 100644 crates/modules/clock/Cargo.toml rename crates/{core/src/module/clock.rs => modules/clock/src/lib.rs} (82%) create mode 100644 crates/services/datatime/Cargo.toml rename crates/{core/src/services/clock.rs => services/datatime/src/lib.rs} (75%) diff --git a/Cargo.lock b/Cargo.lock index 0162e68..46dda94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,16 +98,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "barbar-core" -version = "0.1.0" -dependencies = [ - "chrono", - "iced", - "iced_layershell", - "tokio", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -340,6 +330,13 @@ dependencies = [ "memchr", ] +[[package]] +name = "common" +version = "0.1.0" +dependencies = [ + "iced", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -349,6 +346,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "core" +version = "0.1.0" +dependencies = [ + "chrono", + "common", + "iced", + "iced_layershell", + "module-clock", + "service-datatime", + "tokio", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1702,6 +1712,15 @@ dependencies = [ "paste", ] +[[package]] +name = "module-clock" +version = "0.1.0" +dependencies = [ + "common", + "iced", + "service-datatime", +] + [[package]] name = "naga" version = "27.0.3" @@ -2499,6 +2518,16 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "service-datatime" +version = "0.1.0" +dependencies = [ + "chrono", + "common", + "iced", + "tokio", +] + [[package]] name = "shlex" version = "2.0.1" diff --git a/Cargo.toml b/Cargo.toml index ec6cbe0..19474cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] -members = ["crates/*"] +members = ["crates/core", "crates/modules/clock", "crates/services/datatime", "crates/common"] +resolver = "2" [workspace.package] version = "0.1.0" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml new file mode 100644 index 0000000..23458be --- /dev/null +++ b/crates/common/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "common" +version.workspace = true +edition.workspace = true + +[lib] +name = "common" +path = "src/lib.rs" + +[dependencies] +iced = { workspace = true } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs new file mode 100644 index 0000000..c86caad --- /dev/null +++ b/crates/common/src/lib.rs @@ -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 { + 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, +} + +impl ModuleMsg { + /// Wraps a module's own message; `id` must match its `BarModule::id`. + pub fn new(id: &'static str, msg: T) -> Self { + Self { + id, + payload: Arc::new(msg), + } + } + + /// Downcasts to the module's own message type. + pub fn downcast(&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) -> Element<'_, ModuleMsg>; + + /// Handles a routed message; may return app-level tasks (e.g. popups). + fn update(&mut self, msg: ModuleMsg) -> Task; + + /// What services this module wants. + fn services(&self) -> Vec; + + /// Requested popup size when it's not the generic small menu. + fn popup_size(&self) -> Option<(u32, u32)> { + None + } +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 4e04d09..9cdf4a5 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,13 +1,18 @@ [package] -name = "barbar-core" +name = "core" version.workspace = true edition.workspace = true [[bin]] -name = "barbar-core" +name = "core" [dependencies] +common = { path = "../common" } iced = { workspace = true } iced_layershell = { workspace = true } tokio = { workspace = true } chrono = { workspace = true } +# Modules +module_clock = { package = "module-clock", path = "../modules/clock" } +# Services +service_datatime = { package = "service-datatime", path = "../services/datatime" } diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs index a2214a4..0fb71cc 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -13,13 +13,9 @@ use iced_layershell::reexport::Anchor; use iced_layershell::settings::{LayerShellSettings, Settings, StartMode}; use iced_layershell::to_layer_message; -use crate::module::{BarModule, ModuleMsg}; -use crate::services::Service as ModuleService; -use crate::services::ServicePayloadKind; +use common::{BarModule, ModuleEffect, ModuleMsg, Service as ModuleService, ServicePayloadKind}; -mod module; mod popup; -mod services; /// Height of the bar in logical pixels. const BAR_HEIGHT: u32 = 36; @@ -43,7 +39,7 @@ struct Bar { impl Bar { fn new() -> Self { let mut modules: BTreeMap<&'static str, Box> = BTreeMap::new(); - modules.insert("clock", Box::new(crate::module::clock::Clock::new())); + modules.insert("clock", Box::new(module_clock::Clock::new())); let routes = modules .iter() .map(|(id, module)| (*id, module.services())) @@ -100,6 +96,26 @@ fn namespace() -> String { 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 { + match service { + ModuleService::Clock(_) => service_datatime::ClockTicker::run() + .map(|event| Message::Event(Msg::Subscription(event))), + } +} + fn main() -> Result<(), iced_layershell::Error> { let start_mode = match std::env::args().nth(1) { Some(output) => StartMode::TargetScreen(output), @@ -127,7 +143,7 @@ fn gather_subscriptions(bar: &Bar) -> Subscription { let route_sub = Subscription::batch( bar.routes .values() - .flat_map(|x| x.iter().map(ModuleService::subscription)) + .flat_map(|x| x.iter().map(service_subscription)) .collect::>(), ); 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 { fn handle_event(bar: &mut Bar, event: Msg) -> Task { match event { Msg::Module(m) => match bar.modules.get_mut(m.id) { - // Modules return their own app-level tasks (e.g. popups). - Some(module) => module.update(m), + // Modules return protocol tasks; map effects into `Cmd`s. + Some(module) => module.update(m).map(module_effect_to_message), None => Task::none(), }, @@ -169,10 +185,12 @@ fn handle_event(bar: &mut Bar, event: Msg) -> Task { .is_some_and(|kinds| kinds.contains(&key)) }) .map(|(_, module)| { - module.update(ModuleMsg { - id: module.id(), - payload: payload.clone(), - }) + module + .update(ModuleMsg { + id: module.id(), + payload: payload.clone(), + }) + .map(module_effect_to_message) }) .fold(Task::none(), |acc, t| acc.chain(t)) } diff --git a/crates/core/src/module/mod.rs b/crates/core/src/module/mod.rs deleted file mode 100644 index 3550b43..0000000 --- a/crates/core/src/module/mod.rs +++ /dev/null @@ -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, -} - -impl ModuleMsg { - /// Wraps a module's own message; `id` must match its `BarModule::id`. - pub fn new(id: &'static str, msg: T) -> Self { - Self { - id, - payload: Arc::new(msg), - } - } - - /// Downcasts to the module's own message type. - pub fn downcast(&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) -> Element<'_, ModuleMsg>; - - /// Handles a routed message; may return app-level tasks (e.g. popups). - fn update(&mut self, msg: ModuleMsg) -> Task; - - /// What services this module wants. - fn services(&self) -> Vec; - - /// Requested popup size when it's not the generic small menu. - fn popup_size(&self) -> Option<(u32, u32)> { - None - } -} diff --git a/crates/core/src/services/mod.rs b/crates/core/src/services/mod.rs deleted file mode 100644 index 7921b6e..0000000 --- a/crates/core/src/services/mod.rs +++ /dev/null @@ -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 { - 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 { - match self { - Self::Clock(payload) => Arc::new(payload), - } - } -} diff --git a/crates/modules/clock/Cargo.toml b/crates/modules/clock/Cargo.toml new file mode 100644 index 0000000..d4efd6d --- /dev/null +++ b/crates/modules/clock/Cargo.toml @@ -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 } diff --git a/crates/core/src/module/clock.rs b/crates/modules/clock/src/lib.rs similarity index 82% rename from crates/core/src/module/clock.rs rename to crates/modules/clock/src/lib.rs index 8cf2c58..962f458 100644 --- a/crates/core/src/module/clock.rs +++ b/crates/modules/clock/src/lib.rs @@ -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::{Element, Task}; -use crate::module::{BarModule, ModuleMsg}; -use crate::services::clock::{ClockKind, ClockPayload}; -use crate::services::Service; -use crate::{Cmd, Message}; +use common::{BarModule, ClockKind, ClockPayload, ModuleEffect, ModuleMsg, Service}; -/// 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. +/// Big-text popup size (logical px). +const POPUP_W: u32 = 360; +const POPUP_H: u32 = 160; + +/// A module-local message for the clock. #[derive(Clone)] pub enum ClockMsg { /// Toggle the seconds suffix. @@ -17,10 +20,6 @@ pub enum ClockMsg { OpenPopup, } -/// Big-text popup size (logical px). -const POPUP_W: u32 = 360; -const POPUP_H: u32 = 160; - pub struct Clock { value: String, show_seconds: bool, @@ -48,6 +47,12 @@ impl Clock { } } +impl Default for Clock { + fn default() -> Self { + Self::new() + } +} + impl BarModule for Clock { fn id(&self) -> &'static str { "clock" @@ -69,7 +74,7 @@ impl BarModule for Clock { } } - fn update(&mut self, msg: ModuleMsg) -> Task { + fn update(&mut self, msg: ModuleMsg) -> Task { // Subscription fan-out delivers a raw `ClockPayload`, not `ClockMsg`. if let Some(payload) = msg.downcast::() { self.value = payload.value.clone(); @@ -80,10 +85,10 @@ impl BarModule for Clock { self.show_seconds = !self.show_seconds; 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(), - ))), + )), None => Task::none(), } } diff --git a/crates/services/datatime/Cargo.toml b/crates/services/datatime/Cargo.toml new file mode 100644 index 0000000..476c2dc --- /dev/null +++ b/crates/services/datatime/Cargo.toml @@ -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 } diff --git a/crates/core/src/services/clock.rs b/crates/services/datatime/src/lib.rs similarity index 75% rename from crates/core/src/services/clock.rs rename to crates/services/datatime/src/lib.rs index f07e983..bef8f08 100644 --- a/crates/core/src/services/clock.rs +++ b/crates/services/datatime/src/lib.rs @@ -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 iced::{futures::SinkExt, Subscription}; -use crate::{services::ServicePayloadKind, Message, Msg}; - -/// 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, -} +use common::{ClockKind, ClockPayload, ServicePayloadKind}; /// Emits a payload only when a kind's displayed value changes (minute or /// second rollover). Drive `tick` once per second. @@ -30,20 +22,19 @@ impl ClockTicker { Self::default() } - pub fn run() -> Subscription { + /// Subscription that emits `ServicePayloadKind::Clock` on every change. + pub fn run() -> Subscription { Subscription::run_with((), |_| { iced::stream::channel( 0, - |mut sender: iced::futures::channel::mpsc::Sender| async move { + |mut sender: iced::futures::channel::mpsc::Sender| async move { let mut clock = ClockTicker::new(); let mut interval = tokio::time::interval(std::time::Duration::from_secs(1)); loop { interval.tick().await; for payload in clock.tick() { if sender - .send(Message::Event(Msg::Subscription( - ServicePayloadKind::Clock(payload), - ))) + .send(ServicePayloadKind::Clock(payload)) .await .is_err() {