1use std::path::{Path, PathBuf};
4
5use once_cell::sync::OnceCell;
6
7use crate::errors::{AppStateError, AppStateResult};
8
9const CLIENT_ID_FILE: &str = "client_id";
10
11#[derive(Debug, Clone)]
13pub struct LauncherPaths {
14 pub name: String,
15 pub data_dir: PathBuf,
16 pub config_dir: PathBuf,
17 pub cache_dir: PathBuf,
18}
19
20static PATHS: OnceCell<LauncherPaths> = OnceCell::new();
21static CLIENT_ID: OnceCell<String> = OnceCell::new();
22
23pub struct AppState;
25
26impl AppState {
27 pub fn init(name: impl Into<String>) -> AppStateResult<()> {
33 let name = name.into();
34 let data_dir = dirs::data_dir()
35 .ok_or(AppStateError::MissingPlatformDir("data"))?
36 .join(&name);
37 let config_dir = dirs::config_dir()
38 .ok_or(AppStateError::MissingPlatformDir("config"))?
39 .join(&name);
40 let cache_dir = dirs::cache_dir()
41 .ok_or(AppStateError::MissingPlatformDir("cache"))?
42 .join(&name);
43 PATHS
44 .set(LauncherPaths { name, data_dir, config_dir, cache_dir })
45 .map_err(|_| AppStateError::AlreadyInitialized)
46 }
47
48 pub fn paths() -> &'static LauncherPaths {
53 PATHS.get().expect(
54 "AppState::init(\"<launcher-name>\") must be called once at startup",
55 )
56 }
57
58 pub fn name() -> &'static str {
60 &Self::paths().name
61 }
62
63 pub fn data_dir() -> &'static Path {
65 &Self::paths().data_dir
66 }
67
68 pub fn config_dir() -> &'static Path {
70 &Self::paths().config_dir
71 }
72
73 pub fn cache_dir() -> &'static Path {
75 &Self::paths().cache_dir
76 }
77
78 pub fn app_version() -> &'static str {
80 env!("CARGO_PKG_VERSION")
81 }
82
83 pub fn client_id() -> &'static str {
91 CLIENT_ID.get_or_init(|| {
92 let path = Self::config_dir().join(CLIENT_ID_FILE);
93
94 if let Ok(raw) = std::fs::read_to_string(&path) {
96 let trimmed = raw.trim();
97 if !trimmed.is_empty() {
98 return trimmed.to_string();
99 }
100 }
101
102 let fresh = generate_uuid_v4();
103
104 if let Some(parent) = path.parent() {
107 let _ = std::fs::create_dir_all(parent);
108 }
109 if let Err(e) = std::fs::write(&path, &fresh) {
110 crate::trace_debug!(
111 error = %e,
112 path = %path.display(),
113 "Could not persist client_id; continuing with in-memory value"
114 );
115 }
116
117 fresh
118 })
119 }
120}
121
122fn generate_uuid_v4() -> String {
124 let mut bytes = [0u8; 16];
125 for b in bytes.iter_mut() {
126 *b = fastrand::u8(..);
127 }
128 bytes[6] = (bytes[6] & 0x0f) | 0x40;
130 bytes[8] = (bytes[8] & 0x3f) | 0x80;
132
133 format!(
134 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
135 bytes[0], bytes[1], bytes[2], bytes[3],
136 bytes[4], bytes[5],
137 bytes[6], bytes[7],
138 bytes[8], bytes[9],
139 bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
140 )
141}