Skip to main content

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}