Skip to main content

dot/tui/input/
mod.rs

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}