diff --git a/Cargo.lock b/Cargo.lock index fe03afa..d0eb799 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + [[package]] name = "android-activity" version = "0.6.1" @@ -514,6 +523,8 @@ dependencies = [ "services", "tokio", "toml", + "tracing", + "tracing-subscriber", ] [[package]] @@ -1913,6 +1924,12 @@ dependencies = [ "wayland-protocols-wlr", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.189" @@ -2046,6 +2063,15 @@ dependencies = [ "quote", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.8.3" @@ -2097,11 +2123,13 @@ dependencies = [ name = "modules" version = "0.1.0" dependencies = [ + "chrono", "common", "iced", "serde", "thiserror 2.0.20", "toml", + "tracing", ] [[package]] @@ -2187,6 +2215,15 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2882,6 +2919,23 @@ dependencies = [ "bitflags 2.13.1", ] +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -3217,6 +3271,16 @@ dependencies = [ "serde", "serde_json", "tokio", + "tracing", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", ] [[package]] @@ -3595,6 +3659,15 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3821,6 +3894,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -3904,6 +4007,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index e02b0ad..3c5ff4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ edition = "2021" iced = { version = "0.14", default-features = false, features = ["wgpu", "wayland", "crisp", "web-colors", "thread-pool", "advanced", "tokio"] } iced_layershell = { version = "0.19", default-features = false } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } chrono = "0.4" toml = "1.1" serde = { version = "1", features = ["derive"] } diff --git a/crates/common/src/messages/services/weather.rs b/crates/common/src/messages/services/weather.rs index 19f84de..787aae7 100644 --- a/crates/common/src/messages/services/weather.rs +++ b/crates/common/src/messages/services/weather.rs @@ -47,7 +47,7 @@ pub struct HourlyUnits { pub relative_humidity_2m: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct Hourly { #[serde(deserialize_with = "naive_dt::deserialize")] pub time: Vec, diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index ea15583..7bd7796 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -11,6 +11,8 @@ common = { path = "../common" } iced = { workspace = true } iced_layershell = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } chrono = { workspace = true } clap = {workspace = true} toml = {workspace =true} diff --git a/crates/core/src/app.rs b/crates/core/src/app.rs index 7c9eb2a..cc2b84e 100644 --- a/crates/core/src/app.rs +++ b/crates/core/src/app.rs @@ -39,10 +39,16 @@ impl Bar { .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 + tracing::debug!(?routes, "module routes: wanted msg types per module"); + let inputs: BTreeMap<&'static str, Inbox> = wanted .into_iter() .map(|key| (key, Inbox::new(key))) .collect(); + tracing::debug!( + modules = modules.len(), + services = ?inputs.keys().copied().collect::>(), + "spawning wanted services" + ); Self { active_popup: None, modules, diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs index 2b0acc1..a237a1c 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -27,27 +27,43 @@ pub(crate) use msg::{Message, Msg}; /// Height of the bar in logical pixels. const BAR_HEIGHT: u32 = 36; -fn main() -> Result<(), iced_layershell::Error> { - let args = args::Args::parse(); +/// Default log filter per build profile. Debug builds log barbar's own crates +/// at `debug` while dependency noise (iced_layershell/sctk/cosmic_text/wgpu…) +/// stays at `warn`; release builds log `warn` and above. `RUST_LOG` overrides. +const DEFAULT_FILTER: &str = if cfg!(debug_assertions) { + "warn,core=debug,common=debug,modules=debug,services=debug" +} else { + "warn" +}; - let config_handle = File::open(args.config); +fn main() -> Result<(), iced_layershell::Error> { + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(DEFAULT_FILTER)); + tracing_subscriber::fmt().with_env_filter(filter).init(); + + let args = args::Args::parse(); + tracing::debug!(config = %args.config.display(), "parsed args"); + + let config_handle = File::open(&args.config); let config: BarbarConfig; if let Ok(mut config_file) = config_handle { let mut string = String::new(); config_file.read_to_string(&mut string).unwrap(); config = toml::from_str(&string).unwrap(); + tracing::info!(config = %args.config.display(), "config loaded"); } else { - println!("cant open config file"); + tracing::warn!(config = %args.config.display(), "cannot open config file"); return Ok(()); } - let start_mode = match config.monitor { - Some(x) => StartMode::TargetScreen(x), + let start_mode = match &config.monitor { + Some(x) => StartMode::TargetScreen(x.clone()), None => StartMode::Active, }; let modules = config.modules.clone().unwrap_or_default(); let order = config.order; + tracing::info!(monitor = ?config.monitor, "starting barbar daemon"); daemon( move || { diff --git a/crates/core/src/popup.rs b/crates/core/src/popup.rs index ba7c9ad..8e50811 100644 --- a/crates/core/src/popup.rs +++ b/crates/core/src/popup.rs @@ -48,6 +48,7 @@ pub fn open_popup(bar: &mut Bar, module_id: String, bounds: Rectangle) -> Task Task Task { if let Some(id) = bar.popup_id { + tracing::debug!(?id, "requesting popup close"); return Task::done(Message::Effect(ModuleEffect::ClosePopup(id))); } Task::none() diff --git a/crates/core/src/update.rs b/crates/core/src/update.rs index 2ee9c62..2f635b8 100644 --- a/crates/core/src/update.rs +++ b/crates/core/src/update.rs @@ -26,6 +26,7 @@ fn handle_event(bar: &mut Bar, event: Msg) -> Task { if bar.popup_id == Some(id) { bar.popup_id = None; bar.active_popup = None; + tracing::debug!(?id, "popup window closed"); } Task::none() } @@ -37,12 +38,19 @@ fn route(bar: &mut Bar, wire: Wire) -> Task { match wire.to { Target::Module(id) => match bar.modules.get_mut(id) { Some(module) => module.update(wire).map(Message::Effect), - None => Task::none(), + None => { + tracing::warn!(module = id, "wire routed to unknown module"); + Task::none() + } }, Target::Service(key) => { if let Some(inbox) = bar.inputs.get(key) { - inbox.send(wire); + if !inbox.send(wire) { + tracing::warn!(service = key, "service inbox closed"); + } + } else { + tracing::warn!(service = key, "wire routed to unknown service"); } Task::none() } @@ -51,7 +59,8 @@ fn route(bar: &mut Bar, wire: Wire) -> Task { // 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 + let tasks: Vec<_> = bar + .modules .iter_mut() .filter(|(id, _)| { bar.routes @@ -59,7 +68,9 @@ fn route(bar: &mut Bar, wire: Wire) -> Task { .is_some_and(|keys| keys.contains(&topic)) }) .map(|(_, module)| module.update(wire.clone()).map(Message::Effect)) - .fold(Task::none(), |acc, t| acc.chain(t)) + .collect(); + tracing::debug!(topic = key, subscribers = tasks.len(), "fan-out"); + tasks.into_iter().fold(Task::none(), |acc, t| acc.chain(t)) } } } @@ -70,8 +81,10 @@ fn handle_effect(bar: &mut Bar, effect: ModuleEffect) -> Task { 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()) { + tracing::debug!(module = %module_id, "closing popup"); popup::close_popup(bar) } else { + tracing::debug!(module = %module_id, "opening popup"); popup::capture_bounds(module_id, element_id) } } diff --git a/crates/core/src/view.rs b/crates/core/src/view.rs index 9e6d3e4..9aa1ce7 100644 --- a/crates/core/src/view.rs +++ b/crates/core/src/view.rs @@ -1,5 +1,5 @@ use common::Wire; -use iced::widget::{container, row, text}; +use iced::widget::{container, row, space, text}; use iced::{window, Alignment, Element, Length, Theme}; use crate::app::Bar; @@ -33,21 +33,49 @@ fn bar_view(bar: &Bar) -> Element<'_, Message> { .view(None) .map(|w| Message::Event(Msg::Wire(w))) }) - .collect::>>()); + .collect::>>()) + .spacing(8); + let middle = row(bar .order .1 .iter() - .map(|x| bar.modules.get(x.to_string()).unwrap().view(None))); + .map(|x| { + bar.modules + .get(x.to_string()) + .unwrap() + .view(None) + .map(|w| Message::Event(Msg::Wire(w))) + }) + .collect::>>()) + .spacing(8); let right = row(bar .order .2 .iter() - .map(|x| bar.modules.get(x.to_string()).unwrap().view(None))); + .map(|x| { + bar.modules + .get(x.to_string()) + .unwrap() + .view(None) + .map(|w| Message::Event(Msg::Wire(w))) + }) + .collect::>>()) + .spacing(8); - container(left.width(Length::Fill).align_y(Alignment::Center)) + let row = row![ + left, + space::horizontal(), + middle, + space::horizontal(), + right, + ] + .width(Length::Fill); + + container(row) .padding(8) - .align_x(Alignment::Center) + .width(Length::Fill) + .align_y(Alignment::Center) .into() } diff --git a/crates/modules/Cargo.toml b/crates/modules/Cargo.toml index b4021d7..200c750 100644 --- a/crates/modules/Cargo.toml +++ b/crates/modules/Cargo.toml @@ -11,5 +11,7 @@ path = "src/lib.rs" common = { path = "../common" } iced = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } +chrono = { workspace = true } toml = { workspace = true } serde = {workspace = true} diff --git a/crates/modules/src/clock.rs b/crates/modules/src/clock.rs index 4f24823..bd7d329 100644 --- a/crates/modules/src/clock.rs +++ b/crates/modules/src/clock.rs @@ -29,7 +29,7 @@ pub struct Clock { #[derive(Deserialize, Default)] struct ClockConfig { #[serde(default)] - format: bool, + show_seconds: bool, } impl Clock { @@ -37,7 +37,7 @@ impl Clock { match config_clock(config) { Some(x) => Self { value: "--:--:--".to_string(), - show_seconds: x.format, + show_seconds: x.show_seconds, }, None => Self::default(), } @@ -102,6 +102,7 @@ impl BarModule for Clock { match msg.downcast::() { Some(ClockMsg::ToggleSeconds) => { self.show_seconds = !self.show_seconds; + tracing::debug!(show_seconds = self.show_seconds, "clock toggle"); let want = if self.show_seconds { ClockKind::Seconds } else { diff --git a/crates/modules/src/weather.rs b/crates/modules/src/weather.rs index 05a0dac..b88c289 100644 --- a/crates/modules/src/weather.rs +++ b/crates/modules/src/weather.rs @@ -1,6 +1,10 @@ use std::error::Error; -use common::{BarModule, Endpoint, ModuleEffect, Service, WeatherKind, WeatherMsg, Wire}; +use chrono::Timelike; +use common::{ + BarModule, ClockKind, ClockPayload, Endpoint, Hourly, ModuleEffect, Service, WeatherKind, + WeatherMsg, WeatherPayload, WeatherResponse, Wire, +}; use iced::{ widget::{container, mouse_area, text}, Element, Task, @@ -8,11 +12,37 @@ use iced::{ pub struct WeatherModule { text: String, + /// Cached hourly forecast from the last weather update. + hourly: Option, + /// Current hour, mirrored from the datetime service's clock ticks. + hour: Option, +} + +impl WeatherModule { + /// Shows the forecast temperature for the tracked current hour. + fn refresh(&mut self) { + let (Some(hourly), Some(hour)) = (&self.hourly, self.hour) else { + return; + }; + if let Some(i) = hourly.time.iter().position(|t| t.hour() == hour) { + if let Some(&temp) = hourly.temperature_2m.get(i) { + let value = format!("{}°", temp as i32); + if self.text != value { + self.text = value; + tracing::info!(hour, temp, "weather: current hour"); + } + } + } + } } impl Default for WeatherModule { fn default() -> Self { - Self { text: "hi".into() } + Self { + text: "waiting ".into(), + hourly: None, + hour: None, + } } } @@ -32,18 +62,36 @@ impl BarModule for WeatherModule { } fn update(&mut self, msg: Wire) -> Task { + // Weather state: cache the hourly forecast, then show the current hour. + if let Some(env) = msg.downcast::() { + match env.kind { + WeatherMsg::State => match env.payload.downcast_ref::() { + Some(resp) => { + self.hourly = Some(resp.hourly.clone()); + self.refresh(); + } + None => tracing::warn!("weather payload was not a WeatherResponse"), + }, + } + } + // Clock tick from the datetime service: track the current hour. + if let Some(payload) = msg.downcast::() { + self.hour = payload.value.split(':').next().and_then(|h| h.parse().ok()); + self.refresh(); + } match msg.downcast::() { Some(WeatherKind::Poke) => { + tracing::debug!("weather poked"); self.text = "poked".into(); - Task::none() } - None => Task::none(), + None => (), } + Task::none() } /// Route keys this module subscribes to. fn services(&self) -> Vec { - vec![WeatherMsg::State.key()] + vec![WeatherMsg::State.key(), ClockKind::Seconds.key()] } /// Optional: read `module.weather` from the config table. diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml index 9f96271..a20f044 100644 --- a/crates/services/Cargo.toml +++ b/crates/services/Cargo.toml @@ -11,6 +11,7 @@ path = "src/lib.rs" common = { path = "../common" } iced = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } chrono = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/services/src/datetime.rs b/crates/services/src/datetime.rs index 9191858..b9ce727 100644 --- a/crates/services/src/datetime.rs +++ b/crates/services/src/datetime.rs @@ -39,12 +39,15 @@ impl ClockTicker { iced::stream::channel(0, move |mut sender: mpsc::Sender| async move { let mut clock = ClockTicker::new(); let mut interval = tokio::time::interval(std::time::Duration::from_secs(1)); + tracing::info!(key, "clock service started"); loop { tokio::select! { _ = interval.tick() => { if let Some(payload) = clock.tick() { + tracing::debug!(key, kind = ?payload.kind, "publishing clock"); let wire = Wire::topic(Endpoint::service(key), key, payload); if sender.send(wire).await.is_err() { + tracing::warn!(key, "clock send failed; stopping service"); return; } } diff --git a/crates/services/src/lib.rs b/crates/services/src/lib.rs index aac0e36..1682e8b 100644 --- a/crates/services/src/lib.rs +++ b/crates/services/src/lib.rs @@ -20,10 +20,14 @@ pub trait IntoSubscription { impl IntoSubscription for Service { fn into_subscription(self, inbox: Inbox) -> Subscription { - match self.0 { - key if key.starts_with(datetime::NAMESPACE) => datetime::ClockTicker::run(inbox), - key if key.starts_with(weather::NAMESPACE) => weather::WeatherService::run(inbox), - _ => Subscription::none(), + let key = self.0; + match key { + k if k.starts_with(datetime::NAMESPACE) => datetime::ClockTicker::run(inbox), + k if k.starts_with(weather::NAMESPACE) => weather::WeatherService::run(inbox), + _ => { + tracing::warn!(service = key, "no service impl for route key"); + Subscription::none() + } } } } diff --git a/crates/services/src/weather.rs b/crates/services/src/weather.rs index 2deadaf..333e789 100644 --- a/crates/services/src/weather.rs +++ b/crates/services/src/weather.rs @@ -1,7 +1,10 @@ -use std::time::Duration; +use std::{sync::Arc, time::Duration}; -use common::{Inbox, WeatherResponse, Wire}; -use iced::{futures::channel::mpsc, Subscription}; +use common::{Endpoint, Inbox, WeatherMsg, WeatherPayload, WeatherResponse, Wire}; +use iced::{ + futures::{channel::mpsc, SinkExt}, + Subscription, +}; use tokio::time::interval; pub const NAMESPACE: &str = "weather."; @@ -12,28 +15,37 @@ const FORECAST_URL: &str = "https://api.open-meteo.com/v1/forecast\ pub struct WeatherService { client: reqwest::Client, - latest: Option, } impl WeatherService { fn new() -> Self { Self { client: reqwest::Client::default(), - latest: None, } } pub fn run(inbox: Inbox) -> Subscription { Subscription::run_with(inbox, |inbox| { - let _key = inbox.key(); + let key = inbox.key(); let mut _rx = inbox.take().expect("cant take"); - iced::stream::channel(0, move |mut _sender: mpsc::Sender| async move { + iced::stream::channel(0, move |mut sender: mpsc::Sender| async move { let mut weather = WeatherService::new(); let mut interval = interval(Duration::from_mins(30)); + tracing::info!(key = key, "weather service started"); loop { tokio::select! { _ = interval.tick() => { - weather.latest = weather.get_weather().await; + if let Some(resp) = weather.get_weather().await { + let env = WeatherPayload { + kind: WeatherMsg::State, + payload: Arc::new(resp), + }; + let wire = Wire::topic(Endpoint::service(key), key, env); + if sender.send(wire).await.is_err() { + tracing::warn!(key, "weather send failed; stopping service"); + return; + } + } } } } @@ -41,13 +53,22 @@ impl WeatherService { }) } async fn get_weather(&self) -> Option { - self.client - .get(FORECAST_URL) - .send() - .await - .ok()? - .json::() - .await - .ok() + tracing::debug!(url = FORECAST_URL, "fetching weather"); + match self.client.get(FORECAST_URL).send().await { + Ok(resp) => match resp.json::().await { + Ok(parsed) => { + tracing::debug!("weather fetched"); + Some(parsed) + } + Err(e) => { + tracing::warn!(error = %e, "weather response decode failed"); + None + } + }, + Err(e) => { + tracing::warn!(error = %e, "weather request failed"); + None + } + } } } diff --git a/test.toml b/test.toml index e867ae8..40eb8fb 100644 --- a/test.toml +++ b/test.toml @@ -1,8 +1,9 @@ monitor = "DP-2" + [order] -left = ["weather", "clock"] +left = ["weather"] middle = [] right = ["clock"] [modules.clock] -format = true +show_seconds = false