diff --git a/Cargo.lock b/Cargo.lock index 23f02eb..ea27453 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -802,6 +811,7 @@ dependencies = [ name = "pulse" version = "0.1.0" dependencies = [ + "colored", "reqwest", "serde", "serde_alias", diff --git a/Cargo.toml b/Cargo.toml index 6a49565..ee1e1fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +colored = "3.0.0" reqwest = { version = "0.12.15", features = ["blocking"] } serde = {version = "1.0.219", features = ["derive"]} serde_alias = "0.0.2" diff --git a/src/main.rs b/src/main.rs index 7634505..44807dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,11 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] mod weather; mod redsox; -use tabled::Table; +use tabled::{Table, Tabled}; +use serde::{Deserialize, Serialize}; use tabled::settings::{ peaker::Priority, Width, Style, Alignment, object::Columns }; -use serde::{Deserialize, Serialize}; -use tabled::Tabled; #[derive(Serialize, Deserialize, Debug, Tabled)] #[tabled(rename_all = "UPPERCASE")] @@ -21,7 +20,6 @@ struct TableRow { #[allow(unreachable_code)] fn main() { let baseball_diamond = '\u{f15ec}'; - let sunny = '\u{f0599}'; // Set the weather location here. let location = weather::WeatherOfficeLocation { x: 75, @@ -48,19 +46,17 @@ fn main() { break; } } - let mut forecast_w_icon = String::new(); - forecast_w_icon.push(sunny); - forecast_w_icon.push_str(" "); - forecast_w_icon.push_str(&item.detailed_forecast); let row = TableRow { date: date.to_string(), time_of_day: item.name.clone(), temp: item.temperature, red_sox: sox_status, - forecast: forecast_w_icon, + forecast: item.detailed_forecast.to_string(), }; table_rows.push(row); } + + // here is where we actually render the table. let mut table = Table::new(table_rows); table.with(Style::modern()); table.with(( diff --git a/src/nerdfont.rs b/src/nerdfont.rs new file mode 100644 index 0000000..78e4770 --- /dev/null +++ b/src/nerdfont.rs @@ -0,0 +1,10 @@ +pub struct NerdFontIcon { + pub icon_code: String, +} + +impl NerdFontIcon { + /// Get the single font by name. + pub fn get_icon(&self) -> Option { + u32::from_str_radix(&self.icon_code, 16).ok().and_then(char::from_u32) + } +} diff --git a/src/redsox/mod.rs b/src/redsox.rs similarity index 100% rename from src/redsox/mod.rs rename to src/redsox.rs diff --git a/src/weather.rs b/src/weather.rs new file mode 100644 index 0000000..47a6948 --- /dev/null +++ b/src/weather.rs @@ -0,0 +1,123 @@ +use reqwest::header::USER_AGENT; +use serde::{Deserialize, Serialize}; +use serde_alias::serde_alias; +#[path = "nerdfont.rs"] mod nerdfont; + +#[derive(Debug, Serialize, Deserialize)] +struct ForecastWrapper { + properties: Properties, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Properties { + periods: Vec, +} + +#[serde_alias(CamelCase,SnakeCase)] +#[derive(Serialize, Deserialize, Debug)] +pub struct WeatherPeriod { + pub name: String, + pub temperature: u64, + pub wind_direction: String, + pub wind_speed: String, + pub detailed_forecast: String, + pub short_forecast: String, + pub start_time: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct WeatherOfficeLocation { + pub x: i32, + pub y: i32, + pub code: String, +} + +impl WeatherOfficeLocation { + pub fn build_url(&self) -> String { + format!( + "https://api.weather.gov/gridpoints/{}/{},{}/forecast", + self.code, + self.x, + self.y, + ) + } +} + +// Gets the full forecast from the response. +pub fn get_full_forecast(location: WeatherOfficeLocation) -> Vec { + let url = WeatherOfficeLocation::build_url(&location); + let client = reqwest::blocking::Client::new(); + let forecast = client.get(&url) + .header(USER_AGENT, "My SuperAwesome Weather App") + .send() + .expect("Unable to get data") + .text().unwrap().to_string(); + let json: ForecastWrapper = serde_json::from_str(&forecast).expect("JSON was not well-formatted"); + let weather_periods: Vec = json.properties.periods.into_iter().collect(); + let icon_forecasts = enhance_forecasts(&weather_periods); + + icon_forecasts +} + +pub fn enhance_forecasts(periods: &Vec) -> Vec { + let mut rebuilt_periods: Vec = vec![]; + for period in periods { + let icon = detect_icon(&period.short_forecast).unwrap(); + let icon_forecast = format!("{} {}", icon, &period.detailed_forecast); + let rebuilt_period = WeatherPeriod { + name: period.name.to_string(), + temperature: period.temperature, + wind_direction: period.wind_direction.to_string(), + wind_speed: period.wind_speed.to_string(), + detailed_forecast: icon_forecast, + short_forecast: period.short_forecast.to_string(), + start_time: period.start_time.to_string(), + }; + rebuilt_periods.push(rebuilt_period); + } + + rebuilt_periods +} + +pub fn detect_icon(short_forecast: &String) -> Option { + if short_forecast.contains("Sunny") { + let icon = nerdfont::NerdFontIcon { + icon_code: "f0599".to_string(), + }; + let icon_code = icon.get_icon(); + icon_code + } + else if short_forecast.contains("Rain") && short_forecast.contains("Snow") { + let icon = nerdfont::NerdFontIcon { + icon_code: "f067f".to_string(), + }; + let icon_code = icon.get_icon(); + icon_code + } + else if short_forecast.contains("Snow") { + let icon = nerdfont::NerdFontIcon { + icon_code: "f0f36".to_string(), + }; + let icon_code = icon.get_icon(); + icon_code + } + else if short_forecast.contains("Rain") { + let icon = nerdfont::NerdFontIcon { + icon_code: "e239".to_string(), + }; + let icon_code = icon.get_icon(); + icon_code + } + else if short_forecast.contains("Cloudy") { + let icon = nerdfont::NerdFontIcon { + icon_code: "e312".to_string(), + }; + let icon_code = icon.get_icon(); + icon_code + } + else { + None + } +} + + diff --git a/src/weather/mod.rs b/src/weather/mod.rs deleted file mode 100644 index 4cf9a0f..0000000 --- a/src/weather/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -use reqwest::header::USER_AGENT; -use serde::{Deserialize, Serialize}; -use serde_alias::serde_alias; - -#[derive(Debug, Serialize, Deserialize)] -pub struct ForecastWrapper { - properties: Properties, -} - -#[derive(Debug, Serialize, Deserialize)] -struct Properties { - periods: Vec, -} - -#[serde_alias(CamelCase,SnakeCase)] -#[derive(Serialize, Deserialize, Debug)] -pub struct WeatherPeriod { - pub name: String, - pub temperature: u64, - pub wind_direction: String, - pub wind_speed: String, - pub detailed_forecast: String, - pub start_time: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct WeatherOfficeLocation { - pub x: i32, - pub y: i32, - pub code: String, -} - -impl WeatherOfficeLocation { - pub fn build_url(&self) -> String { - format!( - "https://api.weather.gov/gridpoints/{}/{},{}/forecast", - self.code, - self.x, - self.y - ) - } -} - -// Gets the full forecast from the response. -pub fn get_full_forecast(location: WeatherOfficeLocation) -> Vec { - let url = WeatherOfficeLocation::build_url(&location); - let client = reqwest::blocking::Client::new(); - let forecast = client.get(&url).header(USER_AGENT, "My SuperAwesome Weather App").send().expect("Unable to get data").text().unwrap().to_string(); - let json: ForecastWrapper = serde_json::from_str(&forecast).expect("JSON was not well-formatted"); - let periods: Vec = json.properties.periods.into_iter().collect(); - - periods -} - diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..530fcdd --- /dev/null +++ b/todo.md @@ -0,0 +1,12 @@ +# Todo +- [ ] weather: Parse each period and detect type of forecast +- [ ] weather: add colors to each icon for forecast +(sunny = yellow) :: (rainy = blue) :: (cloudy = gray) +- [ ] redsox: Only show one game per date +- [ ] main: clean things up in the function + +## Notes +### Detecting weather forecast type +Need to change the default return from get_full_forecast. +It should be a DATE => [forecast1, forecast2] +where each forecast has an icon associated with is