Skip to main content

mxr_tui/
input.rs

1use crate::action::Action;
2use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3use std::time::{Duration, Instant};
4
5const MULTI_KEY_TIMEOUT: Duration = Duration::from_millis(500);
6
7#[derive(Debug)]
8pub enum KeyState {
9    Normal,
10    WaitingForSecond { first: char, deadline: Instant },
11}
12
13pub struct InputHandler {
14    state: KeyState,
15}
16
17impl Default for InputHandler {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl InputHandler {
24    pub fn new() -> Self {
25        Self {
26            state: KeyState::Normal,
27        }
28    }
29
30    pub fn is_pending(&self) -> bool {
31        matches!(self.state, KeyState::WaitingForSecond { .. })
32    }
33
34    pub fn check_timeout(&mut self) -> Option<Action> {
35        if let KeyState::WaitingForSecond { deadline, .. } = &self.state {
36            if Instant::now() > *deadline {
37                self.state = KeyState::Normal;
38                return None;
39            }
40        }
41        None
42    }
43
44    pub fn handle_key(&mut self, key: KeyEvent) -> Option<Action> {
45        self.check_timeout();
46
47        match (&self.state, key.code, key.modifiers) {
48            // Multi-key: g prefix
49            (KeyState::Normal, KeyCode::Char('g'), KeyModifiers::NONE) => {
50                self.state = KeyState::WaitingForSecond {
51                    first: 'g',
52                    deadline: Instant::now() + MULTI_KEY_TIMEOUT,
53                };
54                None
55            }
56            (
57                KeyState::WaitingForSecond { first: 'g', .. },
58                KeyCode::Char('g'),
59                KeyModifiers::NONE,
60            ) => {
61                self.state = KeyState::Normal;
62                Some(Action::JumpTop)
63            }
64            (
65                KeyState::WaitingForSecond { first: 'g', .. },
66                KeyCode::Char('i'),
67                KeyModifiers::NONE,
68            ) => {
69                self.state = KeyState::Normal;
70                Some(Action::GoToInbox)
71            }
72            (
73                KeyState::WaitingForSecond { first: 'g', .. },
74                KeyCode::Char('s'),
75                KeyModifiers::NONE,
76            ) => {
77                self.state = KeyState::Normal;
78                Some(Action::GoToStarred)
79            }
80            (
81                KeyState::WaitingForSecond { first: 'g', .. },
82                KeyCode::Char('t'),
83                KeyModifiers::NONE,
84            ) => {
85                self.state = KeyState::Normal;
86                Some(Action::GoToSent)
87            }
88            (
89                KeyState::WaitingForSecond { first: 'g', .. },
90                KeyCode::Char('d'),
91                KeyModifiers::NONE,
92            ) => {
93                self.state = KeyState::Normal;
94                Some(Action::GoToDrafts)
95            }
96            (
97                KeyState::WaitingForSecond { first: 'g', .. },
98                KeyCode::Char('a'),
99                KeyModifiers::NONE,
100            ) => {
101                self.state = KeyState::Normal;
102                Some(Action::GoToAllMail)
103            }
104            (
105                KeyState::WaitingForSecond { first: 'g', .. },
106                KeyCode::Char('l'),
107                KeyModifiers::NONE,
108            ) => {
109                self.state = KeyState::Normal;
110                Some(Action::GoToLabel)
111            }
112            (
113                KeyState::WaitingForSecond { first: 'g', .. },
114                KeyCode::Char('c'),
115                KeyModifiers::NONE,
116            ) => {
117                self.state = KeyState::Normal;
118                Some(Action::EditConfig)
119            }
120            (
121                KeyState::WaitingForSecond { first: 'g', .. },
122                KeyCode::Char('L'),
123                KeyModifiers::SHIFT,
124            ) => {
125                self.state = KeyState::Normal;
126                Some(Action::OpenLogs)
127            }
128
129            // Multi-key: zz
130            (KeyState::Normal, KeyCode::Char('z'), KeyModifiers::NONE) => {
131                self.state = KeyState::WaitingForSecond {
132                    first: 'z',
133                    deadline: Instant::now() + MULTI_KEY_TIMEOUT,
134                };
135                None
136            }
137            (
138                KeyState::WaitingForSecond { first: 'z', .. },
139                KeyCode::Char('z'),
140                KeyModifiers::NONE,
141            ) => {
142                self.state = KeyState::Normal;
143                Some(Action::CenterCurrent)
144            }
145
146            (KeyState::WaitingForSecond { .. }, _, _) => {
147                self.state = KeyState::Normal;
148                self.handle_key(key)
149            }
150
151            // Single keys
152            (KeyState::Normal, KeyCode::Char('j') | KeyCode::Down, _) => Some(Action::MoveDown),
153            (KeyState::Normal, KeyCode::Char('k') | KeyCode::Up, _) => Some(Action::MoveUp),
154            (KeyState::Normal, KeyCode::Char('G'), KeyModifiers::SHIFT) => Some(Action::JumpBottom),
155            (KeyState::Normal, KeyCode::Char('d'), KeyModifiers::CONTROL) => Some(Action::PageDown),
156            (KeyState::Normal, KeyCode::Char('u'), KeyModifiers::CONTROL) => Some(Action::PageUp),
157            (KeyState::Normal, KeyCode::Char('H'), KeyModifiers::SHIFT) => {
158                Some(Action::ViewportTop)
159            }
160            (KeyState::Normal, KeyCode::Char('M'), KeyModifiers::SHIFT) => {
161                Some(Action::ViewportMiddle)
162            }
163            (KeyState::Normal, KeyCode::Char('L'), KeyModifiers::SHIFT) => {
164                Some(Action::ViewportBottom)
165            }
166            (KeyState::Normal, KeyCode::Tab, _) => Some(Action::SwitchPane),
167            (KeyState::Normal, KeyCode::Enter, _)
168            | (KeyState::Normal, KeyCode::Char('o'), KeyModifiers::NONE) => {
169                Some(Action::OpenSelected)
170            }
171            (KeyState::Normal, KeyCode::Esc, _) => Some(Action::Back),
172            (KeyState::Normal, KeyCode::Char('q'), _) => Some(Action::QuitView),
173            (KeyState::Normal, KeyCode::Char('1'), KeyModifiers::NONE) => Some(Action::OpenTab1),
174            (KeyState::Normal, KeyCode::Char('2'), KeyModifiers::NONE) => Some(Action::OpenTab2),
175            (KeyState::Normal, KeyCode::Char('3'), KeyModifiers::NONE) => Some(Action::OpenTab3),
176            (KeyState::Normal, KeyCode::Char('4'), KeyModifiers::NONE) => Some(Action::OpenTab4),
177            (KeyState::Normal, KeyCode::Char('5'), KeyModifiers::NONE) => Some(Action::OpenTab5),
178
179            // Search
180            (KeyState::Normal, KeyCode::Char('/'), KeyModifiers::NONE) => Some(Action::OpenSearch),
181            (KeyState::Normal, KeyCode::Char('n'), KeyModifiers::NONE) => {
182                Some(Action::NextSearchResult)
183            }
184            (KeyState::Normal, KeyCode::Char('N'), KeyModifiers::SHIFT) => {
185                Some(Action::PrevSearchResult)
186            }
187
188            // Command palette
189            (KeyState::Normal, KeyCode::Char('p'), KeyModifiers::CONTROL) => {
190                Some(Action::OpenCommandPalette)
191            }
192
193            // Phase 2: Email actions (Gmail-native A005)
194            (KeyState::Normal, KeyCode::Char('c'), KeyModifiers::NONE) => Some(Action::Compose),
195            (KeyState::Normal, KeyCode::Char('r'), KeyModifiers::NONE) => Some(Action::Reply),
196            (KeyState::Normal, KeyCode::Char('a'), KeyModifiers::NONE) => Some(Action::ReplyAll),
197            (KeyState::Normal, KeyCode::Char('f'), KeyModifiers::NONE) => Some(Action::Forward),
198            (KeyState::Normal, KeyCode::Char('e'), KeyModifiers::NONE) => Some(Action::Archive),
199            (KeyState::Normal, KeyCode::Char('m'), KeyModifiers::NONE) => {
200                Some(Action::MarkReadAndArchive)
201            }
202            (KeyState::Normal, KeyCode::Char('#'), _) => Some(Action::Trash),
203            (KeyState::Normal, KeyCode::Char('!'), _) => Some(Action::Spam),
204            (KeyState::Normal, KeyCode::Char('s'), KeyModifiers::NONE) => Some(Action::Star),
205            (KeyState::Normal, KeyCode::Char('I'), KeyModifiers::SHIFT) => Some(Action::MarkRead),
206            (KeyState::Normal, KeyCode::Char('U'), KeyModifiers::SHIFT) => Some(Action::MarkUnread),
207            (KeyState::Normal, KeyCode::Char('l'), KeyModifiers::NONE) => Some(Action::ApplyLabel),
208            (KeyState::Normal, KeyCode::Char('v'), KeyModifiers::NONE) => Some(Action::MoveToLabel),
209            (KeyState::Normal, KeyCode::Char('x'), KeyModifiers::NONE) => {
210                Some(Action::ToggleSelect)
211            }
212            (KeyState::Normal, KeyCode::Char('D'), KeyModifiers::SHIFT) => {
213                Some(Action::Unsubscribe)
214            }
215            (KeyState::Normal, KeyCode::Char('Z'), KeyModifiers::SHIFT) => Some(Action::Snooze),
216            (KeyState::Normal, KeyCode::Char('O'), KeyModifiers::SHIFT) => {
217                Some(Action::OpenInBrowser)
218            }
219            (KeyState::Normal, KeyCode::Char('R'), KeyModifiers::SHIFT) => {
220                Some(Action::ToggleReaderMode)
221            }
222            (KeyState::Normal, KeyCode::Char('S'), KeyModifiers::SHIFT) => {
223                Some(Action::ToggleSignature)
224            }
225            (KeyState::Normal, KeyCode::Char('A'), KeyModifiers::SHIFT) => {
226                Some(Action::AttachmentList)
227            }
228            (KeyState::Normal, KeyCode::Char('V'), KeyModifiers::SHIFT) => {
229                Some(Action::VisualLineMode)
230            }
231            (KeyState::Normal, KeyCode::Char('E'), KeyModifiers::SHIFT) => {
232                Some(Action::ExportThread)
233            }
234            (KeyState::Normal, KeyCode::Char('F'), KeyModifiers::SHIFT) => {
235                Some(Action::ToggleFullscreen)
236            }
237            (KeyState::Normal, KeyCode::Char('?'), _) => Some(Action::Help),
238
239            _ => None,
240        }
241    }
242}