Skip to main content

CommonLibrary/IPC/
SkyEvent.rs

1//! # Sky Event Registry - single source of truth for Mountain → Sky/Wind events
2//!
3//! Mountain emits Tauri events on `sky://…` URIs to notify the webview of
4//! state changes that don't originate from a Wind-initiated `invoke` call.
5//! Historically each emit site used a free-text string literal and each Wind
6//! listener matched against its own free-text string - drift was invisible
7//! until runtime (the listener simply never fired).
8//!
9//! `SkyEvent` is the enumerated registry. Mountain callers dispatch on the
10//! variant; the wire string is produced by `AsStr()` and parsed by `FromStr`.
11//! The matching TypeScript const object lives at
12//! `Element/Wind/Source/IPC/SkyEvent.ts` - kept in sync by convention, same
13//! protocol as the `Channel` registry.
14//!
15//! ## Adding a new event
16//!
17//! 1. Add the variant here AND in `Element/Wind/Source/IPC/SkyEvent.ts`.
18//! 2. Emit from Mountain:
19//!    `ApplicationHandle.emit(SkyEvent::TerminalData.AsStr(), Payload)`.
20//! 3. Subscribe from Wind: `IPCService.events(SkyEvent.TerminalData)`.
21//!
22//! ## Why a declarative macro?
23//!
24//! Same rationale as `Channel`: the variant → wire-string mapping is pure
25//! data. `DefineSkyEvents!` expands it into enum body + `AsStr` + `All` +
26//! `FromStr` in one pass so adding an event is a single-line change that
27//! compilers can't forget.
28
29#![allow(non_snake_case, non_camel_case_types)]
30
31macro_rules! DefineSkyEvents {
32
33	($($Variant:ident => $Wire:literal,)* $(,)?) => {
34
35		/// Enumerated Mountain → Sky/Wind event identifiers.
36		#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
37		pub enum SkyEvent {
38
39			$($Variant,)*
40		}
41
42		impl SkyEvent {
43
44			/// Wire string produced on the Tauri event transport.
45			pub fn AsStr(&self) -> &'static str {
46
47				match self {
48
49					$(Self::$Variant => $Wire,)*
50				}
51			}
52
53			/// Full set of events, in declaration order.
54			pub fn All() -> &'static [Self] {
55
56				&[$(Self::$Variant,)*]
57			}
58		}
59
60		impl ::std::fmt::Display for SkyEvent {
61
62			fn fmt(&self, Formatter:&mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
63
64				Formatter.write_str(self.AsStr())
65			}
66		}
67
68		impl ::std::str::FromStr for SkyEvent {
69
70			type Err = ::std::string::String;
71
72			fn from_str(Wire:&str) -> ::std::result::Result<Self, Self::Err> {
73
74				match Wire {
75
76					$($Wire => Ok(Self::$Variant),)*
77					_ => Err(format!("unknown Sky event: {}", Wire)),
78				}
79			}
80		}
81	};
82}
83
84DefineSkyEvents! {
85
86	// --- Configuration ---
87	ConfigurationChanged                          => "sky://configuration/changed",
88
89	// --- Debug ---
90	DebugDapMessage                               => "sky://debug/dap-message",
91
92	DebugRegister                                 => "sky://debug/register",
93
94	DebugStart                                    => "sky://debug/start",
95
96	DebugStop                                     => "sky://debug/stop",
97
98	// --- Diagnostics ---
99	DiagnosticsChanged                            => "sky://diagnostics/changed",
100
101	// --- CustomEditor ---
102	CustomEditorSaved                             => "sky://customEditor/saved",
103
104	// --- Dialog ---
105	DialogOpen                                    => "sky://dialog/open",
106
107	DialogSave                                    => "sky://dialog/save",
108
109	// --- Documents ---
110	DocumentsOpen                                 => "sky://documents/open",
111
112	DocumentsRenamed                              => "sky://documents/renamed",
113
114	DocumentsSaved                                => "sky://documents/saved",
115
116	// --- Editor ---
117	EditorApplyEdits                              => "sky://editor/applyEdits",
118
119	EditorOpenDocument                            => "sky://editor/openDocument",
120
121	EditorSaveAll                                 => "sky://editor/saveAll",
122
123	// --- Extensions ---
124	ExtensionsInstalled                           => "sky://extensions/installed",
125
126	ExtensionsUninstalled                         => "sky://extensions/uninstalled",
127
128	// --- ExtHost ---
129	ExtHostDebugClose                             => "sky://exthost/debug-close",
130
131	ExtHostDebugReload                            => "sky://exthost/debug-reload",
132
133	// --- Input ---
134	InputBoxShow                                  => "sky://input-box/show",
135
136	// --- Language ---
137	LanguageConfigure                             => "sky://language/configure",
138
139	LanguagesSetDocumentLanguage                  => "sky://languages/setDocumentLanguage",
140
141	// --- Lifecycle ---
142	LifecyclePhaseChanged                         => "sky://lifecycle/phaseChanged",
143
144	LifecycleWillShutdown                         => "sky://lifecycle/willShutdown",
145
146	// --- Native ---
147	NativeOpenExternal                            => "sky://native/openExternal",
148
149	// --- Notifications ---
150	NotificationProgressBegin                     => "sky://notification/progress-begin",
151
152	NotificationProgressEnd                       => "sky://notification/progress-end",
153
154	NotificationProgressUpdate                    => "sky://notification/progress-update",
155
156	NotificationShow                              => "sky://notification/show",
157
158	// --- Output ---
159	OutputAppend                                  => "sky://output/append",
160
161	OutputClear                                   => "sky://output/clear",
162
163	OutputCreate                                  => "sky://output/create",
164
165	OutputDispose                                 => "sky://output/dispose",
166
167	OutputReplace                                 => "sky://output/replace",
168
169	OutputReveal                                  => "sky://output/reveal",
170
171	OutputShow                                    => "sky://output/show",
172
173	// --- Progress ---
174	ProgressBegin                                 => "sky://progress/begin",
175
176	ProgressComplete                              => "sky://progress/complete",
177
178	ProgressEnd                                   => "sky://progress/end",
179
180	ProgressReport                                => "sky://progress/report",
181
182	ProgressStart                                 => "sky://progress/start",
183
184	ProgressUpdate                                => "sky://progress/update",
185
186	// --- QuickPick ---
187	QuickPickShow                                 => "sky://quickpick/show",
188
189	// --- Source Control ---
190	SCMGroupChanged                               => "sky://scm/group/changed",
191
192	SCMProviderAdded                              => "sky://scm/provider/added",
193
194	SCMProviderChanged                            => "sky://scm/provider/changed",
195
196	SCMProviderRemoved                            => "sky://scm/provider/removed",
197
198	SCMRegister                                   => "sky://scm/register",
199
200	SCMUpdateGroup                                => "sky://scm/updateGroup",
201
202	// --- Status bar ---
203	// Canonical prefix is `sky://statusbar/` (no hyphen). The earlier
204	// `sky://status-bar/message` channel was an accidental fork produced by
205	// a separate emit site and has been consolidated onto
206	// `sky://statusbar/set-message`.
207	StatusBarCreate                               => "sky://statusbar/create",
208
209	StatusBarDispose                              => "sky://statusbar/dispose",
210
211	StatusBarDisposeEntry                         => "sky://statusbar/dispose-entry",
212
213	StatusBarDisposeMessage                       => "sky://statusbar/dispose-message",
214
215	StatusBarSetEntry                             => "sky://statusbar/set-entry",
216
217	StatusBarSetMessage                           => "sky://statusbar/set-message",
218
219	StatusBarUpdate                               => "sky://statusbar/update",
220
221	// --- Task ---
222	TaskExecute                                   => "sky://task/execute",
223
224	TaskTerminate                                 => "sky://task/terminate",
225
226	// --- Terminal ---
227	TerminalClosed                                => "sky://terminal/closed",
228
229	TerminalCreate                                => "sky://terminal/create",
230
231	TerminalData                                  => "sky://terminal/data",
232
233	TerminalExit                                  => "sky://terminal/exit",
234
235	TerminalHide                                  => "sky://terminal/hide",
236
237	TerminalOpened                                => "sky://terminal/opened",
238
239	TerminalProcessId                             => "sky://terminal/processId",
240
241	TerminalResize                                => "sky://terminal/resize",
242
243	TerminalShow                                  => "sky://terminal/show",
244
245	// --- Test ---
246	TestRegistered                                => "sky://test/registered",
247
248	TestRunStarted                                => "sky://test/run-started",
249
250	TestRunStatusChanged                          => "sky://test/run-status-changed",
251
252	// --- Theme ---
253	ThemeChange                                   => "sky://theme/change",
254
255	// --- Tree view ---
256	// Canonical prefix is `sky://tree-view/` (kebab-case). The earlier
257	// `sky://treeView/register` camelCase channel was a parallel emission
258	// from `CocoonService/TreeView.rs`; it has been collapsed into
259	// `TreeViewCreate`, which every handler already subscribes to.
260	TreeViewCreate                                => "sky://tree-view/create",
261
262	TreeViewDispose                               => "sky://tree-view/dispose",
263
264	TreeViewNodeExpanded                          => "sky://tree-view/node-expanded",
265
266	TreeViewRefresh                               => "sky://tree-view/refresh",
267
268	TreeViewRestoreState                          => "sky://tree-view/restore-state",
269
270	TreeViewReveal                                => "sky://tree-view/reveal",
271
272	TreeViewSelectionChanged                      => "sky://tree-view/selection-changed",
273
274	TreeViewSetBadge                              => "sky://tree-view/set-badge",
275
276	TreeViewSetMessage                            => "sky://tree-view/set-message",
277
278	TreeViewSetTitle                              => "sky://tree-view/set-title",
279
280	// --- UI ---
281	// `UIShow{InputBox,QuickPick}Request` are deprecated aliases. The
282	// Sky listener channels are `InputBoxShow` and `QuickPickShow`
283	// declared earlier in this enum. `UserInterfaceProvider.rs` now
284	// references those directly so the `UIShow*Request` channel names
285	// below remain reachable only from older code paths and tests.
286	UIShowInputBoxRequest                         => "sky://ui/show-input-box-request",
287
288	UIShowMessageRequest                          => "sky://ui/show-message-request",
289
290	UIShowQuickPickRequest                        => "sky://ui/show-quick-pick-request",
291
292	// --- Virtual file system ---
293	VFSFileChange                                 => "sky://vfs/fileChange",
294
295	// --- Webview ---
296	// Canonical form is kebab-case (`sky://webview/post-message`,
297	// `sky://webview/set-html`). The `…CamelCase` aliases existed because
298	// mod.rs emitted `sky://webview/postMessage` / `sky://webview/setHtml`
299	// inline; those emit sites have been migrated to the enum so Sky only
300	// ever sees the kebab-case form.
301	WebviewCreate                                 => "sky://webview/create",
302
303	WebviewCreated                                => "sky://webview/created",
304
305	WebviewDispose                                => "sky://webview/dispose",
306
307	WebviewDisposed                               => "sky://webview/disposed",
308
309	WebviewMessage                                => "sky://webview/message",
310
311	WebviewOptionsChanged                         => "sky://webview/options-changed",
312
313	WebviewPostMessage                            => "sky://webview/post-message",
314
315	WebviewRevealed                               => "sky://webview/revealed",
316
317	WebviewSetHTML                                => "sky://webview/set-html",
318
319	// --- Window ---
320	WindowShowTextDocument                        => "sky://window/showTextDocument",
321
322	// --- Workspace ---
323	WorkspaceApplyEdit                            => "sky://workspace/applyEdit",
324
325	WorkspacesChanged                             => "sky://workspaces/changed",
326}
327
328#[cfg(test)]
329mod Tests {
330
331	use std::str::FromStr;
332
333	use super::SkyEvent;
334
335	#[test]
336	fn RoundTrip() {
337		for Variant in SkyEvent::All() {
338			let Wire = Variant.AsStr();
339
340			let Parsed = SkyEvent::from_str(Wire).expect("round-trip");
341
342			assert_eq!(*Variant, Parsed, "{} failed round-trip", Wire);
343		}
344	}
345
346	#[test]
347	fn EveryWireStartsWithSkyScheme() {
348		for Variant in SkyEvent::All() {
349			assert!(
350				Variant.AsStr().starts_with("sky://"),
351				"{} does not use the sky:// scheme",
352				Variant.AsStr()
353			);
354		}
355	}
356
357	#[test]
358	fn RejectsUnknown() {
359		assert!(SkyEvent::from_str("mountain://nope").is_err());
360
361		assert!(SkyEvent::from_str("").is_err());
362	}
363
364	/// Guards against drift between this Rust enum and its TS mirror at
365	/// `Element/Wind/Source/IPC/SkyEvent.ts`. Both files are hand-edited,
366	/// so the test scrapes the TS literal array and asserts every wire
367	/// string here exists there, and vice versa. If this fails the two
368	/// tables disagree - add or remove from whichever side is missing.
369	#[test]
370	fn RustAndTypeScriptTablesAgree() {
371		use std::{collections::HashSet, path::PathBuf};
372
373		let TsPath = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../Wind/Source/IPC/SkyEvent.ts");
374
375		let Source = match std::fs::read_to_string(&TsPath) {
376			Ok(S) => S,
377
378			// In packaging contexts where Wind isn't checked out alongside
379			// Common we skip the cross-check silently rather than
380			// failing - the RoundTrip / UniqueWireStrings guards above
381			// still cover the Rust side on its own.
382			Err(_) => return,
383		};
384
385		let mut TsWires:HashSet<String> = HashSet::new();
386
387		for Line in Source.lines() {
388			if let Some(Start) = Line.find("\"sky://") {
389				let Tail = &Line[Start + 1..];
390
391				if let Some(End) = Tail.find('"') {
392					TsWires.insert(Tail[..End].to_string());
393				}
394			}
395		}
396
397		let RsWires:HashSet<String> = SkyEvent::All().iter().map(|V| V.AsStr().to_string()).collect();
398
399		let OnlyInRust:Vec<_> = RsWires.difference(&TsWires).collect();
400
401		let OnlyInTs:Vec<_> = TsWires.difference(&RsWires).collect();
402
403		assert!(
404			OnlyInRust.is_empty() && OnlyInTs.is_empty(),
405			"SkyEvent drift between Rust and TS:\n  only in Rust: {:?}\n  only in TS:   {:?}",
406			OnlyInRust,
407			OnlyInTs
408		);
409	}
410
411	#[test]
412	fn UniqueWireStrings() {
413		let mut Seen = std::collections::HashSet::new();
414
415		for Variant in SkyEvent::All() {
416			assert!(Seen.insert(Variant.AsStr()), "duplicate wire: {}", Variant.AsStr());
417		}
418	}
419}