Add forecast support
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Isaac Mills 2025-07-29 14:29:03 -06:00
parent f719983c62
commit 2c2246ae00
Signed by: fnmain
GPG key ID: B67D7410F33A0F61
4 changed files with 90 additions and 41 deletions

View file

@ -37,13 +37,17 @@ wasm-bindgen-test = "0.3.34"
[profile.release] [profile.release]
# Tell `rustc` to optimize for small code size. # Tell `rustc` to optimize for small code size.
opt-level = "s" opt-level = "s"
panic = "abort"
[profile.dev]
panic = "abort"
[dependencies] [dependencies]
dotenvy_macro = "0.15.7" dotenvy_macro = "0.15.7"
futures-util = "0.3.31" futures-util = "0.3.31"
getrandom = { version = "0.3.3", features = ["wasm_js"] } getrandom = { version = "0.3.3", features = ["wasm_js"] }
heapless = "0.8.0" heapless = "0.8.0"
openweathermap_lib = { git = "https://github.com/StratusFearMe21/rusty-openweathermap-library", branch = "wasm_deps" } openweathermap_lib = { git = "https://github.com/StratusFearMe21/rusty-openweathermap-library", branch = "forecast" }
phf = { version = "0.12.1", features = ["macros"] } phf = { version = "0.12.1", features = ["macros"] }
rand = { version = "0.9.2", default-features = false, features = ["os_rng", "std"] } rand = { version = "0.9.2", default-features = false, features = ["os_rng", "std"] }
tachyonfx = { version = "0.16.0", default-features = false, features = ["web-time"] } tachyonfx = { version = "0.16.0", default-features = false, features = ["web-time"] }

View file

