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}