Skip to main content

notalterra/
config.rs

1//! Path utilities and persistent app configuration.
2//!
3//! The save-folder path and backup root are persisted to `app.ini` under the
4//! platform config directory so they survive between sessions. The disclaimer
5//! acceptance is tracked via a sentinel file in the same directory.
6//!
7//! The backup root defaults to `~/NotAlterra` and can be changed via the
8//! `Set backup location` menu item.
9
10use std::path::{Path, PathBuf};
11use std::sync::Mutex;
12
13/// Custom backup root directory, set via the `Set backup location` menu.
14/// When `None`, falls back to `exe_dir().join("backups")`.
15static BACKUP_ROOT: Mutex<Option<PathBuf>> = Mutex::new(None);
16
17/// Set a custom backup root directory.  Passes ownership.
18pub fn set_backup_root(path: PathBuf) {
19    if let Ok(mut root) = BACKUP_ROOT.lock() {
20        *root = Some(path);
21    }
22}
23
24/// Return the current backup root, or the default user-profile path.
25pub fn get_backup_root() -> PathBuf {
26    BACKUP_ROOT
27        .lock()
28        .ok()
29        .and_then(|r| r.clone())
30        .unwrap_or_else(|| {
31            dirs::home_dir()
32                .map(|h| h.join("NotAlterra"))
33                .unwrap_or_else(exe_dir)
34        })
35}
36
37/// Path to the disclaimer sentinel file in the config directory.
38pub fn sentinel_path() -> PathBuf {
39    config_base_dir().join("NOTALTERRA_LICENSE_ACCEPTED")
40}
41
42/// Return `true` if the disclaimer sentinel exists.
43pub fn disclaimer_accepted() -> bool {
44    sentinel_path().exists()
45}
46
47/// Create the disclaimer sentinel (0-byte file).
48pub fn accept_disclaimer() -> std::io::Result<()> {
49    std::fs::write(sentinel_path(), [])?;
50    Ok(())
51}
52
53/// Remove the disclaimer sentinel if it exists.
54pub fn reject_disclaimer() -> std::io::Result<()> {
55    let p = sentinel_path();
56    if p.exists() {
57        std::fs::remove_file(p)?;
58    }
59    Ok(())
60}
61
62/// Path to the stale `config.ini` from v0.3.0 and earlier.
63pub fn stale_config_path() -> PathBuf {
64    exe_dir().join("config.ini")
65}
66
67/// Remove the stale `config.ini` if it exists.  Returns `true` if removed.
68pub fn cleanup_stale_config() -> bool {
69    let path = stale_config_path();
70    if path.exists() {
71        std::fs::remove_file(&path).ok();
72        true
73    } else {
74        false
75    }
76}
77
78/// Path to the `saves/` directory under the backup root (tar.gz archives).
79/// Auto-creates the directory on first call.
80pub fn backups_saves_dir() -> PathBuf {
81    let p = get_backup_root().join("backups").join("saves");
82    std::fs::create_dir_all(&p).ok();
83    p
84}
85
86/// Path to the `ue5/` subdirectory under the backup root for UE5 Config `.ini`
87/// backup archives.  Auto-creates the directory on first call.
88pub fn backups_config_dir() -> PathBuf {
89    let p = get_backup_root().join("backups").join("ue5");
90    std::fs::create_dir_all(&p).ok();
91    p
92}
93
94/// Return `~/NotAlterra` as the user's data directory for save/ue5 backups.
95/// Falls back to `exe_dir()` if home is not available.
96fn home_notalterra_dir() -> PathBuf {
97    dirs::home_dir()
98        .map(|h| h.join("NotAlterra"))
99        .unwrap_or_else(exe_dir)
100}
101
102// ── persistent app config ────────────────────────────────────────────────────
103
104/// Fixed base directory for `app.ini` and the sentinel file, under the
105/// standard platform config location.  Separate from backup data so that
106/// `~/NotAlterra` remains the user's visible backup data directory.
107fn config_base_dir() -> PathBuf {
108    dirs::data_local_dir()
109        .map(|d| d.join("NotAlterra").join("config"))
110        .unwrap_or_else(exe_dir)
111}
112
113/// Path to the persistent `app.ini` configuration file.
114pub fn app_ini_path() -> PathBuf {
115    let p = config_base_dir().join("app.ini");
116    std::fs::create_dir_all(p.parent().unwrap_or(Path::new("."))).ok();
117    p
118}
119
120/// Session-lifetime configuration loaded from and persisted to `app.ini`.
121#[derive(Debug, Default)]
122pub struct AppConfig {
123    pub save_folder: Option<String>,
124    pub backup_root: Option<String>,
125}
126
127/// Load `app.ini` from the fixed config directory.
128/// Returns default (empty) values if the file does not exist or cannot be read.
129pub fn load_app_config() -> AppConfig {
130    let path = app_ini_path();
131    if !path.exists() {
132        return AppConfig::default();
133    }
134    let content = match std::fs::read_to_string(&path) {
135        Ok(c) => c,
136        Err(_) => return AppConfig::default(),
137    };
138    let mut cfg = AppConfig::default();
139    for line in content.lines() {
140        let line = line.trim();
141        if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
142            continue;
143        }
144        if let Some((key, value)) = line.split_once('=') {
145            let key = key.trim();
146            let val = value.trim();
147            match key {
148                "save_folder" => cfg.save_folder = Some(val.to_string()),
149                "backup_root" => cfg.backup_root = Some(val.to_string()),
150                _ => {}
151            }
152        }
153    }
154    cfg
155}
156
157/// Write the current session paths to `app.ini` in the fixed config directory.
158pub fn save_app_config(save_folder: Option<&str>, backup_root: Option<&str>) {
159    let path = app_ini_path();
160    let mut content = String::from(
161        "# NotAlterra configuration\n\
162         # This file is auto-generated. Edit while the tool is not running.\n\n",
163    );
164    if let Some(s) = save_folder {
165        content.push_str(&format!("save_folder = {s}\n"));
166    }
167    if let Some(r) = backup_root {
168        content.push_str(&format!("backup_root = {r}\n"));
169    }
170    let _ = std::fs::write(&path, content);
171}
172
173/// Ensure a directory exists, creating all parents as needed.
174pub fn ensure_dir(path: PathBuf) {
175    let _ = std::fs::create_dir_all(&path);
176}
177
178/// Return the directory containing the running executable.
179pub fn exe_dir() -> PathBuf {
180    std::env::current_exe()
181        .ok()
182        .and_then(|p| p.parent().map(Path::to_path_buf))
183        .unwrap_or_else(|| PathBuf::from("."))
184}