From 21a2e5299cdfd38771a40f02ff5d73016359a2b4 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Mon, 20 Apr 2026 05:16:16 -0700 Subject: [PATCH] fix failed full weather fetch and implement cache for full --- src/app.rs | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4f80a50..2618b10 100644 --- a/src/app.rs +++ b/src/app.rs @@ -51,6 +51,10 @@ pub struct AppModel { last_updated: String, /// Full weather report for popup full_weather: String, + /// Last error from a full-weather fetch, if any. Kept separate from + /// `full_weather` so that a failed fetch does not poison the cache + /// and block the next popup-open from retrying. + full_weather_error: Option, /// Loading state is_loading: bool, /// Popup window ID @@ -102,6 +106,7 @@ impl cosmic::Application for AppModel { location: String::new(), last_updated: String::new(), full_weather: String::new(), + full_weather_error: None, is_loading: true, popup: None, rectangle: Rectangle::default(), @@ -175,11 +180,24 @@ impl cosmic::Application for AppModel { } fn view_window(&self, _id: window::Id) -> Element<'_, Self::Message> { + // Pick what to render: the cached forecast, a transient error + // (not stored in `full_weather` so the next open retries), or a + // loading placeholder while the in-flight request completes. + let display_text: String = if !self.full_weather.is_empty() { + self.full_weather.clone() + } else if let Some(err) = &self.full_weather_error { + format!( + "Failed to load weather:\n{err}\n\nClose and reopen to retry." + ) + } else { + "Loading…".to_string() + }; + // Calculate the width needed for the monospace content. // The default monospace font is ~8.4px per character at 14px font size, // plus padding for container (12*2) and popup chrome (~32). #[allow(clippy::cast_precision_loss)] - let max_line_len = self.full_weather.lines() + let max_line_len = display_text.lines() .map(|line| line.chars().count()) .max() .unwrap_or(40) as f32; @@ -188,7 +206,7 @@ impl cosmic::Application for AppModel { let popup_width = content_width.max(400.0); let content = container( - widget::text(&self.full_weather) + widget::text(display_text) .font(Font::MONOSPACE) .wrapping(cosmic::iced::widget::text::Wrapping::None) .width(cosmic::iced::Length::Fixed(content_width)) @@ -351,16 +369,25 @@ impl cosmic::Application for AppModel { match result { Ok(weather) => { self.full_weather = weather; + self.full_weather_error = None; } Err(e) => { eprintln!("Full weather fetch error: {e}"); - self.full_weather = "Failed to load weather".to_string(); + // Leave `full_weather` empty so that the next + // popup open triggers a refetch (see toggle_popup). + self.full_weather_error = Some(e); } } Task::none() } Message::RefreshWeather => { self.retry_count = 0; + // Invalidate the popup cache on each scheduled refresh so + // that the next popup open fetches a fresh forecast. This + // piggybacks the popup's freshness on the panel's 15-minute + // refresh cadence (WEATHER_UPDATE_INTERVAL_MINUTES). + self.full_weather.clear(); + self.full_weather_error = None; let units = self.units; Task::perform(fetch_weather(units), |result| { cosmic::Action::App(Message::WeatherUpdate(result)) @@ -371,6 +398,7 @@ impl cosmic::Application for AppModel { self.retry_count = 0; // Clear cached full weather so the popup re-fetches on next open self.full_weather.clear(); + self.full_weather_error = None; let units = self.units; Task::perform(fetch_weather(units), |result| { cosmic::Action::App(Message::WeatherUpdate(result))