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