p2p_chat/ui/
completers.rs

1//! This module provides an autocompletion mechanism for the UI input.
2#[derive(Clone)]
3pub struct ChatCompleter {
4    /// A list of available commands.
5    commands: Vec<String>,
6    /// A list of known friends (e.g., nicknames or PeerIds).
7    friends: Vec<String>,
8    /// A list of recently discovered peers.
9    discovered_peers: Vec<String>,
10}
11
12impl ChatCompleter {
13    /// Creates a new `ChatCompleter` instance.
14    ///
15    /// # Arguments
16    ///
17    /// * `friends` - An initial list of friends to use for autocompletion.
18    pub fn new(friends: Vec<String>) -> Self {
19        let commands = vec![
20            "send".to_string(),
21            "history".to_string(),
22            "friends".to_string(),
23            "friend".to_string(),
24            "peers".to_string(),
25            "info".to_string(),
26            "check".to_string(),
27            "help".to_string(),
28            "exit".to_string(),
29        ];
30
31        Self {
32            commands,
33            friends,
34            discovered_peers: Vec::new(),
35        }
36    }
37
38    /// Updates the list of friends used for autocompletion.
39    pub fn update_friends(&mut self, friends: Vec<String>) {
40        self.friends = friends;
41    }
42
43    /// Updates the list of discovered peers used for autocompletion.
44    pub fn update_discovered_peers(&mut self, peers: Vec<String>) {
45        self.discovered_peers = peers;
46    }
47
48    /// Generates a list of suggestions based on the current input.
49    ///
50    /// This function provides completions for commands and their arguments,
51    /// including friend nicknames/IDs and peer IDs.
52    ///
53    /// # Arguments
54    ///
55    /// * `input` - The current input string from the user.
56    ///
57    /// # Returns
58    ///
59    /// A `Vec` of suggested completion strings.
60    pub fn get_suggestions(&self, input: &str) -> Vec<String> {
61        let trimmed = input.trim();
62        if trimmed.is_empty() {
63            return self.commands.clone();
64        }
65
66        let parts: Vec<&str> = trimmed.split_whitespace().collect();
67
68        match parts.len() {
69            1 => {
70                // Completing command
71                let prefix = parts[0].to_lowercase();
72                let mut suggestions = Vec::new();
73
74                // Exact matches first
75                for cmd in &self.commands {
76                    if cmd.starts_with(&prefix) {
77                        suggestions.push(cmd.clone());
78                    }
79                }
80
81                // Fuzzy matches
82                for cmd in &self.commands {
83                    if !cmd.starts_with(&prefix) && self.fuzzy_match(&prefix, cmd) {
84                        suggestions.push(cmd.clone());
85                    }
86                }
87
88                suggestions
89            }
90            2 => {
91                // Completing first argument
92                match parts[0] {
93                    "send" | "history" => {
94                        // Complete with friend nicknames/IDs
95                        let prefix = parts[1].to_lowercase();
96                        let mut suggestions = Vec::new();
97
98                        for friend in &self.friends {
99                            if friend.to_lowercase().starts_with(&prefix) {
100                                suggestions.push(format!("{} {}", parts[0], friend));
101                            }
102                        }
103
104                        // Add fuzzy matches
105                        for friend in &self.friends {
106                            if !friend.to_lowercase().starts_with(&prefix)
107                                && self.fuzzy_match(&prefix, &friend.to_lowercase())
108                            {
109                                suggestions.push(format!("{} {}", parts[0], friend));
110                            }
111                        }
112
113                        suggestions
114                    }
115                    "friend" => {
116                        // For 'friend' command, suggest discovered peer IDs that match the prefix
117                        let prefix = parts[1].to_lowercase();
118                        let mut suggestions = Vec::new();
119
120                        // Suggest discovered peers that start with the typed prefix
121                        for peer_id in &self.discovered_peers {
122                            if peer_id.to_lowercase().starts_with(&prefix) {
123                                suggestions.push(format!("{} {}", parts[0], peer_id));
124                            }
125                        }
126
127                        // If no matches, show placeholder
128                        if suggestions.is_empty() {
129                            suggestions.push(format!("{} <peer_id>", parts[0]));
130                        }
131
132                        suggestions
133                    }
134                    _ => Vec::new(),
135                }
136            }
137            3 => {
138                // Completing second argument
139                match parts[0] {
140                    "send" => {
141                        // No autocomplete for message content - let users type freely
142                        Vec::new()
143                    }
144                    "friend" => {
145                        // Suggest e2e_key placeholder
146                        vec![format!("{} {} <e2e_public_key>", parts[0], parts[1])]
147                    }
148                    _ => Vec::new(),
149                }
150            }
151            4 => {
152                // Completing third argument
153                match parts[0] {
154                    "friend" => {
155                        // Suggest nickname placeholder
156                        vec![format!(
157                            "{} {} {} <optional_nickname>",
158                            parts[0], parts[1], parts[2]
159                        )]
160                    }
161                    _ => Vec::new(),
162                }
163            }
164            _ => Vec::new(),
165        }
166    }
167
168    /// Performs a fuzzy match between a pattern and a text.
169    ///
170    /// This checks if all characters in the `pattern` appear in the `text`
171    /// in the same order, but not necessarily contiguously.
172    ///
173    /// # Arguments
174    ///
175    /// * `pattern` - The pattern string to match.
176    /// * `text` - The text string to search within.
177    ///
178    /// # Returns
179    ///
180    /// `true` if a fuzzy match is found, `false` otherwise.
181    fn fuzzy_match(&self, pattern: &str, text: &str) -> bool {
182        if pattern.len() > text.len() {
183            return false;
184        }
185
186        let mut pattern_chars = pattern.chars();
187        let mut current_pattern = pattern_chars.next();
188
189        for text_char in text.chars() {
190            if let Some(pattern_char) = current_pattern {
191                if text_char == pattern_char {
192                    current_pattern = pattern_chars.next();
193                }
194            }
195        }
196
197        current_pattern.is_none()
198    }
199}