ln_config/
lib.rs

1use std::{
2    fs::File,
3    io::{Read, Write},
4    path::PathBuf,
5    sync::{LazyLock, OnceLock},
6};
7
8use anyhow::{bail, Context, Result};
9use ratatui::style::Color;
10use serde::{Deserialize, Serialize};
11use toml::Table;
12use xdg::BaseDirectories;
13
14pub static CONFIG: LazyLock<Config> = LazyLock::new(|| {
15    Config::init().unwrap_or_else(|e| {
16        eprintln!("{:?}", e);
17        std::process::exit(1);
18    })
19});
20
21#[derive(Serialize, Deserialize)]
22pub struct Config {
23    pub connection: Connection,
24    pub general: General,
25}
26
27#[derive(Serialize, Deserialize)]
28pub struct General {
29    #[serde(default)]
30    pub accent_color: Color,
31}
32
33#[derive(Serialize, Deserialize)]
34pub struct Connection {
35    pub instance: String,
36    pub username: String,
37    // TODO: make it more secure
38    pub password: String,
39}
40
41const DEFAULT_CONFIG: &str = include_str!("../defaults/config.toml");
42static XDG_DIRS: OnceLock<BaseDirectories> = OnceLock::new();
43static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
44
45impl Config {
46    pub fn init() -> Result<Self> {
47        let table = match Self::table_from_home() {
48            Ok(table) => table,
49            Err(_) => Self::put_default_conf_in_home()?,
50        };
51
52        Self::table_config_verify(&table)?;
53
54        Self::table_to_config(table)
55    }
56
57    fn table_to_config(table: Table) -> Result<Self> {
58        let config_string = table.to_string();
59        let config: Config = toml::from_str(&config_string)?;
60        Ok(config)
61    }
62
63    fn table_config_verify(table: &Table) -> Result<()> {
64        let Some(connection_table) = table.get("connection").unwrap().as_table() else {
65            bail!("expected connection table")
66        };
67
68        connection_table
69            .get("username")
70            .and_then(|username| username.as_str())
71            .with_context(|| {
72                format!(
73                    "no username in {}",
74                    Self::get_config_path().to_str().unwrap()
75                )
76            })?;
77
78        Ok(())
79    }
80
81    fn table_from_home() -> Result<Table> {
82        let xdg_dirs = xdg::BaseDirectories::with_prefix("lemmynator")?;
83        let config_path = xdg_dirs
84            .find_config_file("config.toml")
85            .ok_or_else(|| anyhow::anyhow!("config.toml not found"))?;
86
87        let mut config_buf = String::new();
88        let mut config_file = File::open(config_path).unwrap();
89        config_file.read_to_string(&mut config_buf).unwrap();
90        Ok(toml::from_str(&config_buf)?)
91    }
92
93    fn put_default_conf_in_home() -> Result<Table> {
94        let config_path = Self::get_config_path();
95        let mut config_file = File::create(config_path)?;
96        config_file.write_all(DEFAULT_CONFIG.as_bytes())?;
97        Ok(toml::from_str(DEFAULT_CONFIG)?)
98    }
99
100    pub fn get_xdg_dirs() -> &'static BaseDirectories {
101        XDG_DIRS.get_or_init(|| xdg::BaseDirectories::with_prefix("lemmynator").unwrap())
102    }
103
104    pub fn get_config_path() -> &'static PathBuf {
105        CONFIG_PATH.get_or_init(|| {
106            Self::get_xdg_dirs()
107                .place_config_file("config.toml")
108                .unwrap()
109        })
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    fn invalid_config() -> Table {
118        toml::toml! {
119            [connection]
120            instance = "bad_url"
121        }
122    }
123
124    fn valid_config() -> Table {
125        toml::toml! {
126            [connection]
127            instance = "lemmy.ml"
128        }
129    }
130
131    #[test]
132    fn validates_properly() {
133        let valid_config = valid_config();
134        assert!(Config::table_config_verify(&valid_config).is_ok());
135    }
136
137    #[test]
138    fn invalidates_properly() {
139        let invalid_config = invalid_config();
140        assert!(Config::table_config_verify(&invalid_config).is_err());
141    }
142}