1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum GitKeyAction {
11 Down,
13 Up,
15 Left,
17 Right,
19 GotoTop,
21 GotoBottom,
23 HunkNext,
25 HunkPrev,
27 FileNext,
29 FilePrev,
31 ViewMode(u8),
34 ToggleSidebar,
36 CycleWhitespace,
38 ToggleWrap,
40 Stage,
42 Unstage,
44 Commit,
46 Refresh,
48 Close,
50}
51
52pub fn match_git_key(key: &KeyEvent) -> Option<GitKeyAction> {
55 let alt = key.modifiers.contains(KeyModifiers::ALT);
56
57 if let KeyCode::Char(c) = key.code
59 && !alt
60 && !key.modifiers.contains(KeyModifiers::CONTROL)
61 {
62 match c {
63 '1' => return Some(GitKeyAction::ViewMode(1)),
64 '2' => return Some(GitKeyAction::ViewMode(2)),
65 '3' => return Some(GitKeyAction::ViewMode(3)),
66 '4' => return Some(GitKeyAction::ViewMode(4)),
67 _ => {}
68 }
69 }
70
71 if alt {
73 return match key.code {
74 KeyCode::Down => Some(GitKeyAction::HunkNext),
75 KeyCode::Up => Some(GitKeyAction::HunkPrev),
76 _ => None,
77 };
78 }
79
80 if key.modifiers.contains(KeyModifiers::CONTROL) {
84 return None;
85 }
86
87 match key.code {
88 KeyCode::Char('j') => Some(GitKeyAction::Down),
89 KeyCode::Char('k') => Some(GitKeyAction::Up),
90 KeyCode::Char('h') => Some(GitKeyAction::Left),
91 KeyCode::Char('l') => Some(GitKeyAction::Right),
92 KeyCode::Char('g') => Some(GitKeyAction::GotoTop),
93 KeyCode::Char('G') => Some(GitKeyAction::GotoBottom),
94 KeyCode::Char(']') => Some(GitKeyAction::FileNext),
95 KeyCode::Char('[') => Some(GitKeyAction::FilePrev),
96 KeyCode::Char('v') => Some(GitKeyAction::ToggleSidebar),
97 KeyCode::Char('b') => Some(GitKeyAction::CycleWhitespace),
98 KeyCode::Char('w') => Some(GitKeyAction::ToggleWrap),
99 KeyCode::Char('s') => Some(GitKeyAction::Stage),
100 KeyCode::Char('u') => Some(GitKeyAction::Unstage),
101 KeyCode::Char('c') => Some(GitKeyAction::Commit),
102 KeyCode::Char('r') => Some(GitKeyAction::Refresh),
103 KeyCode::Char('q') => Some(GitKeyAction::Close),
104 KeyCode::Esc => Some(GitKeyAction::Close),
105 _ => None,
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn k(code: KeyCode) -> KeyEvent {
114 KeyEvent::new(code, KeyModifiers::NONE)
115 }
116
117 fn alt(code: KeyCode) -> KeyEvent {
118 KeyEvent::new(code, KeyModifiers::ALT)
119 }
120
121 #[test]
122 fn vim_motions_map() {
123 assert_eq!(
124 match_git_key(&k(KeyCode::Char('j'))),
125 Some(GitKeyAction::Down)
126 );
127 assert_eq!(
128 match_git_key(&k(KeyCode::Char('k'))),
129 Some(GitKeyAction::Up)
130 );
131 assert_eq!(
132 match_git_key(&k(KeyCode::Char('h'))),
133 Some(GitKeyAction::Left)
134 );
135 assert_eq!(
136 match_git_key(&k(KeyCode::Char('l'))),
137 Some(GitKeyAction::Right)
138 );
139 assert_eq!(
140 match_git_key(&k(KeyCode::Char('g'))),
141 Some(GitKeyAction::GotoTop)
142 );
143 assert_eq!(
144 match_git_key(&k(KeyCode::Char('G'))),
145 Some(GitKeyAction::GotoBottom)
146 );
147
148 assert_eq!(
150 match_git_key(&alt(KeyCode::Down)),
151 Some(GitKeyAction::HunkNext)
152 );
153 assert_eq!(
154 match_git_key(&alt(KeyCode::Up)),
155 Some(GitKeyAction::HunkPrev)
156 );
157
158 assert_eq!(
160 match_git_key(&k(KeyCode::Char(']'))),
161 Some(GitKeyAction::FileNext)
162 );
163 assert_eq!(
164 match_git_key(&k(KeyCode::Char('['))),
165 Some(GitKeyAction::FilePrev)
166 );
167 }
168
169 #[test]
170 fn number_keys_select_view() {
171 assert_eq!(
172 match_git_key(&k(KeyCode::Char('1'))),
173 Some(GitKeyAction::ViewMode(1))
174 );
175 assert_eq!(
176 match_git_key(&k(KeyCode::Char('2'))),
177 Some(GitKeyAction::ViewMode(2))
178 );
179 assert_eq!(
180 match_git_key(&k(KeyCode::Char('3'))),
181 Some(GitKeyAction::ViewMode(3))
182 );
183 assert_eq!(
184 match_git_key(&k(KeyCode::Char('4'))),
185 Some(GitKeyAction::ViewMode(4))
186 );
187 assert_eq!(match_git_key(&k(KeyCode::Char('5'))), None);
189 assert_eq!(match_git_key(&k(KeyCode::Char('0'))), None);
190 }
191
192 #[test]
193 fn whitespace_and_wrap_toggles() {
194 assert_eq!(
195 match_git_key(&k(KeyCode::Char('b'))),
196 Some(GitKeyAction::CycleWhitespace)
197 );
198 assert_eq!(
199 match_git_key(&k(KeyCode::Char('w'))),
200 Some(GitKeyAction::ToggleWrap)
201 );
202 assert_eq!(
204 match_git_key(&k(KeyCode::Char('s'))),
205 Some(GitKeyAction::Stage)
206 );
207 assert_eq!(
208 match_git_key(&k(KeyCode::Char('u'))),
209 Some(GitKeyAction::Unstage)
210 );
211 assert_eq!(
212 match_git_key(&k(KeyCode::Char('c'))),
213 Some(GitKeyAction::Commit)
214 );
215 assert_eq!(
216 match_git_key(&k(KeyCode::Char('r'))),
217 Some(GitKeyAction::Refresh)
218 );
219 assert_eq!(
221 match_git_key(&k(KeyCode::Char('v'))),
222 Some(GitKeyAction::ToggleSidebar)
223 );
224 }
225
226 #[test]
227 fn close_keys() {
228 assert_eq!(
229 match_git_key(&k(KeyCode::Char('q'))),
230 Some(GitKeyAction::Close)
231 );
232 assert_eq!(match_git_key(&k(KeyCode::Esc)), Some(GitKeyAction::Close));
233 }
234
235 fn ctrl(code: KeyCode) -> KeyEvent {
236 KeyEvent::new(code, KeyModifiers::CONTROL)
237 }
238
239 #[test]
240 fn ctrl_c_does_not_commit() {
241 assert_eq!(match_git_key(&ctrl(KeyCode::Char('c'))), None);
245 assert_eq!(
247 match_git_key(&k(KeyCode::Char('c'))),
248 Some(GitKeyAction::Commit)
249 );
250 }
251
252 #[test]
253 fn ctrl_s_does_not_stage() {
254 assert_eq!(match_git_key(&ctrl(KeyCode::Char('s'))), None);
255 assert_eq!(
256 match_git_key(&k(KeyCode::Char('s'))),
257 Some(GitKeyAction::Stage)
258 );
259 }
260
261 #[test]
262 fn ctrl_r_and_ctrl_q_do_not_hijack() {
263 assert_eq!(match_git_key(&ctrl(KeyCode::Char('r'))), None);
266 assert_eq!(match_git_key(&ctrl(KeyCode::Char('q'))), None);
267 assert_eq!(
268 match_git_key(&k(KeyCode::Char('r'))),
269 Some(GitKeyAction::Refresh)
270 );
271 assert_eq!(
272 match_git_key(&k(KeyCode::Char('q'))),
273 Some(GitKeyAction::Close)
274 );
275 }
276}