1mod modes;
2mod mouse;
3mod popups;
4
5use std::path::Path;
6use std::time::{Duration, Instant};
7
8use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9
10use crate::tui::app::{App, AppMode};
11
12pub use mouse::handle_mouse;
13
14fn path_exists(path: &str) -> bool {
15 let resolved = if path.starts_with('~') {
16 std::env::var("HOME")
17 .map(|h| path.replacen('~', &h, 1))
18 .unwrap_or_else(|_| path.to_string())
19 } else {
20 path.to_string()
21 };
22 Path::new(&resolved).exists()
23}
24
25pub enum InputAction {
26 AnswerQuestion(String),
27 AnswerPermission(String),
28 None,
29 SendMessage(String),
30 Quit,
31 CancelStream,
32 ScrollUp(u32),
33 ScrollDown(u32),
34 ScrollToTop,
35 ScrollToBottom,
36 ClearConversation,
37 NewConversation,
38 OpenModelSelector,
39 OpenAgentSelector,
40 ToggleAgent,
41 OpenThinkingSelector,
42 OpenSessionSelector,
43 SelectModel {
44 provider: String,
45 model: String,
46 },
47 SelectAgent {
48 name: String,
49 },
50 ResumeSession {
51 id: String,
52 },
53 SetThinkingLevel(u32),
54 ToggleThinking,
55 CycleThinkingLevel,
56 TruncateToMessage(usize),
57 ForkFromMessage(usize),
58 RevertToMessage(usize),
59 CopyMessage(usize),
60 LoadSkill {
61 name: String,
62 },
63 RunCustomCommand {
64 name: String,
65 args: String,
66 },
67 OpenRenamePopup,
68 RenameSession(String),
69 ExportSession(Option<String>),
70 OpenExternalEditor,
71 OpenLoginPopup,
72 LoginSubmitApiKey {
73 provider: String,
74 key: String,
75 },
76 LoginOAuth {
77 provider: String,
78 create_key: bool,
79 code: String,
80 verifier: String,
81 },
82 AskAside {
83 question: String,
84 },
85}
86
87enum PasteItem {
88 Path(String),
89 Plain(String),
90}
91
92pub fn handle_paste(app: &mut App, text: String) -> InputAction {
93 if app.login_popup.visible {
94 let trimmed = text.trim().to_string();
95 if !trimmed.is_empty() {
96 match app.login_popup.step {
97 crate::tui::widgets::LoginStep::OAuthWaiting => {
98 app.login_popup.code_input.push_str(&trimmed);
99 }
100 crate::tui::widgets::LoginStep::EnterApiKey => {
101 app.login_popup.key_input.push_str(&trimmed);
102 }
103 _ => {}
104 }
105 }
106 return InputAction::None;
107 }
108
109 if app.vim_mode && app.mode != AppMode::Insert {
110 return InputAction::None;
111 }
112
113 let trimmed = text.trim_end_matches('\n');
114 if trimmed.is_empty() {
115 return InputAction::None;
116 }
117
118 let lines: Vec<&str> = trimmed
119 .split('\n')
120 .map(|s| s.trim())
121 .filter(|s| !s.is_empty())
122 .collect();
123
124 let mut items: Vec<PasteItem> = Vec::new();
125 for line in &lines {
126 if let Some(path) = crate::tui::app::normalize_paste_path(line)
127 && path_exists(&path)
128 {
129 items.push(PasteItem::Path(path));
130 continue;
131 }
132 items.push(PasteItem::Plain((*line).to_string()));
133 }
134
135 let mut plain_buf: Vec<String> = Vec::new();
136 for item in items {
137 match item {
138 PasteItem::Path(path) => {
139 if !plain_buf.is_empty() {
140 app.handle_paste(plain_buf.join("\n"));
141 plain_buf.clear();
142 }
143 if crate::tui::app::is_image_path(&path) {
144 if let Err(e) = app.add_image_attachment(&path) {
145 app.status_message = Some(crate::tui::app::StatusMessage::error(e));
146 }
147 } else {
148 app.insert_file_reference(&path);
149 }
150 }
151 PasteItem::Plain(s) => plain_buf.push(s),
152 }
153 }
154 if !plain_buf.is_empty() {
155 app.handle_paste(plain_buf.join("\n"));
156 }
157
158 InputAction::None
159}
160
161pub fn handle_key(app: &mut App, key: KeyEvent) -> InputAction {
162 if app.selection.anchor.is_some() {
163 app.selection.clear();
164 }
165
166 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
167 if app.input_selection_range().is_some() {
168 if let Some(text) = app.copy_input_selection() {
169 crate::tui::app::copy_to_clipboard(&text);
170 }
171 return InputAction::None;
172 }
173 if app.model_selector.visible {
174 app.model_selector.close();
175 return InputAction::None;
176 }
177 if app.agent_selector.visible {
178 app.agent_selector.close();
179 return InputAction::None;
180 }
181 if app.command_palette.visible {
182 app.command_palette.close();
183 return InputAction::None;
184 }
185 if app.file_picker.visible {
186 app.file_picker.close();
187 return InputAction::None;
188 }
189 if app.thinking_selector.visible {
190 app.thinking_selector.close();
191 return InputAction::None;
192 }
193 if app.session_selector.visible {
194 app.session_selector.close();
195 return InputAction::None;
196 }
197 if app.help_popup.visible {
198 app.help_popup.close();
199 return InputAction::None;
200 }
201 if app.is_streaming {
202 return InputAction::CancelStream;
203 }
204 if !app.input.is_empty() || !app.attachments.is_empty() {
205 app.input.clear();
206 app.cursor_pos = 0;
207 app.paste_blocks.clear();
208 app.attachments.clear();
209 app.clear_input_selection();
210 return InputAction::None;
211 }
212 return InputAction::Quit;
213 }
214
215 if key.code == KeyCode::Esc && app.is_streaming {
216 let now = Instant::now();
217 if let Some(hint_until) = app.esc_hint_until
218 && now < hint_until
219 {
220 app.esc_hint_until = None;
221 app.last_escape_time = None;
222 return InputAction::CancelStream;
223 }
224 app.esc_hint_until = Some(now + Duration::from_secs(3));
225 app.last_escape_time = Some(now);
226 return InputAction::None;
227 }
228
229 if app.model_selector.visible {
230 return popups::handle_model_selector(app, key);
231 }
232
233 if app.agent_selector.visible {
234 return popups::handle_agent_selector(app, key);
235 }
236
237 if app.thinking_selector.visible {
238 return popups::handle_thinking_selector(app, key);
239 }
240
241 if app.session_selector.visible {
242 return popups::handle_session_selector(app, key);
243 }
244
245 if app.help_popup.visible {
246 if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
247 app.help_popup.close();
248 }
249 return InputAction::None;
250 }
251
252 if app.aside_popup.visible {
253 return popups::handle_aside_popup(app, key);
254 }
255
256 if app.rename_visible {
257 return popups::handle_rename_popup(app, key);
258 }
259
260 if app.pending_question.is_some() {
261 return popups::handle_question_popup(app, key);
262 }
263
264 if app.pending_permission.is_some() {
265 return popups::handle_permission_popup(app, key);
266 }
267
268 if app.welcome_screen.visible {
269 return popups::handle_welcome_screen(app, key);
270 }
271
272 if app.login_popup.visible {
273 return popups::handle_login_popup(app, key);
274 }
275
276 if app.context_menu.visible {
277 return popups::handle_context_menu(app, key);
278 }
279
280 if app.command_palette.visible {
281 return popups::handle_command_palette(app, key);
282 }
283
284 if app.file_picker.visible {
285 return popups::handle_file_picker(app, key);
286 }
287
288 if key.modifiers.contains(KeyModifiers::CONTROL)
289 && key.code == KeyCode::Char('e')
290 && (!app.vim_mode || app.mode == AppMode::Insert)
291 {
292 return InputAction::OpenExternalEditor;
293 }
294
295 if app.vim_mode {
296 match app.mode {
297 AppMode::Normal => modes::handle_normal(app, key),
298 AppMode::Insert => modes::handle_insert(app, key),
299 }
300 } else {
301 modes::handle_simple(app, key)
302 }
303}