streamduck_core/
config.rs

1//! Core and device configs
2use std::collections::HashMap;
3use tokio::fs;
4use dirs;
5use std::ops::Deref;
6use std::time::{Instant, Duration};
7use std::path::PathBuf;
8use std::sync::{Arc};
9use image::{DynamicImage};
10use serde::{Serialize, Deserialize};
11use serde::de::DeserializeOwned;
12use crate::core::RawButtonPanel;
13use serde_json::Value;
14use streamdeck::Kind;
15use tokio::sync::RwLock;
16use crate::ImageCollection;
17use crate::images::{SDImage, SDSerializedImage};
18use crate::util::{hash_image, hash_str};
19use crate::thread::util::resize_for_streamdeck;
20
21/// Default folder name
22pub const CONFIG_FOLDER: &'static str = "streamduck";
23
24/// Default frame rate to use
25pub const DEFAULT_FRAME_RATE: u32 = 100;
26/// Default reconnect interval
27pub const DEFAULT_RECONNECT_TIME: f32 = 1.0;
28/// Name of the fonts folder
29pub const FONTS_FOLDER: &'static str = "fonts";
30/// Name of the device config folder
31pub const DEVICE_CONFIG_FOLDER: &'static str = "devices";
32/// Name of the plugins folder
33pub const PLUGINS_FOLDER: &'static str = "plugins";
34/// Name of the plugin settings file
35pub const PLUGINS_SETTINGS_FILE: &'static str = "global.json";
36/// Name of the config file
37pub const CONFIG_FILE: &'static str = "config.toml";
38
39/// Reference counted [DeviceConfig]
40pub type UniqueDeviceConfig = Arc<RwLock<DeviceConfig>>;
41
42/// Loads config directory (eg. $HOME/.config/streamduck) or returns the current dir
43fn config_dir() -> PathBuf {
44    match dirs::config_dir() {
45        Some(mut dir) => {
46            dir.push(CONFIG_FOLDER);
47            dir
48        },
49        None => {
50            log::warn!("config_dir not available on this system. Using executable path.");
51            PathBuf::new()
52        }
53    }
54}
55
56/// Loads data directory (eg. $HOME/.local/share/streamduck) or returns the current dir
57fn data_dir() -> PathBuf {
58    match dirs::data_dir() {
59        Some(mut dir) => {
60            dir.push(CONFIG_FOLDER);
61            dir
62        },
63        None => {
64            log::warn!("data_dir not available on this system. Using executable path.");
65            PathBuf::new()
66        }
67    }
68}
69
70/// Struct to keep daemon settings
71#[derive(Serialize, Deserialize, Default, Debug)]
72pub struct Config {
73    /// Frame rate
74    frame_rate: Option<u32>,
75    /// Frequency of checks for disconnected devices
76    reconnect_rate: Option<f32>,
77    /// Path to device configs
78    device_config_path: Option<PathBuf>,
79    /// Path to plugins
80    plugin_path: Option<PathBuf>,
81    /// Path to plugin settings json
82    plugin_settings_path: Option<PathBuf>,
83    /// Path to fonts
84    font_path: Option<PathBuf>,
85
86    /// Config folder
87    config_dir: Option<PathBuf>,
88    /// Data folder
89    data_dir: Option<PathBuf>,
90
91    /// Autosave device configuration
92    autosave: Option<bool>,
93
94    /// If plugin compatibility checks should be performed
95    plugin_compatibility_checks: Option<bool>,
96
97    /// Currently loaded plugin settings
98    #[serde(skip)]
99    pub plugin_settings: RwLock<HashMap<String, Value>>,
100
101    /// Currently loaded device configs
102    #[serde(skip)]
103    pub loaded_configs: RwLock<HashMap<String, UniqueDeviceConfig>>,
104
105    /// Currently loaded image collections
106    #[serde(skip)]
107    pub loaded_images: RwLock<HashMap<String, ImageCollection>>
108}
109
110#[allow(dead_code)]
111impl Config {
112    /// Reads config and retrieves config struct
113    pub async fn get(custom_config_path: Option<PathBuf>) -> Config {
114        let config_dir = config_dir();
115        let data_dir = data_dir();
116
117        let path: PathBuf = custom_config_path.unwrap_or_else(|| {
118            let mut dir = config_dir.clone();
119            dir.push(CONFIG_FILE);
120            dir
121        });
122
123        log::info!("Config path: {}", path.display());
124
125        let mut config: Config = match fs::read_to_string(path).await {
126            Ok(content) => {
127                match toml::from_str(&content) {
128                    Ok(config) => config,
129                    Err(e) => {
130                        log::error!("Config error: {}", e);
131                        log::warn!("Using default configuration");
132                        Default::default()
133                    }
134                }
135            },
136            Err(e) => {
137                match e.kind() {
138                    std::io::ErrorKind::NotFound => log::warn!("The config file was not found. Did you create the file yet?"),
139                    _ => log::warn!("Could not access config file. Error: \"{}\".", e)
140                }
141                log::warn!("Using default configuration");
142                Default::default()
143            }
144        };
145
146        if config.data_dir == None {
147            config.data_dir = Some(data_dir);
148        }
149
150        if config.config_dir == None {
151            config.config_dir = Some(config_dir);
152        }
153
154        config.load_plugin_settings().await;
155
156        log::debug!("config: {:#?}", config);
157        config
158    }
159
160    /// Pool rate, defaults to [DEFAULT_FRAME_RATE] if not set
161    pub fn frame_rate(&self) -> u32 {
162        self.frame_rate.unwrap_or(DEFAULT_FRAME_RATE)
163    }
164
165    /// Reconnect rate, defaults to [DEFAULT_RECONNECT_TIME] if not set
166    pub fn reconnect_rate(&self) -> f32 {
167        self.reconnect_rate.unwrap_or(DEFAULT_RECONNECT_TIME)
168    }
169
170    /// Autosave option, defaults to true if not set
171    pub fn autosave(&self) -> bool {
172        self.autosave.unwrap_or(true)
173    }
174
175    /// Plugin compatibility checks, defaults to true if not set
176    pub fn plugin_compatibility_checks(&self) -> bool {
177        self.plugin_compatibility_checks.unwrap_or(true)
178    }
179
180    /// Device config path, defaults to [data_dir]/[DEVICE_CONFIG_FOLDER] or [DEVICE_CONFIG_FOLDER] if not set
181    pub fn device_config_path(&self) -> PathBuf {
182        self.device_config_path.clone().unwrap_or_else(|| {
183                let mut dir = self.data_dir().clone();
184                dir.push(DEVICE_CONFIG_FOLDER);
185                dir
186            }
187        )
188    }
189
190    /// Plugin folder path, defaults to [config_dir]/[PLUGINS_FOLDER] or [PLUGINS_FOLDER] if not set
191    pub fn plugin_path(&self) -> PathBuf {
192        self.plugin_path.clone().unwrap_or_else(|| {
193                let mut dir = self.config_dir().clone();
194                dir.push(PLUGINS_FOLDER);
195                dir
196            }
197        )
198    }
199
200    /// Fonts folder path, defaults to [config_dir]/[FONTS_FOLDER] or [FONTS_FOLDER] if not set
201    pub fn font_path(&self) -> PathBuf {
202        self.font_path.clone().unwrap_or_else(|| {
203                let mut dir = self.config_dir().clone();
204                dir.push(FONTS_FOLDER);
205                dir
206            }
207        )
208    }
209
210    /// Plugin settings file path, defaults to [data_dir]/[PLUGINS_SETTINGS_FILE] or [PLUGINS_SETTINGS_FILE] if not set
211    pub fn plugin_settings_path(&self) -> PathBuf {
212        self.plugin_settings_path.clone().unwrap_or_else(|| {
213                let mut dir = self.data_dir().clone();
214                dir.push(PLUGINS_SETTINGS_FILE);
215                dir
216        })
217    }
218
219    /// Data path, defaults to [dirs::data_dir()] if not set
220    pub fn data_dir(&self) -> &PathBuf {
221        &self.data_dir.as_ref().expect("data_dir not available")
222    }
223
224    /// Config path, defaults to [dirs::config_dir()] if not set
225    pub fn config_dir(&self) -> &PathBuf {
226        &self.config_dir.as_ref().expect("config_dir not available")
227    }
228
229    /// Loads plugin settings from file
230    pub async fn load_plugin_settings(&self) {
231        if let Ok(settings) = fs::read_to_string(self.plugin_settings_path()).await {
232            let mut lock = self.plugin_settings.write().await;
233
234            match serde_json::from_str(&settings) {
235                Ok(vals) => *lock = vals,
236                Err(err) => log::error!("Failed to parse plugin settings: {:?}", err),
237            }
238        }
239    }
240
241    /// Retrieves plugin settings if it exists
242    pub async fn get_plugin_settings<T: PluginConfig + DeserializeOwned>(&self) -> Option<T> {
243        let lock = self.plugin_settings.read().await;
244        Some(serde_json::from_value(lock.get(T::NAME)?.clone()).ok()?)
245    }
246
247    /// Sets plugin settings
248    pub async fn set_plugin_settings<T: PluginConfig + Serialize>(&self, value: T) {
249        let mut lock = self.plugin_settings.write().await;
250        lock.insert(T::NAME.to_string(), serde_json::to_value(value).unwrap());
251        drop(lock);
252
253        self.write_plugin_settings().await;
254    }
255
256    /// Writes plugin settings to file
257    pub async fn write_plugin_settings(&self) {
258        let lock = self.plugin_settings.read().await;
259        if let Err(err) = fs::write(self.plugin_settings_path(), serde_json::to_string(lock.deref()).unwrap()).await {
260            log::error!("Failed to write plugin settings: {:?}", err);
261        }
262    }
263
264    /// Reloads device config for specified serial
265    pub async fn reload_device_config(&self, serial: &str) -> Result<(), ConfigError> {
266        // Clearing image collection to make sure it's fresh for reload
267        self.get_image_collection(serial).await.write().await.clear();
268
269        let mut devices = self.loaded_configs.write().await;
270
271        let mut path = self.device_config_path();
272        path.push(format!("{}.json", serial));
273
274        let content = fs::read_to_string(path).await?;
275        let device = serde_json::from_str::<DeviceConfig>(&content)?;
276
277
278        if let Some(device_config) = devices.get(serial) {
279            *device_config.write().await = device;
280        } else {
281            devices.insert(serial.to_string(), Arc::new(RwLock::new(device)));
282        }
283
284        self.update_collection(devices.get(serial).unwrap()).await;
285
286        Ok(())
287    }
288
289    /// Reloads all device configs
290    pub async fn reload_device_configs(&self) -> Result<(), ConfigError> {
291        let mut devices = self.loaded_configs.write().await;
292
293        let mut dir = fs::read_dir(self.device_config_path()).await?;
294
295        while let Some(item) = dir.next_entry().await? {
296            if item.path().is_file() {
297                if let Some(extension) = item.path().extension() {
298                    if extension == "json" {
299                        let content = fs::read_to_string(item.path()).await?;
300
301                        let device = serde_json::from_str::<DeviceConfig>(&content)?;
302                        let serial = device.serial.to_string();
303
304                        // Clearing image collection so it's fresh for reload
305                        self.get_image_collection(&device.serial).await.write().await.clear();
306                        if let Some(device_config) = devices.get(&serial) {
307                            *device_config.write().await = device;
308                        } else {
309                            devices.insert(serial.to_string(), Arc::new(RwLock::new(device)));
310                        }
311
312                        self.update_collection(devices.get(&serial).unwrap()).await;
313                    }
314                }
315            }
316        }
317
318        Ok(())
319    }
320
321    /// Saves device config for specified serial
322    pub async fn save_device_config(&self, serial: &str) -> Result<(), ConfigError> {
323        let devices = self.loaded_configs.read().await;
324
325        if let Some(device) = devices.get(serial).cloned() {
326            self.update_collection(&device).await;
327            let path = self.device_config_path();
328            fs::create_dir_all(&path).await.ok();
329            self.write_to_filesystem(device).await?;
330
331            Ok(())
332        } else {
333            Err(ConfigError::DeviceNotFound)
334        }
335    }
336
337    /// Saves device configs for all serials
338    pub async fn save_device_configs(&self) -> Result<(), ConfigError> {
339        let devices = self.loaded_configs.read().await;
340
341        let path = self.device_config_path();
342        fs::create_dir_all(&path).await.ok();
343
344        for (_, device) in devices.iter() {
345            let device = device.clone();
346            self.update_collection(&device).await;
347            self.write_to_filesystem(device).await?
348        }
349
350        Ok(())
351    }
352
353    async fn write_to_filesystem(&self, device: UniqueDeviceConfig) -> Result<(), ConfigError> {
354        let mut path = self.device_config_path();
355        let mut device_conf = device.write().await;
356        path.push(format!("{}.json", device_conf.serial));
357        fs::write(path, serde_json::to_string(device_conf.deref()).unwrap()).await?;
358
359        device_conf.mark_clean();
360
361        Ok(())
362    }
363
364    /// Retrieves device config for specified serial
365    pub async fn get_device_config(&self, serial: &str) -> Option<UniqueDeviceConfig> {
366        self.loaded_configs.read().await.get(serial).cloned()
367    }
368
369    /// Sets device config for specified serial
370    pub async fn set_device_config(&self, serial: &str, config: DeviceConfig) {
371        let mut handle = self.loaded_configs.write().await;
372
373        if let Some(device_config) = handle.get(serial) {
374            *device_config.write().await = config;
375        } else {
376            handle.insert(serial.to_string(), Arc::new(RwLock::new(config)));
377        }
378    }
379
380    /// Gets an array of all device configs
381    pub async fn get_all_device_configs(&self) -> Vec<UniqueDeviceConfig> {
382        self.loaded_configs.read().await.values().map(|x| x.clone()).collect()
383    }
384
385    /// Disables a device config, so it will not be loaded by default
386    pub async fn disable_device_config(&self, serial: &str) -> bool {
387        let path = self.device_config_path();
388
389        let mut initial_path = path.clone();
390        initial_path.push(format!("{}.json", serial));
391
392        let mut new_path = path.clone();
393        new_path.push(format!("{}.json_disabled", serial));
394
395        fs::rename(initial_path, new_path).await.is_ok()
396    }
397
398    /// Restores device config if it exists
399    pub async fn restore_device_config(&self, serial: &str) -> bool {
400        let path = self.device_config_path();
401
402        let mut initial_path = path.clone();
403        initial_path.push(format!("{}.json_disabled", serial));
404
405        let mut new_path = path.clone();
406        new_path.push(format!("{}.json", serial));
407
408        fs::rename(initial_path, new_path).await.is_ok()
409    }
410
411    /// Adds base64 image to device config image collection
412    pub async fn add_image(&self, serial: &str, image: String) -> Option<String> {
413        if let Some(config) = self.get_device_config(serial).await {
414            let mut config_handle = config.write().await;
415            let identifier = hash_str(&image);
416
417            if let Ok(image) = SDImage::from_base64(&image, config_handle.kind().image_size()).await {
418                config_handle.images.insert(identifier.clone(), image.into());
419                drop(config_handle);
420
421                self.update_collection(&config).await;
422                Some(identifier)
423            } else {
424                None
425            }
426        } else {
427            None
428        }
429    }
430
431    /// Encodes image to base64 and adds it to device config image collection
432    pub async fn add_image_encode(&self, serial: &str, image: DynamicImage) -> Option<String> {
433        if let Some(config) = self.get_device_config(serial).await {
434            let mut config_handle = config.write().await;
435            let serialized_image = SDImage::SingleImage(resize_for_streamdeck(config_handle.kind().image_size(), image)).into();
436            let identifier = hash_image(&serialized_image);
437            config_handle.images.insert(identifier.clone(), serialized_image);
438            drop(config_handle);
439
440            self.update_collection(&config).await;
441            return Some(identifier);
442        }
443
444        None
445    }
446
447    /// Gets images from device config
448    pub async fn get_images(&self, serial: &str) -> Option<HashMap<String, SDSerializedImage>> {
449        if let Some(config) = self.get_device_config(serial).await {
450            let config_handle = config.read().await;
451            Some(config_handle.images.clone())
452        } else {
453            None
454        }
455    }
456
457    /// Removes image from device config
458    pub async fn remove_image(&self, serial: &str, identifier: &str) -> bool {
459        if let Some(config) = self.get_device_config(serial).await {
460            let mut config_handle = config.write().await;
461            config_handle.images.remove(identifier);
462            drop(config_handle);
463
464            self.remove_from_collection(serial, identifier).await;
465            true
466        } else {
467            false
468        }
469    }
470
471    /// Syncs images with core
472    pub async fn sync_images(&self, serial: &str) {
473        if let Some(config) = self.get_device_config(serial).await {
474            self.update_collection(&config).await;
475        }
476    }
477
478    /// Retrieves image collection for device if device exists
479    pub async fn get_image_collection(&self, serial: &str) -> ImageCollection {
480        let mut handle = self.loaded_images.write().await;
481
482        if let Some(collection) = handle.get(serial) {
483            collection.clone()
484        } else {
485            let collection: ImageCollection = Default::default();
486            handle.insert(serial.to_string(), collection.clone());
487            collection
488        }
489    }
490
491    /// For making sure image collections strictly follow device config
492    async fn update_collection(&self, device_config: &UniqueDeviceConfig) {
493        let mut device_config = device_config.write().await;
494        let mut handle = self.loaded_images.write().await;
495
496        if let Some(collection) = handle.get_mut(&device_config.serial) {
497            let mut collection_handle = collection.write().await;
498
499            // Adding missing images from device config
500            for (key, image) in &device_config.images {
501                if !collection_handle.contains_key(key) {
502                    if let Ok(image) = image.try_into() {
503                        collection_handle.insert(key.to_string(), image);
504                    }
505                }
506            }
507
508            // Adding any images in collection to device config
509            for (key, image) in collection_handle.iter() {
510                if !device_config.images.contains_key(key) {
511                    device_config.images.insert(key.to_string(), image.into());
512                }
513            }
514        }
515    }
516
517    /// For removing images from image collections
518    async fn remove_from_collection(&self, serial: &str, identifier: &str) {
519        let mut handle = self.loaded_images.write().await;
520
521        if let Some(collection) = handle.get_mut(serial) {
522            let mut collection_handle = collection.write().await;
523            collection_handle.remove(identifier);
524        }
525    }
526}
527
528/// Plugin Config trait for serialization and deserialization methods
529pub trait PluginConfig {
530    /// Name of the plugin in the config
531    const NAME: &'static str;
532}
533
534/// Error enum for various errors while loading and parsing configs
535#[derive(Debug)]
536pub enum ConfigError {
537    /// Failed to read/write the config
538    IoError(std::io::Error),
539    /// Failed to parse the config
540    ParseError(serde_json::Error),
541    /// Device wasn't found
542    DeviceNotFound
543}
544
545impl From<std::io::Error> for ConfigError {
546    fn from(err: std::io::Error) -> Self {
547        ConfigError::IoError(err)
548    }
549}
550
551impl From<serde_json::Error> for ConfigError {
552    fn from(err: serde_json::Error) -> Self {
553        ConfigError::ParseError(err)
554    }
555}
556
557/// Device config struct
558#[derive(Serialize, Deserialize, Debug, Clone, Default)]
559pub struct DeviceConfig {
560    /// Vendor ID
561    pub vid: u16,
562    /// Product ID
563    pub pid: u16,
564    /// Serial number
565    pub serial: String,
566    /// Brightness of the display
567    pub brightness: u8,
568    /// Root panel that should be loaded by default
569    pub layout: RawButtonPanel,
570    /// Image collection
571    pub images: HashMap<String, SDSerializedImage>,
572    /// Device-related plugin data
573    pub plugin_data: HashMap<String, Value>,
574    #[serde(skip)]
575    /// Last time the config was committed
576    pub commit_time: Option<Instant>,
577    #[serde(skip)]
578    /// If config is dirty
579    pub dirty_state: bool
580}
581
582impl DeviceConfig {
583    /// Gets kind of the device
584    pub fn kind(&self) -> Kind {
585        match self.pid {
586            streamdeck::pids::ORIGINAL_V2 => Kind::OriginalV2,
587            streamdeck::pids::MINI => Kind::Mini,
588            streamdeck::pids::MK2 => Kind::Mk2,
589            streamdeck::pids::XL => Kind::Xl,
590
591            _ => Kind::Original,
592        }
593    }
594
595    /// check if there are config changes
596    pub fn is_dirty(&self) -> bool {
597        self.dirty_state
598    }
599
600    /// remove dirty state
601    pub fn mark_clean(&mut self) {
602        self.dirty_state = false
603    }
604
605    /// duration from now to the last commit
606    pub fn commit_duration(&self) -> Duration {
607        Instant::now().duration_since(self.commit_time.unwrap_or(Instant::now()))
608    }
609}
610
611#[cfg(test)]
612mod tests {
613    use super::*;
614
615    #[tokio::test]
616    async fn config_sys_config_dir() {
617        // check if config dir gets created
618        let config = Config::get(None).await;
619        assert_ne!(config.config_dir, None)
620    }
621
622    #[tokio::test]
623    async fn config_sys_data_dir() {
624        // check if data dir gets created
625        let config = Config::get(None).await;
626        assert_ne!(config.data_dir, None)
627    }
628
629    #[tokio::test]
630    async fn config_mark_clean() {
631        // simulate a changed config
632        let mut device_conf = DeviceConfig {
633            vid: Default::default(),
634            pid: Default::default(),
635            serial: String::from("TestSerial1"),
636            brightness: Default::default(),
637            layout: Default::default(),
638            images: Default::default(),
639            plugin_data: Default::default(),
640            commit_time: Default::default(),
641            dirty_state: true
642        };
643        assert_eq!(device_conf.dirty_state, true);
644        device_conf.mark_clean();
645        assert_eq!(device_conf.dirty_state, false);
646    }
647
648    #[tokio::test]
649    async fn config_filesystem_writing() { 
650        let config = Config::get(None).await;
651        // simulate a changed config
652        let device_conf = DeviceConfig {
653            vid: Default::default(),
654            pid: Default::default(),
655            serial: String::from("TestSerial1"),
656            brightness: Default::default(),
657            layout: Default::default(),
658            images: Default::default(),
659            plugin_data: Default::default(),
660            commit_time: Default::default(),
661            dirty_state: true
662        };
663        let serial = device_conf.serial.clone();
664
665        // get the path
666        let mut path = config.device_config_path();
667        fs::create_dir_all(&path).await.ok();
668        path.push(format!("{}.json", serial));
669
670        // delete device data if it exists (clean start)
671        if path.exists() {
672            std::fs::remove_file(&path).unwrap();
673        }
674
675        // is the device config dirty?
676        assert_eq!(device_conf.dirty_state, true);
677        let device_conf = Arc::new(RwLock::new(device_conf));
678
679        // write to the filesystem
680        config.write_to_filesystem(device_conf).await.unwrap();
681
682        // does the path exist?
683        assert_eq!(path.exists(), true);
684
685        // clean up
686        std::fs::remove_file(&path).unwrap();
687
688        // TODO: check if dirty_state is updated
689
690    }
691}