1use 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
21pub const CONFIG_FOLDER: &'static str = "streamduck";
23
24pub const DEFAULT_FRAME_RATE: u32 = 100;
26pub const DEFAULT_RECONNECT_TIME: f32 = 1.0;
28pub const FONTS_FOLDER: &'static str = "fonts";
30pub const DEVICE_CONFIG_FOLDER: &'static str = "devices";
32pub const PLUGINS_FOLDER: &'static str = "plugins";
34pub const PLUGINS_SETTINGS_FILE: &'static str = "global.json";
36pub const CONFIG_FILE: &'static str = "config.toml";
38
39pub type UniqueDeviceConfig = Arc<RwLock<DeviceConfig>>;
41
42fn 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
56fn 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#[derive(Serialize, Deserialize, Default, Debug)]
72pub struct Config {
73 frame_rate: Option<u32>,
75 reconnect_rate: Option<f32>,
77 device_config_path: Option<PathBuf>,
79 plugin_path: Option<PathBuf>,
81 plugin_settings_path: Option<PathBuf>,
83 font_path: Option<PathBuf>,
85
86 config_dir: Option<PathBuf>,
88 data_dir: Option<PathBuf>,
90
91 autosave: Option<bool>,
93
94 plugin_compatibility_checks: Option<bool>,
96
97 #[serde(skip)]
99 pub plugin_settings: RwLock<HashMap<String, Value>>,
100
101 #[serde(skip)]
103 pub loaded_configs: RwLock<HashMap<String, UniqueDeviceConfig>>,
104
105 #[serde(skip)]
107 pub loaded_images: RwLock<HashMap<String, ImageCollection>>
108}
109
110#[allow(dead_code)]
111impl Config {
112 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 pub fn frame_rate(&self) -> u32 {
162 self.frame_rate.unwrap_or(DEFAULT_FRAME_RATE)
163 }
164
165 pub fn reconnect_rate(&self) -> f32 {
167 self.reconnect_rate.unwrap_or(DEFAULT_RECONNECT_TIME)
168 }
169
170 pub fn autosave(&self) -> bool {
172 self.autosave.unwrap_or(true)
173 }
174
175 pub fn plugin_compatibility_checks(&self) -> bool {
177 self.plugin_compatibility_checks.unwrap_or(true)
178 }
179
180 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 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 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 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 pub fn data_dir(&self) -> &PathBuf {
221 &self.data_dir.as_ref().expect("data_dir not available")
222 }
223
224 pub fn config_dir(&self) -> &PathBuf {
226 &self.config_dir.as_ref().expect("config_dir not available")
227 }
228
229 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 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 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 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 pub async fn reload_device_config(&self, serial: &str) -> Result<(), ConfigError> {
266 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 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 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 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 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 pub async fn get_device_config(&self, serial: &str) -> Option<UniqueDeviceConfig> {
366 self.loaded_configs.read().await.get(serial).cloned()
367 }
368
369 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 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 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 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 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 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 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 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 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 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 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 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 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 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
528pub trait PluginConfig {
530 const NAME: &'static str;
532}
533
534#[derive(Debug)]
536pub enum ConfigError {
537 IoError(std::io::Error),
539 ParseError(serde_json::Error),
541 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#[derive(Serialize, Deserialize, Debug, Clone, Default)]
559pub struct DeviceConfig {
560 pub vid: u16,
562 pub pid: u16,
564 pub serial: String,
566 pub brightness: u8,
568 pub layout: RawButtonPanel,
570 pub images: HashMap<String, SDSerializedImage>,
572 pub plugin_data: HashMap<String, Value>,
574 #[serde(skip)]
575 pub commit_time: Option<Instant>,
577 #[serde(skip)]
578 pub dirty_state: bool
580}
581
582impl DeviceConfig {
583 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 pub fn is_dirty(&self) -> bool {
597 self.dirty_state
598 }
599
600 pub fn mark_clean(&mut self) {
602 self.dirty_state = false
603 }
604
605 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 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 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 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 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 let mut path = config.device_config_path();
667 fs::create_dir_all(&path).await.ok();
668 path.push(format!("{}.json", serial));
669
670 if path.exists() {
672 std::fs::remove_file(&path).unwrap();
673 }
674
675 assert_eq!(device_conf.dirty_state, true);
677 let device_conf = Arc::new(RwLock::new(device_conf));
678
679 config.write_to_filesystem(device_conf).await.unwrap();
681
682 assert_eq!(path.exists(), true);
684
685 std::fs::remove_file(&path).unwrap();
687
688 }
691}