msg type -> wire::push() -> update handle wire into module/service/ect
This commit is contained in:
Generated
-1
@@ -1831,7 +1831,6 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"iced",
|
"iced",
|
||||||
"serde",
|
"serde",
|
||||||
"services",
|
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use iced::{window, Rectangle};
|
use iced::{window, Rectangle};
|
||||||
|
|
||||||
|
use crate::Wire;
|
||||||
|
|
||||||
/// What a module asks the app to do — the single effect vocabulary shared by
|
/// 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
|
/// modules and app, so mapping module output into a `Message` is a plain
|
||||||
/// `.map(Message::Effect)` with no mirror enum.
|
/// `.map(Message::Effect)` with no mirror enum.
|
||||||
@@ -12,4 +14,6 @@ pub enum ModuleEffect {
|
|||||||
BoundsFound(String, Rectangle),
|
BoundsFound(String, Rectangle),
|
||||||
/// Request removal of the popup surface.
|
/// Request removal of the popup surface.
|
||||||
ClosePopup(window::Id),
|
ClosePopup(window::Id),
|
||||||
|
/// Route a wire to its target (a module, a service inbox, or a topic).
|
||||||
|
Send(Wire),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,14 @@
|
|||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod effect;
|
mod effect;
|
||||||
|
mod messages;
|
||||||
mod module;
|
mod module;
|
||||||
mod service;
|
mod service;
|
||||||
|
mod wire;
|
||||||
|
|
||||||
pub use config::BarbarConfig;
|
pub use config::BarbarConfig;
|
||||||
pub use effect::ModuleEffect;
|
pub use effect::ModuleEffect;
|
||||||
pub use module::{BarModule, ModuleMsg};
|
pub use messages::{ClockKind, ClockMsg, ClockPayload};
|
||||||
pub use service::{Service, ServiceEvent};
|
pub use module::BarModule;
|
||||||
|
pub use service::Service;
|
||||||
|
pub use wire::{Endpoint, Inbox, Namespace, Target, Wire};
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//! Wire vocabulary: every message type that crosses a crate boundary.
|
||||||
|
//!
|
||||||
|
//! Laid out to mirror the crate that owns the behavior: `modules/*` are
|
||||||
|
//! messages a bar module sends or receives, `services/*` are messages a
|
||||||
|
//! service produces or accepts. Types live here so either side can name a
|
||||||
|
//! payload without depending on the other's crate; only the types are
|
||||||
|
//! shared, the behavior stays in the owning crate.
|
||||||
|
|
||||||
|
pub mod modules;
|
||||||
|
pub mod services;
|
||||||
|
|
||||||
|
pub use modules::*;
|
||||||
|
pub use services::*;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Clock module messages.
|
||||||
|
|
||||||
|
/// A module-local message for the clock (bar/popup interactions).
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ClockMsg {
|
||||||
|
/// Toggle the seconds suffix.
|
||||||
|
ToggleSeconds,
|
||||||
|
/// Open the big-time popup.
|
||||||
|
OpenPopup,
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//! Messages owned by bar modules.
|
||||||
|
|
||||||
|
mod clock;
|
||||||
|
|
||||||
|
pub use clock::ClockMsg;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//! Clock service messages.
|
||||||
|
|
||||||
|
use crate::Service;
|
||||||
|
|
||||||
|
/// What a clock subscription wants: the tick interval and payload selector.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub enum ClockKind {
|
||||||
|
Mins,
|
||||||
|
Seconds,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClockKind {
|
||||||
|
/// Route key a module subscribes under to receive this kind's ticks.
|
||||||
|
pub fn key(self) -> Service {
|
||||||
|
Service(match self {
|
||||||
|
Self::Mins => "clock.mins",
|
||||||
|
Self::Seconds => "clock.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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//! Messages owned by services.
|
||||||
|
|
||||||
|
mod datetime;
|
||||||
|
|
||||||
|
pub use datetime::{ClockKind, ClockPayload};
|
||||||
@@ -1,56 +1,22 @@
|
|||||||
use std::any::Any;
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use iced::window;
|
use iced::window;
|
||||||
use iced::{Element, Task};
|
use iced::{Element, Task};
|
||||||
|
|
||||||
use crate::ModuleEffect;
|
use crate::{ModuleEffect, Wire};
|
||||||
|
|
||||||
/// 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
|
/// 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.
|
/// `Wire` boundary keeps the app ignorant of each module's message enum —
|
||||||
|
/// the module downcasts whatever arrives.
|
||||||
pub trait BarModule: Send {
|
pub trait BarModule: Send {
|
||||||
fn id(&self) -> &'static str;
|
fn id(&self) -> &'static str;
|
||||||
|
|
||||||
/// Bar contents. When `window_id` is `Some`, this is the module's popup
|
/// Bar contents. When `window_id` is `Some`, this is the module's popup
|
||||||
/// surface and the module returns its popup contents instead.
|
/// surface and the module returns its popup contents instead.
|
||||||
fn view(&self, window_id: Option<window::Id>) -> Element<'_, ModuleMsg>;
|
fn view(&self, window_id: Option<window::Id>) -> Element<'_, Wire>;
|
||||||
|
|
||||||
/// Handles a routed message; may return app-level tasks (e.g. popups).
|
/// Handles a routed message; may return app-level tasks (e.g. popups).
|
||||||
fn update(&mut self, msg: ModuleMsg) -> Task<ModuleEffect>;
|
fn update(&mut self, msg: Wire) -> Task<ModuleEffect>;
|
||||||
|
|
||||||
/// What services this module wants.
|
/// What services this module wants.
|
||||||
fn services(&self) -> Vec<crate::Service>;
|
fn services(&self) -> Vec<crate::Service>;
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
use std::any::Any;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// Opaque route key identifying a service subscription. Services name their
|
/// Opaque route key identifying a service subscription. Services name their
|
||||||
/// own keys, so `common` never enumerates them and adding a service never
|
/// own keys, so `common` never enumerates them and adding a service never
|
||||||
/// edits this crate.
|
/// edits this crate.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
pub struct Service(pub &'static str);
|
pub struct Service(pub &'static str);
|
||||||
|
|
||||||
/// A service event: the route key plus an erased typed payload. The app reads
|
|
||||||
/// `key` for fan-out; the consuming module downcasts `payload`.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct ServiceEvent {
|
|
||||||
pub key: Service,
|
|
||||||
pub payload: Arc<dyn Any + Send + Sync>,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
use std::any::Any;
|
||||||
|
use std::fmt;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use iced::futures::channel::mpsc;
|
||||||
|
|
||||||
|
/// Which namespace an endpoint lives in. Module ids and service route keys
|
||||||
|
/// are separate namespaces, so a module and a service may share a name.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub enum Namespace {
|
||||||
|
Module,
|
||||||
|
Service,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wire's sender: a module id or a service route key.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct Endpoint {
|
||||||
|
pub ns: Namespace,
|
||||||
|
pub name: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Endpoint {
|
||||||
|
pub const fn module(name: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
ns: Namespace::Module,
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn service(name: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
ns: Namespace::Service,
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a wire goes. Core owns the routing rule for each variant.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub enum Target {
|
||||||
|
/// One module, by id.
|
||||||
|
Module(&'static str),
|
||||||
|
/// One running service's inbox, by route key.
|
||||||
|
Service(&'static str),
|
||||||
|
/// Every module subscribed to a route key (a service's fan-out).
|
||||||
|
Topic(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One typed message on the wire. Core routes by `to`; the receiver
|
||||||
|
/// downcasts `payload` to one of *its own* types. Sender and receiver keep
|
||||||
|
/// their own concrete types — nothing here enumerates them.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Wire {
|
||||||
|
pub from: Endpoint,
|
||||||
|
pub to: Target,
|
||||||
|
pub payload: Arc<dyn Any + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wire {
|
||||||
|
/// A module-local message: addressed straight back to one module.
|
||||||
|
pub fn module<T: Any + Send + Sync>(from: Endpoint, id: &'static str, msg: T) -> Self {
|
||||||
|
Self {
|
||||||
|
from,
|
||||||
|
to: Target::Module(id),
|
||||||
|
payload: Arc::new(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sent into a service's inbox.
|
||||||
|
pub fn service<T: Any + Send + Sync>(from: Endpoint, key: &'static str, msg: T) -> Self {
|
||||||
|
Self {
|
||||||
|
from,
|
||||||
|
to: Target::Service(key),
|
||||||
|
payload: Arc::new(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Published by a service to every module subscribed to `key`.
|
||||||
|
pub fn topic<T: Any + Send + Sync>(from: Endpoint, key: &'static str, msg: T) -> Self {
|
||||||
|
Self {
|
||||||
|
from,
|
||||||
|
to: Target::Topic(key),
|
||||||
|
payload: Arc::new(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downcasts to the receiver's own message type.
|
||||||
|
pub fn downcast<T: Any + Send + Sync>(&self) -> Option<&T> {
|
||||||
|
self.payload.downcast_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Wire {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("Wire")
|
||||||
|
.field("from", &self.from)
|
||||||
|
.field("to", &self.to)
|
||||||
|
.field("payload", &self.payload.type_id())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A service's inbound mailbox: modules send `Wire`s here, the service's
|
||||||
|
/// subscription task reads them. Cloneable; the receiver is taken once.
|
||||||
|
///
|
||||||
|
/// The `Arc<Mutex<Option<..>>>` lets `Bar` keep a clone and hand the same
|
||||||
|
/// value to `Subscription::run_with` on every rebuild without moving the
|
||||||
|
/// single-consumer receiver out of its reach.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Inbox {
|
||||||
|
key: &'static str,
|
||||||
|
tx: mpsc::UnboundedSender<Wire>,
|
||||||
|
rx: Arc<Mutex<Option<mpsc::UnboundedReceiver<Wire>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inbox {
|
||||||
|
/// Creates the channel backing a service's inbox.
|
||||||
|
pub fn new(key: &'static str) -> Self {
|
||||||
|
let (tx, rx) = mpsc::unbounded();
|
||||||
|
Self {
|
||||||
|
key,
|
||||||
|
tx,
|
||||||
|
rx: Arc::new(Mutex::new(Some(rx))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The service's route key, used as the `Topic` it publishes under.
|
||||||
|
pub fn key(&self) -> &'static str {
|
||||||
|
self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a wire into the service; `false` if the service is gone.
|
||||||
|
pub fn send(&self, wire: Wire) -> bool {
|
||||||
|
self.tx.unbounded_send(wire).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the receiver; the service stream calls this once, when iced
|
||||||
|
/// first starts it.
|
||||||
|
pub fn take(&self) -> Option<mpsc::UnboundedReceiver<Wire>> {
|
||||||
|
self.rx.lock().unwrap().take()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Inbox {
|
||||||
|
/// Identity for `Subscription::run_with`: the route key, so returning
|
||||||
|
/// the same service subscription each pass does not restart its stream.
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.key.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-6
@@ -1,10 +1,11 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use iced::window;
|
use iced::window;
|
||||||
|
|
||||||
use common::{BarModule, Service};
|
use common::{BarModule, Inbox, Service};
|
||||||
|
use toml::Table;
|
||||||
|
|
||||||
/// Application state: module registry + popup bookkeeping.
|
/// Application state: module registry, routing, popup bookkeeping.
|
||||||
pub(crate) struct Bar {
|
pub(crate) struct Bar {
|
||||||
/// Module whose popup is open; None = closed.
|
/// Module whose popup is open; None = closed.
|
||||||
pub(crate) active_popup: Option<String>,
|
pub(crate) active_popup: Option<String>,
|
||||||
@@ -14,22 +15,35 @@ pub(crate) struct Bar {
|
|||||||
pub(crate) popup_id: Option<window::Id>,
|
pub(crate) popup_id: Option<window::Id>,
|
||||||
/// Fan-out routing table: module id -> services it wants.
|
/// Fan-out routing table: module id -> services it wants.
|
||||||
pub(crate) routes: BTreeMap<&'static str, Vec<Service>>,
|
pub(crate) routes: BTreeMap<&'static str, Vec<Service>>,
|
||||||
|
/// One inbound mailbox per running service, keyed by route key. Modules
|
||||||
|
/// send here (`Target::Service`) to talk to a running service.
|
||||||
|
pub(crate) inputs: BTreeMap<&'static str, Inbox>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bar {
|
impl Bar {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new(modules_config_table: &Table) -> Self {
|
||||||
// Registry owns construction: adding a module never edits this file.
|
// Registry owns construction: adding a module never edits this file.
|
||||||
let modules: BTreeMap<&'static str, Box<dyn BarModule>> =
|
let modules: BTreeMap<&'static str, Box<dyn BarModule>> =
|
||||||
modules::all().into_iter().map(|m| (m.id(), m)).collect();
|
modules::all(modules_config_table.clone())
|
||||||
let routes = modules
|
.into_iter()
|
||||||
|
.map(|m| (m.id(), m))
|
||||||
|
.collect();
|
||||||
|
let routes: BTreeMap<&'static str, Vec<Service>> = modules
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(id, module)| (*id, module.services()))
|
.map(|(id, module)| (*id, module.services()))
|
||||||
.collect();
|
.collect();
|
||||||
|
// One inbox per distinct service any module wants.
|
||||||
|
let wanted: BTreeSet<&'static str> = routes.values().flatten().map(|s| s.0).collect();
|
||||||
|
let inputs = wanted
|
||||||
|
.into_iter()
|
||||||
|
.map(|key| (key, Inbox::new(key)))
|
||||||
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
active_popup: None,
|
active_popup: None,
|
||||||
modules,
|
modules,
|
||||||
popup_id: None,
|
popup_id: None,
|
||||||
routes,
|
routes,
|
||||||
|
inputs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,10 @@ fn main() -> Result<(), iced_layershell::Error> {
|
|||||||
None => StartMode::Active,
|
None => StartMode::Active,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let modules = config.modules.clone().unwrap_or_default();
|
||||||
|
|
||||||
daemon(
|
daemon(
|
||||||
app::Bar::new,
|
move || Bar::new(&modules),
|
||||||
subscription::namespace,
|
subscription::namespace,
|
||||||
update::update,
|
update::update,
|
||||||
view::view,
|
view::view,
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
use iced::window;
|
use iced::window;
|
||||||
use iced_layershell::to_layer_message;
|
use iced_layershell::to_layer_message;
|
||||||
|
|
||||||
use common::{ModuleEffect, ModuleMsg, ServiceEvent};
|
use common::{ModuleEffect, Wire};
|
||||||
|
|
||||||
/// Synchronous input: module traffic, subscription payloads, window events.
|
/// Synchronous input: wires plus window events.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) enum Msg {
|
pub(crate) enum Msg {
|
||||||
/// Routed module message; dispatch by `id`, module downcasts.
|
/// One wire; core dispatches by its `to` target.
|
||||||
Module(ModuleMsg),
|
Wire(Wire),
|
||||||
/// Service event; fan out by route key.
|
|
||||||
Subscription(ServiceEvent),
|
|
||||||
/// The compositor confirmed a window closed. The popup is really gone.
|
/// The compositor confirmed a window closed. The popup is really gone.
|
||||||
WindowClosed(window::Id),
|
WindowClosed(window::Id),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ pub fn view(bar: &Bar) -> Element<'_, Message> {
|
|||||||
.map(|module| {
|
.map(|module| {
|
||||||
module
|
module
|
||||||
.view(Some(popup_id))
|
.view(Some(popup_id))
|
||||||
.map(|m| Message::Event(Msg::Module(m)))
|
.map(|w| Message::Event(Msg::Wire(w)))
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| text("unknown module").into());
|
.unwrap_or_else(|| text("unknown module").into());
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use iced::{window, Subscription};
|
use iced::{window, Subscription};
|
||||||
|
|
||||||
|
use common::Service;
|
||||||
use services::IntoSubscription;
|
use services::IntoSubscription;
|
||||||
|
|
||||||
use crate::app::Bar;
|
use crate::app::Bar;
|
||||||
@@ -10,17 +11,15 @@ pub(crate) fn namespace() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Route subscriptions + window-close events (popup really destroyed).
|
/// Route subscriptions + window-close events (popup really destroyed).
|
||||||
/// Each module's wants go through the services registry, so adding a
|
/// One subscription per service inbox, so adding a service never edits this.
|
||||||
/// service never edits this file.
|
|
||||||
pub(crate) fn gather_subscriptions(bar: &Bar) -> Subscription<Message> {
|
pub(crate) fn gather_subscriptions(bar: &Bar) -> Subscription<Message> {
|
||||||
let route_sub = Subscription::batch(
|
let route_sub = Subscription::batch(
|
||||||
bar.routes
|
bar.inputs
|
||||||
.values()
|
.iter()
|
||||||
.flat_map(|wants| wants.iter())
|
.map(|(&key, inbox)| {
|
||||||
.map(|&service| {
|
Service(key)
|
||||||
service
|
.into_subscription(inbox.clone())
|
||||||
.into_subscription()
|
.map(|wire| Message::Event(Msg::Wire(wire)))
|
||||||
.map(|event| Message::Event(Msg::Subscription(event)))
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
|
|||||||
+36
-29
@@ -1,6 +1,6 @@
|
|||||||
use iced::Task;
|
use iced::Task;
|
||||||
|
|
||||||
use common::{ModuleEffect, ModuleMsg};
|
use common::{ModuleEffect, Service, Target, Wire};
|
||||||
|
|
||||||
use crate::app::Bar;
|
use crate::app::Bar;
|
||||||
use crate::msg::{Message, Msg};
|
use crate::msg::{Message, Msg};
|
||||||
@@ -18,34 +18,7 @@ pub(crate) fn update(bar: &mut Bar, msg: Message) -> Task<Message> {
|
|||||||
/// Applies an input event to state; effects come back as `Task`s.
|
/// Applies an input event to state; effects come back as `Task`s.
|
||||||
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::Wire(wire) => route(bar, wire),
|
||||||
// Modules return protocol tasks; wrap effects into messages.
|
|
||||||
Some(module) => module.update(m).map(Message::Effect),
|
|
||||||
None => Task::none(),
|
|
||||||
},
|
|
||||||
|
|
||||||
Msg::Subscription(event) => {
|
|
||||||
// Fan out by route key; the payload is opaque here, the module
|
|
||||||
// downcasts it. New services never touch this file.
|
|
||||||
let key = event.key;
|
|
||||||
let payload = event.payload;
|
|
||||||
bar.modules
|
|
||||||
.iter_mut()
|
|
||||||
.filter(|(id, _)| {
|
|
||||||
bar.routes
|
|
||||||
.get(*id)
|
|
||||||
.is_some_and(|kinds| kinds.contains(&key))
|
|
||||||
})
|
|
||||||
.map(|(_, module)| {
|
|
||||||
module
|
|
||||||
.update(ModuleMsg {
|
|
||||||
id: module.id(),
|
|
||||||
payload: payload.clone(),
|
|
||||||
})
|
|
||||||
.map(Message::Effect)
|
|
||||||
})
|
|
||||||
.fold(Task::none(), |acc, t| acc.chain(t))
|
|
||||||
}
|
|
||||||
|
|
||||||
Msg::WindowClosed(id) => {
|
Msg::WindowClosed(id) => {
|
||||||
// Popup gone; forget it. `popup_id` stays set until this event
|
// Popup gone; forget it. `popup_id` stays set until this event
|
||||||
@@ -59,6 +32,38 @@ fn handle_event(bar: &mut Bar, event: Msg) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delivers a wire by its target — the only place routing lives.
|
||||||
|
fn route(bar: &mut Bar, wire: Wire) -> Task<Message> {
|
||||||
|
match wire.to {
|
||||||
|
Target::Module(id) => match bar.modules.get_mut(id) {
|
||||||
|
Some(module) => module.update(wire).map(Message::Effect),
|
||||||
|
None => Task::none(),
|
||||||
|
},
|
||||||
|
|
||||||
|
Target::Service(key) => {
|
||||||
|
if let Some(inbox) = bar.inputs.get(key) {
|
||||||
|
inbox.send(wire);
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Target::Topic(key) => {
|
||||||
|
// Fan out by route key; the payload is opaque here, each module
|
||||||
|
// downcasts it. New services never touch this file.
|
||||||
|
let topic = Service(key);
|
||||||
|
bar.modules
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(id, _)| {
|
||||||
|
bar.routes
|
||||||
|
.get(*id)
|
||||||
|
.is_some_and(|keys| keys.contains(&topic))
|
||||||
|
})
|
||||||
|
.map(|(_, module)| module.update(wire.clone()).map(Message::Effect))
|
||||||
|
.fold(Task::none(), |acc, t| acc.chain(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs an effect; may set up bar state for it (e.g. which popup is open).
|
/// Runs an effect; may set up bar state for it (e.g. which popup is open).
|
||||||
fn handle_effect(bar: &mut Bar, effect: ModuleEffect) -> Task<Message> {
|
fn handle_effect(bar: &mut Bar, effect: ModuleEffect) -> Task<Message> {
|
||||||
match effect {
|
match effect {
|
||||||
@@ -78,5 +83,7 @@ fn handle_effect(bar: &mut Bar, effect: ModuleEffect) -> Task<Message> {
|
|||||||
// renders until `WindowClosed` confirms it's gone.
|
// renders until `WindowClosed` confirms it's gone.
|
||||||
Task::done(Message::RemoveWindow(id))
|
Task::done(Message::RemoveWindow(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModuleEffect::Send(wire) => route(bar, wire),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ fn bar_view(bar: &Bar) -> Element<'_, Message> {
|
|||||||
let mut children: Vec<Element<Message>> = Vec::new();
|
let mut children: Vec<Element<Message>> = Vec::new();
|
||||||
|
|
||||||
for (i, (_, module)) in bar.modules.iter().enumerate() {
|
for (i, (_, module)) in bar.modules.iter().enumerate() {
|
||||||
children.push(module.view(None).map(|m| Message::Event(Msg::Module(m))));
|
children.push(module.view(None).map(|w| Message::Event(Msg::Wire(w))));
|
||||||
if i < bar.modules.len() - 1 {
|
if i < bar.modules.len() - 1 {
|
||||||
children.push(text("|").size(14).into());
|
children.push(text("|").size(14).into());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ path = "src/lib.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
services = { path = "../services" }
|
|
||||||
iced = { workspace = true }
|
iced = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
|||||||
+43
-24
@@ -1,13 +1,14 @@
|
|||||||
//! Clock module: subscribes to second ticks, renders the time in the bar.
|
//! Clock module: subscribes to the clock service, renders the time in the
|
||||||
//! Left-click toggles the seconds suffix; right-click opens a popup with
|
//! bar. Left-click toggles the seconds suffix — and pokes the running clock
|
||||||
//! the current time in large text.
|
//! service to switch which kind it publishes; right-click opens a popup.
|
||||||
|
|
||||||
use iced::widget::{container, mouse_area, text};
|
use iced::widget::{container, mouse_area, text};
|
||||||
use iced::{Element, Task};
|
use iced::{Element, Task};
|
||||||
|
|
||||||
use common::{BarModule, ModuleEffect, ModuleMsg, Service};
|
use common::{
|
||||||
|
BarModule, ClockKind, ClockMsg, ClockPayload, Endpoint, ModuleEffect, Service, Wire,
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use services::datetime::{ClockKind, ClockPayload};
|
|
||||||
use toml::Table;
|
use toml::Table;
|
||||||
|
|
||||||
/// Big-text popup size (logical px).
|
/// Big-text popup size (logical px).
|
||||||
@@ -22,15 +23,6 @@ pub enum ClockError {
|
|||||||
NotBool(&'static str),
|
NotBool(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A module-local message for the clock.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ClockMsg {
|
|
||||||
/// Toggle the seconds suffix.
|
|
||||||
ToggleSeconds,
|
|
||||||
/// Open the big-time popup.
|
|
||||||
OpenPopup,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Clock {
|
pub struct Clock {
|
||||||
value: String,
|
value: String,
|
||||||
show_seconds: bool,
|
show_seconds: bool,
|
||||||
@@ -44,11 +36,10 @@ struct ClockConfig {
|
|||||||
|
|
||||||
impl Clock {
|
impl Clock {
|
||||||
pub fn new(config: Option<Table>) -> Self {
|
pub fn new(config: Option<Table>) -> Self {
|
||||||
let module_config = config_clock(config);
|
match config_clock(config) {
|
||||||
match module_config {
|
|
||||||
Some(x) => Self {
|
Some(x) => Self {
|
||||||
value: "--:--:--".to_string(),
|
value: "--:--:--".to_string(),
|
||||||
show_seconds: { x.format },
|
show_seconds: x.format,
|
||||||
},
|
},
|
||||||
None => Self::default(),
|
None => Self::default(),
|
||||||
}
|
}
|
||||||
@@ -70,7 +61,10 @@ impl Clock {
|
|||||||
|
|
||||||
impl Default for Clock {
|
impl Default for Clock {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(None)
|
Self {
|
||||||
|
value: "--:--:--".to_string(),
|
||||||
|
show_seconds: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +78,8 @@ impl BarModule for Clock {
|
|||||||
"clock"
|
"clock"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, window_id: Option<iced::window::Id>) -> Element<'_, ModuleMsg> {
|
fn view(&self, window_id: Option<iced::window::Id>) -> Element<'_, Wire> {
|
||||||
|
let me = Endpoint::module(self.id());
|
||||||
match window_id {
|
match window_id {
|
||||||
// Popup surface: current time in large text.
|
// Popup surface: current time in large text.
|
||||||
Some(_) => container(text(&self.value).size(56)).padding(20).into(),
|
Some(_) => container(text(&self.value).size(56)).padding(20).into(),
|
||||||
@@ -92,16 +87,16 @@ impl BarModule for Clock {
|
|||||||
// so the popup can anchor to these bounds.
|
// so the popup can anchor to these bounds.
|
||||||
None => container(
|
None => container(
|
||||||
mouse_area(text(self.shown()).size(16))
|
mouse_area(text(self.shown()).size(16))
|
||||||
.on_press(ModuleMsg::new(self.id(), ClockMsg::ToggleSeconds))
|
.on_press(Wire::module(me, self.id(), ClockMsg::ToggleSeconds))
|
||||||
.on_right_press(ModuleMsg::new(self.id(), ClockMsg::OpenPopup)),
|
.on_right_press(Wire::module(me, self.id(), ClockMsg::OpenPopup)),
|
||||||
)
|
)
|
||||||
.id(iced::widget::Id::from(self.id()))
|
.id(iced::widget::Id::from(self.id()))
|
||||||
.into(),
|
.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, msg: ModuleMsg) -> Task<ModuleEffect> {
|
fn update(&mut self, msg: Wire) -> Task<ModuleEffect> {
|
||||||
// Subscription fan-out delivers a raw `ClockPayload`, not `ClockMsg`.
|
// The service publishes a raw `ClockPayload`; UI sends `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();
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -109,7 +104,17 @@ impl BarModule for Clock {
|
|||||||
match msg.downcast::<ClockMsg>() {
|
match msg.downcast::<ClockMsg>() {
|
||||||
Some(ClockMsg::ToggleSeconds) => {
|
Some(ClockMsg::ToggleSeconds) => {
|
||||||
self.show_seconds = !self.show_seconds;
|
self.show_seconds = !self.show_seconds;
|
||||||
Task::none()
|
let want = if self.show_seconds {
|
||||||
|
ClockKind::Seconds
|
||||||
|
} else {
|
||||||
|
ClockKind::Mins
|
||||||
|
};
|
||||||
|
// Poke the running clock service: tell it which kind to publish.
|
||||||
|
Task::done(ModuleEffect::Send(Wire::service(
|
||||||
|
Endpoint::module(self.id()),
|
||||||
|
ClockKind::Seconds.key().0,
|
||||||
|
want,
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
Some(ClockMsg::OpenPopup) => Task::done(ModuleEffect::RequestPopup(
|
Some(ClockMsg::OpenPopup) => Task::done(ModuleEffect::RequestPopup(
|
||||||
self.id().to_string(),
|
self.id().to_string(),
|
||||||
@@ -127,3 +132,17 @@ impl BarModule for Clock {
|
|||||||
Some((POPUP_W, POPUP_H))
|
Some((POPUP_W, POPUP_H))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Regression: `new(None)` used to call `default()`, which called
|
||||||
|
/// `new(None)` — infinite recursion (hard stack overflow) whenever the
|
||||||
|
/// clock config was absent or failed to parse.
|
||||||
|
#[test]
|
||||||
|
fn no_config_terminates() {
|
||||||
|
assert!(!Clock::new(None).show_seconds);
|
||||||
|
assert!(!Clock::default().show_seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
mod clock;
|
mod clock;
|
||||||
|
|
||||||
pub use clock::{Clock, ClockError, ClockMsg};
|
pub use clock::{Clock, ClockError};
|
||||||
use common::BarModule;
|
use common::BarModule;
|
||||||
use toml::Table;
|
use toml::Table;
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,33 @@
|
|||||||
//! Clock service: emits a `ServiceEvent` whenever a clock kind's displayed
|
//! Clock service: publishes a `Wire` on every change of the displayed
|
||||||
//! value changes (minute or second rollover).
|
//! value, and accepts `ClockKind` wires from modules that want it to switch
|
||||||
|
//! which kind it publishes.
|
||||||
//!
|
//!
|
||||||
//! The ticker knows nothing about the app's `Message` type; it produces
|
//! The ticker knows nothing about the app's `Message` type; it produces
|
||||||
//! protocol events from `common` that core routes to subscribed modules.
|
//! protocol wires from `common` that core routes to subscribers.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use iced::{futures::SinkExt, Subscription};
|
use iced::futures::{channel::mpsc, SinkExt, StreamExt};
|
||||||
|
use iced::Subscription;
|
||||||
|
|
||||||
use common::{Service, ServiceEvent};
|
use common::{ClockKind, ClockPayload, Endpoint, Inbox, Wire};
|
||||||
|
|
||||||
/// Route-key namespace owned by this service; keys are `"clock.<kind>"`.
|
/// Route-key namespace owned by this service; keys are `"clock.<kind>"`.
|
||||||
pub const NAMESPACE: &str = "clock.";
|
pub const NAMESPACE: &str = "clock.";
|
||||||
|
|
||||||
/// What a clock subscription wants: the tick interval and payload selector.
|
/// Drives the clock, publishing only the kind modules last asked for.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
||||||
pub enum ClockKind {
|
|
||||||
Mins,
|
|
||||||
Seconds,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClockKind {
|
|
||||||
/// Route key a module subscribes under to receive this kind's ticks.
|
|
||||||
pub fn key(self) -> Service {
|
|
||||||
Service(match self {
|
|
||||||
Self::Mins => "clock.mins",
|
|
||||||
Self::Seconds => "clock.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
|
|
||||||
/// second rollover). Drive `tick` once per second.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ClockTicker {
|
pub struct ClockTicker {
|
||||||
last: HashMap<ClockKind, String>,
|
last: HashMap<ClockKind, String>,
|
||||||
|
want: ClockKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ClockTicker {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
last: HashMap::new(),
|
||||||
|
want: ClockKind::Seconds,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClockTicker {
|
impl ClockTicker {
|
||||||
@@ -50,52 +35,63 @@ impl ClockTicker {
|
|||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscription that emits a `ServiceEvent` on every change.
|
/// Subscription that reads inbound wires from `inbox` and publishes
|
||||||
pub fn run() -> Subscription<ServiceEvent> {
|
/// changes to every module subscribed to this service's route key.
|
||||||
Subscription::run_with((), |_| {
|
pub fn run(inbox: Inbox) -> Subscription<Wire> {
|
||||||
|
Subscription::run_with(inbox, |inbox| {
|
||||||
|
let key = inbox.key();
|
||||||
|
// ponytail: the builder runs once (identity is stable, see `Inbox`
|
||||||
|
// hashing). If a module ever makes `services()` dynamic and a key
|
||||||
|
// is dropped then recreated, `take()` returns None and this panics
|
||||||
|
// — make `Bar` rebuild the inbox per start instead.
|
||||||
|
let mut rx = inbox.take().expect("inbox receiver is taken once");
|
||||||
iced::stream::channel(
|
iced::stream::channel(
|
||||||
0,
|
0,
|
||||||
|mut sender: iced::futures::channel::mpsc::Sender<ServiceEvent>| async move {
|
move |mut sender: mpsc::Sender<Wire>| 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;
|
tokio::select! {
|
||||||
for payload in clock.tick() {
|
_ = interval.tick() => {
|
||||||
let event = ServiceEvent {
|
if let Some(payload) = clock.tick() {
|
||||||
key: payload.kind.key(),
|
let wire = Wire::topic(Endpoint::service(key), key, payload);
|
||||||
payload: Arc::new(payload),
|
if sender.send(wire).await.is_err() {
|
||||||
};
|
|
||||||
if sender.send(event).await.is_err() {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
msg = rx.next() => match msg {
|
||||||
|
Some(wire) => {
|
||||||
|
// A module poking us: switch published kind.
|
||||||
|
if let Some(kind) = wire.downcast::<ClockKind>() {
|
||||||
|
clock.want = *kind;
|
||||||
|
clock.last.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a payload per kind whose value changed since the last tick.
|
/// Latest value for the wanted kind, or `None` if unchanged.
|
||||||
/// The second that rolls into a new minute yields `[Seconds, Mins]`.
|
pub fn tick(&mut self) -> Option<ClockPayload> {
|
||||||
pub fn tick(&mut self) -> Vec<ClockPayload> {
|
let value = chrono::Local::now()
|
||||||
let now = chrono::Local::now();
|
.format(match self.want {
|
||||||
[ClockKind::Seconds, ClockKind::Mins]
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|kind| {
|
|
||||||
let value = now
|
|
||||||
.format(match kind {
|
|
||||||
ClockKind::Mins => "%H:%M",
|
ClockKind::Mins => "%H:%M",
|
||||||
ClockKind::Seconds => "%H:%M:%S",
|
ClockKind::Seconds => "%H:%M:%S",
|
||||||
})
|
})
|
||||||
.to_string();
|
.to_string();
|
||||||
match self.last.get(&kind) {
|
if self.last.get(&self.want) == Some(&value) {
|
||||||
Some(prev) if *prev == value => None,
|
return None;
|
||||||
_ => {
|
|
||||||
self.last.insert(kind, value.clone());
|
|
||||||
Some(ClockPayload { kind, value })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
self.last.insert(self.want, value.clone());
|
||||||
|
Some(ClockPayload {
|
||||||
|
kind: self.want,
|
||||||
|
value,
|
||||||
})
|
})
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,23 +4,23 @@
|
|||||||
|
|
||||||
use iced::Subscription;
|
use iced::Subscription;
|
||||||
|
|
||||||
use common::{Service, ServiceEvent};
|
use common::{Inbox, Service, Wire};
|
||||||
|
|
||||||
pub mod datetime;
|
pub mod datetime;
|
||||||
|
|
||||||
/// Conversion from a route key to the subscription backing it.
|
/// Conversion from a route key + its inbox to the subscription backing it.
|
||||||
///
|
///
|
||||||
/// An extension trait (not `impl From<Service> for Subscription<...>`) because
|
/// An extension trait (not `impl From<Service> for Subscription<...>`) because
|
||||||
/// both `Service` and `Subscription` are foreign to this crate — the orphan
|
/// both `Service` and `Subscription` are foreign to this crate — the orphan
|
||||||
/// rule blocks a foreign trait (`From`) on a foreign type (`Subscription`).
|
/// rule blocks a foreign trait (`From`) on a foreign type (`Subscription`).
|
||||||
pub trait IntoSubscription {
|
pub trait IntoSubscription {
|
||||||
fn into_subscription(self) -> Subscription<ServiceEvent>;
|
fn into_subscription(self, inbox: Inbox) -> Subscription<Wire>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoSubscription for Service {
|
impl IntoSubscription for Service {
|
||||||
fn into_subscription(self) -> Subscription<ServiceEvent> {
|
fn into_subscription(self, inbox: Inbox) -> Subscription<Wire> {
|
||||||
match self.0 {
|
match self.0 {
|
||||||
key if key.starts_with(datetime::NAMESPACE) => datetime::ClockTicker::run(),
|
key if key.starts_with(datetime::NAMESPACE) => datetime::ClockTicker::run(inbox),
|
||||||
_ => Subscription::none(),
|
_ => Subscription::none(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user