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 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}