steer_tui/tui/handlers/
simple.rs1use 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 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 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 let content = self.input_panel_state.content();
37 if content.is_empty() {
38 self.enter_edit_selection_mode();
40 } else {
41 self.input_panel_state.clear();
43 self.pending_attachments.clear();
44 }
45 self.double_tap_tracker.clear_key(&KeyCode::Esc);
47 } else {
48 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 }
62 }
63
64 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 let command = content[1..].trim().to_string();
91 self.client.execute_bash_command(command).await?;
92 } else if content.starts_with('/') {
93 self.handle_slash_command(content).await?;
95 } else {
96 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 self.input_panel_state
109 .textarea
110 .set_placeholder_text("Enter bash command...");
111 self.switch_mode(InputMode::BashCommand);
112 } else {
113 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 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 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 self.input_panel_state.handle_input(Input::from(key));
143 self.sync_attachments_from_input_tokens();
144 }
145 }
146
147 KeyCode::Char('@') => {
148 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 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 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 if self.handle_text_manipulation(key)? {
208 return Ok(false);
209 }
210
211 self.input_panel_state.handle_input(Input::from(key));
213 self.sync_attachments_from_input_tokens();
214
215 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}