p2p_chat/ui/chat_mode/
input.rs

1//! This module contains the input handling logic for the chat UI mode.
2use super::super::{UIAction, UIState};
3use super::ChatMode;
4use anyhow::Result;
5use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6use tokio::sync::mpsc;
7use tracing::debug;
8
9impl ChatMode {
10    /// Handles a key event in chat mode.
11    ///
12    /// This function processes various key presses, including typing characters,
13    /// navigating input history, moving the cursor, and executing commands.
14    ///
15    /// # Arguments
16    ///
17    /// * `state` - The current UI state.
18    /// * `key` - The `KeyEvent` to handle.
19    /// * `action_tx` - The sender for dispatching UI actions.
20    ///
21    /// # Errors
22    ///
23    /// This function returns an error if a command execution fails.
24    pub async fn handle_key(
25        &mut self,
26        state: &mut UIState,
27        key: KeyEvent,
28        action_tx: &mpsc::UnboundedSender<UIAction>,
29    ) -> Result<()> {
30        match key.code {
31            KeyCode::Enter => {
32                if !state.input_buffer.trim().is_empty() {
33                    let input = state.input_buffer.clone();
34                    self.input_history.push(input.clone());
35                    self.history_index = None;
36
37                    if let Err(e) = self.execute_command(&input, action_tx).await {
38                        debug!("Error executing command '{}': {}", input, e);
39                    }
40
41                    state.input_buffer.clear();
42                    state.cursor_pos = 0;
43                }
44            }
45            KeyCode::Char(c) => {
46                state.safe_insert_char(c);
47                self.history_index = None;
48                self.update_suggestion(state);
49            }
50            KeyCode::Backspace => {
51                if state.safe_remove_char_before() {
52                    self.history_index = None;
53                    self.update_suggestion(state);
54                }
55            }
56            KeyCode::Delete => {
57                state.safe_remove_char_at();
58            }
59            KeyCode::Left => {
60                state.safe_cursor_left();
61            }
62            KeyCode::Right => {
63                let char_count = state.input_buffer.chars().count();
64                if state.cursor_pos == char_count {
65                    if let Some(suggestion) = &self.current_suggestion {
66                        state.input_buffer = suggestion.clone();
67                        state.safe_cursor_end();
68                        self.current_suggestion = None;
69                    }
70                } else {
71                    state.safe_cursor_right();
72                }
73            }
74            KeyCode::Home => {
75                if key.modifiers.contains(KeyModifiers::CONTROL) {
76                    state.horizontal_scroll_offset =
77                        state.horizontal_scroll_offset.saturating_sub(10);
78                } else {
79                    state.safe_cursor_home();
80                }
81            }
82            KeyCode::End => {
83                if key.modifiers.contains(KeyModifiers::CONTROL) {
84                    state.horizontal_scroll_offset =
85                        state.horizontal_scroll_offset.saturating_add(10);
86                } else {
87                    state.safe_cursor_end();
88                }
89            }
90            KeyCode::Up => {
91                self.navigate_history(state, true);
92            }
93            KeyCode::Down => {
94                self.navigate_history(state, false);
95            }
96            KeyCode::PageUp => {
97                state.scroll_offset = state.scroll_offset.saturating_add(10);
98                state.update_chat_scroll_state(state.terminal_size.1 as usize);
99            }
100            KeyCode::PageDown => {
101                state.scroll_offset = state.scroll_offset.saturating_sub(10);
102                state.update_chat_scroll_state(state.terminal_size.1 as usize);
103            }
104            KeyCode::Esc => {
105                state.jump_to_bottom_chat();
106            }
107            KeyCode::Tab => {
108                let suggestions = self.completer.get_suggestions(&state.input_buffer);
109                if let Some(first) = suggestions.first() {
110                    state.input_buffer = first.clone();
111                    state.safe_cursor_end();
112                    self.current_suggestion = None;
113                }
114            }
115            _ => {}
116        }
117
118        Ok(())
119    }
120
121    /// Updates the current input suggestion based on the input buffer content.
122    ///
123    /// This is typically used for command autocompletion.
124    fn update_suggestion(&mut self, state: &UIState) {
125        let char_count = state.input_buffer.chars().count();
126        if state.cursor_pos == char_count && !state.input_buffer.trim().is_empty() {
127            let suggestions = self.completer.get_suggestions(&state.input_buffer);
128            if let Some(suggestion) = suggestions.first() {
129                if suggestion.starts_with(&state.input_buffer) && suggestion != &state.input_buffer
130                {
131                    self.current_suggestion = Some(suggestion.clone());
132                } else {
133                    self.current_suggestion = None;
134                }
135            } else {
136                self.current_suggestion = None;
137            }
138        } else {
139            self.current_suggestion = None;
140        }
141    }
142
143    /// Navigates through the input history.
144    ///
145    /// # Arguments
146    ///
147    /// * `state` - The current UI state.
148    /// * `up` - If `true`, navigates up (older entries); if `false`, navigates down (newer entries).
149    fn navigate_history(&mut self, state: &mut UIState, up: bool) {
150        if self.input_history.is_empty() {
151            return;
152        }
153
154        let new_index = if up {
155            match self.history_index {
156                None => Some(self.input_history.len() - 1),
157                Some(0) => Some(0),
158                Some(i) => Some(i - 1),
159            }
160        } else {
161            match self.history_index {
162                None => None,
163                Some(i) if i + 1 >= self.input_history.len() => None,
164                Some(i) => Some(i + 1),
165            }
166        };
167
168        self.history_index = new_index;
169
170        if let Some(index) = new_index {
171            state.input_buffer = self.input_history[index].clone();
172            state.safe_cursor_end();
173        } else {
174            state.input_buffer.clear();
175            state.safe_cursor_home();
176        }
177    }
178
179    /// Executes a command based on the input string.
180    ///
181    /// This function parses the input, identifies commands like "send", and
182    /// dispatches appropriate `UIAction`s.
183    ///
184    /// # Arguments
185    ///
186    /// * `input` - The input string to parse and execute.
187    /// * `action_tx` - The sender for dispatching UI actions.
188    ///
189    /// # Errors
190    ///
191    /// This function returns an error if the action sender fails.
192    async fn execute_command(
193        &self,
194        input: &str,
195        action_tx: &mpsc::UnboundedSender<UIAction>,
196    ) -> Result<()> {
197        let parts: Vec<&str> = input.split_whitespace().collect();
198        if parts.is_empty() {
199            return Ok(());
200        }
201
202        let command = parts[0];
203        match command {
204            "send" => {
205                if parts.len() >= 3 {
206                    let recipient = parts[1].to_string();
207                    let message = parts[2..].join(" ");
208                    let _ = action_tx.send(UIAction::SendMessage(recipient, message));
209                } else {
210                    debug!("Usage: send <recipient> <message>");
211                }
212            }
213            _ => {
214                let _ = action_tx.send(UIAction::ExecuteCommand(input.to_string()));
215            }
216        }
217
218        Ok(())
219    }
220}