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 match crate::hosts::blocked_launcher_domains(&[]) {
50 Ok(entries) if !entries.is_empty() => crate::trace_warn!(
51 entries = %entries.join(", "),
52 "Hosts file intercepts domains the launcher needs"
53 ),
54 Err(err) => crate::trace_debug!(error = %err, "Could not read the hosts file"),
55 _ => {}
56 }
57
58 Ok(())
59 }
60
61 pub fn paths() -> &'static LauncherPaths {
66 PATHS.get().expect(
67 "AppState::init(\"<launcher-name>\") must be called once at startup",
68 )
69 }
70
71 pub fn name() -> &'static str {
73 &Self::paths().name
74 }
75
76 pub fn data_dir() -> &'static Path {
78 &Self::paths().data_dir
79 }
80
81 pub fn config_dir() -> &'static Path {
83 &Self::paths().config_dir
84 }
85
86 pub fn cache_dir() -> &'static Path {
88 &Self::paths().cache_dir
89 }
90
91 pub fn app_version() -> &'static str {
93 env!("CARGO_PKG_VERSION")
94 }
95
96 pub fn client_id() -> &'static str {
104 CLIENT_ID.get_or_init(|| {
105 let path = Self::config_dir().join(CLIENT_ID_FILE);
106
107 if let Ok(raw) = std::fs::read_to_string(&path) {
109 let trimmed = raw.trim();
110 if !trimmed.is_empty() {
111 return trimmed.to_string();
112 }
113 }
114
115 let fresh = generate_uuid_v4();
116
117 if let Some(parent) = path.parent() {
120 let _ = std::fs::create_dir_all(parent);
121 }
122 if let Err(e) = std::fs::write(&path, &fresh) {
123 crate::trace_debug!(
124 error = %e,
125 path = %path.display(),
126 "Could not persist client_id; continuing with in-memory value"
127 );
128 }
129
130 fresh
131 })
132 }
133}
134
135fn generate_uuid_v4() -> String {
137 let mut bytes = [0u8; 16];
138 for b in bytes.iter_mut() {
139 *b = fastrand::u8(..);
140 }
141 bytes[6] = (bytes[6] & 0x0f) | 0x40;
143 bytes[8] = (bytes[8] & 0x3f) | 0x80;
145
146 format!(
147 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
148 bytes[0], bytes[1], bytes[2], bytes[3],
149 bytes[4], bytes[5],
150 bytes[6], bytes[7],
151 bytes[8], bytes[9],
152 bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
153 )
154}