1mod modes;
2mod mouse;
3mod popups;
4
5use std::time::{Duration, Instant};
6
7use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
8
9use crate::tui::app::{App, AppMode};
10
11pub use mouse::handle_mouse;
12
13pub enum InputAction {
14 AnswerQuestion(String),
15 AnswerPermission(String),
16 None,
17 SendMessage(String),
18 Quit,
19 CancelStream,
20 ScrollUp(u16),
21 ScrollDown(u16),
22 ScrollToTop,
23 ScrollToBottom,
24 ClearConversation,
25 NewConversation,
26 OpenModelSelector,
27 OpenAgentSelector,
28 ToggleAgent,
29 OpenThinkingSelector,
30 OpenSessionSelector,
31 SelectModel { provider: String, model: String },
32 SelectAgent { name: String },
33 ResumeSession { id: String },
34 SetThinkingLevel(u32),
35 ToggleThinking,
36 CycleThinkingLevel,
37 TruncateToMessage(usize),
38 ForkFromMessage(usize),
39 RevertToMessage(usize),
40 CopyMessage(usize),
41 LoadSkill { name: String },
42 RunCustomCommand { name: String, args: String },
43 OpenRenamePopup,
44 RenameSession(String),
45 ExportSession(Option<String>),
46 OpenExternalEditor,
47}
48
49pub fn handle_paste(app: &mut App, text: String) -> InputAction {
50 if app.vim_mode && app.mode != AppMode::Insert {
51 return InputAction::None;
52 }
53
54 let trimmed = text.trim_end_matches('\n').to_string();
55 if trimmed.is_empty() {
56 return InputAction::None;
57 }
58
59 if crate::tui::app::is_image_path(trimmed.trim()) {
60 let path = trimmed.trim().trim_matches('"').trim_matches('\'');
61 match app.add_image_attachment(path) {
62 Ok(()) => {}
63 Err(e) => app.status_message = Some(crate::tui::app::StatusMessage::error(e)),
64 }
65 return InputAction::None;
66 }
67
68 app.handle_paste(trimmed);
69 InputAction::None
70}
71
72pub fn handle_key(app: &mut App, key: KeyEvent) -> InputAction {
73 if app.selection.anchor.is_some() {
74 app.selection.clear();
75 }
76
77 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
78 if app.model_selector.visible {
79 app.model_selector.close();
80 return InputAction::None;
81 }
82 if app.agent_selector.visible {
83 app.agent_selector.close();
84 return InputAction::None;
85 }
86 if app.command_palette.visible {
87 app.command_palette.close();
88 return InputAction::None;
89 }
90 if app.file_picker.visible {
91 app.file_picker.close();
92 return InputAction::None;
93 }
94 if app.thinking_selector.visible {
95 app.thinking_selector.close();
96 return InputAction::None;
97 }
98 if app.session_selector.visible {
99 app.session_selector.close();
100 return InputAction::None;
101 }
102 if app.help_popup.visible {
103 app.help_popup.close();
104 return InputAction::None;
105 }
106 if app.is_streaming {
107 return InputAction::CancelStream;
108 }
109 if !app.input.is_empty() || !app.attachments.is_empty() {
110 app.input.clear();
111 app.cursor_pos = 0;
112 app.paste_blocks.clear();
113 app.attachments.clear();
114 return InputAction::None;
115 }
116 return InputAction::Quit;
117 }
118
119 if key.code == KeyCode::Esc && app.is_streaming {
120 let now = Instant::now();
121 if let Some(hint_until) = app.esc_hint_until
122 && now < hint_until
123 {
124 app.esc_hint_until = None;
125 app.last_escape_time = None;
126 return InputAction::CancelStream;
127 }
128 app.esc_hint_until = Some(now + Duration::from_secs(3));
129 app.last_escape_time = Some(now);
130 return InputAction::None;
131 }
132
133 if app.model_selector.visible {
134 return popups::handle_model_selector(app, key);
135 }
136
137 if app.agent_selector.visible {
138 return popups::handle_agent_selector(app, key);
139 }
140
141 if app.thinking_selector.visible {
142 return popups::handle_thinking_selector(app, key);
143 }
144
145 if app.session_selector.visible {
146 return popups::handle_session_selector(app, key);
147 }
148
149 if app.help_popup.visible {
150 if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
151 app.help_popup.close();
152 }
153 return InputAction::None;
154 }
155
156 if app.rename_visible {
157 return popups::handle_rename_popup(app, key);
158 }
159
160 if app.pending_question.is_some() {
161 return popups::handle_question_popup(app, key);
162 }
163
164 if app.pending_permission.is_some() {
165 return popups::handle_permission_popup(app, key);
166 }
167
168 if app.context_menu.visible {
169 return popups::handle_context_menu(app, key);
170 }
171
172 if app.command_palette.visible {
173 return popups::handle_command_palette(app, key);
174 }
175
176 if app.file_picker.visible {
177 return popups::handle_file_picker(app, key);
178 }
179
180 if key.modifiers.contains(KeyModifiers::CONTROL)
181 && key.code == KeyCode::Char('e')
182 && (!app.vim_mode || app.mode == AppMode::Insert)
183 {
184 return InputAction::OpenExternalEditor;
185 }
186
187 if app.vim_mode {
188 match app.mode {
189 AppMode::Normal => modes::handle_normal(app, key),
190 AppMode::Insert => modes::handle_insert(app, key),
191 }
192 } else {
193 modes::handle_simple(app, key)
194 }
195}