diff --git a/src/app.rs b/src/app.rs index 63b1b1c..e89010a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,9 +1,18 @@ use crate::settings::Settings; +use crate::weather::prepare_data; +use crate::weather::ForecastResponse; use std::error; /// Application result type. pub type AppResult = std::result::Result>; +#[derive(Debug)] +pub enum CurrentScreen { + MainMenu, + SchedulingMenu, + WeatherForecast, +} + /// Application. #[derive(Debug)] pub struct App { @@ -11,13 +20,18 @@ pub struct App { pub running: bool, /// counter pub counter: u8, + pub current_screen: CurrentScreen, + pub weather_requested: ForecastResponse, } impl Default for App { fn default() -> Self { + let weather_data: ForecastResponse = prepare_data().unwrap(); Self { running: true, counter: 0, + current_screen: CurrentScreen::MainMenu, + weather_requested: weather_data, } } } diff --git a/src/handler.rs b/src/handler.rs index 3cea937..51ffad8 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,4 +1,4 @@ -use crate::app::{App, AppResult}; +use crate::app::{App, AppResult, CurrentScreen}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; /// Handles the key events and updates the state of [`App`]. @@ -14,6 +14,19 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { app.quit(); } } + KeyCode::Char('s') | KeyCode::Char('S') => match app.current_screen { + CurrentScreen::MainMenu => app.current_screen = CurrentScreen::SchedulingMenu, + _ => {} + }, + KeyCode::Char('b') | KeyCode::Char('B') => match app.current_screen { + CurrentScreen::SchedulingMenu => app.current_screen = CurrentScreen::MainMenu, + CurrentScreen::WeatherForecast => app.current_screen = CurrentScreen::SchedulingMenu, + _ => {} + }, + KeyCode::Char('w') | KeyCode::Char('W') => match app.current_screen { + CurrentScreen::SchedulingMenu => app.current_screen = CurrentScreen::WeatherForecast, + _ => {} + }, // Counter handlers KeyCode::Right => { app.increment_counter(); diff --git a/src/ui.rs b/src/ui.rs index bbe2773..ac770fb 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,11 +1,18 @@ use ratatui::{ layout::Alignment, style::{Color, Style}, - widgets::{Block, BorderType, Paragraph}, + widgets::{Block, BorderType, Cell, Paragraph, Row, Table}, Frame, }; +use serde::{Deserialize, Serialize}; +use serde_json::{Error, Result}; + +use ratatui::prelude::*; use crate::app::App; +use crate::app::CurrentScreen; +use crate::weather::prepare_data; +use crate::weather::ForecastResponse; /// Renders the user interface widgets. pub fn render(app: &mut App, frame: &mut Frame) { @@ -13,22 +20,241 @@ pub fn render(app: &mut App, frame: &mut Frame) { // See the following resources: // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html // - https://github.com/ratatui-org/ratatui/tree/master/examples + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Min(1), + Constraint::Length(3), + ]) + .split(frame.size()); + match app.current_screen { + CurrentScreen::MainMenu => { + render_main_menu(app, frame, layout); + } + CurrentScreen::SchedulingMenu => { + render_scheduling_menu(app, frame, layout); + } + CurrentScreen::WeatherForecast => { + render_weather_forecast(app, frame, layout); + } + } +} + +/// Centers a rect inside an area +/// +/// * `r`: The area where to insert resulting rect +/// * `percent_x`: width of resulting rect in percent of `r.width()` +/// * `percent_y`: height of resulting rect in percent of `r.height()` +fn centered_rect(r: Rect, percent_x: u16, percent_y: u16) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1] +} + +fn render_main_menu( + app: &mut App, + frame: &mut Frame, + layout: std::rc::Rc<[ratatui::layout::Rect]>, +) { + frame.render_widget( + Paragraph::new("AsteroidTUI") + .block( + Block::bordered() + //.title("Template") + //.title_alignment(Alignment::Center) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::Red).bg(Color::Black)) + .centered(), + layout[0], + ); frame.render_widget( - Paragraph::new(format!( - "This is a tui template.\n\ - Press `Esc`, `Ctrl-C` or `q` to stop running.\n\ - Press left and right to increment and decrement the counter respectively.\n\ - Counter: {}", - app.counter - )) - .block( - Block::bordered() - .title("Template") - .title_alignment(Alignment::Center) - .border_type(BorderType::Rounded), + Paragraph::new("") + .block(Block::default()) + .style(Style::default().bg(Color::Black)), + layout[1], + ); + frame.render_widget( + Paragraph::new( + "Main Menu\n\ + \n\n\ + c - Configuration\n\ + s - Scheduling\n\ + q - quit", ) - .style(Style::default().fg(Color::Cyan).bg(Color::Black)) + .style(Style::default().fg(Color::Red).bg(Color::Black)) .centered(), - frame.size(), - ) + layout[2], + ); + frame.render_widget( + Paragraph::new("Press q or Ctrl+C to quit") + .block(Block::bordered().border_type(BorderType::Rounded)) + .style(Style::default().fg(Color::Red).bg(Color::Black)) + .centered(), + layout[3], + ); +} + +fn render_scheduling_menu( + app: &mut App, + frame: &mut Frame, + layout: std::rc::Rc<[ratatui::layout::Rect]>, +) { + frame.render_widget( + Paragraph::new("AsteroidTUI") + .block( + Block::bordered() + //.title("Template") + //.title_alignment(Alignment::Center) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::Red).bg(Color::Black)) + .centered(), + layout[0], + ); + frame.render_widget( + Paragraph::new("") + .block(Block::default()) + .style(Style::default().bg(Color::Black)), + layout[1], + ); + frame.render_widget( + Paragraph::new( + "Scheduling Menu\n\ + \n\n\ + w - Weather Forecast\n\ + b - Back to Main Menu\n\ + q - Quit", + ) + .style(Style::default().fg(Color::Red).bg(Color::Black)) + .centered(), + layout[2], + ); + frame.render_widget( + Paragraph::new("Press q or Ctrl+C to quit") + .block(Block::bordered().border_type(BorderType::Rounded)) + .style(Style::default().fg(Color::Red).bg(Color::Black)) + .centered(), + layout[3], + ); +} + +fn create_table(data: &ForecastResponse) -> Table { + // Create the table header + let header = vec![ + "Timepoint", + "Cloud Cover", + "Seeing", + "Transparency", + "Lifted Index", + "RH2m", + "Wind10m", + "Temp2m", + "Prec Type", + ] + .into_iter() + .map(Cell::from) + .collect::() + .style(Style::default().add_modifier(Modifier::BOLD)); + + // Create table rows + let rows = data + .dataseries + .iter() + .map(|forecast| { + Row::new(vec![ + Cell::from(forecast.timepoint.to_string()), + Cell::from(forecast.cloud_cover.to_string()), // Assuming CloudCover, Seeing, Transparency, Wind10m have a to_string() method + Cell::from(forecast.seeing.to_string()), + Cell::from(forecast.transparency.to_string()), + Cell::from(forecast.lifted_index.to_string()), + Cell::from(forecast.rh2m.to_string()), + Cell::from(forecast.wind10m.direction.to_string()), + Cell::from(forecast.wind10m.speed.to_string()), + Cell::from(forecast.temp2m.to_string()), + Cell::from(forecast.prec_type.clone()), + ]) + }) + .collect::>(); + let widths = [ + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(15), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(15), + Constraint::Percentage(10), + Constraint::Percentage(15), + ]; + + // Configure the table + Table::new(rows, widths).header(header) +} + +fn render_weather_forecast( + app: &mut App, + frame: &mut Frame, + layout: std::rc::Rc<[ratatui::layout::Rect]>, +) { + let header = Row::new(vec![ + Cell::from("Time"), + Cell::from("Cloud Cover"), + Cell::from("Seeing"), + Cell::from("Transparency"), + Cell::from("Lifted Index"), + Cell::from("RH (2m)"), + Cell::from("Wind Dir (10m)"), + Cell::from("Wind Speed (10m)"), + Cell::from("Temp (2m)"), + Cell::from("Prec Type"), + ]); + frame.render_widget( + Paragraph::new("AsteroidTUI") + .block( + Block::bordered() + //.title("Template") + //.title_alignment(Alignment::Center) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::Red).bg(Color::Black)) + .centered(), + layout[0], + ); + frame.render_widget( + Paragraph::new("") + .block(Block::default()) + .style(Style::default().bg(Color::Black)), + layout[1], + ); + frame.render_widget( + create_table(&app.weather_requested) + .header(header) + .style(Style::default().bg(Color::Black).fg(Color::Red)), + layout[2], + ); + frame.render_widget( + Paragraph::new("Press q or Ctrl+C to quit") + .block(Block::bordered().border_type(BorderType::Rounded)) + .style(Style::default().fg(Color::Red).bg(Color::Black)) + .centered(), + layout[3], + ); } diff --git a/src/weather.rs b/src/weather.rs index 34682eb..da982b2 100644 --- a/src/weather.rs +++ b/src/weather.rs @@ -8,30 +8,30 @@ use std::fmt; use std::fmt::Display; #[derive(Debug, Deserialize)] -struct Wind10m { - direction: String, - speed: Wind10mVelocity, +pub struct Wind10m { + pub direction: String, + pub speed: Wind10mVelocity, } #[derive(Debug, Deserialize)] -struct Forecast { - timepoint: i8, +pub struct Forecast { + pub timepoint: i8, #[serde(rename = "cloudcover")] - cloud_cover: CloudCover, - seeing: Seeing, - transparency: Transparency, - lifted_index: i8, - rh2m: i8, - wind10m: Wind10m, - temp2m: i8, - prec_type: String, + pub cloud_cover: CloudCover, + pub seeing: Seeing, + pub transparency: Transparency, + pub lifted_index: LiftedIndex, + pub rh2m: i8, + pub wind10m: Wind10m, + pub temp2m: i8, + pub prec_type: String, } #[derive(Debug, Deserialize)] pub struct ForecastResponse { product: String, init: String, - dataseries: Vec, + pub dataseries: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize_repr)] @@ -139,29 +139,29 @@ impl Display for Transparency { } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize_repr)] -#[repr(u8)] +#[repr(i8)] pub enum LiftedIndex { - ZeroThree = 1, + BelowSeven = -10, + SevenFive = -6, + FiveThree = -4, + ThreeZero = -1, ZeroFour = 2, - ZeroFive = 3, - ZeroSix = 4, - ZeroSeven = 5, - ZeroEight = 6, - One = 7, - MoreOne = 8, + FourEight = 6, + EightEleven = 10, + OverEleven = 15, } impl LiftedIndex { pub const fn to_str(self) -> &'static str { match self { - LiftedIndex::ZeroThree => "<0.3", - LiftedIndex::ZeroFour => "0.3-0.4", - LiftedIndex::ZeroFive => "0.4-0.5", - LiftedIndex::ZeroSix => "0.5-0.6", - LiftedIndex::ZeroSeven => "0.6-0.7", - LiftedIndex::ZeroEight => "0.7-0.85", - LiftedIndex::One => "0.85-1", - LiftedIndex::MoreOne => ">1", + LiftedIndex::BelowSeven => "Below -7", + LiftedIndex::SevenFive => "-7 - -5", + LiftedIndex::FiveThree => "-5 - -3", + LiftedIndex::ThreeZero => "-3 - 0", + LiftedIndex::ZeroFour => "0 - 4", + LiftedIndex::FourEight => "4 - 8", + LiftedIndex::EightEleven => "8 - 11", + LiftedIndex::OverEleven => "Over 11", } } } @@ -175,7 +175,7 @@ impl Display for LiftedIndex { /// Returns a string with lifted index /// /// * `index`: the index from json response -fn get_lifted_index_value(index: i8) -> Option<&'static str> { +pub fn get_lifted_index_value(index: i8) -> Option<&'static str> { let lifted_index = HashMap::from([ (-10, "Below -7"), (-6, "-7 - -5"), @@ -192,7 +192,7 @@ fn get_lifted_index_value(index: i8) -> Option<&'static str> { /// Returns a string with rh range /// /// * `index`: the index from json response -fn get_rh2m_value(index: i8) -> Option<&'static str> { +pub fn get_rh2m_value(index: i8) -> Option<&'static str> { let rh2m = HashMap::from([ (-4, "0%-5%"), (-3, "5%-10%"),