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 (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::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 (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 (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 (KeyState::Normal, KeyCode::Char('p'), KeyModifiers::CONTROL) => {
174 Some(Action::OpenCommandPalette)
175 }
176
177 (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}