1use std::path::PathBuf;
113
114use CommonLibrary::{
115 Error::CommonError::CommonError,
116 IPC::SkyEvent::SkyEvent,
117 UserInterface::{
118 DTO::{
119 InputBoxOptionsDTO::InputBoxOptionsDTO,
120 MessageSeverity::MessageSeverity,
121 OpenDialogOptionsDTO::OpenDialogOptionsDTO,
122 QuickPickItemDTO::QuickPickItemDTO,
123 QuickPickOptionsDTO::QuickPickOptionsDTO,
124 SaveDialogOptionsDTO::SaveDialogOptionsDTO,
125 },
126 UserInterfaceProvider::UserInterfaceProvider,
127 },
128};
129use async_trait::async_trait;
130use serde::Serialize;
131use serde_json::{Value, json};
132use tauri::Emitter;
133use tauri_plugin_dialog::{DialogExt, FilePath};
134use tokio::time::{Duration, timeout};
135use uuid::Uuid;
136
137use super::{MountainEnvironment::MountainEnvironment, Utility};
138use crate::dev_log;
139
140#[derive(Serialize, Clone)]
141struct UserInterfaceRequest<TPayload:Serialize + Clone> {
142 pub RequestIdentifier:String,
143
144 pub Payload:TPayload,
145}
146
147#[async_trait]
148impl UserInterfaceProvider for MountainEnvironment {
149 async fn ShowMessage(
152 &self,
153
154 Severity:MessageSeverity,
155
156 Message:String,
157
158 Options:Option<Value>,
159 ) -> Result<Option<String>, CommonError> {
160 dev_log!("window", "[UserInterfaceProvider] Showing interactive message: {}", Message);
161
162 let Payload = json!({ "severity": Severity, "message": Message, "options": Options });
166
167 let ResponseValue = SendUserInterfaceRequest(self, SkyEvent::UIShowMessageRequest.AsStr(), Payload).await?;
168
169 Ok(ResponseValue.as_str().map(String::from))
170 }
171
172 async fn ShowOpenDialog(&self, Options:Option<OpenDialogOptionsDTO>) -> Result<Option<Vec<PathBuf>>, CommonError> {
175 dev_log!("window", "[UserInterfaceProvider] Showing open dialog.");
176
177 let mut Builder = self.ApplicationHandle.dialog().file();
178
179 let (CanSelectMany, CanSelectFolders, CanSelectFiles) = if let Some(ref opts) = Options {
180 if let Some(title) = &opts.Base.Title {
181 Builder = Builder.set_title(title);
182 }
183
184 if let Some(path_string) = &opts.Base.DefaultPath {
185 Builder = Builder.set_directory(PathBuf::from(path_string));
186 }
187
188 if let Some(filters) = &opts.Base.FilterList {
189 for filter in filters {
190 let extensions:Vec<&str> = filter.ExtensionList.iter().map(AsRef::as_ref).collect();
191
192 Builder = Builder.add_filter(&filter.Name, &extensions);
193 }
194 }
195
196 (
197 opts.CanSelectMany.unwrap_or(false),
198 opts.CanSelectFolders.unwrap_or(false),
199 opts.CanSelectFiles.unwrap_or(true),
200 )
201 } else {
202 (false, false, true)
203 };
204
205 let PickedPaths:Option<Vec<FilePath>> = tokio::task::spawn_blocking(move || {
206 if CanSelectFolders {
207 if CanSelectMany {
208 Builder.blocking_pick_folders()
209 } else {
210 Builder.blocking_pick_folder().map(|p| vec![p])
211 }
212 } else if CanSelectFiles {
213 if CanSelectMany {
214 Builder.blocking_pick_files()
215 } else {
216 Builder.blocking_pick_file().map(|p| vec![p])
217 }
218 } else {
219 None
220 }
221 })
222 .await
223 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:format!("Dialog task failed: {}", Error) })?;
224
225 Ok(PickedPaths.map(|paths| paths.into_iter().filter_map(|p| p.into_path().ok()).collect()))
226 }
227
228 async fn ShowSaveDialog(&self, Options:Option<SaveDialogOptionsDTO>) -> Result<Option<PathBuf>, CommonError> {
230 dev_log!("window", "[UserInterfaceProvider] Showing save dialog.");
231
232 let mut Builder = self.ApplicationHandle.dialog().file();
233
234 if let Some(options) = Options {
235 if let Some(title) = options.Base.Title {
236 Builder = Builder.set_title(title);
237 }
238
239 if let Some(path_string) = options.Base.DefaultPath {
240 let path = PathBuf::from(path_string);
241
242 if let Some(parent) = path.parent() {
243 Builder = Builder.set_directory(parent);
244 }
245
246 if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
247 Builder = Builder.set_file_name(file_name);
248 }
249 }
250
251 if let Some(filters) = options.Base.FilterList {
252 for filter in filters {
253 let extensions:Vec<&str> = filter.ExtensionList.iter().map(AsRef::as_ref).collect();
254
255 Builder = Builder.add_filter(filter.Name, &extensions);
256 }
257 }
258 }
259
260 let PickedFile = tokio::task::spawn_blocking(move || Builder.blocking_save_file())
261 .await
262 .map_err(|Error| {
263 CommonError::UserInterfaceInteraction { Reason:format!("Dialog task failed: {}", Error) }
264 })?;
265
266 Ok(PickedFile.and_then(|p| p.into_path().ok()))
267 }
268
269 async fn ShowQuickPick(
271 &self,
272
273 Items:Vec<QuickPickItemDTO>,
274
275 Options:Option<QuickPickOptionsDTO>,
276 ) -> Result<Option<Vec<String>>, CommonError> {
277 dev_log!(
278 "window",
279 "[UserInterfaceProvider] Showing quick pick with {} items.",
280 Items.len()
281 );
282
283 let Payload = json!({ "items": Items, "options": Options });
285
286 let ResponseValue = SendUserInterfaceRequest(self, SkyEvent::QuickPickShow.AsStr(), Payload).await?;
291
292 serde_json::from_value(ResponseValue).map_err(|Error| {
293 CommonError::SerializationError {
294 Description:format!("Failed to deserialize quick pick response: {}", Error),
295 }
296 })
297 }
298
299 async fn ShowInputBox(&self, Options:Option<InputBoxOptionsDTO>) -> Result<Option<String>, CommonError> {
301 dev_log!("window", "[UserInterfaceProvider] Showing input box.");
302
303 let ResponseValue = SendUserInterfaceRequest(self, SkyEvent::InputBoxShow.AsStr(), Options).await?;
307
308 serde_json::from_value(ResponseValue).map_err(|Error| {
309 CommonError::SerializationError {
310 Description:format!("Failed to deserialize input box response: {}", Error),
311 }
312 })
313 }
314}
315
316pub(crate) async fn SendUserInterfaceRequest<TPayload:Serialize + Clone>(
326 Environment:&MountainEnvironment,
327
328 EventName:&str,
329
330 Payload:TPayload,
331) -> Result<Value, CommonError> {
332 let RequestIdentifier = Uuid::new_v4().to_string();
333
334 let (Sender, Receiver) = tokio::sync::oneshot::channel();
335
336 {
337 let mut PendingRequestsGuard = Environment
338 .ApplicationState
339 .UI
340 .PendingUserInterfaceRequest
341 .lock()
342 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
343
344 PendingRequestsGuard.insert(RequestIdentifier.clone(), Sender);
345 }
346
347 let EventPayload = UserInterfaceRequest { RequestIdentifier:RequestIdentifier.clone(), Payload };
348
349 Environment.ApplicationHandle.emit(EventName, EventPayload).map_err(|Error| {
350 CommonError::UserInterfaceInteraction {
351 Reason:format!("Failed to emit UI request '{}': {}", EventName, Error.to_string()),
352 }
353 })?;
354
355 match timeout(Duration::from_secs(300), Receiver).await {
356 Ok(Ok(Ok(Value))) => Ok(Value),
357
358 Ok(Ok(Err(Error))) => Err(Error),
359
360 Ok(Err(_)) => {
361 Err(CommonError::UserInterfaceInteraction {
362 Reason:format!("UI response channel closed for request ID: {}", RequestIdentifier),
363 })
364 },
365
366 Err(_) => {
367 dev_log!(
368 "window",
369 "warn: [UserInterfaceProvider] UI request '{}' with ID {} timed out.",
370 EventName,
371 RequestIdentifier
372 );
373
374 let mut Guard = Environment
375 .ApplicationState
376 .UI
377 .PendingUserInterfaceRequest
378 .lock()
379 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
380
381 Guard.remove(&RequestIdentifier);
382
383 Err(CommonError::UserInterfaceInteraction {
384 Reason:format!("UI request timed out for request ID: {}", RequestIdentifier),
385 })
386 },
387 }
388}