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    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}