Skip to main content

Mountain/Track/Effect/CreateEffectForRequest/
WindowUI.rs

1//! Window-namespace UI commands from Cocoon's window shim.
2//! ShowMessage is fire-and-forget (no selection reply needed).
3//! ShowQuickPick / ShowInputBox / ShowOpenDialog / ShowSaveDialog block on
4//! a oneshot channel that is resolved by the frontend via ResolveUIRequest.
5
6use std::sync::Arc;
7
8use serde_json::{Value, json};
9use tauri::Runtime;
10
11use crate::{
12	ApplicationState::State::ApplicationState::ApplicationState,
13	RunTime::ApplicationRunTime::ApplicationRunTime,
14	Track::Effect::{
15		CreateEffectForRequest::Utilities::Params::{array_unwrap, ensure_array},
16		MappedEffectType::MappedEffect,
17	},
18	dev_log,
19};
20
21pub fn CreateEffect<R:Runtime>(MethodName:&str, Parameters:Value) -> Option<Result<MappedEffect, String>> {
22	match MethodName {
23		"Window.ShowMessage" => {
24			crate::effect!(run_time, {
25				use std::sync::atomic::{AtomicU64, Ordering as AO};
26
27				use tauri::Emitter;
28
29				let AppHandle = run_time.Environment.ApplicationHandle.clone();
30
31				let Payload = array_unwrap(Parameters);
32
33				let Message = Payload.get("message").and_then(Value::as_str).unwrap_or("").to_string();
34
35				let Level = Payload.get("level").and_then(Value::as_str).unwrap_or("info").to_string();
36
37				let Items = Payload.get("items").and_then(Value::as_array).cloned().unwrap_or_default();
38
39				let Options = Payload.get("options").cloned().unwrap_or(json!({}));
40
41				if Items.is_empty() {
42					// Fire-and-forget: no action buttons needed.
43					let _ = AppHandle.emit(
44						"sky://notification/show",
45						json!({
46							"message": Message,
47							"severity": Level,
48							"actions": [],
49							"options": Options,
50						}),
51					);
52
53					return Ok(Value::Null);
54				}
55
56				// Round-trip: emit to the show-message-request channel
57				// (which INotificationService handles with real action
58				// buttons) and block until the user clicks or dismisses.
59				static UI_MSG_SEQ:AtomicU64 = AtomicU64::new(1);
60
61				let Nonce = format!("msg-{}", UI_MSG_SEQ.fetch_add(1, AO::Relaxed));
62
63				let (tx, rx) = tokio::sync::oneshot::channel();
64
65				run_time.Environment.ApplicationState.UI.AddPendingRequest(Nonce.clone(), tx);
66
67				let Actions:Vec<serde_json::Value> = Items
68					.iter()
69					.map(|Item| if Item.is_string() { json!({ "title": Item }) } else { Item.clone() })
70					.collect();
71
72				if let Err(Error) = AppHandle.emit(
73					"sky://ui/show-message-request",
74					json!({
75						"RequestIdentifier": Nonce,
76						"Payload": {
77							"Severity": Level,
78							"Message": Message,
79							"Options": { "Actions": Actions },
80						},
81					}),
82				) {
83					run_time.Environment.ApplicationState.UI.RemovePendingRequest(&Nonce);
84
85					dev_log!("notification", "warn: [Window.ShowMessage] emit failed: {}", Error);
86
87					return Ok(Value::Null);
88				}
89
90				match rx.await {
91					Ok(Ok(Value)) => Ok(Value),
92					_ => Ok(Value::Null),
93				}
94			})
95		},
96
97		"Window.ShowQuickPick" | "Window.ShowInputBox" | "Window.ShowOpenDialog" | "Window.ShowSaveDialog" => {
98			let MethodNameOwned = MethodName.to_string();
99
100			crate::effect!(run_time, {
101				use tauri::Emitter;
102
103				let Args = ensure_array(Parameters);
104
105				let Channel = match MethodNameOwned.as_str() {
106					"Window.ShowQuickPick" => "sky://quickpick/show",
107					"Window.ShowInputBox" => "sky://input-box/show",
108					"Window.ShowOpenDialog" => "sky://dialog/open",
109					"Window.ShowSaveDialog" => "sky://dialog/save",
110					_ => "sky://quickpick/show",
111				};
112
113				use std::sync::atomic::{AtomicU64, Ordering as AO};
114
115				static UI_SEQ:AtomicU64 = AtomicU64::new(1);
116
117				let Nonce = format!("ui-{}", UI_SEQ.fetch_add(1, AO::Relaxed));
118
119				// Register the reply channel before emitting so the
120				// frontend can never race-resolve before we are waiting.
121				let (tx, rx) = tokio::sync::oneshot::channel();
122
123				run_time.Environment.ApplicationState.UI.AddPendingRequest(Nonce.clone(), tx);
124
125				let AppHandle = run_time.Environment.ApplicationHandle.clone();
126
127				if let Err(Error) = AppHandle.emit(Channel, json!({ "nonce": Nonce, "args": Args })) {
128					// Emit failed -- remove the dangling sender so the map
129					// does not grow unboundedly on repeated failures.
130					run_time.Environment.ApplicationState.UI.RemovePendingRequest(&Nonce.clone());
131
132					dev_log!("ipc", "warn: [{}] {} emit failed: {}", MethodNameOwned, Channel, Error);
133
134					return Err(format!("[{}] emit failed: {}", MethodNameOwned, Error));
135				}
136
137				// Block until the frontend calls ResolveUIRequest with
138				// the same nonce, or the sender is dropped (dialog
139				// dismissed / window closed).
140				match rx.await {
141					Ok(Ok(Value)) => Ok(Value),
142					Ok(Err(CommonError)) => Err(CommonError.to_string()),
143					Err(_RecvError) => {
144						// Sender was dropped without a reply -- the user
145						// dismissed the dialog.  Return null so the extension
146						// host sees `undefined` (VS Code contract for cancelled
147						// quick-pick / input-box).
148						dev_log!("ipc", "[{}] dialog dismissed (nonce dropped)", MethodNameOwned);
149
150						Ok(Value::Null)
151					},
152				}
153			})
154		},
155
156		_ => None,
157	}
158}