1use std::time::{Duration, Instant};
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
4
5use crate::tui::app::{App, AppMode};
6use crate::tui::widgets::ThinkingLevel;
7
8pub enum InputAction {
9 AnswerQuestion(String),
10 AnswerPermission(String),
11 None,
12 SendMessage(String),
13 Quit,
14 CancelStream,
15 ScrollUp(u16),
16 ScrollDown(u16),
17 ScrollToTop,
18 ScrollToBottom,
19 ClearConversation,
20 NewConversation,
21 OpenModelSelector,
22 OpenAgentSelector,
23 OpenThinkingSelector,
24 OpenSessionSelector,
25 SelectModel { provider: String, model: String },
26 SelectAgent { name: String },
27 ResumeSession { id: String },
28 SetThinkingLevel(u32),
29 ToggleThinking,
30 CycleThinkingLevel,
31 TruncateToMessage(usize),
32 ForkFromMessage(usize),
33}
34
35pub fn handle_paste(app: &mut App, text: String) -> InputAction {
36 if app.vim_mode && app.mode != AppMode::Insert {
37 return InputAction::None;
38 }
39
40 let trimmed = text.trim_end_matches('\n').to_string();
41 if trimmed.is_empty() {
42 return InputAction::None;
43 }
44
45 if crate::tui::app::is_image_path(trimmed.trim()) {
46 let path = trimmed.trim().trim_matches('"').trim_matches('\'');
47 match app.add_image_attachment(path) {
48 Ok(()) => {}
49 Err(e) => app.error_message = Some(e),
50 }
51 return InputAction::None;
52 }
53
54 app.handle_paste(trimmed);
55 InputAction::None
56}
57
58pub fn handle_key(app: &mut App, key: KeyEvent) -> InputAction {
59 if app.selection.anchor.is_some() {
60 app.selection.clear();
61 }
62
63 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
64 if app.model_selector.visible {
65 app.model_selector.close();
66 return InputAction::None;
67 }
68 if app.agent_selector.visible {
69 app.agent_selector.close();
70 return InputAction::None;
71 }
72 if app.command_palette.visible {
73 app.command_palette.close();
74 return InputAction::None;
75 }
76 if app.thinking_selector.visible {
77 app.thinking_selector.close();
78 return InputAction::None;
79 }
80 if app.session_selector.visible {
81 app.session_selector.close();
82 return InputAction::None;
83 }
84 if app.help_popup.visible {
85 app.help_popup.close();
86 return InputAction::None;
87 }
88 if app.is_streaming {
89 return InputAction::CancelStream;
90 }
91 if !app.input.is_empty() || !app.attachments.is_empty() {
92 app.input.clear();
93 app.cursor_pos = 0;
94 app.paste_blocks.clear();
95 app.attachments.clear();
96 return InputAction::None;
97 }
98 return InputAction::Quit;
99 }
100
101 if key.code == KeyCode::Esc && app.is_streaming {
102 let now = Instant::now();
103 if let Some(hint_until) = app.esc_hint_until {
104 if now < hint_until {
105 app.esc_hint_until = None;
106 app.last_escape_time = None;
107 return InputAction::CancelStream;
108 }
109 }
110 app.esc_hint_until = Some(now + Duration::from_secs(3));
111 app.last_escape_time = Some(now);
112 return InputAction::None;
113 }
114
115 if app.model_selector.visible {
116 return handle_model_selector(app, key);
117 }
118
119 if app.agent_selector.visible {
120 return handle_agent_selector(app, key);
121 }
122
123 if app.thinking_selector.visible {
124 return handle_thinking_selector(app, key);
125 }
126
127 if app.session_selector.visible {
128 return handle_session_selector(app, key);
129 }
130
131 if app.help_popup.visible {
132 if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
133 app.help_popup.close();
134 }
135 return InputAction::None;
136 }
137
138 if app.pending_question.is_some() {
139 return handle_question_popup(app, key);
140 }
141
142 if app.pending_permission.is_some() {
143 return handle_permission_popup(app, key);
144 }
145
146 if app.context_menu.visible {
147 return handle_context_menu(app, key);
148 }
149
150 if app.command_palette.visible {
151 return handle_command_palette(app, key);
152 }
153
154 if app.vim_mode {
155 match app.mode {
156 AppMode::Normal => handle_normal(app, key),
157 AppMode::Insert => handle_insert(app, key),
158 }
159 } else {
160 handle_simple(app, key)
161 }
162}
163
164fn handle_model_selector(app: &mut App, key: KeyEvent) -> InputAction {
165 match key.code {
166 KeyCode::Esc => {
167 app.model_selector.close();
168 InputAction::None
169 }
170 KeyCode::Up => {
171 app.model_selector.up();
172 InputAction::None
173 }
174 KeyCode::Down | KeyCode::Tab => {
175 app.model_selector.down();
176 InputAction::None
177 }
178 KeyCode::Enter => {
179 if let Some(entry) = app.model_selector.confirm() {
180 app.model_name = entry.model.clone();
181 app.provider_name = entry.provider.clone();
182 InputAction::SelectModel {
183 provider: entry.provider,
184 model: entry.model,
185 }
186 } else {
187 InputAction::None
188 }
189 }
190 KeyCode::Backspace => {
191 app.model_selector.query.pop();
192 app.model_selector.apply_filter();
193 InputAction::None
194 }
195 KeyCode::Char(c) => {
196 app.model_selector.query.push(c);
197 app.model_selector.apply_filter();
198 InputAction::None
199 }
200 _ => InputAction::None,
201 }
202}
203
204fn handle_agent_selector(app: &mut App, key: KeyEvent) -> InputAction {
205 match key.code {
206 KeyCode::Esc => {
207 app.agent_selector.close();
208 InputAction::None
209 }
210 KeyCode::Up => {
211 app.agent_selector.up();
212 InputAction::None
213 }
214 KeyCode::Down | KeyCode::Tab => {
215 app.agent_selector.down();
216 InputAction::None
217 }
218 KeyCode::Enter => {
219 if let Some(entry) = app.agent_selector.confirm() {
220 app.agent_name = entry.name.clone();
221 InputAction::SelectAgent { name: entry.name }
222 } else {
223 InputAction::None
224 }
225 }
226 _ => InputAction::None,
227 }
228}
229
230fn handle_thinking_selector(app: &mut App, key: KeyEvent) -> InputAction {
231 match key.code {
232 KeyCode::Esc => {
233 app.thinking_selector.close();
234 InputAction::None
235 }
236 KeyCode::Up => {
237 app.thinking_selector.up();
238 InputAction::None
239 }
240 KeyCode::Down | KeyCode::Tab => {
241 app.thinking_selector.down();
242 InputAction::None
243 }
244 KeyCode::Enter => {
245 if let Some(level) = app.thinking_selector.confirm() {
246 let budget = level.budget_tokens();
247 app.thinking_budget = budget;
248 InputAction::SetThinkingLevel(budget)
249 } else {
250 InputAction::None
251 }
252 }
253 _ => InputAction::None,
254 }
255}
256
257fn handle_session_selector(app: &mut App, key: KeyEvent) -> InputAction {
258 match key.code {
259 KeyCode::Esc => {
260 app.session_selector.close();
261 InputAction::None
262 }
263 KeyCode::Up => {
264 app.session_selector.up();
265 InputAction::None
266 }
267 KeyCode::Down | KeyCode::Tab => {
268 app.session_selector.down();
269 InputAction::None
270 }
271 KeyCode::Enter => {
272 if let Some(id) = app.session_selector.confirm() {
273 InputAction::ResumeSession { id }
274 } else {
275 InputAction::None
276 }
277 }
278 KeyCode::Backspace => {
279 app.session_selector.query.pop();
280 app.session_selector.apply_filter();
281 InputAction::None
282 }
283 KeyCode::Char(c) => {
284 app.session_selector.query.push(c);
285 app.session_selector.apply_filter();
286 InputAction::None
287 }
288 _ => InputAction::None,
289 }
290}
291
292fn handle_command_palette(app: &mut App, key: KeyEvent) -> InputAction {
293 match key.code {
294 KeyCode::Esc => {
295 app.command_palette.close();
296 InputAction::None
297 }
298 KeyCode::Up => {
299 app.command_palette.up();
300 InputAction::None
301 }
302 KeyCode::Down | KeyCode::Tab => {
303 app.command_palette.down();
304 InputAction::None
305 }
306 KeyCode::Enter => {
307 if let Some(cmd_name) = app.command_palette.confirm() {
308 app.input.clear();
309 app.cursor_pos = 0;
310 execute_command(app, cmd_name)
311 } else {
312 InputAction::None
313 }
314 }
315 KeyCode::Backspace => {
316 app.delete_char_before();
317 if app.input.is_empty() || !app.input.starts_with('/') {
318 app.command_palette.close();
319 } else {
320 app.command_palette.update_filter(&app.input);
321 }
322 InputAction::None
323 }
324 KeyCode::Char(c) => {
325 app.insert_char(c);
326 app.command_palette.update_filter(&app.input);
327 if app.command_palette.filtered.is_empty() {
328 app.command_palette.close();
329 }
330 InputAction::None
331 }
332 _ => InputAction::None,
333 }
334}
335
336fn execute_command(app: &mut App, cmd_name: &str) -> InputAction {
337 match cmd_name {
338 "model" => InputAction::OpenModelSelector,
339 "agent" => InputAction::OpenAgentSelector,
340 "thinking" => InputAction::OpenThinkingSelector,
341 "sessions" => InputAction::OpenSessionSelector,
342 "new" => InputAction::NewConversation,
343 "clear" => {
344 app.clear_conversation();
345 InputAction::None
346 }
347 "help" => {
348 app.help_popup.open();
349 InputAction::None
350 }
351 _ => InputAction::None,
352 }
353}
354
355fn handle_context_menu(app: &mut App, key: KeyEvent) -> InputAction {
356 match key.code {
357 KeyCode::Esc => {
358 app.context_menu.close();
359 InputAction::None
360 }
361 KeyCode::Up => {
362 app.context_menu.up();
363 InputAction::None
364 }
365 KeyCode::Down | KeyCode::Tab => {
366 app.context_menu.down();
367 InputAction::None
368 }
369 KeyCode::Enter => {
370 if let Some((action, msg_idx)) = app.context_menu.confirm() {
371 match action {
372 0 => InputAction::TruncateToMessage(msg_idx),
373 1 => InputAction::ForkFromMessage(msg_idx),
374 _ => InputAction::None,
375 }
376 } else {
377 InputAction::None
378 }
379 }
380 _ => InputAction::None,
381 }
382}
383
384fn handle_normal(app: &mut App, key: KeyEvent) -> InputAction {
385 match key.code {
386 KeyCode::Char('q') => InputAction::Quit,
387 KeyCode::Char('i') | KeyCode::Enter => {
388 app.mode = AppMode::Insert;
389 InputAction::None
390 }
391 KeyCode::Char('j') | KeyCode::Down => InputAction::ScrollDown(1),
392 KeyCode::Char('k') | KeyCode::Up => InputAction::ScrollUp(1),
393 KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
394 InputAction::ScrollDown(10)
395 }
396 KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
397 InputAction::ScrollUp(10)
398 }
399 KeyCode::Char('g') => InputAction::ScrollToTop,
400 KeyCode::Char('G') => InputAction::ScrollToBottom,
401 KeyCode::PageUp => InputAction::ScrollUp(20),
402 KeyCode::PageDown => InputAction::ScrollDown(20),
403 KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => {
404 InputAction::ClearConversation
405 }
406 KeyCode::Tab => InputAction::OpenAgentSelector,
407 KeyCode::Char('t') => InputAction::ToggleThinking,
408 _ => InputAction::None,
409 }
410}
411
412fn handle_insert(app: &mut App, key: KeyEvent) -> InputAction {
413 if key.modifiers.contains(KeyModifiers::CONTROL) {
414 match key.code {
415 KeyCode::Char('t') => return InputAction::CycleThinkingLevel,
416 KeyCode::Char('a') => {
417 app.move_cursor_home();
418 return InputAction::None;
419 }
420 KeyCode::Char('e') => {
421 app.move_cursor_end();
422 return InputAction::None;
423 }
424 KeyCode::Char('w') => {
425 app.delete_word_before();
426 return InputAction::None;
427 }
428 KeyCode::Char('k') => {
429 app.delete_to_end();
430 return InputAction::None;
431 }
432 KeyCode::Char('u') => {
433 app.delete_to_start();
434 return InputAction::None;
435 }
436 _ => {}
437 }
438 }
439
440 if app.is_streaming {
441 return match key.code {
442 KeyCode::Esc => {
443 app.mode = AppMode::Normal;
444 InputAction::None
445 }
446 KeyCode::Enter => handle_send(app),
447 KeyCode::Char(c) => handle_char_input(app, c),
448 KeyCode::Backspace => handle_backspace(app),
449 KeyCode::Left => {
450 app.move_cursor_left();
451 InputAction::None
452 }
453 KeyCode::Right => {
454 app.move_cursor_right();
455 InputAction::None
456 }
457 KeyCode::Home => {
458 app.move_cursor_home();
459 InputAction::None
460 }
461 KeyCode::End => {
462 app.move_cursor_end();
463 InputAction::None
464 }
465 _ => InputAction::None,
466 };
467 }
468
469 match key.code {
470 KeyCode::Esc => {
471 app.mode = AppMode::Normal;
472 InputAction::None
473 }
474 KeyCode::Enter => handle_send(app),
475 KeyCode::Char(c) => handle_char_input(app, c),
476 KeyCode::Backspace => handle_backspace(app),
477 KeyCode::Left => {
478 app.move_cursor_left();
479 InputAction::None
480 }
481 KeyCode::Right => {
482 app.move_cursor_right();
483 InputAction::None
484 }
485 KeyCode::Home => {
486 app.move_cursor_home();
487 InputAction::None
488 }
489 KeyCode::End => {
490 app.move_cursor_end();
491 InputAction::None
492 }
493 _ => InputAction::None,
494 }
495}
496
497fn handle_simple(app: &mut App, key: KeyEvent) -> InputAction {
498 if key.modifiers.contains(KeyModifiers::CONTROL) {
499 match key.code {
500 KeyCode::Char('t') => return InputAction::CycleThinkingLevel,
501 KeyCode::Char('a') => {
502 app.move_cursor_home();
503 return InputAction::None;
504 }
505 KeyCode::Char('e') => {
506 app.move_cursor_end();
507 return InputAction::None;
508 }
509 KeyCode::Char('w') => {
510 app.delete_word_before();
511 return InputAction::None;
512 }
513 KeyCode::Char('k') => {
514 app.delete_to_end();
515 return InputAction::None;
516 }
517 KeyCode::Char('u') => {
518 app.delete_to_start();
519 return InputAction::None;
520 }
521 KeyCode::Char('d') => return InputAction::ScrollDown(10),
522 _ => {}
523 }
524 }
525
526 if app.is_streaming {
527 return match key.code {
528 KeyCode::Up => InputAction::ScrollUp(1),
529 KeyCode::Down => InputAction::ScrollDown(1),
530 KeyCode::PageUp => InputAction::ScrollUp(20),
531 KeyCode::PageDown => InputAction::ScrollDown(20),
532 KeyCode::Enter => handle_send(app),
533 KeyCode::Char(c) => handle_char_input(app, c),
534 KeyCode::Backspace => handle_backspace(app),
535 KeyCode::Left => {
536 app.move_cursor_left();
537 InputAction::None
538 }
539 KeyCode::Right => {
540 app.move_cursor_right();
541 InputAction::None
542 }
543 KeyCode::Home => {
544 app.move_cursor_home();
545 InputAction::None
546 }
547 KeyCode::End => {
548 app.move_cursor_end();
549 InputAction::None
550 }
551 _ => InputAction::None,
552 };
553 }
554
555 match key.code {
556 KeyCode::Esc => InputAction::None,
557 KeyCode::Enter => handle_send(app),
558 KeyCode::Up => InputAction::ScrollUp(1),
559 KeyCode::Down => InputAction::ScrollDown(1),
560 KeyCode::PageUp => InputAction::ScrollUp(20),
561 KeyCode::PageDown => InputAction::ScrollDown(20),
562 KeyCode::Tab => InputAction::OpenAgentSelector,
563 KeyCode::Char(c) => handle_char_input(app, c),
564 KeyCode::Backspace => handle_backspace(app),
565 KeyCode::Left => {
566 app.move_cursor_left();
567 InputAction::None
568 }
569 KeyCode::Right => {
570 app.move_cursor_right();
571 InputAction::None
572 }
573 KeyCode::Home => {
574 app.move_cursor_home();
575 InputAction::None
576 }
577 KeyCode::End => {
578 app.move_cursor_end();
579 InputAction::None
580 }
581 _ => InputAction::None,
582 }
583}
584
585fn handle_send(app: &mut App) -> InputAction {
586 parse_at_references(app);
587 if app.is_streaming {
588 app.queue_input();
589 return InputAction::None;
590 }
591 if let Some(msg) = app.take_input() {
592 InputAction::SendMessage(msg)
593 } else {
594 InputAction::None
595 }
596}
597
598fn handle_char_input(app: &mut App, c: char) -> InputAction {
599 app.insert_char(c);
600 if app.input == "/" {
601 app.command_palette.open(&app.input);
602 } else if app.input.starts_with('/') && app.command_palette.visible {
603 app.command_palette.update_filter(&app.input);
604 if app.command_palette.filtered.is_empty() {
605 app.command_palette.close();
606 }
607 }
608 InputAction::None
609}
610
611fn handle_backspace(app: &mut App) -> InputAction {
612 if let Some(pb_idx) = app.paste_block_at_cursor() {
613 app.delete_paste_block(pb_idx);
614 } else {
615 app.delete_char_before();
616 }
617 if app.input.starts_with('/') && !app.input.is_empty() {
618 if !app.command_palette.visible {
619 app.command_palette.open(&app.input);
620 } else {
621 app.command_palette.update_filter(&app.input);
622 }
623 } else if app.command_palette.visible {
624 app.command_palette.close();
625 }
626 InputAction::None
627}
628
629fn rect_contains(r: ratatui::layout::Rect, col: u16, row: u16) -> bool {
630 col >= r.x && col < r.x + r.width && row >= r.y && row < r.y + r.height
631}
632
633pub fn handle_mouse(app: &mut App, mouse: MouseEvent) -> InputAction {
634 let col = mouse.column;
635 let row = mouse.row;
636
637 match mouse.kind {
638 MouseEventKind::ScrollUp => {
639 app.selection.clear();
640 if app.model_selector.visible
641 && let Some(popup) = app.layout.model_selector
642 && rect_contains(popup, col, row)
643 {
644 app.model_selector.up();
645 return InputAction::None;
646 }
647 InputAction::ScrollUp(1)
648 }
649 MouseEventKind::ScrollDown => {
650 app.selection.clear();
651 if app.model_selector.visible
652 && let Some(popup) = app.layout.model_selector
653 && rect_contains(popup, col, row)
654 {
655 app.model_selector.down();
656 return InputAction::None;
657 }
658 InputAction::ScrollDown(1)
659 }
660 MouseEventKind::Down(MouseButton::Left) => {
661 if app.selection.anchor.is_some() && !app.selection.active {
662 app.selection.clear();
663 }
664
665 if app.context_menu.visible {
666 if let Some(popup) = app.layout.context_menu {
667 if rect_contains(popup, col, row) {
668 let relative_row = row.saturating_sub(popup.y + 1) as usize;
669 app.context_menu.selected = relative_row.min(1);
670 if let Some((action, msg_idx)) = app.context_menu.confirm() {
671 return match action {
672 0 => InputAction::TruncateToMessage(msg_idx),
673 1 => InputAction::ForkFromMessage(msg_idx),
674 _ => InputAction::None,
675 };
676 }
677 }
678 }
679 app.context_menu.close();
680 return InputAction::None;
681 }
682
683 if app.model_selector.visible
684 && let Some(popup) = app.layout.model_selector
685 {
686 if !rect_contains(popup, col, row) {
687 app.model_selector.close();
688 }
689 return InputAction::None;
690 }
691
692 if app.agent_selector.visible
693 && let Some(popup) = app.layout.agent_selector
694 {
695 if !rect_contains(popup, col, row) {
696 app.agent_selector.close();
697 }
698 return InputAction::None;
699 }
700
701 if app.help_popup.visible
702 && let Some(popup) = app.layout.help_popup
703 {
704 if !rect_contains(popup, col, row) {
705 app.help_popup.close();
706 }
707 return InputAction::None;
708 }
709
710 if app.thinking_selector.visible
711 && let Some(popup) = app.layout.thinking_selector
712 && rect_contains(popup, col, row)
713 {
714 let relative_row = row.saturating_sub(popup.y + 1) as usize;
715 if relative_row < ThinkingLevel::all().len() {
716 app.thinking_selector.selected = relative_row;
717 if let Some(level) = app.thinking_selector.confirm() {
718 let budget = level.budget_tokens();
719 app.thinking_budget = budget;
720 return InputAction::SetThinkingLevel(budget);
721 }
722 }
723 } else if app.thinking_selector.visible
724 && let Some(popup) = app.layout.thinking_selector
725 {
726 if !rect_contains(popup, col, row) {
727 app.thinking_selector.close();
728 }
729 return InputAction::None;
730 }
731
732 if app.session_selector.visible
733 && let Some(popup) = app.layout.session_selector
734 && !rect_contains(popup, col, row)
735 {
736 app.session_selector.close();
737 return InputAction::None;
738 }
739
740 if app.command_palette.visible
741 && let Some(popup) = app.layout.command_palette
742 {
743 if rect_contains(popup, col, row) {
744 let relative_row = row.saturating_sub(popup.y) as usize;
745 if relative_row < app.command_palette.filtered.len() {
746 app.command_palette.selected = relative_row;
747 if let Some(cmd_name) = app.command_palette.confirm() {
748 app.input.clear();
749 app.cursor_pos = 0;
750 return execute_command(app, cmd_name);
751 }
752 }
753 return InputAction::None;
754 } else {
755 app.command_palette.close();
756 return InputAction::None;
757 }
758 }
759
760 if rect_contains(app.layout.input, col, row) {
761 if app.vim_mode {
762 app.mode = AppMode::Insert;
763 }
764 let inner_x = col.saturating_sub(app.layout.input.x + 3);
765 let inner_y = row.saturating_sub(app.layout.input.y + 1);
766 let target_offset =
767 compute_click_cursor_pos(&app.input, inner_x as usize, inner_y as usize);
768 app.cursor_pos = target_offset;
769 InputAction::None
770 } else if rect_contains(app.layout.messages, col, row) {
771 let content_y = app.layout.messages.y + 1;
772 if row >= content_y {
773 let content_col = col.saturating_sub(app.layout.messages.x);
774 let content_row = row - content_y;
775 let visual_row = app.scroll_offset + content_row;
776 app.selection.start(content_col, visual_row);
777 }
778 if app.vim_mode && app.mode == AppMode::Insert && app.input.is_empty() {
779 app.mode = AppMode::Normal;
780 }
781 InputAction::None
782 } else {
783 InputAction::None
784 }
785 }
786 MouseEventKind::Drag(MouseButton::Left) => {
787 if app.selection.active {
788 let content_y = app.layout.messages.y + 1;
789 let content_height = app.layout.messages.height.saturating_sub(1);
790 let content_col = col.saturating_sub(app.layout.messages.x);
791 let content_row = if row >= content_y {
792 (row - content_y).min(content_height.saturating_sub(1))
793 } else {
794 0
795 };
796 let visual_row = app.scroll_offset + content_row;
797 app.selection.update(content_col, visual_row);
798 }
799 InputAction::None
800 }
801 MouseEventKind::Up(MouseButton::Left) => {
802 if app.selection.active {
803 let content_y = app.layout.messages.y + 1;
804 let content_height = app.layout.messages.height.saturating_sub(1);
805 let content_col = col.saturating_sub(app.layout.messages.x);
806 let content_row = if row >= content_y {
807 (row - content_y).min(content_height.saturating_sub(1))
808 } else {
809 0
810 };
811 let visual_row = app.scroll_offset + content_row;
812 app.selection.update(content_col, visual_row);
813 app.selection.active = false;
814 if !app.selection.is_empty_selection() {
815 if let Some(text) = app.extract_selected_text()
816 && !text.trim().is_empty()
817 {
818 crate::tui::app::copy_to_clipboard(&text);
819 }
820 } else {
821 app.selection.clear();
822 }
823 }
824 InputAction::None
825 }
826 MouseEventKind::Down(MouseButton::Right) => {
827 if app.context_menu.visible {
828 app.context_menu.close();
829 return InputAction::None;
830 }
831 if rect_contains(app.layout.messages, col, row) && !app.is_streaming {
832 let content_y = app.layout.messages.y + 1;
833 if row >= content_y {
834 let visual_row = (app.scroll_offset + (row - content_y)) as usize;
835 if let Some(&msg_idx) = app.message_line_map.get(visual_row) {
836 app.context_menu.open(msg_idx, col, row);
837 }
838 }
839 }
840 InputAction::None
841 }
842 _ => InputAction::None,
843 }
844}
845
846fn parse_at_references(app: &mut App) {
847 let words: Vec<String> = app.input.split_whitespace().map(String::from).collect();
848 for word in &words {
849 if let Some(path) = word.strip_prefix('@')
850 && !path.is_empty()
851 && crate::tui::app::is_image_path(path)
852 {
853 match app.add_image_attachment(path) {
854 Ok(()) => {}
855 Err(e) => {
856 app.error_message = Some(e);
857 }
858 }
859 }
860 }
861}
862
863fn compute_click_cursor_pos(input: &str, target_col: usize, target_row: usize) -> usize {
864 let mut row: usize = 0;
865 let mut col: usize = 0;
866 let mut byte_pos: usize = 0;
867
868 for ch in input.chars() {
869 if row == target_row && col >= target_col {
870 return byte_pos;
871 }
872 if ch == '\n' {
873 if row == target_row {
874 return byte_pos;
875 }
876 row += 1;
877 col = 0;
878 } else {
879 col += 1;
880 }
881 byte_pos += ch.len_utf8();
882 }
883
884 byte_pos
885}
886
887fn handle_question_popup(app: &mut App, key: KeyEvent) -> InputAction {
888 let pq = app.pending_question.as_mut().unwrap();
889 match key.code {
890 KeyCode::Esc => {
891 if let Some(responder) = pq.responder.take() {
892 let _ = responder.0.send("[cancelled]".to_string());
893 }
894 app.pending_question = None;
895 InputAction::None
896 }
897 KeyCode::Up => {
898 if pq.selected > 0 {
899 pq.selected -= 1;
900 }
901 InputAction::None
902 }
903 KeyCode::Down | KeyCode::Tab => {
904 let max = if pq.options.is_empty() {
905 0
906 } else {
907 pq.options.len()
908 };
909 if pq.selected < max {
910 pq.selected += 1;
911 }
912 InputAction::None
913 }
914 KeyCode::Enter => {
915 let answer = if pq.options.is_empty() || pq.selected >= pq.options.len() {
916 if pq.custom_input.is_empty() {
917 "ok".to_string()
918 } else {
919 pq.custom_input.clone()
920 }
921 } else {
922 pq.options[pq.selected].clone()
923 };
924 if let Some(responder) = pq.responder.take() {
925 let _ = responder.0.send(answer.clone());
926 }
927 app.pending_question = None;
928 InputAction::AnswerQuestion(answer)
929 }
930 KeyCode::Char(c) => {
931 pq.custom_input.push(c);
932 pq.selected = pq.options.len();
934 InputAction::None
935 }
936 KeyCode::Backspace => {
937 pq.custom_input.pop();
938 InputAction::None
939 }
940 _ => InputAction::None,
941 }
942}
943
944fn handle_permission_popup(app: &mut App, key: KeyEvent) -> InputAction {
945 let pp = app.pending_permission.as_mut().unwrap();
946 match key.code {
947 KeyCode::Esc => {
948 if let Some(responder) = pp.responder.take() {
949 let _ = responder.0.send("deny".to_string());
950 }
951 app.pending_permission = None;
952 InputAction::None
953 }
954 KeyCode::Up => {
955 if pp.selected > 0 {
956 pp.selected -= 1;
957 }
958 InputAction::None
959 }
960 KeyCode::Down | KeyCode::Tab => {
961 if pp.selected < 1 {
962 pp.selected += 1;
963 }
964 InputAction::None
965 }
966 KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => {
967 let answer = if pp.selected == 0 { "allow" } else { "deny" };
968 if let Some(responder) = pp.responder.take() {
969 let _ = responder.0.send(answer.to_string());
970 }
971 app.pending_permission = None;
972 InputAction::AnswerPermission(answer.to_string())
973 }
974 KeyCode::Char('n') | KeyCode::Char('N') => {
975 if let Some(responder) = pp.responder.take() {
976 let _ = responder.0.send("deny".to_string());
977 }
978 app.pending_permission = None;
979 InputAction::AnswerPermission("deny".to_string())
980 }
981 _ => InputAction::None,
982 }
983}