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	Normal,
45	Low,
46}
47
48macro_rules! DefineChannels {
49	($($Variant:ident => $Wire:literal,)* $(,)?) => {
50		/// Enumerated IPC channel identifiers for Wind ↔ Mountain calls.
51		#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
52		pub enum Channel {
53			$($Variant,)*
54		}
55
56		impl Channel {
57			/// Wire string produced on the Tauri transport.
58			pub fn AsStr(&self) -> &'static str {
59				match self {
60					$(Self::$Variant => $Wire,)*
61				}
62			}
63
64			/// Full set of channels, in declaration order.
65			pub fn All() -> &'static [Self] {
66				&[$(Self::$Variant,)*]
67			}
68		}
69
70		impl ::std::fmt::Display for Channel {
71			fn fmt(&self, Formatter:&mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
72				Formatter.write_str(self.AsStr())
73			}
74		}
75
76		impl ::std::str::FromStr for Channel {
77			type Err = ::std::string::String;
78
79			fn from_str(Wire:&str) -> ::std::result::Result<Self, Self::Err> {
80				match Wire {
81					$($Wire => Ok(Self::$Variant),)*
82					_ => Err(format!("unknown IPC channel: {}", Wire)),
83				}
84			}
85		}
86	};
87}
88
89DefineChannels! {
90	// --- Cocoon bridge ---
91	CocoonExtensionHostMessage                    => "cocoon:extensionHostMessage",
92
93	// --- Commands ---
94	CommandsExecute                               => "commands:execute",
95	CommandsGetAll                                => "commands:getAll",
96
97	// --- Configuration ---
98	ConfigurationGet                              => "configuration:get",
99	ConfigurationUpdate                           => "configuration:update",
100
101	// --- Decorations ---
102	DecorationsClear                              => "decorations:clear",
103	DecorationsGet                                => "decorations:get",
104	DecorationsGetMany                            => "decorations:getMany",
105	DecorationsSet                                => "decorations:set",
106
107	// --- Diagnostics ---
108	DiagnosticLog                                 => "diagnostic:log",
109
110	// --- Encryption ---
111	EncryptionDecrypt                             => "encryption:decrypt",
112	EncryptionEncrypt                             => "encryption:encrypt",
113
114	// --- Environment ---
115	EnvironmentGet                                => "environment:get",
116
117	// --- Extension host debug service ---
118	ExtensionHostDebugServiceAttachSession        => "extensionhostdebugservice:attachSession",
119	ExtensionHostDebugServiceClose                => "extensionhostdebugservice:close",
120	ExtensionHostDebugServiceReload               => "extensionhostdebugservice:reload",
121	ExtensionHostDebugServiceTerminateSession     => "extensionhostdebugservice:terminateSession",
122
123	// --- Extensions ---
124	ExtensionsGet                                 => "extensions:get",
125	ExtensionsGetAll                              => "extensions:getAll",
126	ExtensionsGetExtensions                       => "extensions:getExtensions",
127	ExtensionsGetExtensionsControlManifest        => "extensions:getExtensionsControlManifest",
128	ExtensionsGetInstalled                        => "extensions:getInstalled",
129	ExtensionsGetRecommendations                  => "extensions:getRecommendations",
130	ExtensionsGetUninstalled                      => "extensions:getUninstalled",
131	ExtensionsInstall                             => "extensions:install",
132	ExtensionsIsActive                            => "extensions:isActive",
133	ExtensionsQuery                               => "extensions:query",
134	ExtensionsReinstall                           => "extensions:reinstall",
135	ExtensionsScanSystemExtensions                => "extensions:scanSystemExtensions",
136	ExtensionsScanUserExtensions                  => "extensions:scanUserExtensions",
137	ExtensionsUninstall                           => "extensions:uninstall",
138	ExtensionsUpdateMetadata                      => "extensions:updateMetadata",
139
140	// --- File system ---
141	FileCloneFile                                 => "file:cloneFile",
142	FileClose                                     => "file:close",
143	FileCopy                                      => "file:copy",
144	FileDelete                                    => "file:delete",
145	FileExists                                    => "file:exists",
146	FileMkdir                                     => "file:mkdir",
147	FileMove                                      => "file:move",
148	FileOpen                                      => "file:open",
149	FileRead                                      => "file:read",
150	FileReadBinary                                => "file:readBinary",
151	FileReaddir                                   => "file:readdir",
152	FileReadFile                                  => "file:readFile",
153	FileRealpath                                  => "file:realpath",
154	FileRename                                    => "file:rename",
155	FileStat                                      => "file:stat",
156	FileUnwatch                                   => "file:unwatch",
157	FileWatch                                     => "file:watch",
158	FileWrite                                     => "file:write",
159	FileWriteBinary                               => "file:writeBinary",
160	FileWriteFile                                 => "file:writeFile",
161
162	// --- Git (renderer `localGit` channel; stock VS Code names it
163	//          `ILocalGitService`, shared-process wire "localGit"). Land
164	//          routes each method to a Mountain subprocess handler that
165	//          spawns native `git`.
166	GitCancel                                     => "git:cancel",
167	GitCheckout                                   => "git:checkout",
168	GitClone                                      => "git:clone",
169	GitExec                                       => "git:exec",
170	GitFetch                                      => "git:fetch",
171	GitIsAvailable                                => "git:isAvailable",
172	GitPull                                       => "git:pull",
173	GitRevListCount                               => "git:revListCount",
174	GitRevParse                                   => "git:revParse",
175
176	// --- History ---
177	HistoryCanGoBack                              => "history:canGoBack",
178	HistoryCanGoForward                           => "history:canGoForward",
179	HistoryClear                                  => "history:clear",
180	HistoryGetStack                               => "history:getStack",
181	HistoryGoBack                                 => "history:goBack",
182	HistoryGoForward                              => "history:goForward",
183	HistoryPush                                   => "history:push",
184
185	// --- Keybindings ---
186	KeybindingAdd                                 => "keybinding:add",
187	KeybindingGetAll                              => "keybinding:getAll",
188	KeybindingLookup                              => "keybinding:lookup",
189	KeybindingRemove                              => "keybinding:remove",
190
191	// --- Labels ---
192	LabelGetBase                                  => "label:getBase",
193	LabelGetURI                                   => "label:getUri",
194	LabelGetWorkspace                             => "label:getWorkspace",
195
196	// --- Lifecycle ---
197	LifecycleAdvancePhase                         => "lifecycle:advancePhase",
198	LifecycleGetPhase                             => "lifecycle:getPhase",
199	LifecycleRequestShutdown                      => "lifecycle:requestShutdown",
200	LifecycleSetPhase                             => "lifecycle:setPhase",
201	LifecycleWhenPhase                            => "lifecycle:whenPhase",
202
203	// --- Log (legacy / short) ---
204	LogCreateLogger                               => "log:createLogger",
205	LogRegisterLogger                             => "log:registerLogger",
206
207	// --- Logger (current) ---
208	LoggerCreateLogger                            => "logger:createLogger",
209	LoggerCritical                                => "logger:critical",
210	LoggerDebug                                   => "logger:debug",
211	LoggerDeregisterLogger                        => "logger:deregisterLogger",
212	LoggerError                                   => "logger:error",
213	LoggerFlush                                   => "logger:flush",
214	LoggerGetLevel                                => "logger:getLevel",
215	LoggerGetRegisteredLoggers                    => "logger:getRegisteredLoggers",
216	LoggerInfo                                    => "logger:info",
217	LoggerLog                                     => "logger:log",
218	LoggerRegisterLogger                          => "logger:registerLogger",
219	LoggerSetLevel                                => "logger:setLevel",
220	LoggerSetVisibility                           => "logger:setVisibility",
221	LoggerTrace                                   => "logger:trace",
222	LoggerWarn                                    => "logger:warn",
223
224	// --- Menubar ---
225	MenubarUpdateMenubar                          => "menubar:updateMenubar",
226
227	// --- Model ---
228	ModelClose                                    => "model:close",
229	ModelGet                                      => "model:get",
230	ModelGetAll                                   => "model:getAll",
231	ModelOpen                                     => "model:open",
232	ModelUpdateContent                            => "model:updateContent",
233
234	// --- Native host ---
235	NativeOpenExternal                            => "native:openExternal",
236	NativeShowItemInFolder                        => "native:showItemInFolder",
237
238	// --- Notifications ---
239	NotificationEndProgress                       => "notification:endProgress",
240	NotificationShow                              => "notification:show",
241	NotificationShowProgress                      => "notification:showProgress",
242	NotificationUpdateProgress                    => "notification:updateProgress",
243
244	// --- Output channel ---
245	OutputAppend                                  => "output:append",
246	OutputAppendLine                              => "output:appendLine",
247	OutputClear                                   => "output:clear",
248	OutputCreate                                  => "output:create",
249	OutputShow                                    => "output:show",
250
251	// --- Progress ---
252	ProgressBegin                                 => "progress:begin",
253	ProgressEnd                                   => "progress:end",
254	ProgressReport                                => "progress:report",
255
256	// --- Search ---
257	SearchFindFiles                               => "search:findFiles",
258	SearchFindInFiles                             => "search:findInFiles",
259
260	// --- Storage ---
261	StorageClose                                  => "storage:close",
262	StorageDelete                                 => "storage:delete",
263	StorageGet                                    => "storage:get",
264	StorageGetItems                               => "storage:getItems",
265	StorageIsUsed                                 => "storage:isUsed",
266	StorageKeys                                   => "storage:keys",
267	StorageOptimize                               => "storage:optimize",
268	StorageSet                                    => "storage:set",
269	StorageUpdateItems                            => "storage:updateItems",
270
271	// --- QuickInput (vscode.window.showQuickPick / showInputBox) ---
272	QuickInputShowInputBox                        => "quickInput:showInputBox",
273	QuickInputShowQuickPick                       => "quickInput:showQuickPick",
274
275	// --- TextFile (editor working-copy surface) ---
276	TextFileRead                                  => "textFile:read",
277	TextFileWrite                                 => "textFile:write",
278	TextFileSave                                  => "textFile:save",
279
280	// --- WorkingCopy (dirty-state tracking) ---
281	WorkingCopyGetAllDirty                        => "workingCopy:getAllDirty",
282	WorkingCopyGetDirtyCount                      => "workingCopy:getDirtyCount",
283	WorkingCopyIsDirty                            => "workingCopy:isDirty",
284	WorkingCopySetDirty                           => "workingCopy:setDirty",
285
286	// --- Terminal ---
287	TerminalCreate                                => "terminal:create",
288	TerminalDispose                               => "terminal:dispose",
289	TerminalHide                                  => "terminal:hide",
290	TerminalSendText                              => "terminal:sendText",
291	TerminalShow                                  => "terminal:show",
292
293	// --- Themes ---
294	ThemesGetActive                               => "themes:getActive",
295	ThemesList                                    => "themes:list",
296	ThemesSet                                    => "themes:set",
297
298	// --- Update ---
299	UpdateApplyUpdate                             => "update:applyUpdate",
300	UpdateCheckForUpdates                         => "update:checkForUpdates",
301	UpdateDownloadUpdate                          => "update:downloadUpdate",
302	UpdateIsLatestVersion                         => "update:isLatestVersion",
303	UpdateQuitAndInstall                          => "update:quitAndInstall",
304
305	// --- URL handlers ---
306	URLRegisterExternalURIOpener                  => "url:registerExternalUriOpener",
307
308	// --- Workbench ---
309	WorkbenchGetConfiguration                     => "workbench:getConfiguration",
310
311	// --- Workspaces ---
312	WorkspacesAddFolder                           => "workspaces:addFolder",
313	WorkspacesAddRecentlyOpened                   => "workspaces:addRecentlyOpened",
314	WorkspacesClearRecentlyOpened                 => "workspaces:clearRecentlyOpened",
315	WorkspacesCreateUntitledWorkspace             => "workspaces:createUntitledWorkspace",
316	WorkspacesDeleteUntitledWorkspace             => "workspaces:deleteUntitledWorkspace",
317	WorkspacesEnterWorkspace                      => "workspaces:enterWorkspace",
318	WorkspacesGetDirtyWorkspaces                  => "workspaces:getDirtyWorkspaces",
319	WorkspacesGetFolders                          => "workspaces:getFolders",
320	WorkspacesGetName                             => "workspaces:getName",
321	WorkspacesGetRecentlyOpened                   => "workspaces:getRecentlyOpened",
322	WorkspacesGetWorkspaceIdentifier              => "workspaces:getWorkspaceIdentifier",
323	WorkspacesRemoveFolder                        => "workspaces:removeFolder",
324	WorkspacesRemoveRecentlyOpened                => "workspaces:removeRecentlyOpened",
325
326	// --- Legacy wire-shape channels (non prefix:method) ---
327	// Two historical groups predate the `prefix:method` convention:
328	//   1. `UserInterface.Show*Dialog` - dotted names mirrored from
329	//      Cocoon→Mountain gRPC; the Wind-side Files/Live.ts routes them
330	//      through Tauri IPC today. Rename target: `dialog:showOpen` /
331	//      `dialog:showSave`.
332	//   2. `mountain_get_status` - snake_case Tauri command.
333	// Grouped at the tail so the eventual rename is a single block move.
334	MountainGetStatus                             => "mountain_get_status",
335	UserInterfaceShowOpenDialog                   => "UserInterface.ShowOpenDialog",
336	UserInterfaceShowSaveDialog                   => "UserInterface.ShowSaveDialog",
337}
338
339impl Channel {
340	/// Echo scheduler lane for this channel. See module-level docs for the
341	/// classification rationale.
342	pub fn Priority(&self) -> ChannelPriority {
343		use Channel::*;
344
345		match self {
346			// --- Direct user action → High ---
347			CommandsExecute
348			| CocoonExtensionHostMessage
349			| ExtensionsInstall
350			| ExtensionsUninstall
351			| ExtensionsReinstall
352			| FileRead
353			| FileReadBinary
354			| FileReadFile
355			| FileStat
356			| FileExists
357			| FileOpen
358			| FileWrite
359			| FileWriteBinary
360			| FileWriteFile
361			| FileDelete
362			| FileCopy
363			| FileMove
364			| FileRename
365			| FileMkdir
366			| KeybindingLookup
367			| MenubarUpdateMenubar
368			| ModelUpdateContent
369			| NativeOpenExternal
370			| NativeShowItemInFolder
371			| NotificationShow
372			| NotificationShowProgress
373			| NotificationUpdateProgress
374			| NotificationEndProgress
375			| TerminalCreate
376			| TerminalSendText
377			| TerminalShow
378			| TerminalHide
379			| TerminalDispose
380			| WorkspacesEnterWorkspace
381			| WorkspacesAddFolder
382			| WorkspacesRemoveFolder
383			| WorkspacesCreateUntitledWorkspace
384			| WorkspacesDeleteUntitledWorkspace => ChannelPriority::High,
385
386			// --- Background / deferrable → Low ---
387			GitClone
388			| GitFetch
389			| GitPull
390			| GitRevListCount
391			| SearchFindFiles
392			| SearchFindInFiles
393			| LogCreateLogger
394			| LogRegisterLogger
395			| LoggerCreateLogger
396			| LoggerCritical
397			| LoggerDebug
398			| LoggerDeregisterLogger
399			| LoggerError
400			| LoggerFlush
401			| LoggerGetLevel
402			| LoggerGetRegisteredLoggers
403			| LoggerInfo
404			| LoggerLog
405			| LoggerRegisterLogger
406			| LoggerSetLevel
407			| LoggerSetVisibility
408			| LoggerTrace
409			| LoggerWarn
410			| StorageOptimize
411			| UpdateCheckForUpdates
412			| UpdateDownloadUpdate
413			| UpdateApplyUpdate
414			| UpdateIsLatestVersion
415			| UpdateQuitAndInstall
416			| ExtensionsQuery
417			| ExtensionsGetRecommendations
418			| ExtensionsGetExtensions
419			| ExtensionsGetExtensionsControlManifest
420			| ExtensionsGetUninstalled
421			| ExtensionsUpdateMetadata
422			| DiagnosticLog => ChannelPriority::Low,
423
424			// --- Everything else → Normal ---
425			_ => ChannelPriority::Normal,
426		}
427	}
428}
429
430#[cfg(test)]
431mod Tests {
432	use super::{Channel, ChannelPriority};
433	use std::str::FromStr;
434
435	#[test]
436	fn RoundTrip() {
437		for Variant in Channel::All() {
438			let Wire = Variant.AsStr();
439			let Parsed = Channel::from_str(Wire).expect("round-trip");
440			assert_eq!(*Variant, Parsed, "{} failed round-trip", Wire);
441		}
442	}
443
444	#[test]
445	fn PriorityIsTotal() {
446		// Every variant must match one of three classes; the `_` fallback
447		// returning Normal guarantees totality, so this test just runs the
448		// mapping on every variant to catch any future match panic.
449		for Variant in Channel::All() {
450			let _Class = Variant.Priority();
451		}
452	}
453
454	#[test]
455	fn UserActionIsHigh() {
456		assert_eq!(Channel::CommandsExecute.Priority(), ChannelPriority::High);
457		assert_eq!(Channel::ExtensionsInstall.Priority(), ChannelPriority::High);
458		assert_eq!(Channel::TerminalSendText.Priority(), ChannelPriority::High);
459	}
460
461	#[test]
462	fn BackgroundIsLow() {
463		assert_eq!(Channel::SearchFindInFiles.Priority(), ChannelPriority::Low);
464		assert_eq!(Channel::LoggerInfo.Priority(), ChannelPriority::Low);
465	}
466
467	#[test]
468	fn RejectsUnknown() {
469		assert!(Channel::from_str("nope:nope").is_err());
470		assert!(Channel::from_str("").is_err());
471	}
472
473	#[test]
474	fn UniqueWireStrings() {
475		let mut Seen = std::collections::HashSet::new();
476		for Variant in Channel::All() {
477			assert!(Seen.insert(Variant.AsStr()), "duplicate wire: {}", Variant.AsStr());
478		}
479	}
480}