ratatui_toolkit/widgets/ai_chat/state/
input.rs1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2use ratatui::style::{Color, Modifier, Style};
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Clone)]
8pub struct InputState {
9 text: String,
11 cursor: usize,
13 lines: Vec<String>,
15 current_line: usize,
17 is_file_mode: bool,
19 file_query: String,
21 available_files: Vec<String>,
23 selected_file_index: usize,
25 is_command_mode: bool,
27 command: String,
29}
30
31impl Default for InputState {
32 fn default() -> Self {
33 Self {
34 text: String::new(),
35 cursor: 0,
36 lines: vec![String::new()],
37 current_line: 0,
38 is_file_mode: false,
39 file_query: String::new(),
40 available_files: Vec::new(),
41 selected_file_index: 0,
42 is_command_mode: false,
43 command: String::new(),
44 }
45 }
46}
47
48impl InputState {
49 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn text(&self) -> &str {
56 &self.text
57 }
58
59 pub fn cursor(&self) -> usize {
61 self.cursor
62 }
63
64 pub fn is_file_mode(&self) -> bool {
66 self.is_file_mode
67 }
68
69 pub fn is_command_mode(&self) -> bool {
71 self.is_command_mode
72 }
73
74 pub fn file_query(&self) -> &str {
76 &self.file_query
77 }
78
79 pub fn filtered_files(&self) -> Vec<String> {
81 let query_lower = self.file_query.to_lowercase();
82 self.available_files
83 .iter()
84 .filter(|f| f.to_lowercase().contains(&query_lower))
85 .cloned()
86 .collect()
87 }
88
89 pub fn selected_file_index(&self) -> usize {
91 self.selected_file_index
92 }
93
94 pub fn command(&self) -> &str {
96 &self.command
97 }
98
99 pub fn set_available_files(&mut self, files: Vec<String>) {
101 self.available_files = files;
102 }
103
104 pub fn load_files_from_cwd(&mut self) {
114 let cwd = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
115 let ignore_patterns = [
116 ".git",
117 "node_modules",
118 "target",
119 "__pycache__",
120 ".venv",
121 "venv",
122 "dist",
123 "build",
124 ".DS_Store",
125 ];
126
127 self.available_files = fs::read_dir(&cwd)
128 .into_iter()
129 .flatten()
130 .filter_map(|entry| entry.ok())
131 .filter_map(|entry| entry.file_name().to_str().map(|s| s.to_string()))
132 .filter(|name| {
133 !ignore_patterns
134 .iter()
135 .any(|pattern| name.eq_ignore_ascii_case(pattern) || name.starts_with(pattern))
136 })
137 .collect();
138 }
139
140 pub fn handle_key(&mut self, key: KeyEvent) -> Option<String> {
147 match key.code {
148 KeyCode::Char('@') => {
149 if !self.is_file_mode && !self.is_command_mode {
150 self.is_file_mode = true;
151 }
152 None
153 }
154 KeyCode::Char('/') => {
155 if !self.is_file_mode && !self.is_command_mode {
156 self.is_command_mode = true;
157 }
158 None
159 }
160 KeyCode::Char(c) => {
161 let is_ctrl_j = key.modifiers.contains(KeyModifiers::CONTROL) && c == 'j';
162
163 if is_ctrl_j {
164 self.insert_newline();
165 } else if self.is_file_mode {
166 self.file_query.push(c);
167 self.selected_file_index = 0;
168 } else if self.is_command_mode {
169 self.command.push(c);
170 } else {
171 self.insert_char(c);
172 }
173 None
174 }
175 KeyCode::Backspace => {
176 if self.is_file_mode {
177 if !self.file_query.is_empty() {
178 self.file_query.pop();
179 if self.file_query.is_empty() {
180 self.is_file_mode = false;
181 }
182 }
183 } else if self.is_command_mode {
184 self.command.pop();
185 if self.command.is_empty() {
186 self.is_command_mode = false;
187 }
188 } else {
189 self.backspace();
190 }
191 None
192 }
193 KeyCode::Left | KeyCode::Char('h') => {
194 if !self.is_file_mode && !self.is_command_mode && self.cursor > 0 {
195 self.cursor -= 1;
196 }
197 None
198 }
199 KeyCode::Right | KeyCode::Char('l') => {
200 if !self.is_file_mode && !self.is_command_mode && self.cursor < self.text.len() {
201 self.cursor += 1;
202 }
203 None
204 }
205 KeyCode::Up | KeyCode::Char('k') => {
206 if self.is_file_mode {
207 let filtered = self.filtered_files();
208 if !filtered.is_empty() {
209 self.selected_file_index = if self.selected_file_index == 0 {
210 filtered.len() - 1
211 } else {
212 self.selected_file_index - 1
213 };
214 }
215 }
216 None
217 }
218 KeyCode::Down | KeyCode::Char('j') => {
219 if self.is_file_mode {
220 let filtered = self.filtered_files();
221 if !filtered.is_empty() {
222 self.selected_file_index = (self.selected_file_index + 1) % filtered.len();
223 }
224 }
225 None
226 }
227 KeyCode::Enter => {
228 if self.is_file_mode {
229 let filtered = self.filtered_files();
230 if let Some(file) = filtered.get(self.selected_file_index) {
231 let file = file.clone();
232 self.is_file_mode = false;
233 self.file_query.clear();
234 self.selected_file_index = 0;
235 Some(format!("@{}", file))
236 } else {
237 None
238 }
239 } else if self.is_command_mode {
240 let command = self.command.clone();
241 self.is_command_mode = false;
242 self.command.clear();
243 Some(format!("/{}", command))
244 } else {
245 let text = self.text.clone();
246 self.clear();
247 Some(text)
248 }
249 }
250 KeyCode::Esc => {
251 if self.is_file_mode {
252 self.is_file_mode = false;
253 self.file_query.clear();
254 self.selected_file_index = 0;
255 }
256 if self.is_command_mode {
257 self.is_command_mode = false;
258 self.command.clear();
259 }
260 None
261 }
262 _ => None,
263 }
264 }
265
266 fn insert_char(&mut self, c: char) {
268 self.text.insert(self.cursor, c);
269 self.cursor += 1;
270 self.update_lines();
271 }
272
273 fn insert_newline(&mut self) {
275 self.text.insert(self.cursor, '\n');
276 self.cursor += 1;
277 self.update_lines();
278 }
279
280 fn backspace(&mut self) {
282 if self.cursor > 0 {
283 self.text.remove(self.cursor - 1);
284 self.cursor -= 1;
285 self.update_lines();
286 }
287 }
288
289 pub fn clear(&mut self) {
291 self.text.clear();
292 self.cursor = 0;
293 self.lines = vec![String::new()];
294 self.current_line = 0;
295 }
296
297 fn update_lines(&mut self) {
299 self.lines = self.text.split('\n').map(|s| s.to_string()).collect();
300 if self.lines.is_empty() {
301 self.lines.push(String::new());
302 }
303 }
304
305 fn update_cursor_from_lines(&mut self) {
307 let mut pos = 0;
308 for (i, line) in self.lines.iter().enumerate() {
309 if i == self.current_line {
310 self.cursor = pos + line.len();
311 return;
312 }
313 pos += line.len() + 1;
314 }
315 }
316}