Skip to main content

tauri_plugin_config_manager/
lib.rs

1use notify::{EventKind, RecommendedWatcher, Watcher};
2use std::{
3    path::Path,
4    sync::{Arc, Mutex},
5    time::{Duration, Instant},
6};
7use tauri::{
8    plugin::{Builder, TauriPlugin},
9    Emitter, Manager, Runtime,
10};
11
12mod commands;
13#[cfg(desktop)]
14mod desktop;
15mod error;
16mod models;
17
18pub use error::{Error, Result};
19pub use models::*;
20
21#[cfg(desktop)]
22use desktop::ConfigManager;
23
24pub const CONFIG_CHANGED_EVENT: &str = "config-changed";
25
26/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the config-manager APIs.
27pub trait ConfigManagerExt<R: Runtime> {
28    fn config_manager(&self) -> &ConfigManager<R>;
29}
30
31impl<R: Runtime, T: Manager<R>> crate::ConfigManagerExt<R> for T {
32    fn config_manager(&self) -> &ConfigManager<R> {
33        self.state::<ConfigManager<R>>().inner()
34    }
35}
36
37fn should_handle_event(event: &notify::Event, watched_file_path: &Path) -> bool {
38    let is_relevant_kind = matches!(
39        event.kind,
40        EventKind::Create(_) | EventKind::Modify(_)
41    );
42
43    is_relevant_kind && event.paths.iter().any(|path| path == watched_file_path)
44}
45
46fn watch_config_file<R: Runtime + 'static>(
47    app: &tauri::AppHandle<R>,
48    watched_file_path: std::path::PathBuf,
49) -> Box<dyn FnMut(notify::Result<notify::Event>) + Send + 'static> {
50    let app_handle = app.clone();
51    let debounce_window = Duration::from_millis(250);
52    let last_refresh = Arc::new(Mutex::new(None::<Instant>));
53    Box::new(move |res: notify::Result<notify::Event>| {
54        let Ok(event) = res else {
55            if let Err(e) = res {
56                eprintln!(
57                    "[Config Watcher Callback] Error watching config file: {:?}",
58                    e
59                );
60            }
61            return;
62        };
63
64        if should_handle_event(&event, watched_file_path.as_path()) {
65            let lock_state = last_refresh.lock();
66            let Ok(mut last_refresh_at) = lock_state else {
67                eprintln!("[Config Watcher Callback] Debounce mutex poisoned");
68                return;
69            };
70
71            if last_refresh_at
72                .as_ref()
73                .map(|at| at.elapsed() < debounce_window)
74                .unwrap_or(false)
75            {
76                return;
77            }
78
79            *last_refresh_at = Some(Instant::now());
80
81            // Refrescar el caché del plugin leyendo de disco y luego emitir el evento.
82            let app_for_async = app_handle.clone();
83            tauri::async_runtime::spawn(async move {
84                // Obtener el estado del ConfigManager y actualizar su cache.
85                let state = app_for_async.state::<desktop::ConfigManager<R>>();
86                if let Err(e) = state.inner().refresh_cache_from_file().await {
87                    eprintln!(
88                        "[Config Watcher Callback] Failed to refresh config cache: {}",
89                        e
90                    );
91                }
92                // Emitir evento para frontends
93                app_for_async
94                    .emit(CONFIG_CHANGED_EVENT, ())
95                    .unwrap_or_else(|e| {
96                        eprintln!(
97                            "[Config Watcher Callback] Failed to emit config-changed event: {}",
98                            e
99                        );
100                    });
101            });
102        }
103    })
104}
105
106/// Initializes the plugin.
107pub fn init<R: Runtime>() -> TauriPlugin<R> {
108    Builder::new("config-manager")
109        .invoke_handler(tauri::generate_handler![
110            commands::read_config,
111            commands::write_config,
112            commands::set_darkmode,
113            commands::get_schemes,
114            commands::get_scheme_by_id
115        ])
116        .setup(|app, api| {
117            let config_manager = desktop::init(app, api)?;
118            let config_path = config_manager.config_path()?;
119            app.manage(config_manager);
120
121            let watch_target = if config_path.exists() {
122                config_path.clone()
123            } else {
124                config_path
125                    .parent()
126                    .map(std::path::Path::to_path_buf)
127                    .ok_or_else(|| {
128                        Error::Other(format!(
129                            "Invalid config path without parent: {}",
130                            config_path.display()
131                        ))
132                    })?
133            };
134
135            let app_handle_for_watcher = app.clone();
136            let event_handler = watch_config_file(&app_handle_for_watcher, config_path.clone());
137
138            let mut watcher: RecommendedWatcher = notify::recommended_watcher(event_handler)
139                .map_err(|e| {
140                    Error::Other(format!("Cannot create watcher for config file: {}", e))
141                })?;
142
143            watcher
144                .watch(watch_target.as_path(), notify::RecursiveMode::NonRecursive)
145                .map_err(|e| {
146                    Error::Other(format!(
147                        "Failed to watch config path {}: {}",
148                        watch_target.display(),
149                        e
150                    ))
151                })?;
152
153            app.manage(Mutex::new(watcher));
154
155            Ok(())
156        })
157        .build()
158}