diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..91c0ba8 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,187 @@ +// Copyright 2024 Robin Arnold +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// WxFetch - config.rs + +use std::{fs::File, io::Read}; + +use chrono::TimeDelta; +use toml::{Table, Value}; + +use crate::{ + api::check_icao_code, + position::{LatLong, Position}, + Args, Secrets, +}; + +pub struct Config { + pub position: Position, + pub cloud_minimum: i64, + pub cloud_marginal: i64, + pub temp_minimum: i64, + pub spread_minimum: i64, + pub wind_var_maximum: i64, + pub wind_maximum: i64, + pub gust_maximum: i64, + pub age_maximum: TimeDelta, + pub age_marginal: TimeDelta, + pub visibility_minimum: i64, + pub visibility_marginal: i64, +} + +impl Default for Config { + fn default() -> Self { + Self { + position: Position::GeoIP, + cloud_minimum: 6, + cloud_marginal: 15, + temp_minimum: 0, + spread_minimum: 3, + wind_var_maximum: 45, + wind_maximum: 15, + gust_maximum: 10, + age_maximum: TimeDelta::hours(6), + age_marginal: TimeDelta::hours(1), + visibility_minimum: 1500, + visibility_marginal: 5000, + } + } +} + +pub async fn get_config(secrets: &Secrets, args: &Args) -> Config { + let mut config: Config = read_config_file(args.config_file.clone()); + + if let Some(icao) = args.airfield.clone() { + config.position = Position::Airfield(icao.clone()); + } else if let Some(lat) = args.latitude { + if let Some(long) = args.longitude { + config.position = Position::LatLong(LatLong(lat, long)); + } + println!("Please provide both Latitude and Longitude. Defaulting to geoip..."); + } + + if let Position::Airfield(ref icao) = config.position { + if !check_icao_code(icao, secrets).await { + println!("Invalid airfield {icao}. Defaulting to geoip..."); + config.position = Position::GeoIP; + } + } + config +} + +fn read_config_file(config_filepath: Option) -> Config { + let msg = "Failed to load config."; + let mut config = Config::default(); + let config_filepath = config_filepath + .unwrap_or(std::env::var("HOME").expect(msg) + "/.config/wxfetch/config.toml"); + let config_file = File::open(config_filepath.clone()); + if config_file.is_err() { + println!("Could not open config file at {config_filepath}. Proceeding with defaults..."); + return config; + } + let mut config_file = config_file.unwrap(); + let mut contents = String::new(); + config_file.read_to_string(&mut contents).expect(msg); + let contents = contents.parse::().expect(msg); + + if contents.contains_key("position") { + if let Some(airfield) = contents["position"].get("airfield").and_then(Value::as_str) { + config.position = Position::Airfield(airfield.to_string()); + } + if let Some(lat) = contents["position"].get("lat").and_then(Value::as_float) { + if let Some(lon) = contents["position"].get("lon").and_then(Value::as_float) { + config.position = Position::LatLong(LatLong(lat, lon)); + } + } + } + + if contents.contains_key("clouds") { + if let Some(minimum) = contents["clouds"] + .get("cloud_minimum") + .and_then(Value::as_integer) + { + config.cloud_minimum = minimum; + } + if let Some(marginal) = contents["clouds"] + .get("cloud_marginal") + .and_then(Value::as_integer) + { + config.cloud_marginal = marginal; + } + } + + if contents.contains_key("temperature") { + if let Some(minimum) = contents["temperature"] + .get("temp_minimum") + .and_then(Value::as_integer) + { + config.temp_minimum = minimum; + } + if let Some(spread_minimum) = contents["temperature"] + .get("spread_minimum") + .and_then(Value::as_integer) + { + config.spread_minimum = spread_minimum; + } + } + + if contents.contains_key("wind") { + if let Some(var_maximum) = contents["wind"] + .get("wind_var_maximum") + .and_then(Value::as_integer) + { + config.wind_var_maximum = var_maximum; + } + if let Some(maximum) = contents["wind"] + .get("wind_maximum") + .and_then(Value::as_integer) + { + config.wind_maximum = maximum; + } + if let Some(gust_maximum) = contents["wind"] + .get("gust_maximum") + .and_then(Value::as_integer) + { + config.gust_maximum = gust_maximum; + } + } + + if contents.contains_key("age") { + if let Some(maximum) = contents["age"] + .get("age_maximum") + .and_then(Value::as_integer) + { + config.age_maximum = TimeDelta::seconds(maximum); + } + if let Some(marginal) = contents["age"] + .get("age_marginal") + .and_then(Value::as_integer) + { + config.age_marginal = TimeDelta::seconds(marginal); + } + } + + if contents.contains_key("visibility") { + if let Some(minimum) = contents["visibility"] + .get("visibility_minimum") + .and_then(Value::as_integer) + { + config.visibility_minimum = minimum; + } + if let Some(marginal) = contents["visibility"] + .get("visibility_marginal") + .and_then(Value::as_integer) + { + config.visibility_marginal = marginal; + } + } + + config +} diff --git a/src/main.rs b/src/main.rs index 42bc622..12f70f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,21 +10,24 @@ // limitations under the License. // WxFetch - main.rs -use api::{check_icao_code, request_wx}; -use chrono::TimeDelta; +use std::fs::File; +use std::io::BufReader; + +use api::request_wx; use clap::Parser; use colored::ColoredString; -use std::{fs::File, io::Read}; -use toml::{Table, Value}; mod metar; use metar::Metar; mod position; -use position::{LatLong, Position}; mod api; +mod config; +use config::get_config; +use config::Config; + #[derive(Parser, Debug)] #[command(version, about)] /// Console utility for accessing aviation weather information from the commmand line. @@ -39,40 +42,8 @@ struct Args { longitude: Option, #[arg(short, long, value_name = "Config File")] config_file: Option, -} - -struct Config { - position: Position, - cloud_minimum: i64, - cloud_marginal: i64, - temp_minimum: i64, - spread_minimum: i64, - wind_var_maximum: i64, - wind_maximum: i64, - gust_maximum: i64, - age_maximum: TimeDelta, - age_marginal: TimeDelta, - visibility_minimum: i64, - visibility_marginal: i64, -} - -impl Default for Config { - fn default() -> Self { - Self { - position: Position::GeoIP, - cloud_minimum: 6, - cloud_marginal: 15, - temp_minimum: 0, - spread_minimum: 3, - wind_var_maximum: 45, - wind_maximum: 15, - gust_maximum: 10, - age_maximum: TimeDelta::hours(6), - age_marginal: TimeDelta::hours(1), - visibility_minimum: 1500, - visibility_marginal: 5000, - } - } + #[arg(short, long = "file", value_name = "JSON Source File")] + file: Option, } struct Secrets { @@ -84,138 +55,6 @@ fn get_secrets() -> Secrets { Secrets { avwx_api_key } } -async fn get_config(secrets: &Secrets) -> Config { - let args = Args::parse(); - let mut config: Config = read_config_file(args.config_file); - - if let Some(icao) = args.airfield { - config.position = Position::Airfield(icao.clone()); - } else if let Some(lat) = args.latitude { - if let Some(long) = args.longitude { - config.position = Position::LatLong(LatLong(lat, long)); - } - println!("Please provide both Latitude and Longitude. Defaulting to geoip..."); - } - - if let Position::Airfield(ref icao) = config.position { - if !check_icao_code(icao, secrets).await { - println!("Invalid airfield {icao}. Defaulting to geoip..."); - config.position = Position::GeoIP; - } - } - config -} - -fn read_config_file(config_filepath: Option) -> Config { - let msg = "Failed to load config."; - let mut config = Config::default(); - let config_filepath = config_filepath - .unwrap_or(std::env::var("HOME").expect(msg) + "/.config/wxfetch/config.toml"); - let config_file = File::open(config_filepath.clone()); - if config_file.is_err() { - println!("Could not open config file at {config_filepath}. Proceeding with defaults..."); - return config; - } - let mut config_file = config_file.unwrap(); - let mut contents = String::new(); - config_file.read_to_string(&mut contents).expect(msg); - let contents = contents.parse::
().expect(msg); - - if contents.contains_key("position") { - if let Some(airfield) = contents["position"].get("airfield").and_then(Value::as_str) { - config.position = Position::Airfield(airfield.to_string()); - } - if let Some(lat) = contents["position"].get("lat").and_then(Value::as_float) { - if let Some(lon) = contents["position"].get("lon").and_then(Value::as_float) { - config.position = Position::LatLong(LatLong(lat, lon)); - } - } - } - - if contents.contains_key("clouds") { - if let Some(minimum) = contents["clouds"] - .get("cloud_minimum") - .and_then(Value::as_integer) - { - config.cloud_minimum = minimum; - } - if let Some(marginal) = contents["clouds"] - .get("cloud_marginal") - .and_then(Value::as_integer) - { - config.cloud_marginal = marginal; - } - } - - if contents.contains_key("temperature") { - if let Some(minimum) = contents["temperature"] - .get("temp_minimum") - .and_then(Value::as_integer) - { - config.temp_minimum = minimum; - } - if let Some(spread_minimum) = contents["temperature"] - .get("spread_minimum") - .and_then(Value::as_integer) - { - config.spread_minimum = spread_minimum; - } - } - - if contents.contains_key("wind") { - if let Some(var_maximum) = contents["wind"] - .get("wind_var_maximum") - .and_then(Value::as_integer) - { - config.wind_var_maximum = var_maximum; - } - if let Some(maximum) = contents["wind"] - .get("wind_maximum") - .and_then(Value::as_integer) - { - config.wind_maximum = maximum; - } - if let Some(gust_maximum) = contents["wind"] - .get("gust_maximum") - .and_then(Value::as_integer) - { - config.gust_maximum = gust_maximum; - } - } - - if contents.contains_key("age") { - if let Some(maximum) = contents["age"] - .get("age_maximum") - .and_then(Value::as_integer) - { - config.age_maximum = TimeDelta::seconds(maximum); - } - if let Some(marginal) = contents["age"] - .get("age_marginal") - .and_then(Value::as_integer) - { - config.age_marginal = TimeDelta::seconds(marginal); - } - } - - if contents.contains_key("visibility") { - if let Some(minimum) = contents["visibility"] - .get("visibility_minimum") - .and_then(Value::as_integer) - { - config.visibility_minimum = minimum; - } - if let Some(marginal) = contents["visibility"] - .get("visibility_marginal") - .and_then(Value::as_integer) - { - config.visibility_marginal = marginal; - } - } - - config -} - async fn get_weather(config: &Config, secrets: &Secrets) -> ColoredString { let json = request_wx(config, secrets) .await @@ -224,11 +63,24 @@ async fn get_weather(config: &Config, secrets: &Secrets) -> ColoredString { metar.colorise(config) } +fn get_weather_from_file(filename: String, config: &Config) -> ColoredString { + let file = File::open(filename).expect("No such file: {filename}"); + let reader = BufReader::new(file); + let json = serde_json::from_reader(reader).expect("Failed to read data from file {filename}"); + let metar = Metar::from_json(&json, config).expect("Invalid weather data received..."); + + metar.colorise(config) +} + #[tokio::main] async fn main() { + let args = Args::parse(); let secrets = get_secrets(); - let config = get_config(&secrets).await; - let wx_string = get_weather(&config, &secrets).await; + let config = get_config(&secrets, &args).await; + let wx_string = match args.file { + Some(filename) => get_weather_from_file(filename, &config), + None => get_weather(&config, &secrets).await, + }; println!("{wx_string}"); } diff --git a/src/metar.rs b/src/metar.rs index d3d44ea..6f18f22 100644 --- a/src/metar.rs +++ b/src/metar.rs @@ -10,7 +10,7 @@ // limitations under the License. // WxFetch - metar.rs -use crate::{Config, Position}; +use crate::{position::Position, Config}; use chrono::DateTime; use chrono::FixedOffset; use chrono::Utc; @@ -145,7 +145,9 @@ fn colourise_wx_code( _ => Color::White, }); - format!("{intensitystr}{descrstr}{codestr}{proximity}").magenta() + let proxstr = format!("{proximity}").white(); + + format!("{intensitystr}{descrstr}{codestr}{proxstr}").into() } fn colourise_qnh(qnh: i64, unit: PressureUnit, _config: &Config) -> ColoredString { @@ -398,6 +400,8 @@ mod tests { use units::{AltitudeUnit, DistanceUnit}; + use crate::position::LatLong; + use super::*; #[test] @@ -453,7 +457,7 @@ mod tests { #[test] fn test_is_exact_match_latlong() { let config = Config { - position: Position::LatLong(crate::LatLong(10.0, 10.0)), + position: Position::LatLong(LatLong(10.0, 10.0)), ..Default::default() }; assert!(is_exact_match("EDRK", &config)); @@ -664,8 +668,8 @@ mod tests { #[test] fn test_colourise_clouds() { let config = Config::default(); - let clouds = WxField::Clouds(Clouds::Sct, 5000); - let expected = colourise_clouds(&Clouds::Sct, 5000, &config); + let clouds = WxField::Clouds(Clouds::Sct, 50); + let expected = colourise_clouds(&Clouds::Sct, 50, &config); let actual = clouds.colourise(&config); assert_eq!(actual, expected); } @@ -679,4 +683,82 @@ mod tests { let actual = timestamp.colourise(&config); assert_eq!(actual, expected); } + + #[test] + fn test_colourise_clouds_marginal() { + let config = Config::default(); + let clouds = WxField::Clouds(Clouds::Ovc, 8); + let expected = colourise_clouds(&Clouds::Ovc, 8, &config); + let actual = clouds.colourise(&config); + assert_eq!(actual, expected); + } + + #[test] + fn test_colourise_clouds_bad() { + let config = Config::default(); + let clouds = WxField::Clouds(Clouds::Brk, 5); + let expected = colourise_clouds(&Clouds::Brk, 5, &config); + let actual = clouds.colourise(&config); + assert_eq!(actual, expected); + } + + // #[test] + // fn test_colourise_wxcode_sn() { + // let config = Config::default(); + // let wxcode = WxField::WxCode( + // WxCode::Sn, + // WxCodeIntensity::Moderate, + // WxCodeProximity::OnStation, + // WxCodeDescription::None, + // ); + // let expected_colour = Color::Red; + // let actual = wxcode.colourise(&config); + // assert_eq!(actual.fgcolor().unwrap(), expected_colour); + // } + + // #[test] + // fn test_colourise_wxcode_gs() { + // let config = Config::default(); + // let wxcode = WxField::WxCode( + // WxCode::Gs, + // WxCodeIntensity::Moderate, + // WxCodeProximity::OnStation, + // WxCodeDescription::None, + // ); + // let expected_colour = Color::Yellow; + // let actual = wxcode.colourise(&config); + // assert_eq!(actual.fgcolor().unwrap(), expected_colour); + // } + + // #[test] + // fn test_colourise_wxcode_po() { + // let config = Config::default(); + // let wxcode = WxField::WxCode( + // WxCode::Po, + // WxCodeIntensity::Moderate, + // WxCodeProximity::OnStation, + // WxCodeDescription::None, + // ); + // let expected_colour = Color::BrightRed; + // let actual = wxcode.colourise(&config); + // assert_eq!(actual.fgcolor().unwrap(), expected_colour); + // } + + // #[test] + // fn test_colourise_wxcode_ic() { + // let config = Config::default(); + // let wxcode = WxField::WxCode( + // WxCode::Ic, + // WxCodeIntensity::Moderate, + // WxCodeProximity::OnStation, + // WxCodeDescription::None, + // ); + // let expected_colour = Color::White; + // let actual = wxcode.colourise(&config); + // assert_eq!(actual.fgcolor().unwrap(), expected_colour); + // } + // // WxCode::Gr | WxCode::Sn | WxCode::Up => Color::Red, + // WxCode::Gs => Color::Yellow, + // WxCode::Po => Color::BrightRed, + // _ => Color::White, }