use config::{Config, ConfigError, File};
use rand::Rng;
use std::fs;
#[derive(serde_derive::Deserialize, serde_derive::Serialize, Debug, Clone)]
pub struct General {
pub lang: String,
#[serde(default = "default_mpc_auth_token")]
pub mpc_auth_token: String,
}
pub(crate) fn default_mpc_auth_token() -> String {
std::env::var("MPC_AUTH_TOKEN")
.unwrap_or_else(|_| "W5eBzzw9Clj4tJVzkz0z%2F2EK18jvSS%2BffHxZpAshylg%3D".to_string())
}
#[derive(serde_derive::Deserialize, serde_derive::Serialize, Debug, Clone)]
pub struct Observatory {
pub place: String,
pub latitude: f32,
pub longitude: f32,
pub altitude: f32,
pub observatory_name: String,
pub observer_name: String,
pub mpc_code: String,
pub north_altitude: i32,
pub south_altitude: i32,
pub east_altitude: i32,
pub west_altitude: i32,
}
#[derive(serde_derive::Deserialize, serde_derive::Serialize, Debug, Clone)]
pub struct Settings {
pub general: General,
pub observatory: Observatory,
}
fn default_settings() -> Settings {
let test = false; let mut rng = rand::rng();
let default_general: General = General {
lang: "en".to_string(),
mpc_auth_token: default_mpc_auth_token(),
};
let default_observatory: Observatory = match test {
false => Observatory {
place: "default".to_string(),
latitude: rng.random_range(0.1..89.9) as f32,
longitude: rng.random_range(0.1..179.9) as f32,
altitude: rng.random_range(0.1..100.0) as f32,
observatory_name: "default".to_string(),
observer_name: "default".to_string(),
mpc_code: "500".to_string(),
north_altitude: 30,
east_altitude: 45,
south_altitude: 20,
west_altitude: 70,
},
true => Observatory {
place: "default".to_string(),
latitude: 0.0, longitude: 0.0, altitude: 0.0, observatory_name: "default".to_string(),
observer_name: "default".to_string(),
mpc_code: "500".to_string(),
north_altitude: 30,
east_altitude: 45,
south_altitude: 20,
west_altitude: 70,
},
};
Settings {
general: default_general,
observatory: default_observatory,
}
}
fn parse_float64(value: &str) -> Result<f64, Box<dyn std::error::Error>> {
match value.parse::<f64>() {
Ok(value) => Ok(value),
Err(_) => Err("Could not parse value as float".into()),
}
}
fn parse_integer64(value: &str) -> Result<i64, Box<dyn std::error::Error>> {
match value.parse::<i64>() {
Ok(value) => Ok(value),
Err(_) => Err("Could not parse value as float".into()),
}
}
pub fn modify_field_in_file(key: String, value: &str) -> Result<(), Box<dyn std::error::Error>> {
let config_dir = dirs::config_local_dir()
.ok_or("Failed to get config directory")?;
let config_path = config_dir
.join("asteroid_tui")
.join("config.toml");
let config_path_str = config_path
.to_str()
.ok_or("Failed to convert config path to string")?;
let contents = fs::read_to_string(config_path_str)?;
let mut settings: toml::Value = toml::from_str(&contents)?;
match key.as_str() {
"lang" => settings["general"]["lang"] = toml::Value::String(value.to_string()),
"place" => settings["observatory"]["place"] = toml::Value::String(value.to_string()),
"latitude" => {
settings["observatory"]["latitude"] = toml::Value::Float(
parse_float64(value)
.map_err(|e| format!("Failed to parse latitude: {}", e))?
)
}
"longitude" => {
settings["observatory"]["longitude"] = toml::Value::Float(
parse_float64(value)
.map_err(|e| format!("Failed to parse longitude: {}", e))?
)
}
"altitude" => {
settings["observatory"]["altitude"] = toml::Value::Float(
parse_float64(value)
.map_err(|e| format!("Failed to parse altitude: {}", e))?
)
}
"observatory_name" => {
settings["observatory"]["observatory_name"] = toml::Value::String(value.to_string())
}
"observer_name" => {
settings["observatory"]["observer_name"] = toml::Value::String(value.to_string())
}
"mpc_code" => settings["observatory"]["mpc_code"] = toml::Value::String(value.to_string()),
"north_altitude" => {
settings["observatory"]["north_altitude"] = toml::Value::Integer(
parse_integer64(value)
.map_err(|e| format!("Failed to parse north_altitude: {}", e))?
)
}
"south_altitude" => {
settings["observatory"]["south_altitude"] = toml::Value::Integer(
parse_integer64(value)
.map_err(|e| format!("Failed to parse south_altitude: {}", e))?
)
}
"east_altitude" => {
settings["observatory"]["east_altitude"] = toml::Value::Integer(
parse_integer64(value)
.map_err(|e| format!("Failed to parse east_altitude: {}", e))?
)
}
"west_altitude" => {
settings["observatory"]["west_altitude"] = toml::Value::Integer(
parse_integer64(value)
.map_err(|e| format!("Failed to parse west_altitude: {}", e))?
)
}
_ => {}
}
let updated_contents = toml::to_string(&settings)?;
fs::write(config_path_str, updated_contents)?;
Ok(())
}
impl Settings {
pub fn new() -> Result<Self, ConfigError> {
let config_dir = dirs::config_local_dir()
.ok_or_else(|| ConfigError::Message("Failed to get config directory".to_string()))?;
let asteroid_tui_dir = config_dir.join("asteroid_tui");
let config_file = asteroid_tui_dir.join("config.toml");
if fs::metadata(&asteroid_tui_dir).is_err() {
fs::create_dir_all(&asteroid_tui_dir)
.map_err(|e| ConfigError::Message(format!("Failed to create config directory: {}", e)))?;
}
if fs::metadata(&config_file).is_err() {
let default_settings = default_settings();
let default_toml = toml::to_string(&default_settings)
.map_err(|e| ConfigError::Message(format!("Failed to serialize default settings: {}", e)))?;
let config_file_str = config_file
.to_str()
.ok_or_else(|| ConfigError::Message("Failed to convert config file path to string".to_string()))?;
fs::write(config_file_str, default_toml)
.map_err(|e| ConfigError::Message(format!("Failed to write default config file: {}", e)))?;
}
let config_file_str = config_file
.to_str()
.ok_or_else(|| ConfigError::Message("Failed to convert config file path to string".to_string()))?;
let s = Config::builder()
.add_source(File::with_name(config_file_str))
.build()?;
s.try_deserialize()
}
pub fn get_lang(&self) -> &String {
&self.general.lang
}
pub fn set_lang(&mut self, lang: String) -> Result<(), Box<dyn std::error::Error>> {
modify_field_in_file("lang".to_string(), &lang)?;
Ok(())
}
pub fn get_mpc_auth_token(&self) -> String {
std::env::var("MPC_AUTH_TOKEN")
.unwrap_or_else(|_| self.general.mpc_auth_token.clone())
}
pub fn set_settings(&mut self, settings: Settings) -> Result<(), Box<dyn std::error::Error>> {
self.observatory = settings.observatory;
let config_path = dirs::config_local_dir()
.ok_or("Failed to get config dir")?
.join("asteroid_tui")
.join("config.toml");
let toml = toml::to_string(&self)?;
std::fs::write(config_path, toml)?;
Ok(())
}
pub fn get_place(&self) -> &String {
&self.observatory.place
}
pub fn get_observatory_name(&self) -> &String {
&self.observatory.observatory_name
}
pub fn get_observer_name(&self) -> &String {
&self.observatory.observer_name
}
pub fn get_mpc_code(&self) -> &String {
&self.observatory.mpc_code
}
pub fn get_latitude(&self) -> &f32 {
&self.observatory.latitude
}
pub fn get_longitude(&self) -> &f32 {
&self.observatory.longitude
}
pub fn get_altitude(&self) -> &f32 {
&self.observatory.altitude
}
pub fn get_north_altitude(&self) -> &i32 {
&self.observatory.north_altitude
}
pub fn get_south_altitude(&self) -> &i32 {
&self.observatory.south_altitude
}
pub fn get_east_altitude(&self) -> &i32 {
&self.observatory.east_altitude
}
pub fn get_west_altitude(&self) -> &i32 {
&self.observatory.west_altitude
}
pub fn get_all_settings(&self) -> Settings {
self.clone()
}
pub fn set_latitude(&mut self, latitude: f32) {
self.observatory.latitude = latitude;
}
pub fn set_longitude(&mut self, longitude: f32) {
self.observatory.longitude = longitude;
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_new_settings() {
let s = Settings::new().unwrap();
assert!(s.observatory.place.is_ascii());
}
#[test]
fn test_get_values() {
let s = Settings::new().unwrap();
assert!(s.get_lang().is_ascii());
assert!(s.get_place().is_ascii());
assert!(s.get_observatory_name().is_ascii());
assert!(s.get_observer_name().is_ascii());
assert!(s.get_mpc_code().is_ascii());
assert!(s.get_latitude().is_finite());
assert!(s.get_longitude().is_finite());
assert!(s.get_altitude().is_finite());
assert!(s.get_north_altitude().is_positive());
assert!(s.get_south_altitude().is_positive());
assert!(s.get_east_altitude().is_positive());
assert!(s.get_west_altitude().is_positive());
}
}