diff --git a/Cargo.toml b/Cargo.toml index 4ca3b0d..371ac91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ chrono = "0.4.42" dirs = "6.0.0" i-slint-backend-winit = { version = "1.14.1", features = ["x11"] } serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" slint = { version = "1.14.1", features = ["backend-winit"] } toml = "0.9.8" diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index c0f0dd2..03d8f9e 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -4,6 +4,7 @@ mod common; mod datewidget; mod timewidget; mod volumewidget; +mod weatherwidget; mod wifiwidget; use crate::TopBar; @@ -15,7 +16,8 @@ pub fn install_callbacks(ui: &TopBar) { volumewidget::install(ui); brightnesswidget::install(ui); wifiwidget::install(ui); + weatherwidget::install(ui); // In the future: - // weatherwidget::install(ui); + // notifwidget::install(ui); } diff --git a/src/widgets/weatherwidget.rs b/src/widgets/weatherwidget.rs new file mode 100644 index 0000000..33abb38 --- /dev/null +++ b/src/widgets/weatherwidget.rs @@ -0,0 +1,120 @@ +use super::common::run_cmd; +use crate::TopBar; +use serde::Deserialize; +use slint::{ComponentHandle, SharedString, Timer, TimerMode}; +use std::{fs, path::PathBuf, time::Duration}; + +/// The subset of the JSON we actually need. +#[derive(Deserialize)] +struct WeatherJson { + weather: Vec, + main: MainEntry, +} + +#[derive(Deserialize)] +struct WeatherEntry { + icon: String, // e.g. "09n" + main: String, // e.g. "Mist" +} + +#[derive(Deserialize)] +struct MainEntry { + temp: f32, // e.g. 21.09 +} + +/// Expand `~` to the user home dir +fn expand_home(path: &str) -> PathBuf { + if let Some(home) = dirs::home_dir() + && path.starts_with("~/") + { + return home.join(path.trim_start_matches("~/")); + } + PathBuf::from(path) +} + +/// Parse the JSON file; return (temp, cond, icon code) +fn read_weather_file() -> Option<(f32, String, String)> { + let path = expand_home("~/.cache/candydesktop/owm_widget.json"); + let data = fs::read_to_string(path).ok()?; + let parsed: WeatherJson = serde_json::from_str(&data).ok()?; + + let temp = parsed.main.temp; + let cond = parsed.weather.first()?.main.clone(); // e.g. "Mist" + let icon_code = parsed.weather.first()?.icon.clone(); // e.g. "09n" + + Some((temp, cond, icon_code)) +} + +/// Pick glyph based on OWM icon code +fn icon_from_code(code: &str) -> &'static str { + match code { + // Thunderstorm + "11d" | "11n" => "󰙾", + + // Drizzle + "09d" | "09n" => "󰖗", + + // Rain + "10d" => "󰖖", + "10n" => "󰼳", + + // Snow + "13d" | "13n" => "󰙿", + + // Atmosphere (mist, fog, haze, dust) + "50d" | "50n" => "󰖑", + + // Clouds + "02d" | "03d" | "04d" => "󰖐", + "02n" | "03n" | "04n" => "󰼱", + + // Clear sky + "01d" => "󰖙", + "01n" => "󰖔", + + // Fallback (unknown) + _ => "󰖩", + } +} + +/// Format tooltip: "21.1 °C, Mist" +fn tooltip(temp: f32, cond: &str) -> SharedString { + SharedString::from(format!("{temp:.1} °C, {cond}")) +} + +/// Timer to refresh weather every 100 seconds +fn start_weather_updater(ui: &TopBar) { + let weak = ui.as_weak(); + + // One-shot update on startup + if let Some(ui) = weak.upgrade() + && let Some((t, c, icon_code)) = read_weather_file() + { + ui.set_weather_icon(icon_from_code(&icon_code).into()); + ui.set_weather_tooltip(tooltip(t, &c).into()); + } + + // Refresh every 100 sec + let timer = Box::leak(Box::new(Timer::default())); + timer.start(TimerMode::Repeated, Duration::from_secs(100), move || { + if let Some(ui) = weak.upgrade() + && let Some((t, c, icon_code)) = read_weather_file() + { + ui.set_weather_icon(icon_from_code(&icon_code).into()); + ui.set_weather_tooltip(tooltip(t, &c)); + } + }); +} + +/// Public entry: connect callback + start updater +pub fn install(ui: &TopBar) { + let weak = ui.as_weak(); + + ui.on_show_weather(move || { + if weak.upgrade().is_some() { + run_cmd("xclock"); // Fix later + } + }); + + start_weather_updater(ui); +} diff --git a/ui/topbar.slint b/ui/topbar.slint index 7c2b325..908e7b3 100644 --- a/ui/topbar.slint +++ b/ui/topbar.slint @@ -54,6 +54,15 @@ export component TopBar inherits Window { in property wifi_bg_clicked: #4a5f70;*/ callback show_wifi(); + // Weather widget + in property weather_tooltip; + in property weather_icon; + in property weather_icon_color: #ffffff; + in property weather_bg_normal: #8e9162; + in property weather_bg_hover: #6d8a4d; + in property weather_bg_clicked: #4a5f70; + callback show_weather(); + title: "chocobar"; width: bar_width *1px; height: bar_height *1px; @@ -98,6 +107,18 @@ export component TopBar inherits Window { HorizontalLayout { alignment: end; // Right-align + // SquareIconWidget - Weather + SquareIconWidget { + bar_height: root.bar_height; + icon_text: root.weather_icon; + tooltip_text: root.weather_tooltip; + icon_color: root.weather_icon_color; + bg_normal: root.weather_bg_normal; + bg_hover: root.weather_bg_hover; + bg_clicked: root.weather_bg_clicked; + square_btn_callback => root.show_weather(); + } + // SquareIconWidget - Wifi SquareIconWidget { bar_height: root.bar_height;