1use std::path::PathBuf;
36
37use CommonLibrary::{
38 Error::CommonError::CommonError,
39 IPC::SkyEvent::SkyEvent,
40 UserInterface::{
41 DTO::{
42 InputBoxOptionsDTO::InputBoxOptionsDTO,
43 MessageSeverity::MessageSeverity,
44 OpenDialogOptionsDTO::OpenDialogOptionsDTO,
45 QuickPickItemDTO::QuickPickItemDTO,
46 QuickPickOptionsDTO::QuickPickOptionsDTO,
47 SaveDialogOptionsDTO::SaveDialogOptionsDTO,
48 },
49 UserInterfaceProvider::UserInterfaceProvider,
50 },
51};
52use async_trait::async_trait;
53use serde::Serialize;
54use serde_json::{Value, json};
55use tauri::Emitter;
56use tauri_plugin_dialog::{DialogExt, FilePath};
57use tokio::time::{Duration, timeout};
58use uuid::Uuid;
59
60use super::{MountainEnvironment::MountainEnvironment, Utility};
61use crate::dev_log;
62
63#[derive(Serialize, Clone)]
70struct UserInterfaceRequest<TPayload:Serialize + Clone> {
71 pub RequestIdentifier:String,
72
73 pub Payload:TPayload,
74}
75
76#[async_trait]
77impl UserInterfaceProvider for MountainEnvironment {
78 async fn ShowMessage(
81 &self,
82
83 Severity:MessageSeverity,
84
85 Message:String,
86
87 Options:Option<Value>,
88 ) -> Result<Option<String>, CommonError> {
89 dev_log!("window", "[UserInterfaceProvider] Showing interactive message: {}", Message);
90
91 let Payload = json!({ "severity": Severity, "message": Message, "options": Options });
95
96 let ResponseValue = SendUserInterfaceRequest(self, SkyEvent::UIShowMessageRequest.AsStr(), Payload).await?;
97
98 Ok(ResponseValue.as_str().map(String::from))
99 }
100
101 async fn ShowOpenDialog(&self, Options:Option<OpenDialogOptionsDTO>) -> Result<Option<Vec<PathBuf>>, CommonError> {
104 dev_log!("window", "[UserInterfaceProvider] Showing open dialog.");
105
106 let mut Builder = self.ApplicationHandle.dialog().file();
107
108 let (CanSelectMany, CanSelectFolders, CanSelectFiles) = if let Some(ref opts) = Options {
109 if let Some(title) = &opts.Base.Title {
110 Builder = Builder.set_title(title);
111 }
112
113 if let Some(path_string) = &opts.Base.DefaultPath {
114 Builder = Builder.set_directory(PathBuf::from(path_string));
115 }
116
117 if let Some(filters) = &opts.Base.FilterList {
118 for filter in filters {
119 let extensions:Vec<&str> = filter.ExtensionList.iter().map(AsRef::as_ref).collect();
120
121 Builder = Builder.add_filter(&filter.Name, &extensions);
122 }
123 }
124
125 (
126 opts.CanSelectMany.unwrap_or(false),
127 opts.CanSelectFolders.unwrap_or(false),
128 opts.CanSelectFiles.unwrap_or(true),
129 )
130 } else {
131 (false, false, true)
132 };
133
134 let PickedPaths:Option<Vec<FilePath>> = tokio::task::spawn_blocking(move || {
135 if CanSelectFolders {
136 if CanSelectMany {
137 Builder.blocking_pick_folders()
138 } else {
139 Builder.blocking_pick_folder().map(|p| vec![p])
140 }
141 } else if CanSelectFiles {
142 if CanSelectMany {
143 Builder.blocking_pick_files()
144 } else {
145 Builder.blocking_pick_file().map(|p| vec![p])
146 }
147 } else {
148 None
149 }
150 })
151 .await
152 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:format!("Dialog task failed: {}", Error) })?;
153
154 Ok(PickedPaths.map(|paths| paths.into_iter().filter_map(|p| p.into_path().ok()).collect()))
155 }
156
157 async fn ShowSaveDialog(&self, Options:Option<SaveDialogOptionsDTO>) -> Result<Option<PathBuf>, CommonError> {
159 dev_log!("window", "[UserInterfaceProvider] Showing save dialog.");
160
161 let mut Builder = self.ApplicationHandle.dialog().file();
162
163 if let Some(options) = Options {
164 if let Some(title) = options.Base.Title {
165 Builder = Builder.set_title(title);
166 }
167
168 if let Some(path_string) = options.Base.DefaultPath {
169 let path = PathBuf::from(path_string);
170
171 if let Some(parent) = path.parent() {
172 Builder = Builder.set_directory(parent);
173 }
174
175 if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
176 Builder = Builder.set_file_name(file_name);
177 }
178 }
179
180 if let Some(filters) = options.Base.FilterList {
181 for filter in filters {
182 let extensions:Vec<&str> = filter.ExtensionList.iter().map(AsRef::as_ref).collect();
183
184 Builder = Builder.add_filter(filter.Name, &extensions);
185 }
186 }
187 }
188
189 let PickedFile = tokio::task::spawn_blocking(move || Builder.blocking_save_file())
190 .await
191 .map_err(|Error| {
192 CommonError::UserInterfaceInteraction { Reason:format!("Dialog task failed: {}", Error) }
193 })?;
194
195 Ok(PickedFile.and_then(|p| p.into_path().ok()))
196 }
197
198 async fn ShowQuickPick(
200 &self,
201
202 Items:Vec<QuickPickItemDTO>,
203
204 Options:Option<QuickPickOptionsDTO>,
205 ) -> Result<Option<Vec<String>>, CommonError> {
206 dev_log!(
207 "window",
208 "[UserInterfaceProvider] Showing quick pick with {} items.",
209 Items.len()
210 );
211
212 let Payload = json!({ "items": Items, "options": Options });
214
215 let ResponseValue = SendUserInterfaceRequest(self, SkyEvent::QuickPickShow.AsStr(), Payload).await?;
220
221 serde_json::from_value(ResponseValue).map_err(|Error| {
222 CommonError::SerializationError {
223 Description:format!("Failed to deserialize quick pick response: {}", Error),
224 }
225 })
226 }
227
228 async fn ShowInputBox(&self, Options:Option<InputBoxOptionsDTO>) -> Result<Option<String>, CommonError> {
230 dev_log!("window", "[UserInterfaceProvider] Showing input box.");
231
232 let ResponseValue = SendUserInterfaceRequest(self, SkyEvent::InputBoxShow.AsStr(), Options).await?;
236
237 serde_json::from_value(ResponseValue).map_err(|Error| {
238 CommonError::SerializationError {
239 Description:format!("Failed to deserialize input box response: {}", Error),
240 }
241 })
242 }
243}
244
245pub(crate) async fn SendUserInterfaceRequest<TPayload:Serialize + Clone>(
255 Environment:&MountainEnvironment,
256
257 EventName:&str,
258
259 Payload:TPayload,
260) -> Result<Value, CommonError> {
261 let RequestIdentifier = Uuid::new_v4().to_string();
262
263 let (Sender, Receiver) = tokio::sync::oneshot::channel();
264
265 {
266 let mut PendingRequestsGuard = Environment
267 .ApplicationState
268 .UI
269 .PendingUserInterfaceRequest
270 .lock()
271 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
272
273 PendingRequestsGuard.insert(RequestIdentifier.clone(), Sender);
274 }
275
276 let EventPayload = UserInterfaceRequest { RequestIdentifier:RequestIdentifier.clone(), Payload };
277
278 Environment.ApplicationHandle.emit(EventName, EventPayload).map_err(|Error| {
279 CommonError::UserInterfaceInteraction {
280 Reason:format!("Failed to emit UI request '{}': {}", EventName, Error.to_string()),
281 }
282 })?;
283
284 match timeout(Duration::from_secs(300), Receiver).await {
285 Ok(Ok(Ok(Value))) => Ok(Value),
286
287 Ok(Ok(Err(Error))) => Err(Error),
288
289 Ok(Err(_)) => {
290 Err(CommonError::UserInterfaceInteraction {
291 Reason:format!("UI response channel closed for request ID: {}", RequestIdentifier),
292 })
293 },
294
295 Err(_) => {
296 dev_log!(
297 "window",
298 "warn: [UserInterfaceProvider] UI request '{}' with ID {} timed out.",
299 EventName,
300 RequestIdentifier
301 );
302
303 let mut Guard = Environment
304 .ApplicationState
305 .UI
306 .PendingUserInterfaceRequest
307 .lock()
308 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
309
310 Guard.remove(&RequestIdentifier);
311
312 Err(CommonError::UserInterfaceInteraction {
313 Reason:format!("UI request timed out for request ID: {}", RequestIdentifier),
314 })
315 },
316 }
317}