diff --git a/thecockpit/Cargo.toml b/thecockpit/Cargo.toml index 356c209..d7b6141 100644 --- a/thecockpit/Cargo.toml +++ b/thecockpit/Cargo.toml @@ -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"] } diff --git a/thecockpit/src/app/mod.rs b/thecockpit/src/app/mod.rs index 21522ae..161cb72 100644 --- a/thecockpit/src/app/mod.rs +++ b/thecockpit/src/app/mod.rs @@ -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 { selected_tab: SelectedTab, password_locker: PasswordLocker, current_effect: Effect, - weather: Rc>, + forecast_state: ListState, + weather: Rc>, executor: E, } @@ -135,18 +137,24 @@ impl App { 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 App { 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 App { 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 App { 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 App { 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 App { 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(&[ diff --git a/thecockpit/src/app/weather_icon.rs b/thecockpit/src/app/weather_icon.rs index edca7e9..595ec5c 100644 --- a/thecockpit/src/app/weather_icon.rs +++ b/thecockpit/src/app/weather_icon.rs @@ -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 { diff --git a/thecockpit/src/main.rs b/thecockpit/src/main.rs index 482f65c..f401d0d 100644 --- a/thecockpit/src/main.rs +++ b/thecockpit/src/main.rs @@ -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), ..