stem_rs/interpreter/autocomplete.rs
1//! Tab completion for the interpreter prompt.
2//!
3//! This module provides autocompletion functionality for the Tor interpreter,
4//! enabling tab completion of commands, options, and arguments.
5//!
6//! # Overview
7//!
8//! The [`Autocompleter`] queries Tor for available commands and options,
9//! building a comprehensive list of completions including:
10//!
11//! - Interpreter commands (`/help`, `/events`, `/info`, etc.)
12//! - Tor control commands (`GETINFO`, `GETCONF`, `SETCONF`, etc.)
13//! - Command arguments (config options, event types, signals)
14//! - Help topics
15//!
16//! # Architecture
17//!
18//! On initialization, the autocompleter queries Tor for:
19//! - `info/names` - Available GETINFO options
20//! - `config/names` - Configuration options for GETCONF/SETCONF/RESETCONF
21//! - `events/names` - Event types for SETEVENTS
22//! - `features/names` - Features for USEFEATURE
23//! - `signal/names` - Signals for SIGNAL command
24//!
25//! These are combined with built-in commands to create the completion list.
26//!
27//! # Example
28//!
29//! ```rust,no_run
30//! use stem_rs::Controller;
31//! use stem_rs::interpreter::autocomplete::Autocompleter;
32//!
33//! # async fn example() -> Result<(), stem_rs::Error> {
34//! let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
35//! controller.authenticate(None).await?;
36//!
37//! let autocompleter = Autocompleter::new(&mut controller).await;
38//!
39//! // Get all matches for partial input
40//! let matches = autocompleter.matches("GETINFO");
41//! for m in matches {
42//! println!("{}", m);
43//! }
44//!
45//! // Get specific completion by index (for readline integration)
46//! if let Some(completion) = autocompleter.complete("GETINFO", 0) {
47//! println!("First match: {}", completion);
48//! }
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! # Python Stem Equivalent
54//!
55//! This module corresponds to Python Stem's `stem.interpreter.autocomplete` module.
56
57use crate::controller::Controller;
58
59/// Tab completion provider for the interpreter.
60///
61/// `Autocompleter` maintains a list of valid commands and provides
62/// case-insensitive prefix matching for tab completion.
63///
64/// # Conceptual Role
65///
66/// The autocompleter integrates with readline-style interfaces to provide
67/// interactive tab completion. It queries Tor once at initialization to
68/// build a comprehensive command list.
69///
70/// # Thread Safety
71///
72/// `Autocompleter` is `Send` and `Sync` after construction, as it only
73/// contains an immutable command list.
74///
75/// # Example
76///
77/// ```rust,no_run
78/// use stem_rs::Controller;
79/// use stem_rs::interpreter::autocomplete::Autocompleter;
80///
81/// # async fn example() -> Result<(), stem_rs::Error> {
82/// # let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
83/// # controller.authenticate(None).await?;
84/// let autocompleter = Autocompleter::new(&mut controller).await;
85///
86/// // Case-insensitive matching
87/// let matches = autocompleter.matches("getinfo");
88/// assert!(matches.iter().any(|m| m.starts_with("GETINFO")));
89/// # Ok(())
90/// # }
91/// ```
92pub struct Autocompleter {
93 /// List of all available commands for completion.
94 commands: Vec<String>,
95}
96
97impl Autocompleter {
98 /// Creates a new autocompleter by querying Tor for available commands.
99 ///
100 /// This queries Tor for available options and builds a comprehensive
101 /// list of completions. If any query fails, fallback completions are
102 /// used for that category.
103 ///
104 /// # Arguments
105 ///
106 /// * `controller` - An authenticated controller connection
107 ///
108 /// # Example
109 ///
110 /// ```rust,no_run
111 /// use stem_rs::Controller;
112 /// use stem_rs::interpreter::autocomplete::Autocompleter;
113 ///
114 /// # async fn example() -> Result<(), stem_rs::Error> {
115 /// # let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
116 /// # controller.authenticate(None).await?;
117 /// let autocompleter = Autocompleter::new(&mut controller).await;
118 /// # Ok(())
119 /// # }
120 /// ```
121 pub async fn new(controller: &mut Controller) -> Self {
122 let commands = build_command_list(controller).await;
123 Self { commands }
124 }
125
126 /// Returns all commands matching the given prefix.
127 ///
128 /// Matching is case-insensitive. The returned strings preserve their
129 /// original case.
130 ///
131 /// # Arguments
132 ///
133 /// * `text` - The prefix to match against
134 ///
135 /// # Returns
136 ///
137 /// A vector of references to matching commands.
138 ///
139 /// # Example
140 ///
141 /// ```rust,no_run
142 /// use stem_rs::Controller;
143 /// use stem_rs::interpreter::autocomplete::Autocompleter;
144 ///
145 /// # async fn example() -> Result<(), stem_rs::Error> {
146 /// # let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
147 /// # controller.authenticate(None).await?;
148 /// let autocompleter = Autocompleter::new(&mut controller).await;
149 ///
150 /// // Get all interpreter commands
151 /// let matches = autocompleter.matches("/");
152 /// assert!(matches.contains(&"/help"));
153 ///
154 /// // Case-insensitive matching
155 /// let matches = autocompleter.matches("signal");
156 /// // Returns SIGNAL commands
157 /// # Ok(())
158 /// # }
159 /// ```
160 pub fn matches(&self, text: &str) -> Vec<&str> {
161 let lowercase_text = text.to_lowercase();
162 self.commands
163 .iter()
164 .filter(|cmd| cmd.to_lowercase().starts_with(&lowercase_text))
165 .map(|s| s.as_str())
166 .collect()
167 }
168
169 /// Returns the completion at the given index, for readline integration.
170 ///
171 /// This method is designed to work with readline's `set_completer`
172 /// function, which calls the completer repeatedly with increasing
173 /// state values until `None` is returned.
174 ///
175 /// # Arguments
176 ///
177 /// * `text` - The prefix to match against
178 /// * `state` - The index of the match to return (0-based)
179 ///
180 /// # Returns
181 ///
182 /// The completion at the given index, or `None` if the index is
183 /// out of bounds.
184 ///
185 /// # Example
186 ///
187 /// ```rust,no_run
188 /// use stem_rs::Controller;
189 /// use stem_rs::interpreter::autocomplete::Autocompleter;
190 ///
191 /// # async fn example() -> Result<(), stem_rs::Error> {
192 /// # let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
193 /// # controller.authenticate(None).await?;
194 /// let autocompleter = Autocompleter::new(&mut controller).await;
195 ///
196 /// // Iterate through all matches
197 /// let mut state = 0;
198 /// while let Some(completion) = autocompleter.complete("/", state) {
199 /// println!("{}", completion);
200 /// state += 1;
201 /// }
202 /// # Ok(())
203 /// # }
204 /// ```
205 pub fn complete(&self, text: &str, state: usize) -> Option<&str> {
206 self.matches(text).get(state).copied()
207 }
208}
209
210/// Builds the complete list of commands for autocompletion.
211///
212/// Queries Tor for available options and combines them with built-in
213/// interpreter commands. Falls back to generic completions if queries fail.
214async fn build_command_list(controller: &mut Controller) -> Vec<String> {
215 let mut commands = vec![
216 "/help".to_string(),
217 "/events".to_string(),
218 "/info".to_string(),
219 "/python".to_string(),
220 "/quit".to_string(),
221 "SAVECONF".to_string(),
222 "MAPADDRESS".to_string(),
223 "EXTENDCIRCUIT".to_string(),
224 "SETCIRCUITPURPOSE".to_string(),
225 "SETROUTERPURPOSE".to_string(),
226 "ATTACHSTREAM".to_string(),
227 "REDIRECTSTREAM".to_string(),
228 "CLOSESTREAM".to_string(),
229 "CLOSECIRCUIT".to_string(),
230 "QUIT".to_string(),
231 "RESOLVE".to_string(),
232 "PROTOCOLINFO".to_string(),
233 "TAKEOWNERSHIP".to_string(),
234 "AUTHCHALLENGE".to_string(),
235 "DROPGUARDS".to_string(),
236 "ADD_ONION NEW:BEST".to_string(),
237 "ADD_ONION NEW:RSA1024".to_string(),
238 "ADD_ONION NEW:ED25519-V3".to_string(),
239 "ADD_ONION RSA1024:".to_string(),
240 "ADD_ONION ED25519-V3:".to_string(),
241 "ONION_CLIENT_AUTH_ADD".to_string(),
242 "ONION_CLIENT_AUTH_REMOVE".to_string(),
243 "ONION_CLIENT_AUTH_VIEW".to_string(),
244 "DEL_ONION".to_string(),
245 "HSFETCH".to_string(),
246 "HSPOST".to_string(),
247 ];
248
249 if let Ok(info_names) = controller.get_info("info/names").await {
250 for line in info_names.lines() {
251 if let Some(option) = line.split(' ').next() {
252 let option = option.trim_end_matches('*');
253 commands.push(format!("GETINFO {}", option));
254 }
255 }
256 } else {
257 commands.push("GETINFO ".to_string());
258 }
259
260 if let Ok(config_names) = controller.get_info("config/names").await {
261 for line in config_names.lines() {
262 if let Some(option) = line.split(' ').next() {
263 commands.push(format!("GETCONF {}", option));
264 commands.push(format!("SETCONF {}", option));
265 commands.push(format!("RESETCONF {}", option));
266 }
267 }
268 } else {
269 commands.push("GETCONF ".to_string());
270 commands.push("SETCONF ".to_string());
271 commands.push("RESETCONF ".to_string());
272 }
273
274 if let Ok(event_names) = controller.get_info("events/names").await {
275 for event in event_names.split_whitespace() {
276 commands.push(format!("SETEVENTS {}", event));
277 }
278 } else {
279 commands.push("SETEVENTS ".to_string());
280 }
281
282 if let Ok(feature_names) = controller.get_info("features/names").await {
283 for feature in feature_names.split_whitespace() {
284 commands.push(format!("USEFEATURE {}", feature));
285 }
286 } else {
287 commands.push("USEFEATURE ".to_string());
288 }
289
290 if let Ok(signal_names) = controller.get_info("signal/names").await {
291 for signal in signal_names.split_whitespace() {
292 commands.push(format!("SIGNAL {}", signal));
293 }
294 } else {
295 commands.push("SIGNAL ".to_string());
296 }
297
298 commands.push("/help HELP".to_string());
299 commands.push("/help EVENTS".to_string());
300 commands.push("/help INFO".to_string());
301 commands.push("/help PYTHON".to_string());
302 commands.push("/help QUIT".to_string());
303 commands.push("/help GETINFO".to_string());
304 commands.push("/help GETCONF".to_string());
305 commands.push("/help SETCONF".to_string());
306 commands.push("/help RESETCONF".to_string());
307 commands.push("/help SIGNAL".to_string());
308 commands.push("/help SETEVENTS".to_string());
309 commands.push("/help USEFEATURE".to_string());
310 commands.push("/help SAVECONF".to_string());
311 commands.push("/help LOADCONF".to_string());
312 commands.push("/help MAPADDRESS".to_string());
313 commands.push("/help POSTDESCRIPTOR".to_string());
314 commands.push("/help EXTENDCIRCUIT".to_string());
315 commands.push("/help SETCIRCUITPURPOSE".to_string());
316 commands.push("/help CLOSECIRCUIT".to_string());
317 commands.push("/help ATTACHSTREAM".to_string());
318 commands.push("/help REDIRECTSTREAM".to_string());
319 commands.push("/help CLOSESTREAM".to_string());
320 commands.push("/help ADD_ONION".to_string());
321 commands.push("/help DEL_ONION".to_string());
322 commands.push("/help HSFETCH".to_string());
323 commands.push("/help HSPOST".to_string());
324 commands.push("/help RESOLVE".to_string());
325 commands.push("/help TAKEOWNERSHIP".to_string());
326 commands.push("/help PROTOCOLINFO".to_string());
327
328 commands
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 fn create_test_autocompleter() -> Autocompleter {
336 Autocompleter {
337 commands: vec![
338 "/help".to_string(),
339 "/events".to_string(),
340 "/info".to_string(),
341 "/python".to_string(),
342 "/quit".to_string(),
343 "GETINFO version".to_string(),
344 "GETINFO config-file".to_string(),
345 "GETCONF SocksPort".to_string(),
346 "SETCONF SocksPort".to_string(),
347 "SIGNAL NEWNYM".to_string(),
348 ],
349 }
350 }
351
352 #[test]
353 fn test_matches_interpreter_commands() {
354 let ac = create_test_autocompleter();
355 let matches = ac.matches("/");
356 assert!(matches.contains(&"/help"));
357 assert!(matches.contains(&"/events"));
358 assert!(matches.contains(&"/info"));
359 assert!(matches.contains(&"/python"));
360 assert!(matches.contains(&"/quit"));
361 }
362
363 #[test]
364 fn test_matches_case_insensitive() {
365 let ac = create_test_autocompleter();
366 let matches = ac.matches("getinfo");
367 assert!(matches.contains(&"GETINFO version"));
368 assert!(matches.contains(&"GETINFO config-file"));
369 }
370
371 #[test]
372 fn test_matches_partial() {
373 let ac = create_test_autocompleter();
374 let matches = ac.matches("/he");
375 assert_eq!(matches.len(), 1);
376 assert!(matches.contains(&"/help"));
377 }
378
379 #[test]
380 fn test_matches_empty() {
381 let ac = create_test_autocompleter();
382 let matches = ac.matches("nonexistent");
383 assert!(matches.is_empty());
384 }
385
386 #[test]
387 fn test_complete_first() {
388 let ac = create_test_autocompleter();
389 let result = ac.complete("/", 0);
390 assert!(result.is_some());
391 }
392
393 #[test]
394 fn test_complete_out_of_bounds() {
395 let ac = create_test_autocompleter();
396 let result = ac.complete("/", 100);
397 assert!(result.is_none());
398 }
399
400 #[test]
401 fn test_complete_sequential() {
402 let ac = create_test_autocompleter();
403 let matches = ac.matches("/");
404 for (i, expected) in matches.iter().enumerate() {
405 let result = ac.complete("/", i);
406 assert_eq!(result, Some(*expected));
407 }
408 }
409}