Skip to main content

open_timeline_gui/
config.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3//!
4//! OpenTimeline GUI config
5//!
6
7use crate::app_colours::{AppColours, ColourTheme};
8use directories_next::ProjectDirs;
9use log::info;
10use open_timeline_crud::{CrudError, setup_database_at_path};
11use serde::{Deserialize, Serialize};
12use sqlx::SqlitePool;
13use std::fs::{self, File};
14use std::path::PathBuf;
15use std::sync::Arc;
16use tokio::sync::RwLock;
17
18const PROJECT_QUALIFIER: &str = "org";
19const ORG_NAME: &str = "OpenTimeline";
20const APPLICATION_NAME: &str = "OpenTimeline";
21const CONFIG_FILE_NAME: &str = "config.json";
22const DEFAULT_DATABASE_FILE_NAME: &str = "timeline.sqlite";
23
24pub type SharedConfig = Arc<RwLock<RuntimeConfig>>;
25
26/// The config that's available across the application at runtime
27#[derive(Debug, Clone)]
28pub struct RuntimeConfig {
29    pub db_pool: SqlitePool,
30    pub config: Config,
31}
32
33/// The config that's saved to disk
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct Config {
36    /// Path to the database
37    database_path: PathBuf,
38
39    /// GUI colour theme
40    pub colour_theme: ColourTheme,
41
42    /// The custom theme
43    pub custom_theme: AppColours,
44}
45
46impl Config {
47    // TODO: this should assume that the config exists, because `ensure_exists`
48    // exists and should have been called during start up.  We should assume
49    // that the file it not directly touched
50    pub fn load() -> Result<Self, CrudError> {
51        info!("Loading config");
52        let config_file_path = config_file_path()?;
53        let data = fs::read_to_string(config_file_path)?;
54        info!("JSON config loaded = {data}");
55        let config: Config = serde_json::from_str(&data)?;
56        info!("Config loaded = {config:?}");
57        Ok(config)
58    }
59
60    pub fn set_to_default(&mut self) {
61        let default = default_config();
62        self.colour_theme = default.colour_theme();
63        self.database_path = default.database_path();
64    }
65
66    pub fn colour_theme(&self) -> ColourTheme {
67        self.colour_theme
68    }
69
70    pub fn set_colour_theme(&mut self, colour_theme: ColourTheme) {
71        self.colour_theme = colour_theme.to_owned();
72    }
73
74    pub fn database_path(&self) -> PathBuf {
75        self.database_path.clone()
76    }
77
78    pub fn set_database_path(&mut self, path: &PathBuf) {
79        self.database_path = path.to_owned();
80    }
81
82    pub async fn ensure_setup() -> Result<(), CrudError> {
83        info!("Ensuring config exists");
84        let config_file_path = config_file_path()?;
85        if !config_file_path.exists() {
86            info!("No config file found");
87            let new_config = default_config();
88            new_config.save().await?;
89            info!("Config created = {new_config:?}");
90            setup_database_at_path(&new_config.database_path).await?;
91            info!("Database setup at {}", &new_config.database_path.display());
92        };
93        info!("Config is setup");
94        Ok(())
95    }
96
97    pub async fn save(&self) -> Result<(), CrudError> {
98        // Setup database
99        let path = self.database_path.to_owned();
100        setup_database_at_path(&path).await?;
101
102        // Save config to file
103        let config_path = config_file_path()?;
104        ensure_config_file_exists(&config_path)?;
105        info!("Saving config to {config_path:?}");
106        let json = serde_json::to_string_pretty(self)?;
107        fs::write(config_path, json)?;
108
109        // Log success
110        info!("Config saved");
111        Ok(())
112    }
113}
114
115/// Get the default config
116fn default_config() -> Config {
117    info!("Creating default config");
118    let database_path = default_db_file_path();
119    Config {
120        colour_theme: ColourTheme::System,
121        database_path,
122        custom_theme: AppColours::default(),
123    }
124}
125
126/// Get the project directories (e.g. where the config is stored)
127#[cfg(debug_assertions)]
128fn project_dirs() -> Result<ProjectDirs, CrudError> {
129    info!("Getting project directories (dev build)");
130    ProjectDirs::from(
131        PROJECT_QUALIFIER,
132        ORG_NAME,
133        &format!("{APPLICATION_NAME} Dev"),
134    )
135    .ok_or(CrudError::Config)
136}
137
138/// Get the project directories (e.g. where the config is stored)
139#[cfg(not(debug_assertions))]
140fn project_dirs() -> Result<ProjectDirs, CrudError> {
141    info!("Getting project directories");
142    ProjectDirs::from(PROJECT_QUALIFIER, ORG_NAME, APPLICATION_NAME).ok_or(CrudError::Config)
143}
144
145/// Get the path to the config
146fn config_file_path() -> Result<PathBuf, CrudError> {
147    info!("Getting config file path");
148    let config_file = project_dirs()?
149        .config_dir()
150        .to_path_buf()
151        .join(CONFIG_FILE_NAME);
152    info!("Config file path = {config_file:?}");
153    Ok(config_file)
154}
155
156/// Ensure the config file exists (create if it doesn't)
157fn ensure_config_file_exists(path: &PathBuf) -> Result<(), CrudError> {
158    info!("Ensuring config path exists: {path:?}");
159    if let Some(parent) = path.parent() {
160        fs::create_dir_all(parent)?;
161    }
162    File::create(path)?;
163    Ok(())
164}
165
166/// Get the default path to the database
167fn default_db_file_path() -> PathBuf {
168    project_dirs()
169        .unwrap()
170        .data_dir()
171        .to_path_buf()
172        .join(DEFAULT_DATABASE_FILE_NAME)
173}