@ -6,6 +6,7 @@ use std::{
}; };
use openweathermap_lib::{ use openweathermap_lib::{
forecast::{ForecastClient, ForecastResponse},
location::Location, location::Location,
weather::{WeatherClient, WeatherResponse}, weather::{WeatherClient, WeatherResponse},
}; };
@ -17,7 +18,7 @@ use dotenvy_macro::dotenv;
use ratatui::{ use ratatui::{
layout::{Constraint, Direction, Layout, Margin, Rect}, layout::{Constraint, Direction, Layout, Margin, Rect},
style::{Color, Style, Stylize}, style::{Color, Style, Stylize},
widgets::{Block, Borders, Paragraph, Tabs}, widgets::{Block, Borders, List, ListState, Paragraph, Tabs},
Frame, Frame,
}; };
use tachyonfx::{fx, Effect, Interpolation, Shader}; use tachyonfx::{fx, Effect, Interpolation, Shader};
@ -25,7 +26,7 @@ use web_time::Instant;
use crate::app::{ use crate::app::{
password_locker::PasswordLocker, password_locker::PasswordLocker,
weather_icon::{WeatherIcon, WEATHER_ICONS}, weather_icon::{WeatherIcon, WEATHER_ICONS, WEATHER_ICONS_SMALL},
}; };
mod password_locker; mod password_locker;
@ -86,7 +87,8 @@ pub struct App<E: AppExecutor> {
selected_tab: SelectedTab, selected_tab: SelectedTab,
password_locker: PasswordLocker, password_locker: PasswordLocker,
current_effect: Effect, current_effect: Effect,
weather: Rc<OnceCell<WeatherResponse>>, forecast_state: ListState,
weather: Rc<OnceCell<(WeatherResponse, ForecastResponse)>>,
executor: E, executor: E,
} }
@ -135,18 +137,24 @@ impl<E: AppExecutor> App<E> {
let weather = Rc::new(OnceCell::new()); let weather = Rc::new(OnceCell::new());
executor.execute( executor.execute(
async move { async move {
let weather_client = WeatherClient::new( let location = Location {
Location { zip: String::from("84111"),
zip: String::from("84111"), name: String::from("Salt Lake City"),
name: String::from("Salt Lake City"), lat: 40.7660851712019,
lat: 40.7660851712019, lon: -111.89066476757807,
lon: -111.89066476757807, country: String::from("US"),
country: String::from("US"), };
}, let units = "imperial".to_owned();
"imperial".to_owned(), let api_key = dotenv!("OPENWEATHERMAP_API_KEY").to_owned();
dotenv!("OPENWEATHERMAP_API_KEY").to_owned(), let weather_client =
); WeatherClient::new(location.clone(), units.clone(), api_key.clone());
weather_client.get_current_weather().await.unwrap() let forecast_client = ForecastClient::new(location, units, api_key, None);
futures_util::future::try_join(
weather_client.get_current_weather(),
forecast_client.get_current_forecast(),
)
.await
.unwrap()
}, },
Rc::clone(&weather), Rc::clone(&weather),
); );
@ -156,6 +164,7 @@ impl<E: AppExecutor> App<E> {
selected_tab: SelectedTab::default(), selected_tab: SelectedTab::default(),
password_locker: PasswordLocker::default(), password_locker: PasswordLocker::default(),
current_effect: fx::fade_from_fg(Color::Black, (0, Interpolation::CircIn)), current_effect: fx::fade_from_fg(Color::Black, (0, Interpolation::CircIn)),
forecast_state: ListState::default(),
weather, weather,
executor, executor,
} }
@ -225,33 +234,22 @@ impl<E: AppExecutor> App<E> {
let layout = Layout::new( let layout = Layout::new(
Direction::Vertical, Direction::Vertical,
[Constraint::Length(20), Constraint::Min(0)], [Constraint::Length(12), Constraint::Min(0)],
) )
.split(weather_area.inner(Margin::new(2, 2))); .split(weather_area.inner(Margin::new(2, 2)));
let layout_upper = Layout::new( let layout_upper = Layout::new(
if weather_area.width < (weather_area.height * 2) { Direction::Horizontal,
Direction::Vertical [Constraint::Length(32), Constraint::Min(0)],
} else {
Direction::Horizontal
},
[
Constraint::Length(if weather_area.width < (weather_area.height * 2) {
28 / 2
} else {
28
}),
Constraint::Min(0),
],
) )
.split(layout[0]); .split(layout[0]);
frame.render_widget( frame.render_widget(
WeatherIcon( WeatherIcon(
WEATHER_ICONS WEATHER_ICONS
.get(&weather.weather[0].icon[..weather.weather[0].icon.len() - 1]) .get(&weather.0.weather[0].icon[..weather.0.weather[0].icon.len() - 1])
.unwrap(), .unwrap(),
weather.weather[0].icon[weather.weather[0].icon.len() - 1..] weather.0.weather[0].icon[weather.0.weather[0].icon.len() - 1..]
.bytes() .bytes()
.next() .next()
.unwrap(), .unwrap(),
@ -261,13 +259,13 @@ impl<E: AppExecutor> App<E> {
frame.render_widget( frame.render_widget(
Paragraph::new(format!( Paragraph::new(format!(
"{} {}°F\n~{}°F -{}°F +{}°F\n{} {} {}mph\nVisibility: {}km", "{} {}°F\n~{}°F -{}°F +{}°F\n{} {} {}mph\nVisibility: {}km",
weather.name, weather.0.name,
weather.main.temp.unwrap_or(f64::NAN), weather.0.main.temp.unwrap_or(f64::NAN),
weather.main.feels_like.unwrap_or(f64::NAN), weather.0.main.feels_like.unwrap_or(f64::NAN),
weather.main.temp_min.unwrap_or(f64::NAN), weather.0.main.temp_min.unwrap_or(f64::NAN),
weather.main.temp_max.unwrap_or(f64::NAN), weather.0.main.temp_max.unwrap_or(f64::NAN),
weather.weather[0].main, weather.0.weather[0].main,
match weather.wind.deg { match weather.0.wind.deg {
0..=45 => "🡓", 0..=45 => "🡓",
46..=90 => "🡗", 46..=90 => "🡗",
91..=135 => "🡐", 91..=135 => "🡐",
@ -277,13 +275,32 @@ impl<E: AppExecutor> App<E> {
271..=315 => "🡒", 271..=315 => "🡒",
_ => "🡖", _ => "🡖",
}, },
weather.wind.speed, weather.0.wind.speed,
weather.visibility as f64 / 1000.0, weather.0.visibility as f64 / 1000.0,
)) ))
.fg(Color::Rgb(226, 190, 89)) .fg(Color::Rgb(226, 190, 89))
.bg(Color::Black), .bg(Color::Black),
layout_upper[1], layout_upper[1],
); );
frame.render_stateful_widget(
List::new(weather.1.list.iter().map(|item| {
format!(
"{} {:<20} {:<6}°F ~{:<6}°F {}",
WEATHER_ICONS_SMALL
.get(&item.weather[0].icon[..item.weather[0].icon.len() - 1])
.unwrap(),
item.weather[0].description.as_str(),
item.main.temp.unwrap_or(f64::NAN),
item.main.temp_max.unwrap_or(f64::NAN),
item.dt_txt
)
}))
.block(Block::new().title("5-day forecast").borders(Borders::all()))
.highlight_symbol("> "),
layout[1],
&mut self.forecast_state,
);
} else { } else {
frame.render_widget( frame.render_widget(
Paragraph::new("Loading") Paragraph::new("Loading")
@ -346,6 +363,14 @@ impl<E: AppExecutor> App<E> {
self.tab_transition(); self.tab_transition();
} }
pub fn next_item(&mut self) {
self.forecast_state.select_next();
}
pub fn prev_item(&mut self) {
self.forecast_state.select_previous();
}
fn tab_transition(&mut self) { fn tab_transition(&mut self) {
self.transition_instant = Instant::now(); self.transition_instant = Instant::now();
self.current_effect = fx::sequence(&[ self.current_effect = fx::sequence(&[

View file

@ -267,6 +267,18 @@ pub const WEATHER_ICONS: phf::Map<&'static str, [&'static str; 3]> = phf_map! {
], ],
}; };
pub const WEATHER_ICONS_SMALL: phf::Map<&'static str, &'static str> = phf_map! {
"01" => "☀️",
"02" => "",
"03" => "🌥️",
"04" => "☁️",
"09" => "🌧️",
"10" => "🌦️",
"11" => "🌩️",
"13" => "❄️",
"50" => "🌫️",
};
pub struct WeatherIcon(pub &'static [&'static str; 3], pub u8); pub struct WeatherIcon(pub &'static [&'static str; 3], pub u8);
impl Widget for WeatherIcon { impl Widget for WeatherIcon {

View file

@ -45,6 +45,14 @@ fn main() {
code: event::KeyCode::Right, code: event::KeyCode::Right,
.. ..
}) => app.next_tab(), }) => app.next_tab(),
event::Event::Key(event::KeyEvent {
code: event::KeyCode::Up,
..
}) => app.prev_item(),
event::Event::Key(event::KeyEvent {
code: event::KeyCode::Down,
..
}) => app.next_item(),
event::Event::Key(event::KeyEvent { event::Event::Key(event::KeyEvent {
code: event::KeyCode::Char(c), code: event::KeyCode::Char(c),
.. ..