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. Unset: `widgetContainer` on macOS (a Developer ID
60    /// extension without a provisioning profile cannot open a `group.*` container),
61    /// `appGroup` elsewhere.
62    #[serde(default)]
63    pub transport: Option<TransportKind>,
64    /// App Group id, e.g. `group.com.example.app`.
65    pub app_group: Option<String>,
66    /// Widget extension bundle id (macOS container path). Default: `{app_id}.widgetkit`.
67    pub extension_bundle_id: Option<String>,
68}
69
70impl WidgetsPluginConfig {
71    /// Fill `appGroup` with `group.<identifier>` when the app did not set it, and make it
72    /// the default group for commands that omit `group`.
73    pub fn with_defaults(mut self, identifier: &str) -> Self {
74        let missing = self
75            .app_group
76            .as_deref()
77            .map(str::trim)
78            .unwrap_or("")
79            .is_empty();
80        if missing && !identifier.trim().is_empty() {
81            self.app_group = Some(format!("group.{}", identifier.trim()));
82        }
83        if let Some(group) = self.app_group.as_deref() {
84            crate::group::set_default_group(group);
85        }
86        self
87    }
88}
89
90/// `WIDGET_TRANSPORT` overrides `plugins.widgets.transport` when set.
91pub fn env_transport_override() -> Option<TransportKind> {
92    std::env::var("WIDGET_TRANSPORT")
93        .ok()
94        .filter(|s| !s.is_empty())
95        .and_then(|s| TransportKind::parse(&s))
96}
97
98/// Merge conf + env into the effective kind.
99pub fn effective_transport(cfg: &WidgetsPluginConfig) -> TransportKind {
100    env_transport_override()
101        .or(cfg.transport)
102        .unwrap_or(if cfg!(target_os = "macos") {
103            TransportKind::WidgetContainer
104        } else {
105            TransportKind::AppGroup
106        })
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn parse_accepts_common_spellings() {
115        let cases = [
116            ("appGroup", TransportKind::AppGroup),
117            ("appgroup", TransportKind::AppGroup),
118            ("app_group", TransportKind::AppGroup),
119            ("app-group", TransportKind::AppGroup),
120            (" App-Group ", TransportKind::AppGroup),
121            ("userDefaults", TransportKind::UserDefaults),
122            ("user_defaults", TransportKind::UserDefaults),
123            ("user-defaults", TransportKind::UserDefaults),
124            ("defaults", TransportKind::UserDefaults),
125            ("widgetContainer", TransportKind::WidgetContainer),
126            ("widget_container", TransportKind::WidgetContainer),
127            ("widget-container", TransportKind::WidgetContainer),
128            ("container", TransportKind::WidgetContainer),
129            ("auto", TransportKind::Auto),
130            ("AUTO", TransportKind::Auto),
131        ];
132        for (raw, want) in cases {
133            assert_eq!(TransportKind::parse(raw), Some(want), "parse({raw:?})");
134        }
135    }
136
137    #[test]
138    fn parse_rejects_unknown() {
139        assert_eq!(TransportKind::parse(""), None);
140        assert_eq!(TransportKind::parse("app group"), None);
141        assert_eq!(TransportKind::parse("fanout"), None);
142    }
143
144    #[test]
145    fn as_str_roundtrips_through_parse() {
146        for kind in [
147            TransportKind::AppGroup,
148            TransportKind::UserDefaults,
149            TransportKind::WidgetContainer,
150            TransportKind::Auto,
151        ] {
152            assert_eq!(TransportKind::parse(kind.as_str()), Some(kind));
153        }
154    }
155
156    #[test]
157    fn env_overrides_conf() {
158        let cfg = WidgetsPluginConfig {
159            transport: Some(TransportKind::AppGroup),
160            ..Default::default()
161        };
162        // SAFETY: serial tests; we restore afterwards.
163        let prev = std::env::var_os("WIDGET_TRANSPORT");
164        std::env::set_var("WIDGET_TRANSPORT", "widget-container");
165        assert_eq!(effective_transport(&cfg), TransportKind::WidgetContainer);
166        std::env::set_var("WIDGET_TRANSPORT", "");
167        assert_eq!(
168            effective_transport(&cfg),
169            TransportKind::AppGroup,
170            "empty env must not override"
171        );
172        match prev {
173            Some(v) => std::env::set_var("WIDGET_TRANSPORT", v),
174            None => std::env::remove_var("WIDGET_TRANSPORT"),
175        }
176    }
177}