Skip to main content

picoframe_core/
lib.rs

1//! Core types and the base frame plugin for picoframe Tauri apps.
2//!
3//! Every picoframe plugin command returns a [`CliResult`] — a small, uniform
4//! envelope the frontend's typed `defineCommand` bindings unwrap. This mirrors
5//! the engineer-assist `CliResult` pattern but trimmed to the essentials.
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use tauri::{
10    plugin::{Builder, TauriPlugin},
11    AppHandle, Runtime,
12};
13
14#[cfg(target_os = "macos")]
15mod mouse_nav;
16
17pub mod sidecar;
18pub use sidecar::{Sidecar, SidecarOptions};
19
20/// Uniform result envelope returned by picoframe plugin commands.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CliResult {
23    pub success: bool,
24    #[serde(skip_serializing_if = "Option::is_none", default)]
25    pub data: Option<Value>,
26    #[serde(skip_serializing_if = "Option::is_none", default)]
27    pub error: Option<String>,
28}
29
30impl CliResult {
31    /// A successful result carrying a JSON payload.
32    pub fn ok(data: Value) -> Self {
33        Self { success: true, data: Some(data), error: None }
34    }
35
36    /// A failed result carrying an error message.
37    pub fn err(message: impl Into<String>) -> Self {
38        Self { success: false, data: None, error: Some(message.into()) }
39    }
40}
41
42/// The base frame plugin. Registered first in every picoframe app's builder.
43///
44/// On macOS its `setup` installs a native NSEvent monitor that emits `mouse-nav`
45/// for the X1/X2 mouse buttons (the frame's `useMouseNavigation` hook handles the
46/// DOM path on Windows/Linux). It registers no commands yet — window/theme
47/// commands land later — and anchors the generated `main.rs`
48/// `.plugin(picoframe_core::init())` call above the plugin markers.
49pub fn init<R: Runtime>() -> TauriPlugin<R> {
50    Builder::new("picoframe")
51        .setup(|_app, _api| {
52            #[cfg(target_os = "macos")]
53            mouse_nav::install(_app.clone());
54            Ok(())
55        })
56        .build()
57}
58
59/// Reveal the app's `main` window with a background colour matching the OS
60/// light/dark appearance, so no default-white webview frame flashes before the
61/// UI paints. The native window pane that appears first follows the OS theme, so
62/// matching the webview background to it keeps the two seamless.
63///
64/// The `main` window must be created hidden (`"visible": false` in
65/// `tauri.conf.json`): the webview paints white the instant it becomes visible,
66/// too early for any in-page script to prevent. Call this from the app's
67/// `.setup()` hook, where config windows already exist (they do not during
68/// plugin setup). A missing window or a failed colour/show call is ignored - the
69/// worst case is the unfixed flash, never a hidden window on the happy path.
70pub fn reveal_main_window<R: Runtime>(app: &AppHandle<R>) {
71    use tauri::{window::Color, Manager, Theme};
72
73    let Some(window) = app.get_webview_window("main") else {
74        return;
75    };
76
77    // Mirrors picoframe's `--background` tokens: dark is hsl(240 6% 7%), which is
78    // rgb(17, 17, 19); light is white.
79    let color = match window.theme() {
80        Ok(Theme::Dark) => Color(17, 17, 19, 255),
81        _ => Color(255, 255, 255, 255),
82    };
83    let _ = window.set_background_color(Some(color));
84    let _ = window.show();
85}
86
87/// The single disk store file picoframe apps share, in the app data dir. Both the JS
88/// side (`@picoframe/store`'s `createTauriStore`) and Rust plugins open this file, so
89/// no plugin needs to own "the" store. Namespace keys by convention, e.g. `"hello.draft"`.
90pub const STORE_PATH: &str = "picoframe.json";
91
92/// Handle to the shared picoframe store for the Rust side. Requires the app to register
93/// `tauri_plugin_store::Builder::default().build()` (the CLI app template does this).
94pub fn store<R: Runtime>(
95    app: &AppHandle<R>,
96) -> Result<std::sync::Arc<tauri_plugin_store::Store<R>>, tauri_plugin_store::Error> {
97    use tauri_plugin_store::StoreExt;
98    app.store(STORE_PATH)
99}