asked the clanker to change the arcidecture a bit

This commit is contained in:
2026-09-09 15:50:07 +01:00
parent 3490af52e4
commit b9c7e73824
15 changed files with 185 additions and 74 deletions
Generated
+53 -4
View File
@@ -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"
+4 -1
View File
@@ -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.
+1
View File
@@ -9,3 +9,4 @@ path = "src/lib.rs"
[dependencies]
iced = { workspace = true }
toml = { workspace = true }
+16 -4
View File
@@ -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<dyn Error>> {
Ok(())
}
}
+3 -4
View File
@@ -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" }
+32 -54
View File
@@ -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<dyn BarModule>> = 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<dyn BarModule>> =
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<Message>`.
#[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<Message> {
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<Message> {
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::<Vec<_>>(),
);
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<Message> {
let route_sub = Subscription::batch(
bar.routes
.values()
.flat_map(|x| x.iter().map(service_subscription))
.collect::<Vec<_>>(),
);
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<Message> {
fn update(bar: &mut Bar, msg: Message) -> Task<Message> {
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<Message> {
fn handle_event(bar: &mut Bar, event: Msg) -> Task<Message> {
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<Message> {
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<Message> {
}
/// 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<Message> {
match cmd {
Cmd::RequestPopup(module_id, element_id) => {
fn handle_effect(bar: &mut Bar, effect: ModuleEffect) -> Task<Message> {
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<Message> {
}
}
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))
+7 -4
View File
@@ -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<M
/// `Msg::WindowClosed`, so popup content keeps rendering until then.
pub fn close_popup(bar: &mut Bar) -> Task<Message> {
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<Message> {
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()
+12
View File
@@ -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" }
+2 -1
View File
@@ -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 }
+16
View File
@@ -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<dyn std::error::Error>> {
if let toml::Value::Boolean(e) = config["format"] {
if e {
self.show_seconds = true;
};
}
Ok(())
}
}
+10
View File
@@ -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<Box<dyn BarModule>> {
vec![Box::new(module_clock::Clock::new())]
}
+13
View File
@@ -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 }
@@ -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]
+14
View File
@@ -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<ServicePayloadKind> {
match service {
Service::Clock(_) => service_datetime::ClockTicker::run(),
}
}