Skip to main content

victauri_core/
registry.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::sync::{Arc, RwLock};
4
5/// Metadata for a registered Tauri command, including intent and schema information.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct CommandInfo {
8    /// Fully qualified command name (e.g. "get_settings").
9    pub name: String,
10    /// Plugin namespace, if the command belongs to a Tauri plugin.
11    pub plugin: Option<String>,
12    /// Human-readable description of what the command does.
13    pub description: Option<String>,
14    /// Ordered list of arguments the command accepts.
15    pub args: Vec<CommandArg>,
16    /// Rust return type as a string (e.g. "Result<Settings, Error>").
17    pub return_type: Option<String>,
18    /// Whether the command handler is async.
19    pub is_async: bool,
20    /// Natural-language intent phrase for NL-to-command resolution.
21    pub intent: Option<String>,
22    /// Grouping category (e.g. "settings", "counter").
23    pub category: Option<String>,
24    /// Example natural-language queries that should resolve to this command.
25    pub examples: Vec<String>,
26}
27
28/// Schema for a single argument of a registered command.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct CommandArg {
31    /// Argument name as declared in the Rust function signature.
32    pub name: String,
33    /// Rust type name (e.g. "String", "`Option<u32>`").
34    pub type_name: String,
35    /// Whether the argument must be provided (not `Option`).
36    pub required: bool,
37    /// Optional JSON Schema for the argument's expected shape.
38    pub schema: Option<serde_json::Value>,
39}
40
41/// Thread-safe registry of known Tauri commands, indexed by name.
42#[derive(Debug, Clone)]
43pub struct CommandRegistry {
44    commands: Arc<RwLock<BTreeMap<String, CommandInfo>>>,
45}
46
47impl CommandRegistry {
48    /// Creates an empty command registry.
49    ///
50    /// ```
51    /// use victauri_core::CommandRegistry;
52    ///
53    /// let registry = CommandRegistry::new();
54    /// assert_eq!(registry.count(), 0);
55    /// assert!(registry.list().is_empty());
56    /// ```
57    pub fn new() -> Self {
58        Self {
59            commands: Arc::new(RwLock::new(BTreeMap::new())),
60        }
61    }
62
63    /// Registers a command, replacing any existing entry with the same name.
64    ///
65    /// ```
66    /// use victauri_core::{CommandRegistry, CommandInfo};
67    ///
68    /// let registry = CommandRegistry::new();
69    /// registry.register(CommandInfo {
70    ///     name: "greet".to_string(),
71    ///     plugin: None,
72    ///     description: Some("Say hello".to_string()),
73    ///     args: vec![],
74    ///     return_type: None,
75    ///     is_async: false,
76    ///     intent: None,
77    ///     category: None,
78    ///     examples: vec![],
79    /// });
80    /// assert_eq!(registry.count(), 1);
81    /// assert!(registry.get("greet").is_some());
82    /// ```
83    pub fn register(&self, info: CommandInfo) {
84        self.commands
85            .write()
86            .unwrap_or_else(|e| e.into_inner())
87            .insert(info.name.clone(), info);
88    }
89
90    /// Looks up a command by exact name.
91    pub fn get(&self, name: &str) -> Option<CommandInfo> {
92        self.commands
93            .read()
94            .unwrap_or_else(|e| e.into_inner())
95            .get(name)
96            .cloned()
97    }
98
99    /// Returns all registered commands in alphabetical order.
100    pub fn list(&self) -> Vec<CommandInfo> {
101        self.commands
102            .read()
103            .unwrap_or_else(|e| e.into_inner())
104            .values()
105            .cloned()
106            .collect()
107    }
108
109    /// Returns the number of registered commands.
110    pub fn count(&self) -> usize {
111        self.commands
112            .read()
113            .unwrap_or_else(|e| e.into_inner())
114            .len()
115    }
116
117    /// Searches commands by substring match on name or description (case-insensitive).
118    pub fn search(&self, query: &str) -> Vec<CommandInfo> {
119        let query_lower = query.to_lowercase();
120        self.commands
121            .read()
122            .unwrap_or_else(|e| e.into_inner())
123            .values()
124            .filter(|cmd| {
125                cmd.name.to_lowercase().contains(&query_lower)
126                    || cmd
127                        .description
128                        .as_ref()
129                        .is_some_and(|d| d.to_lowercase().contains(&query_lower))
130            })
131            .cloned()
132            .collect()
133    }
134
135    /// Resolves a natural-language query to commands ranked by relevance score.
136    pub fn resolve(&self, query: &str) -> Vec<ScoredCommand> {
137        let query_lower = query.to_lowercase();
138        let query_words: Vec<&str> = query_lower.split_whitespace().collect();
139        if query_words.is_empty() {
140            return Vec::new();
141        }
142
143        let mut scored: Vec<ScoredCommand> = self
144            .commands
145            .read()
146            .unwrap_or_else(|e| e.into_inner())
147            .values()
148            .filter_map(|cmd| {
149                let score = score_command(cmd, &query_lower, &query_words);
150                if score > 0.0 {
151                    Some(ScoredCommand {
152                        command: cmd.clone(),
153                        score,
154                    })
155                } else {
156                    None
157                }
158            })
159            .collect();
160
161        scored.sort_by(|a, b| b.score.total_cmp(&a.score));
162        scored
163    }
164}
165
166impl Default for CommandRegistry {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172/// A command paired with its relevance score from natural-language resolution.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ScoredCommand {
175    /// The matched command metadata.
176    pub command: CommandInfo,
177    /// Relevance score (higher is better); 0 means no match.
178    pub score: f64,
179}
180
181fn score_command(cmd: &CommandInfo, query_lower: &str, query_words: &[&str]) -> f64 {
182    let mut score = 0.0;
183    let name_lower = cmd.name.to_lowercase();
184    let name_words: Vec<&str> = name_lower.split('_').collect();
185
186    if name_lower == query_lower.replace(' ', "_") {
187        score += 10.0;
188    }
189
190    for word in query_words {
191        if name_lower.contains(word) {
192            score += 3.0;
193        }
194        if name_words.contains(word) {
195            score += 2.0;
196        }
197    }
198
199    if let Some(desc) = &cmd.description {
200        let desc_lower = desc.to_lowercase();
201        for word in query_words {
202            if desc_lower.contains(word) {
203                score += 1.5;
204            }
205        }
206    }
207
208    if let Some(intent) = &cmd.intent {
209        let intent_lower = intent.to_lowercase();
210        for word in query_words {
211            if intent_lower.contains(word) {
212                score += 2.5;
213            }
214        }
215    }
216
217    if let Some(category) = &cmd.category {
218        let cat_lower = category.to_lowercase();
219        for word in query_words {
220            if cat_lower.contains(word) {
221                score += 1.0;
222            }
223        }
224    }
225
226    for example in &cmd.examples {
227        let ex_lower = example.to_lowercase();
228        if ex_lower.contains(query_lower) {
229            score += 4.0;
230            break;
231        }
232        for word in query_words {
233            if ex_lower.contains(word) {
234                score += 0.5;
235            }
236        }
237    }
238
239    score
240}