Skip to main content

CommonLibrary/IPC/
Channel.rs

1//! # Channel Registry - single source of truth for Wind ↔ Mountain IPC
2//!
3//! Every Tauri `invoke` request from Wind is a string-typed RPC. Historically
4//! that string was hand-kept on both sides (Rust `match command.as_str()` in
5//! `Mountain/Source/IPC/WindServiceHandlers.rs` and string literals in Wind's
6//! `Effect/*/Live.ts` files). The result: drift - a channel could be declared
7//! in Wind, referenced in Mountain's match, and still have no implementation
8//! (the `extensions:install` no-op stub shipped for months).
9//!
10//! `Channel` is the enumerated registry. Rust callers dispatch on the variant;
11//! the wire string is produced by `AsStr()` and parsed by `FromStr`. The
12//! matching TypeScript const object lives at
13//! `Element/Wind/Source/IPC/Channel.ts` - kept in sync by convention (a grep
14//! diff is cheap; a full codegen would be overkill for 147 strings).
15//!
16//! ## Why a declarative macro?
17//!
18//! The variant → wire-string mapping is pure data. `DefineChannels!` expands
19//! it into the enum body, `AsStr`, `All`, and `FromStr` in one pass so adding
20//! a channel is a single-line change that compilers can't forget.
21//!
22//! ## Channel priority classes (Atom O3)
23//!
24//! `Priority` returns the Echo scheduler lane a given channel should dispatch
25//! on. Used by the O1 wrap in `mountain_ipc_invoke` so user-facing latency
26//! never queues behind background work. Three classes:
27//!
28//!   - `High`: direct user action (commands, file read, terminal input,
29//!     notifications, VSIX install).
30//!   - `Low`: background / deferrable (search, logging, update checks,
31//!     offline-gallery stubs).
32//!   - `Normal`: everything else.
33
34#![allow(non_snake_case, non_camel_case_types)]
35
36/// Lane selector for Echo scheduler dispatch.
37///
38/// Deliberately isolated from `Echo::Task::Priority` so Common stays
39/// dependency-free on Echo. Mountain's `mountain_ipc_invoke` wrapper maps
40/// `ChannelPriority` → `Echo::Task::Priority` at the single submit site.
41#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
42pub enum ChannelPriority {
43	High,
44
45	Normal,
46
47	Low,
48}
49
50macro_rules! DefineChannels {
51
52	($($Variant:ident => $Wire:literal,)* $(,)?) => {
53
54		/// Enumerated IPC channel identifiers for Wind ↔ Mountain calls.
55		#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
56		pub enum Channel {
57
58			$($Variant,)*
59		}
60
61		impl Channel {
62
63			/// Wire string produced on the Tauri transport.
64			pub fn AsStr(&self) -> &'static str {
65
66				match self {
67
68					$(Self::$Variant => $Wire,)*
69				}
70			}
71
72			/// Full set of channels, in declaration order.
73			pub fn All() -> &'static [Self] {
74
75				&[$(Self::$Variant,)*]
76			}
77		}
78
79		impl ::std::fmt::Display for Channel {
80
81			fn fmt(&self, Formatter:&mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
82
83				Formatter.write_str(self.AsStr())
84			}
85		}
86
87		impl ::std::str::FromStr for Channel {
88
89			type Err = ::std::string::String;
90
91			fn from_str(Wire:&str) -> ::std::result::Result<Self, Self::Err> {
92
93				match Wire {
94
95					$($Wire => Ok(Self::$Variant),)*
96					_ => Err(format!("unknown IPC channel: {}", Wire)),
97				}
98			}
99		}
100	};
101}
102
103DefineChannels! {
104
105	// --- Cocoon bridge ---
106	CocoonExtensionHostMessage                    => "cocoon:extensionHostMessage",
107
108	// --- Commands ---
109	CommandsExecute                               => "commands:execute",
110
111	CommandsGetAll                                => "commands:getAll",
112
113	// --- Configuration ---
114	ConfigurationGet                              => "configuration:get",
115
116	ConfigurationUpdate                           => "configuration:update",
117
118	// --- Decorations ---
119	DecorationsClear                              => "decorations:clear",
120
121	DecorationsGet                                => "decorations:get",
122
123	DecorationsGetMany                            => "decorations:getMany",
124
125	DecorationsSet                                => "decorations:set",
126
127	// --- Diagnostics ---
128	DiagnosticLog                                 => "diagnostic:log",
129
130	// --- Encryption ---
131	EncryptionDecrypt                             => "encryption:decrypt",
132
133	EncryptionEncrypt                             => "encryption:encrypt",
134
135	// --- Environment ---
136	EnvironmentGet                                => "environment:get",
137
138	// --- Extension host debug service ---
139	ExtensionHostDebugServiceAttachSession        => "extensionhostdebugservice:attachSession",
140
141	ExtensionHostDebugServiceClose                => "extensionhostdebugservice:close",
142
143	ExtensionHostDebugServiceReload               => "extensionhostdebugservice:reload",
144
145	ExtensionHostDebugServiceTerminateSession     => "extensionhostdebugservice:terminateSession",
146
147	// --- Extensions ---
148	ExtensionsGet                                 => "extensions:get",
149
150	ExtensionsGetAll                              => "extensions:getAll",
151
152	ExtensionsGetExtensions                       => "extensions:getExtensions",
153
154	ExtensionsGetExtensionsControlManifest        => "extensions:getExtensionsControlManifest",
155
156	ExtensionsGetInstalled                        => "extensions:getInstalled",
157
158	ExtensionsGetRecommendations                  => "extensions:getRecommendations",
159
160	ExtensionsGetUninstalled                      => "extensions:getUninstalled",
161
162	ExtensionsInstall                             => "extensions:install",
163
164	ExtensionsIsActive                            => "extensions:isActive",
165
166	ExtensionsQuery                               => "extensions:query",
167
168	ExtensionsReinstall                           => "extensions:reinstall",
169
170	ExtensionsScanSystemExtensions                => "extensions:scanSystemExtensions",
171
172	ExtensionsScanUserExtensions                  => "extensions:scanUserExtensions",
173
174	ExtensionsUninstall                           => "extensions:uninstall",
175
176	ExtensionsUpdateMetadata                      => "extensions:updateMetadata",
177
178	// --- File system ---
179	FileCloneFile                                 => "file:cloneFile",
180
181	FileClose                                     => "file:close",
182
183	FileCopy                                      => "file:copy",
184
185	FileDelete                                    => "file:delete",
186
187	FileExists                                    => "file:exists",
188
189	FileMkdir                                     => "file:mkdir",
190
191	FileMove                                      => "file:move",
192
193	FileOpen                                      => "file:open",
194
195	FileRead                                      => "file:read",
196
197	FileReadBinary                                => "file:readBinary",
198
199	FileReaddir                                   => "file:readdir",
200
201	FileReadFile                                  => "file:readFile",
202
203	FileRealpath                                  => "file:realpath",
204
205	FileRename                                    => "file:rename",
206
207	FileStat                                      => "file:stat",
208
209	FileUnwatch                                   => "file:unwatch",
210
211	FileWatch                                     => "file:watch",
212
213	FileWrite                                     => "file:write",
214
215	FileWriteBinary                               => "file:writeBinary",
216
217	FileWriteFile                                 => "file:writeFile",
218
219	// --- Git (renderer `localGit` channel; stock VS Code names it
220	//          `ILocalGitService`, shared-process wire "localGit"). Land
221	//          routes each method to a Mountain subprocess handler that
222	//          spawns native `git`.
223	GitCancel                                     => "git:cancel",
224
225	GitCheckout                                   => "git:checkout",
226
227	GitClone                                      => "git:clone",
228
229	GitExec                                       => "git:exec",
230
231	GitFetch                                      => "git:fetch",
232
233	GitIsAvailable                                => "git:isAvailable",
234
235	GitPull                                       => "git:pull",
236
237	GitRevListCount                               => "git:revListCount",
238
239	GitRevParse                                   => "git:revParse",
240
241	// --- History ---
242	HistoryCanGoBack                              => "history:canGoBack",
243
244	HistoryCanGoForward                           => "history:canGoForward",
245
246	HistoryClear                                  => "history:clear",
247
248	HistoryGetStack                               => "history:getStack",
249
250	HistoryGoBack                                 => "history:goBack",
251
252	HistoryGoForward                              => "history:goForward",
253
254	HistoryPush                                   => "history:push",
255
256	// --- Keybindings ---
257	KeybindingAdd                                 => "keybinding:add",
258
259	KeybindingGetAll                              => "keybinding:getAll",
260
261	KeybindingLookup                              => "keybinding:lookup",
262
263	KeybindingRemove                              => "keybinding:remove",
264
265	// --- Labels ---
266	LabelGetBase                                  => "label:getBase",
267
268	LabelGetURI                                   => "label:getUri",
269
270	LabelGetWorkspace                             => "label:getWorkspace",
271
272	// --- Lifecycle ---
273	LifecycleAdvancePhase                         => "lifecycle:advancePhase",
274
275	LifecycleGetPhase                             => "lifecycle:getPhase",
276
277	LifecycleRequestShutdown                      => "lifecycle:requestShutdown",
278
279	LifecycleSetPhase                             => "lifecycle:setPhase",
280
281	LifecycleWhenPhase                            => "lifecycle:whenPhase",
282
283	// --- Log (legacy / short) ---
284	LogCreateLogger                               => "log:createLogger",
285
286	LogRegisterLogger                             => "log:registerLogger",
287
288	// --- Logger (current) ---
289	LoggerCreateLogger                            => "logger:createLogger",
290
291	LoggerCritical                                => "logger:critical",
292
293	LoggerDebug                                   => "logger:debug",
294
295	LoggerDeregisterLogger                        => "logger:deregisterLogger",
296
297	LoggerError                                   => "logger:error",
298
299	LoggerFlush                                   => "logger:flush",
300
301	LoggerGetLevel                                => "logger:getLevel",
302
303	LoggerGetRegisteredLoggers                    => "logger:getRegisteredLoggers",
304
305	LoggerInfo                                    => "logger:info",
306
307	LoggerLog                                     => "logger:log",
308
309	LoggerRegisterLogger                          => "logger:registerLogger",
310
311	LoggerSetLevel                                => "logger:setLevel",
312
313	LoggerSetVisibility                           => "logger:setVisibility",
314
315	LoggerTrace                                   => "logger:trace",
316
317	LoggerWarn                                    => "logger:warn",
318
319	// --- Menubar ---
320	MenubarUpdateMenubar                          => "menubar:updateMenubar",
321
322	// --- Model ---
323	ModelClose                                    => "model:close",
324
325	ModelGet                                      => "model:get",
326
327	ModelGetAll                                   => "model:getAll",
328
329	ModelOpen                                     => "model:open",
330
331	ModelUpdateContent                            => "model:updateContent",
332
333	// --- Native host ---
334	NativeOpenExternal                            => "native:openExternal",
335
336	NativeShowItemInFolder                        => "native:showItemInFolder",
337
338	// --- Notifications ---
339	NotificationEndProgress                       => "notification:endProgress",
340
341	NotificationShow                              => "notification:show",
342
343	NotificationShowProgress                      => "notification:showProgress",
344
345	NotificationUpdateProgress                    => "notification:updateProgress",
346
347	// --- Output channel ---
348	OutputAppend                                  => "output:append",
349
350	OutputAppendLine                              => "output:appendLine",
351
352	OutputClear                                   => "output:clear",
353
354	OutputCreate                                  => "output:create",
355
356	OutputShow                                    => "output:show",
357
358	// --- Progress ---
359	ProgressBegin                                 => "progress:begin",
360
361	ProgressEnd                                   => "progress:end",
362
363	ProgressReport                                => "progress:report",
364
365	// --- Search ---
366	SearchFindFiles                               => "search:findFiles",
367
368	SearchFindInFiles                             => "search:findInFiles",
369
370	// --- Storage ---
371	StorageClose                                  => "storage:close",
372
373	StorageDelete                                 => "storage:delete",
374
375	StorageGet                                    => "storage:get",
376
377	StorageGetItems                               => "storage:getItems",
378
379	StorageIsUsed                                 => "storage:isUsed",
380
381	StorageKeys                                   => "storage:keys",
382
383	StorageOptimize                               => "storage:optimize",
384
385	StorageSet                                    => "storage:set",
386
387	StorageUpdateItems                            => "storage:updateItems",
388
389	// --- QuickInput (vscode.window.showQuickPick / showInputBox) ---
390	QuickInputShowInputBox                        => "quickInput:showInputBox",
391
392	QuickInputShowQuickPick                       => "quickInput:showQuickPick",
393
394	// --- TextFile (editor working-copy surface) ---
395	TextFileRead                                  => "textFile:read",
396
397	TextFileWrite                                 => "textFile:write",
398
399	TextFileSave                                  => "textFile:save",
400
401	// --- WorkingCopy (dirty-state tracking) ---
402	WorkingCopyGetAllDirty                        => "workingCopy:getAllDirty",
403
404	WorkingCopyGetDirtyCount                      => "workingCopy:getDirtyCount",
405
406	WorkingCopyIsDirty                            => "workingCopy:isDirty",
407
408	WorkingCopySetDirty                           => "workingCopy:setDirty",
409
410	// --- Terminal ---
411	TerminalCreate                                => "terminal:create",
412
413	TerminalDispose                               => "terminal:dispose",
414
415	TerminalHide                                  => "terminal:hide",
416
417	TerminalSendText                              => "terminal:sendText",
418
419	TerminalShow                                  => "terminal:show",
420
421	// --- Themes ---
422	ThemesGetActive                               => "themes:getActive",
423
424	ThemesList                                    => "themes:list",
425
426	ThemesSet                                    => "themes:set",
427
428	// --- Update ---
429	UpdateApplyUpdate                             => "update:applyUpdate",
430
431	UpdateCheckForUpdates                         => "update:checkForUpdates",
432
433	UpdateDownloadUpdate                          => "update:downloadUpdate",
434
435	UpdateIsLatestVersion                         => "update:isLatestVersion",
436
437	UpdateQuitAndInstall                          => "update:quitAndInstall",
438
439	// --- URL handlers ---
440	URLRegisterExternalURIOpener                  => "url:registerExternalUriOpener",
441
442	// --- Workbench ---
443	WorkbenchGetConfiguration                     => "workbench:getConfiguration",
444
445	// --- Workspaces ---
446	WorkspacesAddFolder                           => "workspaces:addFolder",
447
448	WorkspacesAddRecentlyOpened                   => "workspaces:addRecentlyOpened",
449
450	WorkspacesClearRecentlyOpened                 => "workspaces:clearRecentlyOpened",
451
452	WorkspacesCreateUntitledWorkspace             => "workspaces:createUntitledWorkspace",
453
454	WorkspacesDeleteUntitledWorkspace             => "workspaces:deleteUntitledWorkspace",
455
456	WorkspacesEnterWorkspace                      => "workspaces:enterWorkspace",
457
458	WorkspacesGetDirtyWorkspaces                  => "workspaces:getDirtyWorkspaces",
459
460	WorkspacesGetFolders                          => "workspaces:getFolders",
461
462	WorkspacesGetName                             => "workspaces:getName",
463
464	WorkspacesGetRecentlyOpened                   => "workspaces:getRecentlyOpened",
465
466	WorkspacesGetWorkspaceIdentifier              => "workspaces:getWorkspaceIdentifier",
467
468	WorkspacesRemoveFolder                        => "workspaces:removeFolder",
469
470	WorkspacesRemoveRecentlyOpened                => "workspaces:removeRecentlyOpened",
471
472	// --- Legacy wire-shape channels (non prefix:method) ---
473	// Two historical groups predate the `prefix:method` convention:
474	//   1. `UserInterface.Show*Dialog` - dotted names mirrored from
475	//      Cocoon→Mountain gRPC; the Wind-side Files/Live.ts routes them
476	//      through Tauri IPC today. Rename target: `dialog:showOpen` /
477	//      `dialog:showSave`.
478	//   2. `mountain_get_status` - snake_case Tauri command.
479	// Grouped at the tail so the eventual rename is a single block move.
480	MountainGetStatus                             => "mountain_get_status",
481
482	UserInterfaceShowOpenDialog                   => "UserInterface.ShowOpenDialog",
483
484	UserInterfaceShowSaveDialog                   => "UserInterface.ShowSaveDialog",
485}
486
487impl Channel {
488	/// Echo scheduler lane for this channel. See module-level docs for the
489	/// classification rationale.
490	pub fn Priority(&self) -> ChannelPriority {
491		use Channel::*;
492
493		match self {
494			// --- Direct user action → High ---
495			CommandsExecute
496			| CocoonExtensionHostMessage
497			| ExtensionsInstall
498			| ExtensionsUninstall
499			| ExtensionsReinstall
500			| FileRead
501			| FileReadBinary
502			| FileReadFile
503			| FileStat
504			| FileExists
505			| FileOpen
506			| FileWrite
507			| FileWriteBinary
508			| FileWriteFile
509			| FileDelete
510			| FileCopy
511			| FileMove
512			| FileRename
513			| FileMkdir
514			| KeybindingLookup
515			| MenubarUpdateMenubar
516			| ModelUpdateContent
517			| NativeOpenExternal
518			| NativeShowItemInFolder
519			| NotificationShow
520			| NotificationShowProgress
521			| NotificationUpdateProgress
522			| NotificationEndProgress
523			| TerminalCreate
524			| TerminalSendText
525			| TerminalShow
526			| TerminalHide
527			| TerminalDispose
528			| WorkspacesEnterWorkspace
529			| WorkspacesAddFolder
530			| WorkspacesRemoveFolder
531			| WorkspacesCreateUntitledWorkspace
532			| WorkspacesDeleteUntitledWorkspace => ChannelPriority::High,
533
534			// --- Background / deferrable → Low ---
535			GitClone
536			| GitFetch
537			| GitPull
538			| GitRevListCount
539			| SearchFindFiles
540			| SearchFindInFiles
541			| LogCreateLogger
542			| LogRegisterLogger
543			| LoggerCreateLogger
544			| LoggerCritical
545			| LoggerDebug
546			| LoggerDeregisterLogger
547			| LoggerError
548			| LoggerFlush
549			| LoggerGetLevel
550			| LoggerGetRegisteredLoggers
551			| LoggerInfo
552			| LoggerLog
553			| LoggerRegisterLogger
554			| LoggerSetLevel
555			| LoggerSetVisibility
556			| LoggerTrace
557			| LoggerWarn
558			| StorageOptimize
559			| UpdateCheckForUpdates
560			| UpdateDownloadUpdate
561			| UpdateApplyUpdate
562			| UpdateIsLatestVersion
563			| UpdateQuitAndInstall
564			| ExtensionsQuery
565			| ExtensionsGetRecommendations
566			| ExtensionsGetExtensions
567			| ExtensionsGetExtensionsControlManifest
568			| ExtensionsGetUninstalled
569			| ExtensionsUpdateMetadata
570			| DiagnosticLog => ChannelPriority::Low,
571
572			// --- Everything else → Normal ---
573			_ => ChannelPriority::Normal,
574		}
575	}
576}
577
578#[cfg(test)]
579mod Tests {
580
581	use std::str::FromStr;
582
583	use super::{Channel, ChannelPriority};
584
585	#[test]
586	fn RoundTrip() {
587		for Variant in Channel::All() {
588			let Wire = Variant.AsStr();
589
590			let Parsed = Channel::from_str(Wire).expect("round-trip");
591
592			assert_eq!(*Variant, Parsed, "{} failed round-trip", Wire);
593		}
594	}
595
596	#[test]
597	fn PriorityIsTotal() {
598		// Every variant must match one of three classes; the `_` fallback
599		// returning Normal guarantees totality, so this test just runs the
600		// mapping on every variant to catch any future match panic.
601		for Variant in Channel::All() {
602			let _Class = Variant.Priority();
603		}
604	}
605
606	#[test]
607	fn UserActionIsHigh() {
608		assert_eq!(Channel::CommandsExecute.Priority(), ChannelPriority::High);
609
610		assert_eq!(Channel::ExtensionsInstall.Priority(), ChannelPriority::High);
611
612		assert_eq!(Channel::TerminalSendText.Priority(), ChannelPriority::High);
613	}
614
615	#[test]
616	fn BackgroundIsLow() {
617		assert_eq!(Channel::SearchFindInFiles.Priority(), ChannelPriority::Low);
618
619		assert_eq!(Channel::LoggerInfo.Priority(), ChannelPriority::Low);
620	}
621
622	#[test]
623	fn RejectsUnknown() {
624		assert!(Channel::from_str("nope:nope").is_err());
625
626		assert!(Channel::from_str("").is_err());
627	}
628
629	#[test]
630	fn UniqueWireStrings() {
631		let mut Seen = std::collections::HashSet::new();
632
633		for Variant in Channel::All() {
634			assert!(Seen.insert(Variant.AsStr()), "duplicate wire: {}", Variant.AsStr());
635		}
636	}
637}