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 LoadSkill { name: String },
40 RunCustomCommand { name: String, args: String },
41 OpenRenamePopup,
42 RenameSession(String),
43 ExportSession(Option<String>),
44 OpenExternalEditor,
45}
46
47pub fn handle_paste(app: &mut App, text: String) -> InputAction {
48 if app.vim_mode && app.mode != AppMode::Insert {
49 return InputAction::None;
50 }
51
52 let trimmed = text.trim_end_matches('\n').to_string();
53 if trimmed.is_empty() {
54 return InputAction::None;
55 }
56
57 if crate::tui::app::is_image_path(trimmed.trim()) {
58 let path = trimmed.trim().trim_matches('"').trim_matches('\'');
59 match app.add_image_attachment(path) {
60 Ok(()) => {}
61 Err(e) => app.status_message = Some(crate::tui::app::StatusMessage::error(e)),
62 }
63 return InputAction::None;
64 }
65
66 app.handle_paste(trimmed);
67 InputAction::None
68}
69
70pub fn handle_key(app: &mut App, key: KeyEvent) -> InputAction {
71 if app.selection.anchor.is_some() {
72 app.selection.clear();
73 }
74
75 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
76 if app.model_selector.visible {
77 app.model_selector.close();
78 return InputAction::None;
79 }
80 if app.agent_selector.visible {
81 app.agent_selector.close();
82 return InputAction::None;
83 }
84 if app.command_palette.visible {
85 app.command_palette.close();
86 return InputAction::None;
87 }
88 if app.file_picker.visible {
89 app.file_picker.close();
90 return InputAction::None;
91 }
92 if app.thinking_selector.visible {
93 app.thinking_selector.close();
94 return InputAction::None;
95 }
96 if app.session_selector.visible {
97 app.session_selector.close();
98 return InputAction::None;
99 }
100 if app.help_popup.visible {
101 app.help_popup.close();
102 return InputAction::None;
103 }
104 if app.is_streaming {
105 return InputAction::CancelStream;
106 }
107 if !app.input.is_empty() || !app.attachments.is_empty() {
108 app.input.clear();
109 app.cursor_pos = 0;
110 app.paste_blocks.clear();
111 app.attachments.clear();
112 return InputAction::None;
113 }
114 return InputAction::Quit;
115 }
116
117 if key.code == KeyCode::Esc && app.is_streaming {
118 let now = Instant::now();
119 if let Some(hint_until) = app.esc_hint_until
120 && now < hint_until
121 {
122 app.esc_hint_until = None;
123 app.last_escape_time = None;
124 return InputAction::CancelStream;
125 }
126 app.esc_hint_until = Some(now + Duration::from_secs(3));
127 app.last_escape_time = Some(now);
128 return InputAction::None;
129 }
130
131 if app.model_selector.visible {
132 return popups::handle_model_selector(app, key);
133 }
134
135 if app.agent_selector.visible {
136 return popups::handle_agent_selector(app, key);
137 }
138
139 if app.thinking_selector.visible {
140 return popups::handle_thinking_selector(app, key);
141 }
142
143 if app.session_selector.visible {
144 return popups::handle_session_selector(app, key);
145 }
146
147 if app.help_popup.visible {
148 if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
149 app.help_popup.close();
150 }
151 return InputAction::None;
152 }
153
154 if app.rename_visible {
155 return popups::handle_rename_popup(app, key);
156 }
157
158 if app.pending_question.is_some() {
159 return popups::handle_question_popup(app, key);
160 }
161
162 if app.pending_permission.is_some() {
163 return popups::handle_permission_popup(app, key);
164 }
165
166 if app.context_menu.visible {
167 return popups::handle_context_menu(app, key);
168 }
169
170 if app.command_palette.visible {
171 return popups::handle_command_palette(app, key);
172 }
173
174 if app.file_picker.visible {
175 return popups::handle_file_picker(app, key);
176 }
177
178 if key.modifiers.contains(KeyModifiers::CONTROL)
179 && key.code == KeyCode::Char('e')
180 && (!app.vim_mode || app.mode == AppMode::Insert)
181 {
182 return InputAction::OpenExternalEditor;
183 }
184
185 if app.vim_mode {
186 match app.mode {
187 AppMode::Normal => modes::handle_normal(app, key),
188 AppMode::Insert => modes::handle_insert(app, key),
189 }
190 } else {
191 modes::handle_simple(app, key)
192 }
193}