Skip to main content

steer_tui/tui/handlers/
simple.rs

1use crate::error::Result;
2use crate::tui::InputMode;
3use crate::tui::NoticeLevel;
4use crate::tui::Tui;
5use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6use std::time::Duration;
7use tui_textarea::Input;
8
9impl Tui {
10    pub async fn handle_simple_mode(&mut self, key: KeyEvent) -> Result<bool> {
11        // Check for special modes first
12        match self.input_mode {
13            InputMode::BashCommand => return self.handle_bash_mode(key).await,
14            InputMode::AwaitingApproval => return self.handle_approval_mode(key).await,
15            InputMode::EditMessageSelection => return self.handle_edit_selection_mode(key).await,
16            InputMode::FuzzyFinder => return self.handle_fuzzy_finder_mode(key).await,
17            InputMode::ConfirmExit => return self.handle_confirm_exit_mode(key).await,
18            InputMode::Setup => return self.handle_setup_mode(key).await,
19            InputMode::Simple | InputMode::VimInsert | InputMode::VimNormal => {}
20        }
21
22        match key.code {
23            KeyCode::Esc => {
24                // Check for double-tap first
25                if self.editing_message_id.is_some() {
26                    self.cancel_edit_mode();
27                    self.double_tap_tracker.clear_key(&KeyCode::Esc);
28                    return Ok(false);
29                }
30
31                if self
32                    .double_tap_tracker
33                    .is_double_tap(KeyCode::Esc, Duration::from_millis(300))
34                {
35                    // Double ESC
36                    let content = self.input_panel_state.content();
37                    if content.is_empty() {
38                        // Empty content - edit previous message
39                        self.enter_edit_selection_mode();
40                    } else {
41                        // Has content - clear it
42                        self.input_panel_state.clear();
43                        self.pending_attachments.clear();
44                    }
45                    // Clear to prevent triple-tap
46                    self.double_tap_tracker.clear_key(&KeyCode::Esc);
47                } else {
48                    // Single ESC. If this cancels an in-flight op with queued work,
49                    // do not count it toward double-tap so restored input isn't cleared.
50                    let canceling_with_queued_item = self.is_processing && self.queued_count > 0;
51                    if canceling_with_queued_item {
52                        self.double_tap_tracker.clear_key(&KeyCode::Esc);
53                    } else {
54                        self.double_tap_tracker.record_key(KeyCode::Esc);
55                    }
56
57                    if self.is_processing {
58                        self.client.cancel_operation().await?;
59                    }
60                    // Don't trigger confirm exit - that's only for Ctrl+C/Ctrl+D
61                }
62            }
63
64            // Multi-line support - handle before regular Enter
65            KeyCode::Enter
66                if key.modifiers.contains(KeyModifiers::SHIFT)
67                    || key.modifiers.contains(KeyModifiers::ALT)
68                    || key.modifiers.contains(KeyModifiers::CONTROL) =>
69            {
70                self.input_panel_state
71                    .handle_input(Input::from(KeyEvent::new(
72                        KeyCode::Char('\n'),
73                        KeyModifiers::empty(),
74                    )));
75                self.sync_attachments_from_input_tokens();
76            }
77
78            KeyCode::Enter => {
79                let content = self.input_panel_state.content().trim().to_string();
80                if self.has_pending_send_content() {
81                    if !self.pending_attachments.is_empty() && content.starts_with('/') {
82                        self.push_notice(
83                            NoticeLevel::Warn,
84                            "Image attachments are only supported for regular prompts.".to_string(),
85                        );
86                        return Ok(false);
87                    }
88                    if content.starts_with('!') && content.len() > 1 {
89                        // Execute as bash command
90                        let command = content[1..].trim().to_string();
91                        self.client.execute_bash_command(command).await?;
92                    } else if content.starts_with('/') {
93                        // Handle as slash command
94                        self.handle_slash_command(content).await?;
95                    } else {
96                        // Send as normal message
97                        self.send_message(content).await?;
98                    }
99                    self.input_panel_state.clear();
100                    self.pending_attachments.clear();
101                }
102            }
103
104            KeyCode::Char('!') => {
105                let content = self.input_panel_state.content();
106                if content.is_empty() {
107                    // First character - enter bash command mode without inserting '!'
108                    self.input_panel_state
109                        .textarea
110                        .set_placeholder_text("Enter bash command...");
111                    self.switch_mode(InputMode::BashCommand);
112                } else {
113                    // Normal ! character
114                    self.input_panel_state.handle_input(Input::from(key));
115                    self.sync_attachments_from_input_tokens();
116                }
117            }
118
119            KeyCode::Char('/') => {
120                let content = self.input_panel_state.content();
121                if content.is_empty() {
122                    // First character - activate command fuzzy finder
123                    self.input_panel_state.handle_input(Input::from(key));
124                    self.input_panel_state.activate_command_fuzzy();
125                    self.switch_mode(InputMode::FuzzyFinder);
126
127                    // Immediately show all commands
128                    let results: Vec<_> = self
129                        .command_registry
130                        .all_commands()
131                        .into_iter()
132                        .map(|cmd| {
133                            crate::tui::widgets::fuzzy_finder::PickerItem::new(
134                                cmd.name.clone(),
135                                format!("/{} ", cmd.name),
136                            )
137                        })
138                        .collect();
139                    self.input_panel_state.fuzzy_finder.update_results(results);
140                } else {
141                    // Normal / character
142                    self.input_panel_state.handle_input(Input::from(key));
143                    self.sync_attachments_from_input_tokens();
144                }
145            }
146
147            KeyCode::Char('@') => {
148                // Always activate file fuzzy finder
149                self.input_panel_state.handle_input(Input::from(key));
150                self.sync_attachments_from_input_tokens();
151                self.input_panel_state.activate_fuzzy();
152                self.switch_mode(InputMode::FuzzyFinder);
153
154                // Immediately show all files (limited to 20)
155                let file_results = self
156                    .input_panel_state
157                    .file_cache()
158                    .fuzzy_search("", Some(20))
159                    .await;
160                let picker_items: Vec<_> = file_results
161                    .into_iter()
162                    .map(|path| {
163                        crate::tui::widgets::fuzzy_finder::PickerItem::new(
164                            path.clone(),
165                            format!("@{path} "),
166                        )
167                    })
168                    .collect();
169                self.input_panel_state
170                    .fuzzy_finder
171                    .update_results(picker_items);
172            }
173
174            KeyCode::Char('c' | 'd') if key.modifiers.contains(KeyModifiers::CONTROL) => {
175                if self.is_processing {
176                    self.client.cancel_operation().await?;
177                } else {
178                    self.switch_mode(InputMode::ConfirmExit);
179                }
180            }
181
182            KeyCode::Up if key.modifiers.contains(KeyModifiers::ALT) => {
183                if let Some(head) = &self.queued_head {
184                    if let Err(e) = self.client.dequeue_queued_item().await {
185                        self.push_notice(NoticeLevel::Error, Self::format_grpc_error(&e));
186                        return Ok(false);
187                    }
188
189                    let content = match head.kind {
190                        steer_grpc::client_api::QueuedWorkKind::DirectBash => {
191                            format!("!{}", head.content)
192                        }
193                        _ => head.content.clone(),
194                    };
195                    self.input_panel_state.replace_content(&content, None);
196                    self.sync_attachments_from_input_tokens();
197                }
198            }
199
200            // Toggle view mode with Ctrl+R
201            KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
202                self.chat_viewport.state_mut().toggle_view_mode();
203                self.chat_viewport.state_mut().scroll_to_bottom();
204            }
205            _ => {
206                // Try common text manipulation first
207                if self.handle_text_manipulation(key)? {
208                    return Ok(false);
209                }
210
211                // Normal text input
212                self.input_panel_state.handle_input(Input::from(key));
213                self.sync_attachments_from_input_tokens();
214
215                // Reset placeholder if needed
216                if self.input_panel_state.content().is_empty() {
217                    self.input_panel_state
218                        .textarea
219                        .set_placeholder_text("Type your message here...");
220                }
221            }
222        }
223
224        Ok(false)
225    }
226}