1use serde::Deserialize;
4
5#[cfg(feature = "schema")]
6use schemars::JsonSchema;
7
8#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
12#[cfg_attr(feature = "schema", derive(JsonSchema))]
13#[serde(rename_all = "camelCase")]
14pub enum TransportKind {
15 #[default]
18 AppGroup,
19 UserDefaults,
21 WidgetContainer,
23 Auto,
25}
26
27impl TransportKind {
28 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 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#[derive(Debug, Clone, Deserialize, Default)]
56#[cfg_attr(feature = "schema", derive(JsonSchema))]
57#[serde(rename_all = "camelCase")]
58pub struct WidgetsPluginConfig {
59 #[serde(default)]
63 pub transport: Option<TransportKind>,
64 pub app_group: Option<String>,
66 pub extension_bundle_id: Option<String>,
68 #[serde(default)]
70 pub restore_windows_on_start: bool,
71 #[serde(default)]
76 pub activity_surface: Option<String>,
77}
78
79impl WidgetsPluginConfig {
80 pub fn with_defaults(mut self, identifier: &str) -> Self {
83 let missing = self
84 .app_group
85 .as_deref()
86 .map(str::trim)
87 .unwrap_or("")
88 .is_empty();
89 if missing && !identifier.trim().is_empty() {
90 self.app_group = Some(format!("group.{}", identifier.trim()));
91 }
92 if let Some(group) = self.app_group.as_deref() {
93 crate::group::set_default_group(group);
94 }
95 self
96 }
97}
98
99pub fn env_transport_override() -> Option<TransportKind> {
101 std::env::var("WIDGET_TRANSPORT")
102 .ok()
103 .filter(|s| !s.is_empty())
104 .and_then(|s| TransportKind::parse(&s))
105}
106
107pub fn effective_transport(cfg: &WidgetsPluginConfig) -> TransportKind {
109 env_transport_override()
110 .or(cfg.transport)
111 .unwrap_or(if cfg!(target_os = "macos") {
112 TransportKind::WidgetContainer
113 } else {
114 TransportKind::AppGroup
115 })
116}
117
118#[cfg(test)]
119pub(crate) mod tests {
120 use super::*;
121
122 pub(crate) static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
124
125 const KINDS: [TransportKind; 4] = [
126 TransportKind::AppGroup,
127 TransportKind::UserDefaults,
128 TransportKind::WidgetContainer,
129 TransportKind::Auto,
130 ];
131
132 #[test]
135 fn transport_kind_spellings_parse_reject_and_roundtrip() {
136 let accepted = [
138 ("appGroup", TransportKind::AppGroup),
139 ("appgroup", TransportKind::AppGroup),
140 ("app_group", TransportKind::AppGroup),
141 ("app-group", TransportKind::AppGroup),
142 (" App-Group ", TransportKind::AppGroup),
143 ("userDefaults", TransportKind::UserDefaults),
144 ("user_defaults", TransportKind::UserDefaults),
145 ("user-defaults", TransportKind::UserDefaults),
146 ("defaults", TransportKind::UserDefaults),
147 ("widgetContainer", TransportKind::WidgetContainer),
148 ("widget_container", TransportKind::WidgetContainer),
149 ("widget-container", TransportKind::WidgetContainer),
150 ("container", TransportKind::WidgetContainer),
151 ("auto", TransportKind::Auto),
152 ("AUTO", TransportKind::Auto),
153 ];
154 for (raw, want) in accepted {
155 assert_eq!(TransportKind::parse(raw), Some(want), "parse({raw:?})");
156 }
157 for raw in ["", "app group", "fanout"] {
159 assert_eq!(TransportKind::parse(raw), None, "parse({raw:?})");
160 }
161 for kind in KINDS {
163 assert_eq!(TransportKind::parse(kind.as_str()), Some(kind));
164 }
165 assert_eq!(TransportKind::default(), TransportKind::AppGroup);
166 }
167
168 #[test]
171 fn plugin_config_defaults_then_effective_transport() {
172 let conf: WidgetsPluginConfig =
174 serde_json::from_value(serde_json::json!({"appGroup": " "})).unwrap();
175 assert_eq!(conf.transport, None);
176 assert!(!conf.restore_windows_on_start);
177 let filled = conf.with_defaults(" com.ex.app ");
178 assert_eq!(filled.app_group.as_deref(), Some("group.com.ex.app"));
179 assert_eq!(
180 WidgetsPluginConfig::default()
181 .with_defaults("com.ex.app")
182 .app_group
183 .as_deref(),
184 Some("group.com.ex.app")
185 );
186 let kept: WidgetsPluginConfig = serde_json::from_value(
188 serde_json::json!({"appGroup": "group.custom", "transport": "appGroup"}),
189 )
190 .unwrap();
191 let kept = kept.with_defaults("com.ex.app");
192 assert_eq!(kept.app_group.as_deref(), Some("group.custom"));
193 assert_eq!(kept.transport, Some(TransportKind::AppGroup));
194 assert_eq!(
196 WidgetsPluginConfig::default().with_defaults(" ").app_group,
197 None
198 );
199
200 let _env = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
202 let prev = std::env::var_os("WIDGET_TRANSPORT");
203 std::env::remove_var("WIDGET_TRANSPORT");
204 let os_default = if cfg!(target_os = "macos") {
205 TransportKind::WidgetContainer
206 } else {
207 TransportKind::AppGroup
208 };
209 assert_eq!(effective_transport(&filled), os_default);
210 assert_eq!(effective_transport(&kept), TransportKind::AppGroup);
211 std::env::set_var("WIDGET_TRANSPORT", "widget-container");
212 assert_eq!(
213 env_transport_override(),
214 Some(TransportKind::WidgetContainer)
215 );
216 assert_eq!(effective_transport(&kept), TransportKind::WidgetContainer);
217 std::env::set_var("WIDGET_TRANSPORT", "");
218 assert_eq!(
219 effective_transport(&kept),
220 TransportKind::AppGroup,
221 "empty env must not override"
222 );
223 std::env::set_var("WIDGET_TRANSPORT", "bogus");
224 assert_eq!(effective_transport(&kept), TransportKind::AppGroup);
225 match prev {
226 Some(v) => std::env::set_var("WIDGET_TRANSPORT", v),
227 None => std::env::remove_var("WIDGET_TRANSPORT"),
228 }
229 }
230}