diff --git a/Cargo.lock b/Cargo.lock index 46dda94..51ba97a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,7 @@ name = "common" version = "0.1.0" dependencies = [ "iced", + "toml", ] [[package]] @@ -354,8 +355,8 @@ dependencies = [ "common", "iced", "iced_layershell", - "module-clock", - "service-datatime", + "modules", + "services", "tokio", ] @@ -1718,7 +1719,16 @@ version = "0.1.0" dependencies = [ "common", "iced", - "service-datatime", + "thiserror 2.0.20", + "toml", +] + +[[package]] +name = "modules" +version = "0.1.0" +dependencies = [ + "common", + "module-clock", ] [[package]] @@ -2519,7 +2529,16 @@ dependencies = [ ] [[package]] -name = "service-datatime" +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "service-datetime" version = "0.1.0" dependencies = [ "chrono", @@ -2528,6 +2547,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "services" +version = "0.1.0" +dependencies = [ + "common", + "iced", + "service-datetime", +] + [[package]] name = "shlex" version = "2.0.1" @@ -2929,6 +2957,21 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "toml" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -2959,6 +3002,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tracing" version = "0.1.44" diff --git a/Cargo.toml b/Cargo.toml index 19474cc..fcd7f52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/core", "crates/modules/clock", "crates/services/datatime", "crates/common"] +members = ["crates/core", "crates/common", "crates/modules", "crates/modules/clock", "crates/services", "crates/services/datetime"] resolver = "2" [workspace.package] @@ -11,6 +11,9 @@ iced = { version = "0.14", default-features = false, features = ["wgpu", "waylan iced_layershell = { version = "0.19", default-features = false } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] } chrono = "0.4" +toml = "1.1" +serde = { version = "1", features = ["derive"] } +thiserror = "2" # Optimize all dependencies even in dev builds; workspace members keep dev # defaults (opt-level 0) and release settings under --release. diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 23458be..69d140c 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -9,3 +9,4 @@ path = "src/lib.rs" [dependencies] iced = { workspace = true } +toml = { workspace = true } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index c86caad..d3d1db9 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,15 +1,16 @@ //! Barbar's protocol crate: the types that cross crate boundaries. //! -//! Nothing here may reference app-level types (`Message`, `Cmd`, `Msg`, +//! Nothing here may reference app-level types (`Message`, `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::error::Error; use std::fmt; use std::sync::Arc; use iced::window; -use iced::{Element, Task}; +use iced::{Element, Rectangle, Task}; // --------------------------------------------------------------------------- // Service model @@ -95,12 +96,16 @@ impl fmt::Debug for ModuleMsg { } } -/// What a module asks the app to do. Modules never name app types; the app -/// maps these into its own effects (`Cmd`). +/// What a module asks the app to do — the single effect vocabulary shared by +/// modules and app, so mapping module output into a `Message` is a plain +/// `.map(Message::Effect)` with no mirror enum. #[derive(Debug, Clone)] pub enum ModuleEffect { /// Toggle a popup for the given module id (element id anchors it). RequestPopup(String, String), + /// A widget-tree pass reported a module's laid-out bounds; anchor the + /// popup there. App-internal, but one effect type keeps routing trivial. + BoundsFound(String, Rectangle), /// Request removal of the popup surface. ClosePopup(window::Id), } @@ -124,4 +129,11 @@ pub trait BarModule: Send { fn popup_size(&self) -> Option<(u32, u32)> { None } + + /// Digest config from `module.{module-id}`. Error is boxed because this + /// runs through `dyn BarModule`; each impl keeps its own concrete error + /// internally and may surface it pre-box via its constructor instead. + fn config(&mut self, _config: toml::Table) -> Result<(), Box> { + Ok(()) + } } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9cdf4a5..b771611 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -12,7 +12,6 @@ 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" } +# Modules + services +modules = { path = "../modules" } +services = { path = "../services" } diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs index 0fb71cc..04f5e76 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; use iced::widget::{column, container, text}; -use iced::{window, Alignment, Element, Length, Rectangle, Subscription, Task, Theme}; +use iced::{window, Alignment, Element, Length, Subscription, Task, Theme}; use iced_layershell::daemon; use iced_layershell::reexport::Anchor; use iced_layershell::settings::{LayerShellSettings, Settings, StartMode}; @@ -38,8 +38,9 @@ struct Bar { impl Bar { fn new() -> Self { - let mut modules: BTreeMap<&'static str, Box> = BTreeMap::new(); - modules.insert("clock", Box::new(module_clock::Clock::new())); + // Registry owns construction: adding a module never edits this file. + let modules: BTreeMap<&'static str, Box> = + modules::all().into_iter().map(|m| (m.id(), m)).collect(); let routes = modules .iter() .map(|(id, module)| (*id, module.services())) @@ -68,24 +69,15 @@ pub(crate) enum Msg { WindowClosed(window::Id), } -/// App-level effects (popups, window ops), emitted via `Task`. -#[derive(Debug, Clone)] -pub(crate) enum Cmd { - /// Toggle a module's popup: close if open for it, else open it. - RequestPopup(String, String), - /// Widget-tree pass reported a module's laid-out bounds; anchor popup. - BoundsFound(String, Rectangle), - /// Request removal of the popup surface. - ClosePopup(window::Id), -} - /// iced needs one `Message` type. `to_layer_message` must sit here: it /// injects the layer-shell effect variants + their `TryInto` impl. #[to_layer_message(multi)] #[derive(Debug, Clone)] pub(crate) enum Message { Event(Msg), - Effect(Cmd), + /// Effects. `common::ModuleEffect` is the one effect vocabulary, so + /// mapping module output is a plain `.map(Message::Effect)`. + Effect(ModuleEffect), } // --------------------------------------------------------------------------- @@ -96,24 +88,22 @@ 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))), - } +/// Route subscriptions + window-close events (popup really destroyed). +/// Each module's wants go through the services registry, so adding a +/// service never edits this file. +fn gather_subscriptions(bar: &Bar) -> Subscription { + let route_sub = Subscription::batch( + bar.routes + .values() + .flat_map(|wants| wants.iter()) + .map(|service| { + services::subscription(service) + .map(|event| Message::Event(Msg::Subscription(event))) + }) + .collect::>(), + ); + let close_events = window::close_events().map(|id| Message::Event(Msg::WindowClosed(id))); + Subscription::batch(vec![route_sub, close_events]) } fn main() -> Result<(), iced_layershell::Error> { @@ -138,18 +128,6 @@ fn main() -> Result<(), iced_layershell::Error> { .run() } -/// Route subscriptions + window-close events (popup really destroyed). -fn gather_subscriptions(bar: &Bar) -> Subscription { - let route_sub = Subscription::batch( - bar.routes - .values() - .flat_map(|x| x.iter().map(service_subscription)) - .collect::>(), - ); - let close_events = window::close_events().map(|id| Message::Event(Msg::WindowClosed(id))); - Subscription::batch(vec![route_sub, close_events]) -} - // --------------------------------------------------------------------------- // Update // --------------------------------------------------------------------------- @@ -157,7 +135,7 @@ fn gather_subscriptions(bar: &Bar) -> Subscription { fn update(bar: &mut Bar, msg: Message) -> Task { match msg { Message::Event(event) => handle_event(bar, event), - Message::Effect(cmd) => handle_effect(bar, cmd), + Message::Effect(effect) => handle_effect(bar, effect), // Layer-shell variants injected by `to_layer_message`. _ => Task::none(), } @@ -167,8 +145,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 protocol tasks; map effects into `Cmd`s. - Some(module) => module.update(m).map(module_effect_to_message), + // Modules return protocol tasks; wrap effects into messages. + Some(module) => module.update(m).map(Message::Effect), None => Task::none(), }, @@ -190,7 +168,7 @@ fn handle_event(bar: &mut Bar, event: Msg) -> Task { id: module.id(), payload: payload.clone(), }) - .map(module_effect_to_message) + .map(Message::Effect) }) .fold(Task::none(), |acc, t| acc.chain(t)) } @@ -208,9 +186,9 @@ fn handle_event(bar: &mut Bar, event: Msg) -> Task { } /// Runs an effect; may set up bar state for it (e.g. which popup is open). -fn handle_effect(bar: &mut Bar, cmd: Cmd) -> Task { - match cmd { - Cmd::RequestPopup(module_id, element_id) => { +fn handle_effect(bar: &mut Bar, effect: ModuleEffect) -> Task { + match effect { + ModuleEffect::RequestPopup(module_id, element_id) => { // Toggle: close if already open for this module, else open. if bar.active_popup.as_deref() == Some(module_id.as_str()) { popup::close_popup(bar) @@ -219,9 +197,9 @@ fn handle_effect(bar: &mut Bar, cmd: Cmd) -> Task { } } - Cmd::BoundsFound(module_id, bounds) => popup::open_popup(bar, module_id, bounds), + ModuleEffect::BoundsFound(module_id, bounds) => popup::open_popup(bar, module_id, bounds), - Cmd::ClosePopup(id) => { + ModuleEffect::ClosePopup(id) => { // Request removal only; keep popup state so the popup content // renders until `WindowClosed` confirms it's gone. Task::done(Message::RemoveWindow(id)) diff --git a/crates/core/src/popup.rs b/crates/core/src/popup.rs index a1d9374..d192d0b 100644 --- a/crates/core/src/popup.rs +++ b/crates/core/src/popup.rs @@ -10,7 +10,9 @@ use iced::{Alignment, Element, Length, Rectangle, Task}; use iced_layershell::actions::IcedNewPopupSettings; use iced_layershell::reexport::{PopupAnchor, PopupGravity}; -use crate::{Bar, Cmd, Message, Msg}; +use common::ModuleEffect; + +use crate::{Bar, Message, Msg}; /// Default (small menu) popup size. const POPUP_W: u32 = 150; @@ -52,7 +54,7 @@ pub fn open_popup(bar: &mut Bar, module_id: String, bounds: Rectangle) -> Task Task { if let Some(id) = bar.popup_id { - return Task::done(Message::Effect(Cmd::ClosePopup(id))); + return Task::done(Message::Effect(ModuleEffect::ClosePopup(id))); } Task::none() } @@ -93,7 +95,7 @@ pub fn capture_bounds(module_id: String, element_id: String) -> Task { target, found: None, }) - .map(move |bounds| Message::Effect(Cmd::BoundsFound(module_id.clone(), bounds))) + .map(move |bounds| Message::Effect(ModuleEffect::BoundsFound(module_id.clone(), bounds))) } /// Popup widget tree on its own LayerShell surface; the active module @@ -117,7 +119,8 @@ pub fn view(bar: &Bar) -> Element<'_, Message> { .width(Length::Fill) .align_x(Alignment::Center), content, - mouse_area(text("close").size(12)).on_press(Message::Effect(Cmd::ClosePopup(popup_id))), + mouse_area(text("close").size(12)) + .on_press(Message::Effect(ModuleEffect::ClosePopup(popup_id))), ] .spacing(1) .into() diff --git a/crates/modules/Cargo.toml b/crates/modules/Cargo.toml new file mode 100644 index 0000000..dffa7c1 --- /dev/null +++ b/crates/modules/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "modules" +version.workspace = true +edition.workspace = true + +[lib] +name = "modules" +path = "src/lib.rs" + +[dependencies] +common = { path = "../common" } +module_clock = { package = "module-clock", path = "clock" } diff --git a/crates/modules/clock/Cargo.toml b/crates/modules/clock/Cargo.toml index d4efd6d..3afb47e 100644 --- a/crates/modules/clock/Cargo.toml +++ b/crates/modules/clock/Cargo.toml @@ -9,5 +9,6 @@ path = "src/lib.rs" [dependencies] common = { path = "../../common" } -service_datatime = { package = "service-datatime", path = "../../services/datatime" } iced = { workspace = true } +thiserror = { workspace = true } +toml = { workspace = true } diff --git a/crates/modules/clock/src/lib.rs b/crates/modules/clock/src/lib.rs index 962f458..f783933 100644 --- a/crates/modules/clock/src/lib.rs +++ b/crates/modules/clock/src/lib.rs @@ -11,6 +11,14 @@ use common::{BarModule, ClockKind, ClockPayload, ModuleEffect, ModuleMsg, Servic const POPUP_W: u32 = 360; const POPUP_H: u32 = 160; +/// Errors digesting the `module.clock` config table. +#[derive(Debug, thiserror::Error)] +pub enum ClockError { + /// Config value was present but not a bool. + #[error("module.clock.{0} must be a bool")] + NotBool(&'static str), +} + /// A module-local message for the clock. #[derive(Clone)] pub enum ClockMsg { @@ -100,4 +108,12 @@ impl BarModule for Clock { fn popup_size(&self) -> Option<(u32, u32)> { Some((POPUP_W, POPUP_H)) } + fn config(&mut self, config: toml::Table) -> Result<(), Box> { + if let toml::Value::Boolean(e) = config["format"] { + if e { + self.show_seconds = true; + }; + } + Ok(()) + } } diff --git a/crates/modules/src/lib.rs b/crates/modules/src/lib.rs new file mode 100644 index 0000000..26d93f6 --- /dev/null +++ b/crates/modules/src/lib.rs @@ -0,0 +1,10 @@ +//! Bar module registry: constructs every enabled module. Adding a module is +//! a new crate under `crates/modules/` plus one entry in `all()` — core +//! never changes. + +use common::BarModule; + +/// One instance of every enabled module, ready to insert by `id()`. +pub fn all() -> Vec> { + vec![Box::new(module_clock::Clock::new())] +} diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml new file mode 100644 index 0000000..5ab13d6 --- /dev/null +++ b/crates/services/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "services" +version.workspace = true +edition.workspace = true + +[lib] +name = "services" +path = "src/lib.rs" + +[dependencies] +common = { path = "../common" } +service_datetime = { package = "service-datetime", path = "datetime" } +iced = { workspace = true } diff --git a/crates/services/datatime/Cargo.toml b/crates/services/datetime/Cargo.toml similarity index 81% rename from crates/services/datatime/Cargo.toml rename to crates/services/datetime/Cargo.toml index 476c2dc..0aedec3 100644 --- a/crates/services/datatime/Cargo.toml +++ b/crates/services/datetime/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "service-datatime" +name = "service-datetime" version.workspace = true edition.workspace = true [lib] -name = "service_datatime" +name = "service_datetime" path = "src/lib.rs" [dependencies] diff --git a/crates/services/datatime/src/lib.rs b/crates/services/datetime/src/lib.rs similarity index 100% rename from crates/services/datatime/src/lib.rs rename to crates/services/datetime/src/lib.rs diff --git a/crates/services/src/lib.rs b/crates/services/src/lib.rs new file mode 100644 index 0000000..2ad0e16 --- /dev/null +++ b/crates/services/src/lib.rs @@ -0,0 +1,14 @@ +//! Service registry: turns a route key into the subscription that backs it. +//! Adding a service is a new crate under `crates/services/` plus one arm in +//! `subscription` — core never changes. + +use iced::Subscription; + +use common::{Service, ServicePayloadKind}; + +/// The subscription backing a route key. +pub fn subscription(service: &Service) -> Subscription { + match service { + Service::Clock(_) => service_datetime::ClockTicker::run(), + } +}