use serde::Deserialize;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum TransportKind {
#[default]
AppGroup,
UserDefaults,
WidgetContainer,
Auto,
}
impl TransportKind {
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"appgroup" | "app_group" | "app-group" => Some(Self::AppGroup),
"userdefaults" | "user_defaults" | "user-defaults" | "defaults" => {
Some(Self::UserDefaults)
}
"widgetcontainer" | "widget_container" | "widget-container" | "container" => {
Some(Self::WidgetContainer)
}
"auto" => Some(Self::Auto),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::AppGroup => "appGroup",
Self::UserDefaults => "userDefaults",
Self::WidgetContainer => "widgetContainer",
Self::Auto => "auto",
}
}
}
#[derive(Debug, Clone, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct WidgetsPluginConfig {
#[serde(default)]
pub transport: Option<TransportKind>,
pub app_group: Option<String>,
pub extension_bundle_id: Option<String>,
#[serde(default)]
pub restore_windows_on_start: bool,
#[serde(default)]
pub activity_surface: Option<String>,
}
impl WidgetsPluginConfig {
pub fn with_defaults(mut self, identifier: &str) -> Self {
let missing = self
.app_group
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty();
if missing && !identifier.trim().is_empty() {
self.app_group = Some(format!("group.{}", identifier.trim()));
}
if let Some(group) = self.app_group.as_deref() {
crate::group::set_default_group(group);
}
self
}
}
pub fn env_transport_override() -> Option<TransportKind> {
std::env::var("WIDGET_TRANSPORT")
.ok()
.filter(|s| !s.is_empty())
.and_then(|s| TransportKind::parse(&s))
}
pub fn effective_transport(cfg: &WidgetsPluginConfig) -> TransportKind {
env_transport_override()
.or(cfg.transport)
.unwrap_or(if cfg!(target_os = "macos") {
TransportKind::WidgetContainer
} else {
TransportKind::AppGroup
})
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub(crate) static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
const KINDS: [TransportKind; 4] = [
TransportKind::AppGroup,
TransportKind::UserDefaults,
TransportKind::WidgetContainer,
TransportKind::Auto,
];
#[test]
fn transport_kind_spellings_parse_reject_and_roundtrip() {
let accepted = [
("appGroup", TransportKind::AppGroup),
("appgroup", TransportKind::AppGroup),
("app_group", TransportKind::AppGroup),
("app-group", TransportKind::AppGroup),
(" App-Group ", TransportKind::AppGroup),
("userDefaults", TransportKind::UserDefaults),
("user_defaults", TransportKind::UserDefaults),
("user-defaults", TransportKind::UserDefaults),
("defaults", TransportKind::UserDefaults),
("widgetContainer", TransportKind::WidgetContainer),
("widget_container", TransportKind::WidgetContainer),
("widget-container", TransportKind::WidgetContainer),
("container", TransportKind::WidgetContainer),
("auto", TransportKind::Auto),
("AUTO", TransportKind::Auto),
];
for (raw, want) in accepted {
assert_eq!(TransportKind::parse(raw), Some(want), "parse({raw:?})");
}
for raw in ["", "app group", "fanout"] {
assert_eq!(TransportKind::parse(raw), None, "parse({raw:?})");
}
for kind in KINDS {
assert_eq!(TransportKind::parse(kind.as_str()), Some(kind));
}
assert_eq!(TransportKind::default(), TransportKind::AppGroup);
}
#[test]
fn plugin_config_defaults_then_effective_transport() {
let conf: WidgetsPluginConfig =
serde_json::from_value(serde_json::json!({"appGroup": " "})).unwrap();
assert_eq!(conf.transport, None);
assert!(!conf.restore_windows_on_start);
let filled = conf.with_defaults(" com.ex.app ");
assert_eq!(filled.app_group.as_deref(), Some("group.com.ex.app"));
assert_eq!(
WidgetsPluginConfig::default()
.with_defaults("com.ex.app")
.app_group
.as_deref(),
Some("group.com.ex.app")
);
let kept: WidgetsPluginConfig = serde_json::from_value(
serde_json::json!({"appGroup": "group.custom", "transport": "appGroup"}),
)
.unwrap();
let kept = kept.with_defaults("com.ex.app");
assert_eq!(kept.app_group.as_deref(), Some("group.custom"));
assert_eq!(kept.transport, Some(TransportKind::AppGroup));
assert_eq!(
WidgetsPluginConfig::default().with_defaults(" ").app_group,
None
);
let _env = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev = std::env::var_os("WIDGET_TRANSPORT");
std::env::remove_var("WIDGET_TRANSPORT");
let os_default = if cfg!(target_os = "macos") {
TransportKind::WidgetContainer
} else {
TransportKind::AppGroup
};
assert_eq!(effective_transport(&filled), os_default);
assert_eq!(effective_transport(&kept), TransportKind::AppGroup);
std::env::set_var("WIDGET_TRANSPORT", "widget-container");
assert_eq!(
env_transport_override(),
Some(TransportKind::WidgetContainer)
);
assert_eq!(effective_transport(&kept), TransportKind::WidgetContainer);
std::env::set_var("WIDGET_TRANSPORT", "");
assert_eq!(
effective_transport(&kept),
TransportKind::AppGroup,
"empty env must not override"
);
std::env::set_var("WIDGET_TRANSPORT", "bogus");
assert_eq!(effective_transport(&kept), TransportKind::AppGroup);
match prev {
Some(v) => std::env::set_var("WIDGET_TRANSPORT", v),
None => std::env::remove_var("WIDGET_TRANSPORT"),
}
}
}