use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
sync::{LazyLock, OnceLock},
};
use anyhow::{bail, Context, Result};
use ratatui::style::Color;
use serde::{Deserialize, Serialize};
use toml::Table;
use xdg::BaseDirectories;
pub static CONFIG: LazyLock<Config> = LazyLock::new(|| {
Config::init().unwrap_or_else(|e| {
eprintln!("{:?}", e);
std::process::exit(1);
})
});
#[derive(Serialize, Deserialize)]
pub struct Config {
pub connection: Connection,
pub general: General,
}
#[derive(Serialize, Deserialize)]
pub struct General {
#[serde(default)]
pub accent_color: Color,
}
#[derive(Serialize, Deserialize)]
pub struct Connection {
pub instance: String,
pub username: String,
pub password: String,
}
const DEFAULT_CONFIG: &str = include_str!("../defaults/config.toml");
static XDG_DIRS: OnceLock<BaseDirectories> = OnceLock::new();
static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
impl Config {
pub fn init() -> Result<Self> {
let table = match Self::table_from_home() {
Ok(table) => table,
Err(_) => Self::put_default_conf_in_home()?,
};
Self::table_config_verify(&table)?;
Self::table_to_config(table)
}
fn table_to_config(table: Table) -> Result<Self> {
let config_string = table.to_string();
let config: Config = toml::from_str(&config_string)?;
Ok(config)
}
fn table_config_verify(table: &Table) -> Result<()> {
let Some(connection_table) = table.get("connection").unwrap().as_table() else {
bail!("expected connection table")
};
connection_table
.get("username")
.and_then(|username| username.as_str())
.with_context(|| {
format!(
"no username in {}",
Self::get_config_path().to_str().unwrap()
)
})?;
Ok(())
}
fn table_from_home() -> Result<Table> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("lemmynator")?;
let config_path = xdg_dirs
.find_config_file("config.toml")
.ok_or_else(|| anyhow::anyhow!("config.toml not found"))?;
let mut config_buf = String::new();
let mut config_file = File::open(config_path).unwrap();
config_file.read_to_string(&mut config_buf).unwrap();
Ok(toml::from_str(&config_buf)?)
}
fn put_default_conf_in_home() -> Result<Table> {
let config_path = Self::get_config_path();
let mut config_file = File::create(config_path)?;
config_file.write_all(DEFAULT_CONFIG.as_bytes())?;
Ok(toml::from_str(DEFAULT_CONFIG)?)
}
pub fn get_xdg_dirs() -> &'static BaseDirectories {
XDG_DIRS.get_or_init(|| xdg::BaseDirectories::with_prefix("lemmynator").unwrap())
}
pub fn get_config_path() -> &'static PathBuf {
CONFIG_PATH.get_or_init(|| {
Self::get_xdg_dirs()
.place_config_file("config.toml")
.unwrap()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn invalid_config() -> Table {
toml::toml! {
[connection]
instance = "bad_url"
}
}
fn valid_config() -> Table {
toml::toml! {
[connection]
instance = "lemmy.ml"
}
}
#[test]
fn validates_properly() {
let valid_config = valid_config();
assert!(Config::table_config_verify(&valid_config).is_ok());
}
#[test]
fn invalidates_properly() {
let invalid_config = invalid_config();
assert!(Config::table_config_verify(&invalid_config).is_err());
}
}