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}