Mountain/Environment/
UserInterfaceProvider.rs1#![allow(non_snake_case, non_camel_case_types)]
15
16use std::path::PathBuf;
17
18use Common::{
19 Error::CommonError::CommonError,
20 UserInterface::{
21 DTO::{
22 InputBoxOptionsDTO::InputBoxOptionsDTO,
23 MessageSeverity::MessageSeverity,
24 OpenDialogOptionsDTO::OpenDialogOptionsDTO,
25 QuickPickItemDTO::QuickPickItemDTO,
26 QuickPickOptionsDTO::QuickPickOptionsDTO,
27 SaveDialogOptionsDTO::SaveDialogOptionsDTO,
28 },
29 UserInterfaceProvider::UserInterfaceProvider,
30 },
31};
32use async_trait::async_trait;
33use log::{info, warn};
34use serde::Serialize;
35use serde_json::{Value, json};
36use tauri::Emitter;
37use tauri_plugin_dialog::{DialogExt, FilePath};
38use tokio::time::{Duration, timeout};
39use uuid::Uuid;
40
41use super::{MountainEnvironment::MountainEnvironment, Utility};
42
43#[derive(Serialize, Clone)]
44struct UserInterfaceRequest<TPayload:Serialize + Clone> {
45 pub RequestIdentifier:String,
46
47 pub Payload:TPayload,
48}
49
50#[async_trait]
51impl UserInterfaceProvider for MountainEnvironment {
52 async fn ShowMessage(
55 &self,
56
57 Severity:MessageSeverity,
58
59 Message:String,
60
61 Options:Option<Value>,
62 ) -> Result<Option<String>, CommonError> {
63 info!("[UserInterfaceProvider] Showing interactive message: {}", Message);
64
65 let Payload = json!({ "Severity": Severity, "Message": Message, "Options": Options });
66
67 let ResponseValue = SendUserInterfaceRequest(self, "sky://ui/show-message-request", Payload).await?;
68
69 Ok(ResponseValue.as_str().map(String::from))
70 }
71
72 async fn ShowOpenDialog(&self, Options:Option<OpenDialogOptionsDTO>) -> Result<Option<Vec<PathBuf>>, CommonError> {
75 info!("[UserInterfaceProvider] Showing open dialog.");
76
77 let mut Builder = self.ApplicationHandle.dialog().file();
78
79 let (CanSelectMany, CanSelectFolders, CanSelectFiles) = if let Some(ref opts) = Options {
80 if let Some(title) = &opts.Base.Title {
81 Builder = Builder.set_title(title);
82 }
83
84 if let Some(path_string) = &opts.Base.DefaultPath {
85 Builder = Builder.set_directory(PathBuf::from(path_string));
86 }
87
88 if let Some(filters) = &opts.Base.FilterList {
89 for filter in filters {
90 let extensions:Vec<&str> = filter.ExtensionList.iter().map(AsRef::as_ref).collect();
91
92 Builder = Builder.add_filter(&filter.Name, &extensions);
93 }
94 }
95
96 (
97 opts.CanSelectMany.unwrap_or(false),
98 opts.CanSelectFolders.unwrap_or(false),
99 opts.CanSelectFiles.unwrap_or(true),
100 )
101 } else {
102 (false, false, true)
103 };
104
105 let PickedPaths:Option<Vec<FilePath>> = tokio::task::spawn_blocking(move || {
106 if CanSelectFolders {
107 if CanSelectMany {
108 Builder.blocking_pick_folders()
109 } else {
110 Builder.blocking_pick_folder().map(|p| vec![p])
111 }
112 } else if CanSelectFiles {
113 if CanSelectMany {
114 Builder.blocking_pick_files()
115 } else {
116 Builder.blocking_pick_file().map(|p| vec![p])
117 }
118 } else {
119 None
120 }
121 })
122 .await
123 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:format!("Dialog task failed: {}", Error) })?;
124
125 Ok(PickedPaths.map(|paths| paths.into_iter().filter_map(|p| p.into_path().ok()).collect()))
126 }
127
128 async fn ShowSaveDialog(&self, Options:Option<SaveDialogOptionsDTO>) -> Result<Option<PathBuf>, CommonError> {
130 info!("[UserInterfaceProvider] Showing save dialog.");
131
132 let mut Builder = self.ApplicationHandle.dialog().file();
133
134 if let Some(options) = Options {
135 if let Some(title) = options.Base.Title {
136 Builder = Builder.set_title(title);
137 }
138
139 if let Some(path_string) = options.Base.DefaultPath {
140 let path = PathBuf::from(path_string);
141
142 if let Some(parent) = path.parent() {
143 Builder = Builder.set_directory(parent);
144 }
145
146 if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
147 Builder = Builder.set_file_name(file_name);
148 }
149 }
150
151 if let Some(filters) = options.Base.FilterList {
152 for filter in filters {
153 let extensions:Vec<&str> = filter.ExtensionList.iter().map(AsRef::as_ref).collect();
154
155 Builder = Builder.add_filter(filter.Name, &extensions);
156 }
157 }
158 }
159
160 let PickedFile = tokio::task::spawn_blocking(move || Builder.blocking_save_file())
161 .await
162 .map_err(|Error| {
163 CommonError::UserInterfaceInteraction { Reason:format!("Dialog task failed: {}", Error) }
164 })?;
165
166 Ok(PickedFile.and_then(|p| p.into_path().ok()))
167 }
168
169 async fn ShowQuickPick(
171 &self,
172
173 Items:Vec<QuickPickItemDTO>,
174
175 Options:Option<QuickPickOptionsDTO>,
176 ) -> Result<Option<Vec<String>>, CommonError> {
177 info!("[UserInterfaceProvider] Showing quick pick with {} items.", Items.len());
178
179 let Payload = json!({ "Items": Items, "Options": Options });
180
181 let ResponseValue = SendUserInterfaceRequest(self, "sky://ui/show-quick-pick-request", Payload).await?;
182
183 serde_json::from_value(ResponseValue).map_err(|Error| {
184 CommonError::SerializationError {
185 Description:format!("Failed to deserialize quick pick response: {}", Error),
186 }
187 })
188 }
189
190 async fn ShowInputBox(&self, Options:Option<InputBoxOptionsDTO>) -> Result<Option<String>, CommonError> {
192 info!("[UserInterfaceProvider] Showing input box.");
193
194 let ResponseValue = SendUserInterfaceRequest(self, "sky://ui/show-input-box-request", Options).await?;
195
196 serde_json::from_value(ResponseValue).map_err(|Error| {
197 CommonError::SerializationError {
198 Description:format!("Failed to deserialize input box response: {}", Error),
199 }
200 })
201 }
202}
203
204async fn SendUserInterfaceRequest<TPayload:Serialize + Clone>(
209 Environment:&MountainEnvironment,
210
211 EventName:&str,
212
213 Payload:TPayload,
214) -> Result<Value, CommonError> {
215 let RequestIdentifier = Uuid::new_v4().to_string();
216
217 let (Sender, Receiver) = tokio::sync::oneshot::channel();
218
219 {
220 let mut PendingRequestsGuard = Environment
221 .ApplicationState
222 .PendingUserInterfaceRequests
223 .lock()
224 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
225
226 PendingRequestsGuard.insert(RequestIdentifier.clone(), Sender);
227 }
228
229 let EventPayload = UserInterfaceRequest { RequestIdentifier:RequestIdentifier.clone(), Payload };
230
231 Environment.ApplicationHandle.emit(EventName, EventPayload).map_err(|Error| {
232 CommonError::UserInterfaceInteraction {
233 Reason:format!("Failed to emit UI request '{}': {}", EventName, Error.to_string()),
234 }
235 })?;
236
237 match timeout(Duration::from_secs(300), Receiver).await {
238 Ok(Ok(Ok(Value))) => Ok(Value),
239
240 Ok(Ok(Err(Error))) => Err(Error),
241
242 Ok(Err(_)) => {
243 Err(CommonError::UserInterfaceInteraction {
244 Reason:format!("UI response channel closed for request ID: {}", RequestIdentifier),
245 })
246 },
247
248 Err(_) => {
249 warn!(
250 "[UserInterfaceProvider] UI request '{}' with ID {} timed out.",
251 EventName, RequestIdentifier
252 );
253
254 let mut Guard = Environment
255 .ApplicationState
256 .PendingUserInterfaceRequests
257 .lock()
258 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
259
260 Guard.remove(&RequestIdentifier);
261
262 Err(CommonError::UserInterfaceInteraction {
263 Reason:format!("UI request timed out for request ID: {}", RequestIdentifier),
264 })
265 },
266 }
267}