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            // Multi-key: zz
114            (KeyState::Normal, KeyCode::Char('z'), KeyModifiers::NONE) => {
115                self.state = KeyState::WaitingForSecond {
116                    first: 'z',
117                    deadline: Instant::now() + MULTI_KEY_TIMEOUT,
118                };
119                None
120            }
121            (
122                KeyState::WaitingForSecond { first: 'z', .. },
123                KeyCode::Char('z'),
124                KeyModifiers::NONE,
125            ) => {
126                self.state = KeyState::Normal;
127                Some(Action::CenterCurrent)
128            }
129
130            (KeyState::WaitingForSecond { .. }, _, _) => {
131                self.state = KeyState::Normal;
132                self.handle_key(key)
133            }
134
135            // Single keys
136            (KeyState::Normal, KeyCode::Char('j') | KeyCode::Down, _) => Some(Action::MoveDown),
137            (KeyState::Normal, KeyCode::Char('k') | KeyCode::Up, _) => Some(Action::MoveUp),
138            (KeyState::Normal, KeyCode::Char('G'), KeyModifiers::SHIFT) => Some(Action::JumpBottom),
139            (KeyState::Normal, KeyCode::Char('d'), KeyModifiers::CONTROL) => Some(Action::PageDown),
140            (KeyState::Normal, KeyCode::Char('u'), KeyModifiers::CONTROL) => Some(Action::PageUp),
141            (KeyState::Normal, KeyCode::Char('H'), KeyModifiers::SHIFT) => {
142                Some(Action::ViewportTop)
143            }
144            (KeyState::Normal, KeyCode::Char('M'), KeyModifiers::SHIFT) => {
145                Some(Action::ViewportMiddle)
146            }
147            (KeyState::Normal, KeyCode::Char('L'), KeyModifiers::SHIFT) => {
148                Some(Action::ViewportBottom)
149            }
150            (KeyState::Normal, KeyCode::Tab, _) => Some(Action::SwitchPane),
151            (KeyState::Normal, KeyCode::Enter, _)
152            | (KeyState::Normal, KeyCode::Char('o'), KeyModifiers::NONE) => {
153                Some(Action::OpenSelected)
154            }
155            (KeyState::Normal, KeyCode::Esc, _) => Some(Action::Back),
156            (KeyState::Normal, KeyCode::Char('q'), _) => Some(Action::QuitView),
157            (KeyState::Normal, KeyCode::Char('1'), KeyModifiers::NONE) => Some(Action::OpenTab1),
158            (KeyState::Normal, KeyCode::Char('2'), KeyModifiers::NONE) => Some(Action::OpenTab2),
159            (KeyState::Normal, KeyCode::Char('3'), KeyModifiers::NONE) => Some(Action::OpenTab3),
160            (KeyState::Normal, KeyCode::Char('4'), KeyModifiers::NONE) => Some(Action::OpenTab4),
161            (KeyState::Normal, KeyCode::Char('5'), KeyModifiers::NONE) => Some(Action::OpenTab5),
162
163            // Search
164            (KeyState::Normal, KeyCode::Char('/'), KeyModifiers::NONE) => Some(Action::OpenSearch),
165            (KeyState::Normal, KeyCode::Char('n'), KeyModifiers::NONE) => {
166                Some(Action::NextSearchResult)
167            }
168            (KeyState::Normal, KeyCode::Char('N'), KeyModifiers::SHIFT) => {
169                Some(Action::PrevSearchResult)
170            }
171
172            // Command palette
173            (KeyState::Normal, KeyCode::Char('p'), KeyModifiers::CONTROL) => {
174                Some(Action::OpenCommandPalette)
175            }
176
177            // Phase 2: Email actions (Gmail-native A005)
178            (KeyState::Normal, KeyCode::Char('c'), KeyModifiers::NONE) => Some(Action::Compose),
179            (KeyState::Normal, KeyCode::Char('r'), KeyModifiers::NONE) => Some(Action::Reply),
180            (KeyState::Normal, KeyCode::Char('a'), KeyModifiers::NONE) => Some(Action::ReplyAll),
181            (KeyState::Normal, KeyCode::Char('f'), KeyModifiers::NONE) => Some(Action::Forward),
182            (KeyState::Normal, KeyCode::Char('e'), KeyModifiers::NONE) => Some(Action::Archive),
183            (KeyState::Normal, KeyCode::Char('#'), _) => Some(Action::Trash),
184            (KeyState::Normal, KeyCode::Char('!'), _) => Some(Action::Spam),
185            (KeyState::Normal, KeyCode::Char('s'), KeyModifiers::NONE) => Some(Action::Star),
186            (KeyState::Normal, KeyCode::Char('I'), KeyModifiers::SHIFT) => Some(Action::MarkRead),
187            (KeyState::Normal, KeyCode::Char('U'), KeyModifiers::SHIFT) => Some(Action::MarkUnread),
188            (KeyState::Normal, KeyCode::Char('l'), KeyModifiers::NONE) => Some(Action::ApplyLabel),
189            (KeyState::Normal, KeyCode::Char('v'), KeyModifiers::NONE) => Some(Action::MoveToLabel),
190            (KeyState::Normal, KeyCode::Char('x'), KeyModifiers::NONE) => {
191                Some(Action::ToggleSelect)
192            }
193            (KeyState::Normal, KeyCode::Char('D'), KeyModifiers::SHIFT) => {
194                Some(Action::Unsubscribe)
195            }
196            (KeyState::Normal, KeyCode::Char('Z'), KeyModifiers::SHIFT) => Some(Action::Snooze),
197            (KeyState::Normal, KeyCode::Char('O'), KeyModifiers::SHIFT) => {
198                Some(Action::OpenInBrowser)
199            }
200            (KeyState::Normal, KeyCode::Char('R'), KeyModifiers::SHIFT) => {
201                Some(Action::ToggleReaderMode)
202            }
203            (KeyState::Normal, KeyCode::Char('S'), KeyModifiers::SHIFT) => {
204                Some(Action::ToggleSignature)
205            }
206            (KeyState::Normal, KeyCode::Char('A'), KeyModifiers::SHIFT) => {
207                Some(Action::AttachmentList)
208            }
209            (KeyState::Normal, KeyCode::Char('V'), KeyModifiers::SHIFT) => {
210                Some(Action::VisualLineMode)
211            }
212            (KeyState::Normal, KeyCode::Char('E'), KeyModifiers::SHIFT) => {
213                Some(Action::ExportThread)
214            }
215            (KeyState::Normal, KeyCode::Char('F'), KeyModifiers::SHIFT) => {
216                Some(Action::ToggleFullscreen)
217            }
218            (KeyState::Normal, KeyCode::Char('?'), _) => Some(Action::Help),
219
220            _ => None,
221        }
222    }
223}