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.model_selector.visible {
168 app.model_selector.close();
169 return InputAction::None;
170 }
171 if app.agent_selector.visible {
172 app.agent_selector.close();
173 return InputAction::None;
174 }
175 if app.command_palette.visible {
176 app.command_palette.close();
177 return InputAction::None;
178 }
179 if app.file_picker.visible {
180 app.file_picker.close();
181 return InputAction::None;
182 }
183 if app.thinking_selector.visible {
184 app.thinking_selector.close();
185 return InputAction::None;
186 }
187 if app.session_selector.visible {
188 app.session_selector.close();
189 return InputAction::None;
190 }
191 if app.help_popup.visible {
192 app.help_popup.close();
193 return InputAction::None;
194 }
195 if app.is_streaming {
196 return InputAction::CancelStream;
197 }
198 if !app.input.is_empty() || !app.attachments.is_empty() {
199 app.input.clear();
200 app.cursor_pos = 0;
201 app.paste_blocks.clear();
202 app.attachments.clear();
203 return InputAction::None;
204 }
205 return InputAction::Quit;
206 }
207
208 if key.code == KeyCode::Esc && app.is_streaming {
209 let now = Instant::now();
210 if let Some(hint_until) = app.esc_hint_until
211 && now < hint_until
212 {
213 app.esc_hint_until = None;
214 app.last_escape_time = None;
215 return InputAction::CancelStream;
216 }
217 app.esc_hint_until = Some(now + Duration::from_secs(3));
218 app.last_escape_time = Some(now);
219 return InputAction::None;
220 }
221
222 if app.model_selector.visible {
223 return popups::handle_model_selector(app, key);
224 }
225
226 if app.agent_selector.visible {
227 return popups::handle_agent_selector(app, key);
228 }
229
230 if app.thinking_selector.visible {
231 return popups::handle_thinking_selector(app, key);
232 }
233
234 if app.session_selector.visible {
235 return popups::handle_session_selector(app, key);
236 }
237
238 if app.help_popup.visible {
239 if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
240 app.help_popup.close();
241 }
242 return InputAction::None;
243 }
244
245 if app.aside_popup.visible {
246 return popups::handle_aside_popup(app, key);
247 }
248
249 if app.rename_visible {
250 return popups::handle_rename_popup(app, key);
251 }
252
253 if app.pending_question.is_some() {
254 return popups::handle_question_popup(app, key);
255 }
256
257 if app.pending_permission.is_some() {
258 return popups::handle_permission_popup(app, key);
259 }
260
261 if app.welcome_screen.visible {
262 return popups::handle_welcome_screen(app, key);
263 }
264
265 if app.login_popup.visible {
266 return popups::handle_login_popup(app, key);
267 }
268
269 if app.context_menu.visible {
270 return popups::handle_context_menu(app, key);
271 }
272
273 if app.command_palette.visible {
274 return popups::handle_command_palette(app, key);
275 }
276
277 if app.file_picker.visible {
278 return popups::handle_file_picker(app, key);
279 }
280
281 if key.modifiers.contains(KeyModifiers::CONTROL)
282 && key.code == KeyCode::Char('e')
283 && (!app.vim_mode || app.mode == AppMode::Insert)
284 {
285 return InputAction::OpenExternalEditor;
286 }
287
288 if app.vim_mode {
289 match app.mode {
290 AppMode::Normal => modes::handle_normal(app, key),
291 AppMode::Insert => modes::handle_insert(app, key),
292 }
293 } else {
294 modes::handle_simple(app, key)
295 }
296}