Mountain/Environment/
UserInterfaceProvider.rs

1//! # UserInterfaceProvider (Environment)
2//!
3//! Implements the `UserInterfaceProvider` trait for `MountainEnvironment`,
4//! orchestrating all modal UI interactions like dialogs, messages, and quick
5//! picks by communicating with the `Sky` frontend.
6//!
7//! ## RESPONSIBILITIES
8//!
9//! ### 1. Modal Dialogs
10//! - Open file/folder selection dialogs (`OpenDialog`)
11//! - Save file dialogs (`SaveDialog`)
12//! - Message boxes (`ShowMessage`, `ShowErrorMessage`)
13//! - Input boxes for text entry (`InputBox`)
14//! - Quick pick lists for selection (`QuickPick`)
15//!
16//! ### 2. Request-Response Pattern
17//! - Send UI requests to Sky frontend via IPC
18//! - Track pending requests with unique IDs
19//! - Wait for responses with timeout handling
20//! - Resolve results via `ResolveUIRequest` callback
21//!
22//! ### 3. Thread Safety
23//! - All methods are async and safe for concurrent access
24//! - Pending requests stored in
25//!   `ApplicationState.UI.PendingUserInterfaceRequests`
26//! - Uses `tokio::sync::oneshot` for request-response coordination
27//!
28//! ## ARCHITECTURAL ROLE
29//!
30//! UserInterfaceProvider is the **UI bridge** for Mountain:
31//!
32//! ```text
33//! Provider ──► UI Request ──► Sky Frontend ──► User Interaction ──► ResolveUIRequest
34//! ```
35//!
36//! ### Position in Mountain
37//! - `Environment` module: UI capability provider
38//! - Implements `CommonLibrary::UserInterface::UserInterfaceProvider` trait
39//! - Accessible via `Environment.Require<dyn UserInterfaceProvider>()`
40//!
41//! ### Dependencies
42//! - `ApplicationState`: Pending request tracking
43//! - `IPCProvider`: For sending messages to Sky
44//! - `tauri::AppHandle`: For window/parent references
45//!
46//! ### Dependents
47//! - Any command that needs to show UI dialogs
48//! - `DispatchLogic::ResolveUIRequest`: Completes the request-response cycle
49//! - Error handlers: Show error messages to users
50//!
51//! ## DTO STRUCTURES
52//!
53//! All UI operations use DTOs for type-safe options:
54//! - `OpenDialogOptionsDTO`: File/folder selection options
55//! - `SaveDialogOptionsDTO`: Save file dialog options
56//! - `QuickPickOptionsDTO`: Quick pick list configuration
57//! - `InputBoxOptionsDTO`: Input box configuration
58//! - `MessageSeverity`: Info, Warning, Error levels
59//!
60//! ## REQUEST FLOW
61//!
62//! 1. Provider method called (e.g., `ShowMessage`)
63//! 2. Generate unique request ID
64//! 3. Store `oneshot::Sender` in `PendingUserInterfaceRequests` map
65//! 4. Send IPC message to Sky with request ID and options
66//! 5. Sky shows UI and waits for user action
67//! 6. User responds → Sky calls `ResolveUIRequest` Tauri command
68//! 7. `ResolveUIRequest` looks up sender by ID and sends result
69//! 8. Provider method returns result to caller
70//!
71//! ## ERROR HANDLING
72//!
73//! - IPC failures: `CommonError::IPCError`
74//! - Timeout: `CommonError::RequestTimeout`
75//! - User cancellation: `None` result (not error)
76//! - Invalid arguments: `CommonError::InvalidArgument`
77//!
78//! ## PERFORMANCE
79//!
80//! - Requests are async and non-blocking
81//! - Timeouts prevent indefinite waiting (default ~30s)
82//! - Request IDs are time-based for uniqueness
83//! - Pending request map uses `Arc<Mutex<>>` for thread safety
84//!
85//! ## VS CODE REFERENCE
86//!
87//! Borrowed from VS Code's UI system:
88//! - `vs/platform/dialogs/common/dialogs.ts` - Dialog service API
89//! - `vs/platform/prompt/common/prompt.ts` - Input and quick pick
90//! - `vs/workbench/services/decorator/common/decorator.ts` - Message service
91//!
92//! ## TODO
93//!
94//! - [ ] Add support for custom dialog buttons and layouts
95//! - [ ] Implement file/folder filters with glob patterns
96//! - [ ] Add dialog position and sizing controls
97//! - [ ] Support modal vs non-modal dialogs
98//! - [ ] Add accessibility features (screen reader support)
99//! - [ ] Implement dialog theming (dark/light mode)
100//! - [ ] Add file type/extension selection in save dialog
101//! - [ ] Support multi-select in quick pick and file dialogs
102//! - [ ] Add async progress reporting during long operations
103//! - [ ] Implement custom input validation (regex, etc.)
104//!
105//! ## MODULE CONTENTS
106//!
107//! - [`UserInterfaceProvider`]: Main struct implementing the trait
108//! - Dialog-specific methods: `ShowMessage`, `OpenDialog`, `SaveDialog`
109//! - Selection methods: `QuickPick`, `InputBox`
110//! - Request-response coordination logic
111
112use 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	/// Shows a message to the user with a given severity and optional action
149	/// buttons.
150	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	/// Shows a dialog for opening files or folders using the
169	/// tauri-plugin-dialog.
170	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	/// Shows a dialog for saving a file using the tauri-plugin-dialog.
225	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	/// Shows a quick pick list to the user.
266	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	/// Shows an input box to solicit a string input from the user.
287	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
300// --- Internal Helper Functions ---
301
302/// A generic helper function to send a request to the Sky UI and wait for a
303/// response.
304async 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}