Skip to main content

tauri_plugin_widgets/
config.rs

1//! Plugin config from `tauri.conf.json` → `plugins.widgets`.
2
3use serde::Deserialize;
4
5#[cfg(feature = "schema")]
6use schemars::JsonSchema;
7
8/// Which Apple host→widget channel to use.
9///
10/// Pick this at build time (you know your signing). Do not rely on runtime fan-out.
11#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
12#[cfg_attr(feature = "schema", derive(JsonSchema))]
13#[serde(rename_all = "camelCase")]
14pub enum TransportKind {
15    /// App Group shared container file (`widget_data.json`). Needs a real Team ID.
16    /// Production-safe default; use `auto` explicitly for ad-hoc probing.
17    #[default]
18    AppGroup,
19    /// App Group `UserDefaults` suite. Same signing requirements as [`AppGroup`].
20    UserDefaults,
21    /// Widget extension container file. Works with ad-hoc signing; macOS host must not be sandboxed.
22    WidgetContainer,
23    /// One-shot probe at startup (dev only). Prefer pinning an explicit driver in conf.
24    Auto,
25}
26
27impl TransportKind {
28    /// Parse `WIDGET_TRANSPORT` / conf string.
29    pub fn parse(s: &str) -> Option<Self> {
30        match s.trim().to_ascii_lowercase().as_str() {
31            "appgroup" | "app_group" | "app-group" => Some(Self::AppGroup),
32            "userdefaults" | "user_defaults" | "user-defaults" | "defaults" => {
33                Some(Self::UserDefaults)
34            }
35            "widgetcontainer" | "widget_container" | "widget-container" | "container" => {
36                Some(Self::WidgetContainer)
37            }
38            "auto" => Some(Self::Auto),
39            _ => None,
40        }
41    }
42
43    /// Wire / conf string for this kind (`appGroup`, `auto`, …).
44    pub fn as_str(self) -> &'static str {
45        match self {
46            Self::AppGroup => "appGroup",
47            Self::UserDefaults => "userDefaults",
48            Self::WidgetContainer => "widgetContainer",
49            Self::Auto => "auto",
50        }
51    }
52}
53
54/// `plugins.widgets` object from `tauri.conf.json`.
55#[derive(Debug, Clone, Deserialize, Default)]
56#[cfg_attr(feature = "schema", derive(JsonSchema))]
57#[serde(rename_all = "camelCase")]
58pub struct WidgetsPluginConfig {
59    /// Host→widget transport driver.
60    #[serde(default)]
61    pub transport: TransportKind,
62    /// App Group id, e.g. `group.com.example.app`.
63    pub app_group: Option<String>,
64    /// Widget extension bundle id (macOS container path). Default: `{app_id}.widgetkit`.
65    pub extension_bundle_id: Option<String>,
66}
67
68/// `WIDGET_TRANSPORT` overrides `plugins.widgets.transport` when set.
69pub fn env_transport_override() -> Option<TransportKind> {
70    std::env::var("WIDGET_TRANSPORT")
71        .ok()
72        .filter(|s| !s.is_empty())
73        .and_then(|s| TransportKind::parse(&s))
74}
75
76/// Merge conf + env into the effective kind.
77pub fn effective_transport(cfg: &WidgetsPluginConfig) -> TransportKind {
78    env_transport_override().unwrap_or(cfg.transport)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn parse_accepts_common_spellings() {
87        let cases = [
88            ("appGroup", TransportKind::AppGroup),
89            ("appgroup", TransportKind::AppGroup),
90            ("app_group", TransportKind::AppGroup),
91            ("app-group", TransportKind::AppGroup),
92            (" App-Group ", TransportKind::AppGroup),
93            ("userDefaults", TransportKind::UserDefaults),
94            ("user_defaults", TransportKind::UserDefaults),
95            ("user-defaults", TransportKind::UserDefaults),
96            ("defaults", TransportKind::UserDefaults),
97            ("widgetContainer", TransportKind::WidgetContainer),
98            ("widget_container", TransportKind::WidgetContainer),
99            ("widget-container", TransportKind::WidgetContainer),
100            ("container", TransportKind::WidgetContainer),
101            ("auto", TransportKind::Auto),
102            ("AUTO", TransportKind::Auto),
103        ];
104        for (raw, want) in cases {
105            assert_eq!(TransportKind::parse(raw), Some(want), "parse({raw:?})");
106        }
107    }
108
109    #[test]
110    fn parse_rejects_unknown() {
111        assert_eq!(TransportKind::parse(""), None);
112        assert_eq!(TransportKind::parse("app group"), None);
113        assert_eq!(TransportKind::parse("fanout"), None);
114    }
115
116    #[test]
117    fn as_str_roundtrips_through_parse() {
118        for kind in [
119            TransportKind::AppGroup,
120            TransportKind::UserDefaults,
121            TransportKind::WidgetContainer,
122            TransportKind::Auto,
123        ] {
124            assert_eq!(TransportKind::parse(kind.as_str()), Some(kind));
125        }
126    }
127
128    #[test]
129    fn env_overrides_conf() {
130        let cfg = WidgetsPluginConfig {
131            transport: TransportKind::AppGroup,
132            ..Default::default()
133        };
134        // SAFETY: serial tests; we restore afterwards.
135        let prev = std::env::var_os("WIDGET_TRANSPORT");
136        std::env::set_var("WIDGET_TRANSPORT", "widget-container");
137        assert_eq!(effective_transport(&cfg), TransportKind::WidgetContainer);
138        std::env::set_var("WIDGET_TRANSPORT", "");
139        assert_eq!(
140            effective_transport(&cfg),
141            TransportKind::AppGroup,
142            "empty env must not override"
143        );
144        match prev {
145            Some(v) => std::env::set_var("WIDGET_TRANSPORT", v),
146            None => std::env::remove_var("WIDGET_TRANSPORT"),
147        }
148    }
149}