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]
# Tell `rustc` to optimize for small code size.
opt-level = "s"
panic = "abort"
[profile.dev]
panic = "abort"
[dependencies]
dotenvy_macro = "0.15.7"
futures-util = "0.3.31"
getrandom = { version = "0.3.3", features = ["wasm_js"] }
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"] }
rand = { version = "0.9.2", default-features = false, features = ["os_rng", "std"] }
tachyonfx = { version = "0.16.0", default-features = false, features = ["web-time"] }

View file

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

View file

@ -45,6 +45,14 @@ fn main() {
code: event::KeyCode::Right,
..
}) => 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 {
code: event::KeyCode::Char(c),
..