gled_core/runtime/
persistent_state.rs1use crate::{
2 action::UiAction,
3 storage::{
4 asset::{palette::Palette, project::Project},
5 asset_id::AssetId,
6 git::GitCredentials,
7 },
8};
9use kanal::{Receiver, Sender, bounded};
10use once_cell::sync::Lazy;
11use parking_lot::Mutex;
12use serde::{Deserialize, Serialize};
13use std::{collections::HashMap, path::PathBuf, sync::atomic::AtomicUsize};
14use tracing::info;
15
16static CURRENT: Lazy<Mutex<PersistentStateInner>> =
17 Lazy::new(|| Mutex::new(PersistentStateInner::load()));
18static PERSISTENT_STATE_ID: AtomicUsize = AtomicUsize::new(0);
19static SENDERS: Lazy<Mutex<HashMap<usize, Sender<PersistentStateInner>>>> =
20 Lazy::new(|| Mutex::new(HashMap::new()));
21
22pub(crate) use gled_protocol::state::FPS_LIMIT_MAX;
25
26static FPS_LIMIT_OVERRIDE: Lazy<Option<f32>> = Lazy::new(|| {
30 std::env::var("GLED_FPS_LIMIT")
31 .ok()
32 .and_then(|v| v.parse().ok())
33});
34
35pub struct PersistentState {
36 id: usize,
37 receiver: Receiver<PersistentStateInner>,
38 current: PersistentStateInner,
39}
40
41impl std::fmt::Debug for PersistentState {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.debug_struct("PersistentState").finish_non_exhaustive()
44 }
45}
46
47impl Default for PersistentState {
48 fn default() -> Self {
49 let (sender, receiver) = bounded(1);
50 let id = PERSISTENT_STATE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
51 SENDERS.lock().insert(id, sender);
52
53 Self {
54 id,
55 receiver,
56 current: CURRENT.lock().clone(),
57 }
58 }
59}
60
61#[derive(Serialize, Deserialize, Clone)]
62#[serde(default)]
63pub(crate) struct PersistentStateInner {
64 effects_size: f32,
65 fps_limit: f32,
66 ableton_link_read_only: bool,
67 preview_palette: Option<AssetId<Palette>>,
68 last_project_id: Option<AssetId<Project>>,
69 git_url: String,
70 git_credentials: GitCredentials,
71 remote_access: RemoteAccessSettings,
72}
73
74#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
77#[serde(default)]
78pub struct RemoteAccessSettings {
79 pub enabled: bool,
80 pub https_port: u16,
82 pub http_redirect_port: Option<u16>,
84}
85
86impl Default for RemoteAccessSettings {
87 fn default() -> Self {
88 Self {
89 enabled: false,
90 https_port: 8443,
91 http_redirect_port: Some(8080),
92 }
93 }
94}
95
96impl Default for PersistentStateInner {
97 fn default() -> Self {
98 Self {
99 effects_size: 100.0,
100 fps_limit: 120.0,
101 ableton_link_read_only: false,
102 preview_palette: Default::default(),
103 last_project_id: Default::default(),
104 git_url: "https://gitlab.com/photonenkollektiv/gled2_assets.git".to_string(),
105 git_credentials: Default::default(),
106 remote_access: RemoteAccessSettings::default(),
107 }
108 }
109}
110
111impl PersistentStateInner {
112 fn load() -> Self {
113 Self::path()
114 .and_then(|path| std::fs::read(path).ok())
115 .and_then(|contents| {
116 serde_json::from_slice(&contents)
117 .map_err(|err| tracing::warn!("Could not parse {:?}: {err:?}", Self::path()))
118 .ok()
119 })
120 .unwrap_or_default()
121 }
122
123 fn path() -> Option<PathBuf> {
124 crate::paths::persistent_state_file()
125 }
126
127 fn save(self) {
128 std::thread::Builder::new()
129 .name("gled:persist:save".to_string())
130 .spawn(move || {
131 let Some(path) = Self::path() else {
132 UiAction::Error("Could not determine persistent state path".to_string())
133 .enqueue();
134 return;
135 };
136 let Ok(contents) = serde_json::to_string_pretty(&self) else {
137 UiAction::Error("Could not serialize persistent state".to_string()).enqueue();
138 return;
139 };
140
141 if let Err(err) = std::fs::write(path, contents) {
142 UiAction::Error(format!("Could not persist state: {err:?}")).enqueue();
143 }
144
145 info!("Saved persistent state");
146 })
147 .ok();
148 }
149}
150
151impl Drop for PersistentState {
152 fn drop(&mut self) {
153 SENDERS.lock().remove(&self.id);
154 }
155}
156
157impl PersistentState {
158 pub(crate) fn fps_limit(&self) -> f32 {
160 FPS_LIMIT_OVERRIDE.unwrap_or(if self.current.fps_limit >= FPS_LIMIT_MAX {
161 f32::INFINITY
162 } else {
163 self.current.fps_limit
164 })
165 }
166
167 pub(crate) fn fps_limit_setting(&self) -> f32 {
170 self.current.fps_limit
171 }
172
173 pub(crate) fn set_fps_limit(&mut self, limit: f32) {
174 self.current.fps_limit = limit;
175 }
176
177 pub(crate) fn ableton_link_read_only(&self) -> bool {
178 self.current.ableton_link_read_only
179 }
180
181 pub(crate) fn set_ableton_link_read_only(&mut self, read_only: bool) {
182 self.current.ableton_link_read_only = read_only;
183 }
184
185 pub fn effects_size(&self) -> f32 {
186 self.current.effects_size
187 }
188
189 pub(crate) fn git_url(&self) -> String {
190 self.current.git_url.clone()
191 }
192
193 pub(crate) fn set_git_url(&mut self, url: String) {
194 self.current.git_url = url;
195 }
196
197 pub fn remote_access(&self) -> &RemoteAccessSettings {
198 &self.current.remote_access
199 }
200
201 pub fn set_remote_access(&mut self, settings: RemoteAccessSettings) {
202 self.current.remote_access = settings;
203 }
204
205 pub(crate) fn git_credentials(&self) -> GitCredentials {
206 self.current.git_credentials.clone()
207 }
208
209 pub(crate) fn git_credentials_mut(&mut self) -> &mut GitCredentials {
210 &mut self.current.git_credentials
211 }
212
213 pub(crate) fn last_project_id(&self) -> Option<AssetId<Project>> {
214 self.current.last_project_id
215 }
216
217 pub(crate) fn set_last_project_id(&mut self, id: AssetId<Project>) {
218 self.current.last_project_id = Some(id);
219 }
220
221 pub fn preview_palette(&self) -> Option<AssetId<Palette>> {
222 self.current.preview_palette
223 }
224
225 #[cfg_attr(feature = "profiling", profiling::function)]
226 pub(crate) fn update(&mut self) {
227 while let Ok(Some(new)) = self.receiver.try_recv() {
228 self.current = new;
229 }
230 }
231
232 pub fn save(&self) {
233 for (id, sender) in SENDERS.lock().iter() {
234 if id == &self.id {
235 continue;
236 }
237
238 sender
239 .send(self.current.clone())
240 .expect("Could not send PersistentState");
241 }
242
243 CURRENT.lock().clone_from(&self.current);
244
245 self.current.clone().save();
247 }
248}