tauri_plugin_config_manager/
lib.rs1use 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
26pub 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: ¬ify::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 let app_for_async = app_handle.clone();
83 tauri::async_runtime::spawn(async move {
84 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 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
106pub 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}