rightkit_tauri_shell/modules.rs
1//! Modular install: opt individual shell modules out, or install only the state the broker, stream,
2//! recovery, frameless and clipboard commands need.
3//!
4//! ```ignore
5//! let plugin = rightkit_tauri_shell::ShellPlugin::builder()
6//! .shell(rightkit_tauri_shell::Shell::builder("viewright").app_name("ViewRight").build())
7//! .without(Module::Presence)
8//! .without(Module::Watchdog)
9//! .build();
10//!
11//! // Or: broker, stream, recovery, frameless and clipboard only, with no hardening, navigation
12//! // policy, window restore, show-on-ready, watchdog or presence.
13//! let plugin = rightkit_tauri_shell::ShellPlugin::builder()
14//! .shell(rightkit_tauri_shell::Shell::builder("viewright").build())
15//! .install_state_only();
16//! ```
17//!
18//! An opted-out command family answers `unsupported` ("module ... is disabled for this app") rather
19//! than doing nothing, so a renderer never mistakes a disabled module for a silent success.
20//!
21//! The path broker needs no plugin at all: [`crate::broker::PathBroker`] is a plain `Default`
22//! library type. Create one in your own state, grant files with it, and call its `require_*`,
23//! `grant_stream`, `grant_site_root` and recovery helpers from your own commands. Only the plugin's
24//! command wiring is optional.
25
26use tauri::plugin::TauriPlugin;
27use tauri::Wry;
28
29use crate::Shell;
30
31/// A shell module that can be opted out. The comments say what each one controls.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub enum Module {
34 /// Injected DOM hardening script and native webview hardening (`Hardening`).
35 JsHardening,
36 /// Navigation and popup policy (`NavPolicy`, the external-link handler).
37 NavigationPolicy,
38 /// Window geometry restore and persistence.
39 WindowState,
40 /// Show the main window once its webview is ready.
41 ShowOnReady,
42 /// Reveal the main window on a macOS Dock reopen (`RunEvent::Reopen`) and when the OS hands the
43 /// app a URL or file (`RunEvent::Opened`). Off, neither shows, unminimises or focuses anything
44 /// (the URL is still delivered). On in every set except where a host removes it.
45 RevealOnReopen,
46 /// The hang watchdog and its `rightkit-shell://stall` event.
47 Watchdog,
48 /// The presence loop (fullscreen and meeting detection).
49 Presence,
50 /// Path-broker commands (`rk_shell_path_*`).
51 Broker,
52 /// Ranged custom schemes: the stream scheme and the site-root scheme, with their grant commands.
53 Stream,
54 /// Crash-safe setup recovery commands.
55 Recovery,
56 /// Frameless chrome for windows opened through the shell, and traffic-light offsets.
57 Frameless,
58 /// Clipboard commands, including the typed `rk_shell_clipboard_item`.
59 Clipboard,
60}
61
62impl Module {
63 /// Every module, in declaration order.
64 pub const ALL: [Module; 12] = [
65 Module::JsHardening,
66 Module::NavigationPolicy,
67 Module::WindowState,
68 Module::ShowOnReady,
69 Module::RevealOnReopen,
70 Module::Watchdog,
71 Module::Presence,
72 Module::Broker,
73 Module::Stream,
74 Module::Recovery,
75 Module::Frameless,
76 Module::Clipboard,
77 ];
78
79 /// The modules `install_state_only` keeps.
80 pub const STATE: [Module; 5] = [
81 Module::Broker,
82 Module::Stream,
83 Module::Recovery,
84 Module::Frameless,
85 Module::Clipboard,
86 ];
87
88 /// The modules `lifecycle_only` turns off.
89 pub const LIFECYCLE_OFF: [Module; 5] = [
90 Module::JsHardening,
91 Module::NavigationPolicy,
92 Module::Presence,
93 Module::Watchdog,
94 Module::WindowState,
95 ];
96
97 /// Modules `lifecycle_only` leaves off by default because a lifecycle-only host owns its own
98 /// windows: the host turns one back on explicitly (`ShellBuilder::show_on_ready(true)`).
99 pub const LIFECYCLE_HOST_OPT_IN: [Module; 1] = [Module::ShowOnReady];
100
101 /// Stable snake_case name, used in diagnostics (`rk_shell_status`).
102 pub fn name(self) -> &'static str {
103 match self {
104 Module::JsHardening => "js_hardening",
105 Module::NavigationPolicy => "navigation_policy",
106 Module::WindowState => "window_state",
107 Module::ShowOnReady => "show_on_ready",
108 Module::RevealOnReopen => "reveal_on_reopen",
109 Module::Watchdog => "watchdog",
110 Module::Presence => "presence",
111 Module::Broker => "broker",
112 Module::Stream => "stream",
113 Module::Recovery => "recovery",
114 Module::Frameless => "frameless",
115 Module::Clipboard => "clipboard",
116 }
117 }
118}
119
120/// A set of enabled modules.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub struct ModuleSet(u16);
123
124impl ModuleSet {
125 /// Every module enabled (what [`Shell::plugin`] installs).
126 pub fn all() -> Self {
127 Module::ALL.iter().fold(Self(0), |set, m| set.with(*m))
128 }
129
130 /// Only the state-only set (`Module::STATE`), plus `RevealOnReopen`: reopen handling is
131 /// lifecycle, not state, and a state-only install has always revealed on reopen.
132 pub fn state_only() -> Self {
133 Module::STATE
134 .iter()
135 .fold(Self(0), |set, m| set.with(*m))
136 .with(Module::RevealOnReopen)
137 }
138
139 fn bit(module: Module) -> u16 {
140 1 << (module as u16)
141 }
142
143 /// True when `module` is enabled.
144 pub fn contains(self, module: Module) -> bool {
145 self.0 & Self::bit(module) != 0
146 }
147
148 /// This set with `module` enabled.
149 pub fn with(self, module: Module) -> Self {
150 Self(self.0 | Self::bit(module))
151 }
152
153 /// This set with `module` disabled.
154 pub fn without(self, module: Module) -> Self {
155 Self(self.0 & !Self::bit(module))
156 }
157
158 /// Everything except the five webview-policy modules: no hardening script or native hardening,
159 /// no navigation policy, no presence loop, no watchdog and no window-state restore or save.
160 /// `ShowOnReady` is also off (`Module::LIFECYCLE_HOST_OPT_IN`): the host owns its windows and
161 /// turns it on with `ShellBuilder::show_on_ready(true)` if it wants the shell to show the main
162 /// window. `RevealOnReopen` stays on; a host removes it with `ShellBuilder::reveal_on_reopen(false)`.
163 pub fn lifecycle_only() -> Self {
164 Module::LIFECYCLE_OFF
165 .iter()
166 .chain(Module::LIFECYCLE_HOST_OPT_IN.iter())
167 .fold(Self::all(), |set, m| set.without(*m))
168 }
169
170 /// Names of the enabled modules, in declaration order.
171 pub fn names(self) -> Vec<&'static str> {
172 Module::ALL
173 .iter()
174 .filter(|m| self.contains(**m))
175 .map(|m| m.name())
176 .collect()
177 }
178}
179
180/// Entry point for a modular install. See the module docs for examples.
181pub struct ShellPlugin;
182
183impl ShellPlugin {
184 /// Start a modular install. Give it the app's configuration with [`ShellPluginBuilder::shell`].
185 pub fn builder() -> ShellPluginBuilder {
186 ShellPluginBuilder {
187 shell: None,
188 modules: ModuleSet::all(),
189 }
190 }
191}
192
193/// Builder returned by [`ShellPlugin::builder`].
194#[must_use]
195pub struct ShellPluginBuilder {
196 shell: Option<Shell>,
197 modules: ModuleSet,
198}
199
200impl ShellPluginBuilder {
201 /// The app's configuration, built with `Shell::builder(app_id)...build()`.
202 pub fn shell(mut self, shell: Shell) -> Self {
203 self.shell = Some(shell);
204 self
205 }
206
207 /// Opt one module out.
208 pub fn without(mut self, module: Module) -> Self {
209 self.modules = self.modules.without(module);
210 self
211 }
212
213 /// Install the plugin with the modules that remain enabled.
214 ///
215 /// # Panics
216 /// Panics when [`ShellPluginBuilder::shell`] was never called, which is a programmer error at
217 /// startup, the same as an invalid app id.
218 pub fn build(self) -> TauriPlugin<Wry> {
219 let shell = self.shell.expect(
220 "rightkit-tauri-shell: ShellPlugin::builder() needs .shell(Shell::builder(..).build())",
221 );
222 let mut cfg = shell.cfg;
223 cfg.modules = self.modules;
224 crate::install(cfg)
225 }
226
227 /// Install the lifecycle without the webview policy: [`ModuleSet::lifecycle_only`]. Besides the
228 /// module set, a lifecycle-only install defaults `repair_cache` to off and shows the main window
229 /// on ready only when the host called `show_on_ready(true)`; `repair_cache(true)` turns the
230 /// repair back on.
231 pub fn lifecycle_only(mut self) -> TauriPlugin<Wry> {
232 self.modules = ModuleSet::lifecycle_only();
233 if let Some(shell) = self.shell.as_mut() {
234 shell.cfg.lifecycle_only = true;
235 }
236 self.build()
237 }
238
239 /// Install only the state the broker, stream, recovery, frameless and clipboard commands use.
240 /// This replaces any earlier `without` calls: hardening, navigation policy, window restore,
241 /// show-on-ready, watchdog and presence are all off.
242 pub fn install_state_only(mut self) -> TauriPlugin<Wry> {
243 self.modules = ModuleSet::state_only();
244 self.build()
245 }
246}