use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Category {
Core,
User,
Tool,
Fun,
Push,
Admin,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PluginMeta {
pub key: &'static str,
pub name: &'static str,
pub category: Category,
pub version: &'static str,
pub description: &'static str,
pub can_disable: bool,
pub default_enable: bool,
pub hidden: bool,
pub maintain: bool,
pub module_path: &'static str,
}
impl PluginMeta {
pub const DEFAULT: PluginMeta = PluginMeta {
key: "",
name: "",
category: Category::User,
version: "",
description: "",
can_disable: true,
default_enable: true,
hidden: false,
maintain: false,
module_path: "",
};
}
#[derive(Clone, Copy, Debug)]
pub enum TriggerKind {
Command,
Event(crate::event_trigger::EventKind),
}
#[derive(Clone, Debug)]
pub struct TriggerMeta {
pub id: &'static str,
pub key: &'static str,
pub plugin_key: &'static str,
pub name: &'static str,
pub description: &'static str,
pub usage: &'static str,
pub words: &'static [&'static str],
pub args: &'static [crate::args::ArgSpec],
pub order: i32,
pub can_disable: bool,
pub default_enable: bool,
pub hidden: bool,
pub kind: TriggerKind,
pub module_path: &'static str,
}
pub struct PluginSpec {
pub meta: PluginMeta,
}
inventory::collect!(PluginSpec);
pub fn link_trigger<'a>(trigger_path: &str, plugins: &'a [PluginMeta]) -> Option<&'a PluginMeta> {
plugins.iter().filter(|p| is_module_prefix(p.module_path, trigger_path)).max_by_key(|p| p.module_path.len())
}
fn is_module_prefix(prefix: &str, path: &str) -> bool {
path == prefix || (path.starts_with(prefix) && path[prefix.len()..].starts_with("::"))
}
pub fn registered_plugins() -> Vec<PluginMeta> {
inventory::iter::<PluginSpec>.into_iter().map(|s| s.meta.clone()).collect()
}
fn intern(s: &str) -> &'static str {
static CACHE: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(HashSet::new()));
let mut guard = cache.lock().expect("intern cache mutex poisoned");
if let Some(&existing) = guard.get(s) {
return existing;
}
let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
guard.insert(leaked);
leaked
}
fn last_segment(module_path: &str) -> &str {
module_path.rsplit("::").next().unwrap_or(module_path)
}
fn registered_plugins_resolved() -> Vec<PluginMeta> {
registered_plugins()
.into_iter()
.map(|mut p| {
if p.key.is_empty() {
p.key = intern(last_segment(p.module_path));
}
p
})
.collect()
}
pub fn resolve_plugin_for(module_path: &'static str) -> (&'static str, bool, bool) {
let plugins = registered_plugins_resolved();
match link_trigger(module_path, &plugins) {
Some(p) => (p.key, p.default_enable, p.can_disable),
None => (intern(module_path), true, true),
}
}
pub fn trigger_key(plugin_key: &str, id: &str) -> &'static str {
intern(&format!("{plugin_key}.{id}"))
}
#[derive(Clone, Copy, Debug)]
pub enum SwitchKey {
Literal(&'static str),
Trigger { module_path: &'static str, id: &'static str },
}
impl SwitchKey {
pub const fn trigger(module_path: &'static str, id: &'static str) -> Self {
SwitchKey::Trigger { module_path, id }
}
pub fn resolve(&self) -> &'static str {
match *self {
SwitchKey::Literal(s) => s,
SwitchKey::Trigger { module_path, id } => {
let (plugin_key, _, _) = resolve_plugin_for(module_path);
trigger_key(plugin_key, id)
}
}
}
}
impl From<&str> for SwitchKey {
fn from(s: &str) -> Self {
SwitchKey::Literal(intern(s))
}
}
impl From<String> for SwitchKey {
fn from(s: String) -> Self {
SwitchKey::Literal(intern(&s))
}
}
pub fn registered_triggers_resolved() -> Vec<TriggerMeta> {
let plugins = registered_plugins_resolved();
crate::registry::registered_triggers()
.into_iter()
.map(|mut t| {
let pk = match link_trigger(t.module_path, &plugins) {
Some(p) => p.key,
None => intern(t.module_path),
};
t.plugin_key = pk;
t.key = trigger_key(pk, t.id);
t
})
.collect()
}