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