Skip to main content

rightkit_tauri_shell/
commands.rs

1//! Tauri commands the frontend bridge (`@rightkit/tauri-shell`) calls. Register them with
2//! [`crate::handler!`]. They are application commands, so they need no ACL capability entry.
3
4use std::collections::BTreeMap;
5use std::sync::atomic::Ordering;
6
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use tauri::{AppHandle, Manager, State, WebviewWindow, Wry};
10
11use crate::clipboard::{Board, ClipboardContent, ClipboardItem};
12use crate::tray::TrayUpdate;
13use crate::windows::WindowSpec;
14use crate::{b64, Module, ShellState};
15
16/// Refuse a command whose module the app opted out of (see [`crate::modules`]).
17pub(crate) fn require(state: &ShellState, module: Module) -> Result<(), ShellError> {
18    if state.modules.contains(module) {
19        Ok(())
20    } else {
21        Err(ShellError::Unsupported(format!(
22            "module {} is disabled for this app",
23            module.name()
24        )))
25    }
26}
27
28#[tauri::command]
29pub fn rk_shell_open_external(url: String, state: State<'_, ShellState>) -> Result<(), String> {
30    crate::opener::open_checked(state.opener.as_ref(), &url)
31}
32
33/// The frontend is listening: deliver queued deep links. Returns launch facts.
34#[tauri::command]
35pub fn rk_shell_ready(app: AppHandle<Wry>, state: State<'_, ShellState>) -> Value {
36    state.frontend_ready.store(true, Ordering::SeqCst);
37    crate::lifecycle::flush_links(&app);
38    #[cfg(feature = "engine")]
39    let engine = state.engine.as_ref().map(|e| e.lifecycle());
40    #[cfg(not(feature = "engine"))]
41    let engine: Option<Value> = None;
42    json!({"background": crate::lifecycle::launched_in_background(), "appId": state.app_id, "appName": state.app_name, "engine": engine})
43}
44
45/// Diagnostics for tests and support: menu rows, tray, instance role, nav policy.
46#[tauri::command]
47pub fn rk_shell_status(app: AppHandle<Wry>, state: State<'_, ShellState>) -> Value {
48    let tray = app.tray_by_id(crate::tray::TRAY_ID).is_some();
49    // Where the OS placed the status item (screen points). Present only once AppKit/the shell
50    // gave the item real geometry, which makes this an OS-derived fact, not a registry entry.
51    let tray_rect = app
52        .tray_by_id(crate::tray::TRAY_ID)
53        .and_then(|t| t.rect().ok().flatten())
54        .map(|r| {
55            let (p, z) = (
56                r.position.to_physical::<f64>(1.0),
57                r.size.to_physical::<f64>(1.0),
58            );
59            json!({"x": p.x, "y": p.y, "w": z.width, "h": z.height})
60        });
61    let menu: Vec<Value> = state
62        .menu_rows
63        .lock()
64        .unwrap_or_else(|e| e.into_inner())
65        .iter()
66        .map(|(p, l)| json!({"path": p, "label": l}))
67        .collect();
68    #[cfg(any(
69        debug_assertions,
70        feature = "content-devtools",
71        feature = "dev-inspect"
72    ))]
73    let devtools_open = app
74        .get_webview_window(&state.main_label)
75        .map(|w| w.is_devtools_open())
76        .unwrap_or(false);
77    #[cfg(not(any(
78        debug_assertions,
79        feature = "content-devtools",
80        feature = "dev-inspect"
81    )))]
82    let devtools_open = false;
83    let windows: Vec<Value> = app
84        .webview_windows()
85        .iter()
86        .map(|(label, w)| json!({"label": label, "visible": w.is_visible().unwrap_or(false), "title": w.title().unwrap_or_default(), "native": crate::windows::readback(w).map(|native| json!(native)).unwrap_or_else(|e| json!({"error": e}))}))
87        .collect();
88    let menu_enabled: BTreeMap<String, bool> = state
89        .menu_items
90        .lock()
91        .unwrap_or_else(|e| e.into_inner())
92        .iter()
93        .map(|(id, i)| (id.clone(), i.is_enabled().unwrap_or(false)))
94        .collect();
95    json!({
96        "appId": state.app_id,
97        "instance": *state.single_instance.lock().unwrap_or_else(|e| e.into_inner()),
98        "tray": tray,
99        "trayRect": tray_rect,
100        "menu": menu,
101        "windows": windows,
102        "menuEnabled": menu_enabled,
103        "stateDir": state.windows.dir(),
104        "devtoolsOpen": devtools_open,
105        "streamScheme": state.stream_scheme,
106        "streamSchemes": state.stream_schemes,
107        "siteScheme": state.site_scheme,
108        "modules": state.modules.names(),
109        "clipboardDir": state.clipboard_dir,
110        "cacheRepair": state.cache_repair.lock().unwrap_or_else(|e| e.into_inner()).as_ref().map(|(p, o)| json!({"dir": p, "outcome": o})),
111    })
112}
113
114#[tauri::command]
115pub fn rk_shell_window(action: String, window: WebviewWindow<Wry>) -> Result<(), String> {
116    let r: Result<(), String> = match action.as_str() {
117        "minimize" => window.minimize().map_err(|e| e.to_string()),
118        "maximize" => window.maximize().map_err(|e| e.to_string()),
119        "unmaximize" => window.unmaximize().map_err(|e| e.to_string()),
120        "toggle-maximize" => if window.is_maximized().unwrap_or(false) {
121            window.unmaximize()
122        } else {
123            window.maximize()
124        }
125        .map_err(|e| e.to_string()),
126        "fullscreen" => window
127            .set_fullscreen(!window.is_fullscreen().unwrap_or(false))
128            .map_err(|e| e.to_string()),
129        "hide" => {
130            let r = window.hide();
131            if window.label() == "main" {
132                crate::idle::note_hidden(window.app_handle(), false);
133            }
134            if r.is_ok() && window.label() == window.app_handle().state::<ShellState>().main_label {
135                crate::lifecycle::note_visibility(
136                    window.app_handle(),
137                    crate::lifecycle::Visibility::Hidden,
138                );
139            }
140            r.map_err(|e| e.to_string())
141        }
142        "show" => {
143            // The guard decides: never-show makes this a silent no-op.
144            let r = crate::windows::show_window(
145                window.app_handle(),
146                &window,
147                crate::lifecycle::RevealSource::WindowCommand,
148                crate::windows::RevealStyle::ShowFocus,
149            );
150            if window.label() == "main" && matches!(r, Ok(true)) {
151                crate::idle::note_visible(window.app_handle());
152            }
153            r.map(|_| ())
154        }
155        "close" => window.close().map_err(|e| e.to_string()),
156        "start-drag" => window.start_dragging().map_err(|e| e.to_string()),
157        other => return Err(format!("unknown window action {other}")),
158    };
159    r
160}
161
162#[tauri::command]
163pub fn rk_shell_open_window(spec: WindowSpec, app: AppHandle<Wry>) -> Result<String, String> {
164    crate::windows::open(&app, &spec)
165}
166
167#[tauri::command]
168pub fn rk_shell_set_tray(update: TrayUpdate, app: AppHandle<Wry>) -> Result<(), String> {
169    crate::tray::update(&app, &update)
170}
171
172#[tauri::command]
173pub fn rk_shell_set_menu_item_enabled(
174    id: String,
175    enabled: bool,
176    state: State<'_, ShellState>,
177) -> Result<(), String> {
178    let items = state.menu_items.lock().unwrap_or_else(|e| e.into_inner());
179    items
180        .get(&id)
181        .ok_or_else(|| format!("no menu item {id}"))?
182        .set_enabled(enabled)
183        .map_err(|e| e.to_string())
184}
185
186/// `enabled = None` reads; `Some(b)` sets and returns the new status.
187#[tauri::command]
188pub fn rk_shell_autostart(
189    enabled: Option<bool>,
190    state: State<'_, ShellState>,
191) -> Result<crate::autostart::AutostartStatus, String> {
192    match enabled {
193        Some(on) => crate::autostart::set_enabled(&state.autostart, on),
194        None => Ok(crate::autostart::status(&state.autostart)),
195    }
196}
197
198#[derive(Debug, Default, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct ClipboardPayload {
201    pub text: Option<String>,
202    pub html: Option<String>,
203    /// PNG, base64.
204    pub png: Option<String>,
205    #[serde(default)]
206    pub files: Vec<String>,
207    /// format name -> base64.
208    #[serde(default)]
209    pub custom: BTreeMap<String, String>,
210    /// Private pasteboard name (macOS); omit for the general clipboard.
211    pub board: Option<String>,
212}
213
214fn board_of(p: &Option<String>) -> Board {
215    p.clone().map(Board::Named).unwrap_or(Board::General)
216}
217
218#[tauri::command]
219pub fn rk_shell_clipboard_read(
220    board: Option<String>,
221    state: State<'_, ShellState>,
222) -> Result<ClipboardPayload, String> {
223    require(&state, Module::Clipboard).map_err(|e| e.to_string())?;
224    let c = crate::clipboard::read_with(&board_of(&board), &state.clipboard_options)?;
225    Ok(ClipboardPayload {
226        text: c.text,
227        html: c.html,
228        png: c.png.map(|p| b64::encode(&p)),
229        files: c.files,
230        custom: c
231            .custom
232            .into_iter()
233            .map(|(k, v)| (k, b64::encode(&v)))
234            .collect(),
235        board,
236    })
237}
238
239#[tauri::command]
240pub fn rk_shell_clipboard_write(
241    payload: ClipboardPayload,
242    state: State<'_, ShellState>,
243) -> Result<(), String> {
244    require(&state, Module::Clipboard).map_err(|e| e.to_string())?;
245    let mut custom = BTreeMap::new();
246    for (k, v) in &payload.custom {
247        custom.insert(
248            k.clone(),
249            b64::decode(v).ok_or_else(|| format!("custom format {k} is not base64"))?,
250        );
251    }
252    let png = match &payload.png {
253        Some(s) => Some(b64::decode(s).ok_or("png is not base64")?),
254        None => None,
255    };
256    crate::clipboard::write(
257        &board_of(&payload.board),
258        &ClipboardContent {
259            text: payload.text,
260            html: payload.html,
261            png,
262            files: payload.files,
263            custom,
264        },
265    )
266}
267
268/// The typed "open from clipboard" / paste result: files, then a bitmap (written to a granted temp
269/// file), then text, then `{"kind":"none"}`. Blocking clipboard work runs off the UI thread.
270#[tauri::command]
271pub async fn rk_shell_clipboard_item(app: AppHandle<Wry>) -> Result<ClipboardItem, ShellError> {
272    blocking(move || {
273        let state = app.state::<ShellState>();
274        require(&state, Module::Clipboard)?;
275        require(&state, Module::Broker)?;
276        crate::clipboard::take_item_with(
277            &state.broker,
278            &state.clipboard_dir,
279            state.clipboard,
280            &state.clipboard_options,
281        )
282    })
283    .await
284}
285
286#[tauri::command]
287pub async fn rk_shell_engine_call(
288    method: String,
289    params: Option<Value>,
290    app: AppHandle<Wry>,
291) -> Result<Value, String> {
292    #[cfg(feature = "engine")]
293    {
294        // Engine calls block on the sidecar: keep them off the async executor threads.
295        tauri::async_runtime::spawn_blocking(move || {
296            let state = app.state::<ShellState>();
297            let engine = state
298                .engine
299                .clone()
300                .ok_or_else(|| "no engine configured".to_string())?;
301            engine
302                .call(&app, &method, params.unwrap_or(Value::Null))
303                .map_err(|e| format!("{}: {}", e.code, e.message))
304        })
305        .await
306        .map_err(|e| e.to_string())?
307    }
308    #[cfg(not(feature = "engine"))]
309    {
310        let _ = (method, params, app);
311        Err("engine feature disabled".into())
312    }
313}
314
315#[tauri::command]
316pub fn rk_shell_traffic_lights(
317    dx: f64,
318    dy: f64,
319    window: WebviewWindow<Wry>,
320    state: State<'_, ShellState>,
321) {
322    state.traffic.set_offset(&window, dx, dy);
323}
324
325#[tauri::command]
326pub fn rk_shell_quit(app: AppHandle<Wry>) {
327    crate::lifecycle::quit(&app);
328}
329
330/// The renderer finished draining after `rightkit-shell://quit-requested`: mark the app exiting,
331/// then exit. The `ExitRequested` this raises sees `exiting` and does not prompt again.
332#[tauri::command]
333pub fn rk_shell_finish_quit(app: AppHandle<Wry>) {
334    crate::lifecycle::finish_quit(&app);
335}
336
337/// What the host supports, for the renderer to probe once. Read-only.
338#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
339#[serde(rename_all = "camelCase")]
340pub struct ShellCapabilities {
341    /// `rightkit-shell://quit-requested` plus `rk_shell_finish_quit`: the host opted into the drain.
342    pub quit_drain: bool,
343    /// `rk_shell_notify` honours `bypassDoNotDisturb`.
344    pub notify_bypass_dnd: bool,
345}
346
347/// The capabilities for a host whose quit drain is `quit_drain`.
348pub fn capabilities_for(quit_drain: bool) -> ShellCapabilities {
349    ShellCapabilities {
350        quit_drain,
351        notify_bypass_dnd: true,
352    }
353}
354
355#[tauri::command]
356pub fn rk_shell_capabilities(state: State<'_, ShellState>) -> ShellCapabilities {
357    capabilities_for(state.quit.enabled())
358}
359
360/// Mirror the app theme into native chrome (window appearance; tray template icons follow it).
361#[tauri::command]
362pub fn rk_shell_set_theme(theme: String, window: WebviewWindow<Wry>) -> Result<(), String> {
363    let t = match theme.as_str() {
364        "light" => Some(tauri::Theme::Light),
365        "dark" => Some(tauri::Theme::Dark),
366        "system" => None,
367        other => return Err(format!("unknown theme {other}")),
368    };
369    window.set_theme(t).map_err(|e| e.to_string())
370}
371
372// ---------------------------------------------------------------------------------------------
373// Notifications, presence / DnD, idle, path broker, recovery. These reject with the typed
374// `ShellError` envelope (`{code, message}`), unlike the older string-rejecting commands above.
375// ---------------------------------------------------------------------------------------------
376
377use crate::error::ShellError;
378use crate::notify::{NotificationRequest, NotifyOutcome, NotifyStatus};
379use crate::presence::{AutoPrefs, Environment, PresenceState, Rect, WindowObservation};
380
381async fn blocking<T: Send + 'static>(
382    f: impl FnOnce() -> Result<T, ShellError> + Send + 'static,
383) -> Result<T, ShellError> {
384    tauri::async_runtime::spawn_blocking(f)
385        .await
386        .map_err(|e| ShellError::Other(e.to_string()))?
387}
388
389/// Post a native notification. Resolves `{delivered:false, suppressed:"dnd"}` while Do-Not-Disturb
390/// (manual or an automatic meeting) is in effect. Runs off the UI thread: the first call may wait on
391/// the OS permission prompt.
392#[tauri::command]
393pub async fn rk_shell_notify(
394    request: NotificationRequest,
395    app: AppHandle<Wry>,
396) -> Result<NotifyOutcome, ShellError> {
397    let handle = app.clone();
398    let outcome = blocking(move || {
399        let state = handle.state::<ShellState>();
400        crate::notify::post(
401            &request,
402            &state.notifications,
403            state.presence.effective_dnd(),
404        )
405    })
406    .await?;
407    // A frontmost app's banner is suppressed by the OS; ask for the gentler attention cue instead.
408    if outcome.delivered {
409        let main = app.state::<ShellState>().main_label.clone();
410        if let Some(w) = app.get_webview_window(&main) {
411            if w.is_focused().unwrap_or(false) {
412                let _ = w.request_user_attention(Some(tauri::UserAttentionType::Informational));
413            }
414        }
415    }
416    Ok(outcome)
417}
418
419/// Authorization plus what the OS reports as delivered to this app (the QA read-back).
420#[tauri::command]
421pub async fn rk_shell_notify_status(app: AppHandle<Wry>) -> Result<NotifyStatus, ShellError> {
422    blocking(move || {
423        Ok(crate::notify::status(
424            &app.state::<ShellState>().notifications,
425        ))
426    })
427    .await
428}
429
430/// Remove delivered notifications by id; `None` or an empty list removes all of this app's.
431#[tauri::command]
432pub async fn rk_shell_notify_clear(ids: Option<Vec<String>>) -> Result<(), ShellError> {
433    blocking(move || crate::notify::clear_delivered(&ids.unwrap_or_default())).await
434}
435
436#[tauri::command]
437pub fn rk_shell_badge(
438    count: i64,
439    app: AppHandle<Wry>,
440    state: State<'_, ShellState>,
441) -> Result<u32, ShellError> {
442    let n = state.notifications.set_badge(count);
443    if let Some(w) = app.get_webview_window(&state.main_label) {
444        w.set_badge_count((n > 0).then_some(i64::from(n)))
445            .map_err(|e| ShellError::Other(format!("set badge count: {e}")))?;
446    }
447    Ok(n)
448}
449
450#[tauri::command]
451pub fn rk_shell_set_dnd(
452    enabled: bool,
453    app: AppHandle<Wry>,
454    state: State<'_, ShellState>,
455) -> PresenceState {
456    state.presence.set_manual_dnd(enabled);
457    let s = state.presence.snapshot();
458    let _ = tauri::Emitter::emit(&app, crate::EVENT_PRESENCE, s);
459    s
460}
461
462#[tauri::command]
463pub fn rk_shell_presence(state: State<'_, ShellState>) -> PresenceState {
464    state.presence.snapshot()
465}
466
467/// Opt in or out of the automatic controls. Turning one off clears its stale automatic state.
468#[tauri::command]
469pub fn rk_shell_presence_prefs(prefs: AutoPrefs, state: State<'_, ShellState>) -> PresenceState {
470    state.presence.set_prefs(prefs);
471    if !prefs.auto_hide_fullscreen || !prefs.auto_dnd_meetings {
472        let cur = state.presence.snapshot();
473        state.presence.set_auto(crate::presence::AutoState {
474            hidden_for_fullscreen: cur.auto_hidden && prefs.auto_hide_fullscreen,
475            dnd_for_meeting: cur.auto_dnd && prefs.auto_dnd_meetings,
476        });
477    }
478    state.presence.snapshot()
479}
480
481/// Sample the real OS now: `{environment, windows}` (count of visible windows seen).
482#[tauri::command]
483pub async fn rk_shell_presence_probe(app: AppHandle<Wry>) -> Result<Value, ShellError> {
484    blocking(move || {
485        let name = app.state::<ShellState>().app_name.clone();
486        let (env, windows) = crate::presence::detect_live(&name).map_err(ShellError::Unsupported)?;
487        Ok(json!({"environment": env, "windows": windows, "supported": crate::presence::controls_supported()}))
488    })
489    .await
490}
491
492/// Run the detection policy over an injected observation. With `apply`, the result is reconciled
493/// with the current preferences and becomes the automatic state (what the poller would do), so a
494/// meeting can be simulated without a real conferencing app.
495#[tauri::command]
496pub fn rk_shell_presence_evaluate(
497    monitor: Rect,
498    windows: Vec<WindowObservation>,
499    apply: Option<bool>,
500    state: State<'_, ShellState>,
501) -> Value {
502    let env: Environment = crate::presence::detect(monitor, &windows, &state.app_name);
503    if apply.unwrap_or(false) {
504        state
505            .presence
506            .apply_observation(crate::presence::reconcile(state.presence.prefs(), env));
507    }
508    json!({"environment": env, "state": state.presence.snapshot()})
509}
510
511#[tauri::command]
512pub fn rk_shell_idle_status(app: AppHandle<Wry>) -> crate::idle::IdleStatus {
513    crate::idle::status(&app)
514}
515
516/// App-driven hide/show of the main window for the idle machine.
517#[tauri::command]
518pub fn rk_shell_idle_note(hidden: bool, suspend_after_ms: Option<u64>, app: AppHandle<Wry>) {
519    if hidden {
520        crate::idle::note_hidden_after(
521            &app,
522            false,
523            suspend_after_ms.map(std::time::Duration::from_millis),
524        );
525    } else {
526        crate::idle::note_visible(&app);
527    }
528}
529
530/// Canonicalise `path` and require a broker grant (`write: true` checks a save target).
531#[tauri::command]
532pub fn rk_shell_path_require(
533    path: String,
534    write: Option<bool>,
535    state: State<'_, ShellState>,
536) -> Result<String, ShellError> {
537    require(&state, Module::Broker)?;
538    if write.unwrap_or(false) {
539        state.broker.require_write_target(&path)
540    } else {
541        state.broker.require_file(&path)
542    }
543}
544
545#[tauri::command]
546pub fn rk_shell_path_revoke(path: String, state: State<'_, ShellState>) -> Result<(), ShellError> {
547    require(&state, Module::Broker)?;
548    state.broker.revoke_path(&path)
549}
550
551/// Trade a one-shot recent-file id (minted in Rust) for a grant.
552#[tauri::command]
553pub fn rk_shell_path_grant_recent(
554    id: String,
555    state: State<'_, ShellState>,
556) -> Result<String, ShellError> {
557    require(&state, Module::Broker)?;
558    state.broker.grant_known(&id)
559}
560
561/// Re-grant a save-as target the app just wrote.
562#[tauri::command]
563pub fn rk_shell_path_regrant_saved(
564    path: String,
565    state: State<'_, ShellState>,
566) -> Result<String, ShellError> {
567    require(&state, Module::Broker)?;
568    state.broker.regrant_saved_target(&path)
569}
570
571/// Mint the stream URL for a granted file. `scheme` picks one of the registered stream schemes
572/// (default: the first). Rejects `broker_denied` for anything not granted.
573#[tauri::command]
574pub fn rk_shell_stream_grant(
575    path: String,
576    scheme: Option<String>,
577    state: State<'_, ShellState>,
578) -> Result<Value, ShellError> {
579    require(&state, Module::Broker)?;
580    require(&state, Module::Stream)?;
581    let scheme = match scheme {
582        Some(requested) => {
583            let requested = requested.to_ascii_lowercase();
584            if state.stream_schemes.contains(&requested) {
585                requested
586            } else {
587                return Err(ShellError::Invalid(format!(
588                    "stream scheme {requested} is not registered"
589                )));
590            }
591        }
592        None => state
593            .stream_scheme
594            .clone()
595            .ok_or_else(|| ShellError::Unsupported("no stream protocol configured".into()))?,
596    };
597    let id = state.broker.grant_stream(&path)?;
598    let url = if cfg!(windows) {
599        format!("http://{scheme}.localhost/{id}")
600    } else {
601        format!("{scheme}://localhost/{id}")
602    };
603    Ok(json!({"id": id, "url": url}))
604}
605
606/// Grant the site root of a document the person already opened, and return the base URL the
607/// document's `<base href>` uses: `{token, root, baseUrl}`.
608#[tauri::command]
609pub fn rk_shell_site_grant(
610    document: String,
611    state: State<'_, ShellState>,
612) -> Result<Value, ShellError> {
613    require(&state, Module::Broker)?;
614    require(&state, Module::Stream)?;
615    let scheme = state
616        .site_scheme
617        .clone()
618        .ok_or_else(|| ShellError::Unsupported("no site protocol configured".into()))?;
619    let grant = crate::site::grant_document_site(&state.broker, &document, &state.site_policy)?;
620    Ok(json!({
621        "token": grant.token,
622        "root": crate::broker::path_to_string(&grant.root),
623        "baseUrl": crate::site::site_base_url(&scheme, &grant.token),
624    }))
625}
626
627/// Revoke a site-root token when its tab closes. Its URLs stop serving at once and never revive.
628#[tauri::command]
629pub fn rk_shell_site_revoke(token: String, state: State<'_, ShellState>) -> Result<(), ShellError> {
630    require(&state, Module::Broker)?;
631    state.broker.revoke_site_root(&token)
632}
633
634fn recovery_of(state: &ShellState) -> Result<&crate::recovery::RecoveryStore, ShellError> {
635    require(state, Module::Recovery)?;
636    require(state, Module::Broker)?;
637    state
638        .recovery
639        .as_ref()
640        .ok_or_else(|| ShellError::Unsupported("setup recovery is not configured".into()))
641}
642
643#[tauri::command]
644pub fn rk_shell_recovery_save(
645    snapshot: String,
646    state: State<'_, ShellState>,
647) -> Result<(), ShellError> {
648    recovery_of(&state)?.save(&snapshot, &state.broker)
649}
650
651#[tauri::command]
652pub fn rk_shell_recovery_load(state: State<'_, ShellState>) -> Result<Option<String>, ShellError> {
653    recovery_of(&state)?.load(&state.broker)
654}
655
656#[tauri::command]
657pub fn rk_shell_recovery_clear(state: State<'_, ShellState>) -> Result<(), ShellError> {
658    recovery_of(&state)?.clear()
659}