Skip to main content

gled_core/runtime/
persistent_state.rs

1use 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
22/// Highest selectable output frame rate limit. Choosing it, or any stored value
23/// above it, means unlimited.
24pub(crate) use gled_protocol::state::FPS_LIMIT_MAX;
25
26/// Optional override for the FPS limit, read once from the `GLED_FPS_LIMIT`
27/// environment variable. Used by performance tests to raise the limiter above
28/// the persisted value without mutating the user's `~/.gled` config.
29static 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/// Whether browsers and other devices may connect to this machine's engine,
75/// and on which ports.
76#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
77#[serde(default)]
78pub struct RemoteAccessSettings {
79    pub enabled: bool,
80    /// The HTTPS port that serves the interface.
81    pub https_port: u16,
82    /// A plain HTTP port that sends browsers to HTTPS, if any.
83    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    /// Output frame rate limit, `f32::INFINITY` for unlimited.
159    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    /// The output frame rate limit as set, without the environment override
168    /// and with `FPS_LIMIT_MAX` for unlimited.
169    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        // also save to disk
246        self.current.clone().save();
247    }
248}