command_vault/ui/
add.rs

1use std::io::Stdout;
2use anyhow::Result;
3use crossterm::{
4    event::{self, Event, KeyCode, KeyModifiers, KeyEvent},
5    execute,
6    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
7};
8use ratatui::{
9    backend::CrosstermBackend,
10    layout::{Constraint, Direction, Layout, Rect},
11    style::{Color, Style},
12    widgets::{Block, Borders, Paragraph, Clear},
13    Terminal,
14};
15
16/// Type alias for the command result tuple
17pub type CommandResult = Option<(String, Vec<String>, Option<i32>)>;
18
19#[derive(Default)]
20pub struct AddCommandApp {
21    /// The command being entered
22    pub command: String,
23    /// Tags for the command
24    pub tags: Vec<String>,
25    /// Current tag being entered
26    pub current_tag: String,
27    /// Current cursor position in the command
28    pub command_cursor: usize,
29    /// Current line in multi-line command
30    pub command_line: usize,
31    /// Current input mode
32    pub input_mode: InputMode,
33    /// Suggested tags
34    pub suggested_tags: Vec<String>,
35    /// Previous input mode (for returning from help)
36    pub previous_mode: InputMode,
37}
38
39#[derive(Debug, Clone, PartialEq, Default)]
40pub enum InputMode {
41    #[default]
42    Command,
43    Tag,
44    Confirm,
45    Help,
46}
47
48impl AddCommandApp {
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    pub fn run(&mut self) -> Result<CommandResult> {
54        let mut terminal = setup_terminal()?;
55        let result = self.run_app(&mut terminal);
56        restore_terminal(&mut terminal)?;
57        result
58    }
59
60    fn run_app(&mut self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<CommandResult> {
61        loop {
62            terminal.draw(|f| self.ui(f))?;
63
64            if let Event::Key(key) = event::read()? {
65                match self.input_mode {
66                    InputMode::Help => match key.code {
67                        KeyCode::Char('?') | KeyCode::Esc => {
68                            self.input_mode = self.previous_mode.clone();
69                        }
70                        _ => {}
71                    },
72                    _ => match key.code {
73                        KeyCode::Char('?') => {
74                            eprintln!("Debug: ? key pressed, switching to help mode");
75                            self.previous_mode = self.input_mode.clone();
76                            self.input_mode = InputMode::Help;
77                            eprintln!("Debug: Input mode is now Help");
78                        }
79                        _ => match self.input_mode {
80                            InputMode::Command => match key.code {
81                                KeyCode::Enter => {
82                                    if key.modifiers.contains(KeyModifiers::SHIFT) {
83                                        // Add newline to command
84                                        self.command.insert(self.command_cursor, '\n');
85                                        self.command_cursor += 1;
86                                        self.command_line += 1;
87                                    } else {
88                                        if !self.command.is_empty() {
89                                            self.suggest_tags();
90                                            self.input_mode = InputMode::Tag;
91                                        }
92                                    }
93                                }
94                                KeyCode::Char(c) => {
95                                    self.command.insert(self.command_cursor, c);
96                                    self.command_cursor += 1;
97                                }
98                                KeyCode::Backspace => {
99                                    if self.command_cursor > 0 {
100                                        self.command.remove(self.command_cursor - 1);
101                                        self.command_cursor -= 1;
102                                        if self.command_cursor > 0 && self.command.chars().nth(self.command_cursor - 1) == Some('\n') {
103                                            self.command_line -= 1;
104                                        }
105                                    }
106                                }
107                                KeyCode::Left => {
108                                    if self.command_cursor > 0 {
109                                        self.command_cursor -= 1;
110                                        if self.command_cursor > 0 && self.command.chars().nth(self.command_cursor - 1) == Some('\n') {
111                                            self.command_line -= 1;
112                                        }
113                                    }
114                                }
115                                KeyCode::Right => {
116                                    if self.command_cursor < self.command.len() {
117                                        if self.command.chars().nth(self.command_cursor) == Some('\n') {
118                                            self.command_line += 1;
119                                        }
120                                        self.command_cursor += 1;
121                                    }
122                                }
123                                KeyCode::Up => {
124                                    // Move cursor to previous line
125                                    let current_line_start = self.command[..self.command_cursor]
126                                        .rfind('\n')
127                                        .map(|pos| pos + 1)
128                                        .unwrap_or(0);
129                                    if let Some(prev_line_start) = self.command[..current_line_start.saturating_sub(1)]
130                                        .rfind('\n')
131                                        .map(|pos| pos + 1) {
132                                        let column = self.command_cursor - current_line_start;
133                                        self.command_cursor = prev_line_start + column.min(
134                                            self.command[prev_line_start..current_line_start.saturating_sub(1)]
135                                                .chars()
136                                                .count(),
137                                        );
138                                        self.command_line -= 1;
139                                    }
140                                }
141                                KeyCode::Down => {
142                                    // Move cursor to next line
143                                    let current_line_start = self.command[..self.command_cursor]
144                                        .rfind('\n')
145                                        .map(|pos| pos + 1)
146                                        .unwrap_or(0);
147                                    if let Some(next_line_start) = self.command[self.command_cursor..]
148                                        .find('\n')
149                                        .map(|pos| self.command_cursor + pos + 1) {
150                                        let column = self.command_cursor - current_line_start;
151                                        let next_line_end = self.command[next_line_start..]
152                                            .find('\n')
153                                            .map(|pos| next_line_start + pos)
154                                            .unwrap_or_else(|| self.command.len());
155                                        self.command_cursor = next_line_start + column.min(next_line_end - next_line_start);
156                                        self.command_line += 1;
157                                    }
158                                }
159                                KeyCode::Esc => {
160                                    return Ok(None);
161                                }
162                                _ => {}
163                            },
164                            InputMode::Tag => {
165                                match key.code {
166                                    KeyCode::Enter => {
167                                        if !self.current_tag.is_empty() {
168                                            self.tags.push(self.current_tag.clone());
169                                            self.current_tag.clear();
170                                        } else {
171                                            self.input_mode = InputMode::Confirm;
172                                        }
173                                    }
174                                    KeyCode::Char(c) => {
175                                        self.current_tag.push(c);
176                                    }
177                                    KeyCode::Backspace => {
178                                        self.current_tag.pop();
179                                    }
180                                    KeyCode::Tab => {
181                                        if !self.suggested_tags.is_empty() {
182                                            self.tags.push(self.suggested_tags[0].clone());
183                                            self.suggested_tags.remove(0);
184                                        }
185                                    }
186                                    KeyCode::Esc => {
187                                        self.input_mode = InputMode::Command;
188                                    }
189                                    _ => {}
190                                }
191                            }
192                            InputMode::Confirm => {
193                                match key.code {
194                                    KeyCode::Char('y') => {
195                                        return Ok(Some((
196                                            self.command.clone(),
197                                            self.tags.clone(),
198                                            None,
199                                        )));
200                                    }
201                                    KeyCode::Char('n') | KeyCode::Esc => {
202                                        return Ok(None);
203                                    }
204                                    _ => {}
205                                }
206                            }
207                            _ => {}
208                        }
209                    }
210                }
211            }
212        }
213    }
214
215    pub fn set_command(&mut self, command: String) {
216        self.command = command;
217        self.command_cursor = self.command.len();
218    }
219
220    pub fn set_tags(&mut self, tags: Vec<String>) {
221        self.tags = tags;
222    }
223
224    fn ui(&self, f: &mut ratatui::Frame) {
225        match self.input_mode {
226            InputMode::Help => {
227                let help_text = vec![
228                    "Command Vault Help",
229                    "",
230                    "Global Commands:",
231                    "  ?      - Toggle this help screen",
232                    "  Esc    - Go back / Cancel",
233                    "",
234                    "Command Input Mode:",
235                    "  Enter        - Continue to tag input",
236                    "  Shift+Enter  - Add new line",
237                    "  ←/→         - Move cursor",
238                    "  ↑/↓         - Navigate between lines",
239                    "",
240                    "Tag Input Mode:",
241                    "  Enter  - Add tag",
242                    "  Tab    - Show tag suggestions",
243                    "",
244                    "Confirmation Mode:",
245                    "  y/Y    - Save command",
246                    "  n/N    - Cancel",
247                ];
248
249                let help_paragraph = Paragraph::new(help_text.join("\n"))
250                    .style(Style::default().fg(Color::White))
251                    .block(Block::default().borders(Borders::ALL).title("Help (press ? or Esc to close)"));
252
253                // Center the help window
254                let area = centered_rect(60, 80, f.size());
255                f.render_widget(Clear, area); // Clear the background
256                f.render_widget(help_paragraph, area);
257            }
258            _ => {
259                let chunks = Layout::default()
260                    .direction(Direction::Vertical)
261                    .margin(1)
262                    .constraints([
263                        Constraint::Length(3),  // Title
264                        Constraint::Min(5),     // Command input
265                        Constraint::Length(3),  // Tags input
266                        Constraint::Min(0),     // Message/Help
267                    ])
268                    .split(f.size());
269
270                // Title
271                let title = Paragraph::new("Add Command")
272                    .style(Style::default().fg(Color::Cyan))
273                    .block(Block::default().borders(Borders::ALL));
274                f.render_widget(title, chunks[0]);
275
276                // Command input
277                let mut command_text = self.command.clone();
278                if self.input_mode == InputMode::Command {
279                    command_text.insert(self.command_cursor, '│'); // Add cursor
280                }
281                let command_input = Paragraph::new(command_text)
282                    .style(Style::default().fg(if self.input_mode == InputMode::Command {
283                        Color::Yellow
284                    } else {
285                        Color::Gray
286                    }))
287                    .block(Block::default().borders(Borders::ALL).title("Command (Shift+Enter for new line)"))
288                    .wrap(ratatui::widgets::Wrap { trim: false });
289                f.render_widget(command_input, chunks[1]);
290
291                // Tags input
292                let mut tags_text = self.tags.join(", ");
293                if !tags_text.is_empty() {
294                    tags_text.push_str(", ");
295                }
296                tags_text.push_str(&self.current_tag);
297                if self.input_mode == InputMode::Tag {
298                    tags_text.push('│');
299                }
300                let tags_input = Paragraph::new(tags_text)
301                    .style(Style::default().fg(if self.input_mode == InputMode::Tag {
302                        Color::Yellow
303                    } else {
304                        Color::Gray
305                    }))
306                    .block(Block::default().borders(Borders::ALL).title("Tags"));
307                f.render_widget(tags_input, chunks[2]);
308
309                // Help text or confirmation prompt
310                let help_text = match self.input_mode {
311                    InputMode::Command => "Press ? for help",
312                    InputMode::Tag => "Press ? for help",
313                    InputMode::Confirm => "Save command? (y/n)",
314                    InputMode::Help => unreachable!(),
315                };
316                let help = Paragraph::new(help_text)
317                    .style(Style::default().fg(Color::White))
318                    .block(Block::default().borders(Borders::ALL));
319                f.render_widget(help, chunks[3]);
320            }
321        }
322    }
323
324    fn suggest_tags(&mut self) {
325        self.suggested_tags.clear();
326        
327        // Simple tag suggestions based on command content
328        let command = self.command.to_lowercase();
329        
330        if command.contains("git") {
331            self.suggested_tags.push("git".to_string());
332            if command.contains("push") {
333                self.suggested_tags.push("push".to_string());
334            }
335            if command.contains("pull") {
336                self.suggested_tags.push("pull".to_string());
337            }
338        }
339        
340        if command.contains("docker") {
341            self.suggested_tags.push("docker".to_string());
342        }
343        
344        if command.contains("cargo") {
345            self.suggested_tags.push("rust".to_string());
346            self.suggested_tags.push("cargo".to_string());
347        }
348        
349        if command.contains("npm") || command.contains("yarn") {
350            self.suggested_tags.push("javascript".to_string());
351            self.suggested_tags.push("node".to_string());
352        }
353    }
354
355    pub fn handle_key_event(&mut self, key: KeyEvent) {
356        match self.input_mode {
357            InputMode::Help => match key.code {
358                KeyCode::Char('?') | KeyCode::Esc => {
359                    self.input_mode = self.previous_mode.clone();
360                }
361                _ => {}
362            },
363            _ => match key.code {
364                KeyCode::Char('?') => {
365                    self.previous_mode = self.input_mode.clone();
366                    self.input_mode = InputMode::Help;
367                }
368                _ => match self.input_mode {
369                    InputMode::Command => match key.code {
370                        KeyCode::Char(c) => {
371                            self.command.insert(self.command_cursor, c);
372                            self.command_cursor += 1;
373                        }
374                        KeyCode::Backspace => {
375                            if self.command_cursor > 0 {
376                                self.command.remove(self.command_cursor - 1);
377                                self.command_cursor -= 1;
378                            }
379                        }
380                        KeyCode::Left => {
381                            if self.command_cursor > 0 {
382                                self.command_cursor -= 1;
383                            }
384                        }
385                        KeyCode::Right => {
386                            if self.command_cursor < self.command.len() {
387                                self.command_cursor += 1;
388                            }
389                        }
390                        KeyCode::Enter => {
391                            if key.modifiers.contains(KeyModifiers::SHIFT) {
392                                self.command.insert(self.command_cursor, '\n');
393                                self.command_cursor += 1;
394                                self.command_line += 1;
395                            } else if !self.command.is_empty() {
396                                self.input_mode = InputMode::Tag;
397                            }
398                        }
399                        _ => {}
400                    },
401                    InputMode::Tag => match key.code {
402                        KeyCode::Char(c) => {
403                            self.current_tag.push(c);
404                        }
405                        KeyCode::Enter => {
406                            if !self.current_tag.is_empty() {
407                                self.tags.push(self.current_tag.clone());
408                                self.current_tag.clear();
409                            }
410                        }
411                        _ => {}
412                    },
413                    _ => {}
414                }
415            }
416        }
417    }
418}
419
420fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
421    enable_raw_mode()?;
422    let mut stdout = std::io::stdout();
423    execute!(stdout, EnterAlternateScreen)?;
424    let backend = CrosstermBackend::new(stdout);
425    let mut terminal = Terminal::new(backend)?;
426    terminal.hide_cursor()?;
427    Ok(terminal)
428}
429
430fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
431    terminal.show_cursor()?;
432    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
433    disable_raw_mode()?;
434    Ok(())
435}
436
437fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
438    let popup_layout = Layout::default()
439        .direction(Direction::Vertical)
440        .constraints([
441            Constraint::Percentage((100 - percent_y) / 2),
442            Constraint::Percentage(percent_y),
443            Constraint::Percentage((100 - percent_y) / 2),
444        ])
445        .split(r);
446
447    Layout::default()
448        .direction(Direction::Horizontal)
449        .constraints([
450            Constraint::Percentage((100 - percent_x) / 2),
451            Constraint::Percentage(percent_x),
452            Constraint::Percentage((100 - percent_x) / 2),
453        ])
454        .split(popup_layout[1])[1]
455}