1use std::{
2 fmt,
3 str::FromStr,
4};
5
6use crossterm::event::{
7 KeyCode,
8 KeyEvent,
9 KeyModifiers,
10};
11use serde::{
12 Deserialize,
13 Deserializer,
14 Serialize,
15 Serializer,
16 de,
17};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct KeyBinding {
21 pub code: KeyCode,
22 pub modifiers: KeyModifiers,
23}
24
25impl KeyBinding {
26 pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
27 Self { code, modifiers }
28 }
29
30 pub const fn key(code: KeyCode) -> Self {
31 Self::new(code, KeyModifiers::NONE)
32 }
33
34 pub const fn char(ch: char) -> Self {
35 Self::new(KeyCode::Char(ch), KeyModifiers::NONE)
36 }
37
38 pub const fn ctrl(ch: char) -> Self {
39 Self::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
40 }
41
42 pub const fn alt(ch: char) -> Self {
43 Self::new(KeyCode::Char(ch), KeyModifiers::ALT)
44 }
45
46 pub fn matches(&self, key: KeyEvent) -> bool {
47 self.code == key.code && self.modifiers == key.modifiers
48 }
49
50 pub fn display(&self) -> String {
51 format_key_binding(self.code, self.modifiers)
52 }
53
54 pub fn display_without_modifiers(&self) -> String {
55 format_key_code(self.code)
56 }
57
58 pub fn plain_char(&self) -> Option<char> {
59 match (self.code, self.modifiers) {
60 (KeyCode::Char(ch), KeyModifiers::NONE) => Some(ch),
61 _ => None,
62 }
63 }
64}
65
66impl fmt::Display for KeyBinding {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.write_str(&self.display())
69 }
70}
71
72impl FromStr for KeyBinding {
73 type Err = String;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 parse_key_binding(s)
77 }
78}
79
80impl Serialize for KeyBinding {
81 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
82 where
83 S: Serializer,
84 {
85 serializer.serialize_str(&self.display())
86 }
87}
88
89impl<'de> Deserialize<'de> for KeyBinding {
90 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
91 where
92 D: Deserializer<'de>,
93 {
94 let s = String::deserialize(deserializer)?;
95 s.parse().map_err(de::Error::custom)
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Default)]
100pub struct KeyList(pub Vec<KeyBinding>);
101
102impl KeyList {
103 pub fn matches(&self, key: KeyEvent) -> bool {
104 self.0.iter().any(|binding| binding.matches(key))
105 }
106
107 pub fn display(&self) -> String {
108 self.display_with_separator("/")
109 }
110
111 pub fn display_with_separator(&self, sep: &str) -> String {
112 self
113 .0
114 .iter()
115 .map(KeyBinding::display)
116 .collect::<Vec<_>>()
117 .join(sep)
118 }
119
120 pub fn first(&self) -> Option<&KeyBinding> {
121 self.0.first()
122 }
123}
124
125impl From<Vec<KeyBinding>> for KeyList {
126 fn from(bindings: Vec<KeyBinding>) -> Self {
127 Self(bindings)
128 }
129}
130
131impl Serialize for KeyList {
132 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
133 where
134 S: Serializer,
135 {
136 if self.0.len() == 1 {
137 serializer.serialize_str(&self.0[0].display())
138 } else {
139 let values = self.0.iter().map(|b| b.display()).collect::<Vec<_>>();
140 values.serialize(serializer)
141 }
142 }
143}
144
145impl<'de> Deserialize<'de> for KeyList {
146 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147 where
148 D: Deserializer<'de>,
149 {
150 struct Visitor;
151
152 impl<'de> de::Visitor<'de> for Visitor {
153 type Value = KeyList;
154
155 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
156 formatter.write_str("a key binding string or list of key binding strings")
157 }
158
159 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
160 where
161 E: de::Error,
162 {
163 let binding: KeyBinding = v.parse().map_err(E::custom)?;
164 Ok(KeyList(vec![binding]))
165 }
166
167 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
168 where
169 A: de::SeqAccess<'de>,
170 {
171 let mut bindings = Vec::new();
172 while let Some(value) = seq.next_element::<String>()? {
173 bindings.push(value.parse().map_err(de::Error::custom)?);
174 }
175 Ok(KeyList(bindings))
176 }
177 }
178
179 deserializer.deserialize_any(Visitor)
180 }
181}
182
183#[derive(Debug, Clone, Default, Deserialize, Serialize)]
184pub struct TuiKeyBindingsConfig {
185 pub quit: Option<KeyList>,
186 pub switch_pane: Option<KeyList>,
187 pub switch_layout: Option<KeyList>,
188 pub close_popup: Option<KeyList>,
189 pub help: Option<KeyList>,
190 pub page_down: Option<KeyList>,
191 pub page_up: Option<KeyList>,
192 pub page_left: Option<KeyList>,
193 pub page_right: Option<KeyList>,
194 pub scroll_left: Option<KeyList>,
195 pub scroll_right: Option<KeyList>,
196 pub scroll_top: Option<KeyList>,
197 pub scroll_bottom: Option<KeyList>,
198 pub scroll_start: Option<KeyList>,
199 pub scroll_end: Option<KeyList>,
200 pub event_grow_pane: Option<KeyList>,
201 pub event_shrink_pane: Option<KeyList>,
202 pub event_send_ctrl_s: Option<KeyList>,
203 pub event_toggle_follow: Option<KeyList>,
204 pub event_search: Option<KeyList>,
205 pub event_toggle_env: Option<KeyList>,
206 pub event_toggle_cwd: Option<KeyList>,
207 pub event_view_details: Option<KeyList>,
208 pub event_go_to_parent: Option<KeyList>,
209 pub event_backtrace: Option<KeyList>,
210 pub event_copy: Option<KeyList>,
211 pub event_breakpoints: Option<KeyList>,
212 pub event_hits: Option<KeyList>,
213 pub query_execute: Option<KeyList>,
214 pub query_cancel: Option<KeyList>,
215 pub query_toggle_case: Option<KeyList>,
216 pub query_toggle_regex: Option<KeyList>,
217 pub query_next_match: Option<KeyList>,
218 pub query_prev_match: Option<KeyList>,
219 pub query_clear: Option<KeyList>,
220 pub details_scroll_down: Option<KeyList>,
221 pub details_scroll_up: Option<KeyList>,
222 pub details_next_tab: Option<KeyList>,
223 pub details_prev_tab: Option<KeyList>,
224 pub details_cycle_tab: Option<KeyList>,
225 pub details_prev_field: Option<KeyList>,
226 pub details_next_field: Option<KeyList>,
227 pub details_copy: Option<KeyList>,
228 pub details_view_parent: Option<KeyList>,
229 pub next_item: Option<KeyList>,
230 pub prev_item: Option<KeyList>,
231 pub copy_choose: Option<KeyList>,
232 pub copy_target_cmdline: Option<KeyList>,
233 pub copy_target_cmdline_full_env: Option<KeyList>,
234 pub copy_target_cmdline_stdio: Option<KeyList>,
235 pub copy_target_cmdline_fds: Option<KeyList>,
236 pub copy_target_env: Option<KeyList>,
237 pub copy_target_env_diff: Option<KeyList>,
238 pub copy_target_argv: Option<KeyList>,
239 pub copy_target_argv_joined: Option<KeyList>,
240 pub copy_target_filename: Option<KeyList>,
241 pub copy_target_syscall_result: Option<KeyList>,
242 pub copy_target_line: Option<KeyList>,
243 pub go_back: Option<KeyList>,
244 pub breakpoint_delete: Option<KeyList>,
245 pub breakpoint_toggle_active: Option<KeyList>,
246 pub breakpoint_edit: Option<KeyList>,
247 pub breakpoint_new: Option<KeyList>,
248 pub breakpoint_editor_save: Option<KeyList>,
249 pub breakpoint_editor_cancel: Option<KeyList>,
250 pub breakpoint_editor_toggle_stop: Option<KeyList>,
251 pub breakpoint_editor_toggle_active: Option<KeyList>,
252 pub hit_close: Option<KeyList>,
253 pub hit_detach: Option<KeyList>,
254 pub hit_resume: Option<KeyList>,
255 pub hit_edit_default_command: Option<KeyList>,
256 pub hit_run_default_command: Option<KeyList>,
257 pub hit_run_custom_command: Option<KeyList>,
258 pub hit_editor_save: Option<KeyList>,
259 pub hit_editor_cancel: Option<KeyList>,
260 pub hit_editor_clear: Option<KeyList>,
261 pub terminal_toggle_scrollback: Option<KeyList>,
262 pub terminal_scroll_up: Option<KeyList>,
263 pub terminal_scroll_down: Option<KeyList>,
264 pub terminal_page_up: Option<KeyList>,
265 pub terminal_page_down: Option<KeyList>,
266 pub terminal_scroll_top: Option<KeyList>,
267 pub terminal_scroll_bottom: Option<KeyList>,
268}
269
270#[derive(Debug, Clone)]
271pub struct TuiKeyBindings {
272 pub quit: KeyList,
273 pub switch_pane: KeyList,
274 pub switch_layout: KeyList,
275 pub close_popup: KeyList,
276 pub help: KeyList,
277 pub page_down: KeyList,
278 pub page_up: KeyList,
279 pub page_left: KeyList,
280 pub page_right: KeyList,
281 pub scroll_left: KeyList,
282 pub scroll_right: KeyList,
283 pub scroll_top: KeyList,
284 pub scroll_bottom: KeyList,
285 pub scroll_start: KeyList,
286 pub scroll_end: KeyList,
287 pub event_grow_pane: KeyList,
288 pub event_shrink_pane: KeyList,
289 pub event_send_ctrl_s: KeyList,
290 pub event_toggle_follow: KeyList,
291 pub event_search: KeyList,
292 pub event_toggle_env: KeyList,
293 pub event_toggle_cwd: KeyList,
294 pub event_view_details: KeyList,
295 pub event_go_to_parent: KeyList,
296 pub event_backtrace: KeyList,
297 pub event_copy: KeyList,
298 pub event_breakpoints: KeyList,
299 pub event_hits: KeyList,
300 pub query_execute: KeyList,
301 pub query_cancel: KeyList,
302 pub query_toggle_case: KeyList,
303 pub query_toggle_regex: KeyList,
304 pub query_next_match: KeyList,
305 pub query_prev_match: KeyList,
306 pub query_clear: KeyList,
307 pub details_scroll_down: KeyList,
308 pub details_scroll_up: KeyList,
309 pub details_next_tab: KeyList,
310 pub details_prev_tab: KeyList,
311 pub details_cycle_tab: KeyList,
312 pub details_prev_field: KeyList,
313 pub details_next_field: KeyList,
314 pub details_copy: KeyList,
315 pub details_view_parent: KeyList,
316 pub next_item: KeyList,
317 pub prev_item: KeyList,
318 pub copy_choose: KeyList,
319 pub copy_target_cmdline: KeyList,
320 pub copy_target_cmdline_full_env: KeyList,
321 pub copy_target_cmdline_stdio: KeyList,
322 pub copy_target_cmdline_fds: KeyList,
323 pub copy_target_env: KeyList,
324 pub copy_target_env_diff: KeyList,
325 pub copy_target_argv: KeyList,
326 pub copy_target_argv_joined: KeyList,
327 pub copy_target_filename: KeyList,
328 pub copy_target_syscall_result: KeyList,
329 pub copy_target_line: KeyList,
330 pub go_back: KeyList,
331 pub breakpoint_delete: KeyList,
332 pub breakpoint_toggle_active: KeyList,
333 pub breakpoint_edit: KeyList,
334 pub breakpoint_new: KeyList,
335 pub breakpoint_editor_save: KeyList,
336 pub breakpoint_editor_cancel: KeyList,
337 pub breakpoint_editor_toggle_stop: KeyList,
338 pub breakpoint_editor_toggle_active: KeyList,
339 pub hit_close: KeyList,
340 pub hit_detach: KeyList,
341 pub hit_resume: KeyList,
342 pub hit_edit_default_command: KeyList,
343 pub hit_run_default_command: KeyList,
344 pub hit_run_custom_command: KeyList,
345 pub hit_editor_save: KeyList,
346 pub hit_editor_cancel: KeyList,
347 pub hit_editor_clear: KeyList,
348 pub terminal_toggle_scrollback: KeyList,
349 pub terminal_scroll_up: KeyList,
350 pub terminal_scroll_down: KeyList,
351 pub terminal_page_up: KeyList,
352 pub terminal_page_down: KeyList,
353 pub terminal_scroll_top: KeyList,
354 pub terminal_scroll_bottom: KeyList,
355}
356
357impl Default for TuiKeyBindings {
358 fn default() -> Self {
359 Self {
360 quit: KeyList(vec![KeyBinding::char('q')]),
361 switch_pane: KeyList(vec![KeyBinding::ctrl('s')]),
362 switch_layout: KeyList(vec![KeyBinding::new(KeyCode::Char('l'), KeyModifiers::ALT)]),
363 close_popup: KeyList(vec![KeyBinding::char('q')]),
364 help: KeyList(vec![KeyBinding::key(KeyCode::F(1))]),
365 page_down: KeyList(vec![
366 KeyBinding::new(KeyCode::Down, KeyModifiers::CONTROL),
367 KeyBinding::new(KeyCode::Char('j'), KeyModifiers::CONTROL),
368 KeyBinding::key(KeyCode::PageDown),
369 ]),
370 page_up: KeyList(vec![
371 KeyBinding::new(KeyCode::Up, KeyModifiers::CONTROL),
372 KeyBinding::new(KeyCode::Char('k'), KeyModifiers::CONTROL),
373 KeyBinding::key(KeyCode::PageUp),
374 ]),
375 page_left: KeyList(vec![
376 KeyBinding::new(KeyCode::Left, KeyModifiers::CONTROL),
377 KeyBinding::new(KeyCode::Char('h'), KeyModifiers::CONTROL),
378 ]),
379 page_right: KeyList(vec![
380 KeyBinding::new(KeyCode::Right, KeyModifiers::CONTROL),
381 KeyBinding::new(KeyCode::Char('l'), KeyModifiers::CONTROL),
382 ]),
383 scroll_left: KeyList(vec![KeyBinding::key(KeyCode::Left), KeyBinding::char('h')]),
384 scroll_right: KeyList(vec![KeyBinding::key(KeyCode::Right), KeyBinding::char('l')]),
385 scroll_top: KeyList(vec![KeyBinding::key(KeyCode::Home)]),
386 scroll_bottom: KeyList(vec![KeyBinding::key(KeyCode::End)]),
387 scroll_start: KeyList(vec![KeyBinding::new(KeyCode::Home, KeyModifiers::SHIFT)]),
388 scroll_end: KeyList(vec![KeyBinding::new(KeyCode::End, KeyModifiers::SHIFT)]),
389 event_grow_pane: KeyList(vec![KeyBinding::char('g')]),
390 event_shrink_pane: KeyList(vec![KeyBinding::char('s')]),
391 event_send_ctrl_s: KeyList(vec![KeyBinding::new(KeyCode::Char('s'), KeyModifiers::ALT)]),
392 event_toggle_follow: KeyList(vec![KeyBinding::char('f')]),
393 event_search: KeyList(vec![KeyBinding::new(
394 KeyCode::Char('f'),
395 KeyModifiers::CONTROL,
396 )]),
397 event_toggle_env: KeyList(vec![KeyBinding::char('e')]),
398 event_toggle_cwd: KeyList(vec![KeyBinding::char('w')]),
399 event_view_details: KeyList(vec![KeyBinding::char('v')]),
400 event_go_to_parent: KeyList(vec![KeyBinding::char('u')]),
401 event_backtrace: KeyList(vec![KeyBinding::char('t')]),
402 event_copy: KeyList(vec![KeyBinding::char('c')]),
403 event_breakpoints: KeyList(vec![KeyBinding::char('b')]),
404 event_hits: KeyList(vec![KeyBinding::char('z')]),
405 query_execute: KeyList(vec![KeyBinding::key(KeyCode::Enter)]),
406 query_cancel: KeyList(vec![KeyBinding::key(KeyCode::Esc)]),
407 query_toggle_case: KeyList(vec![KeyBinding::new(KeyCode::Char('i'), KeyModifiers::ALT)]),
408 query_toggle_regex: KeyList(vec![KeyBinding::new(KeyCode::Char('r'), KeyModifiers::ALT)]),
409 query_next_match: KeyList(vec![KeyBinding::char('n')]),
410 query_prev_match: KeyList(vec![KeyBinding::char('p')]),
411 query_clear: KeyList(vec![KeyBinding::ctrl('u')]),
412 details_scroll_down: KeyList(vec![KeyBinding::key(KeyCode::Down), KeyBinding::char('j')]),
413 details_scroll_up: KeyList(vec![KeyBinding::key(KeyCode::Up), KeyBinding::char('k')]),
414 details_next_tab: KeyList(vec![KeyBinding::key(KeyCode::Right), KeyBinding::char('l')]),
415 details_prev_tab: KeyList(vec![KeyBinding::key(KeyCode::Left), KeyBinding::char('h')]),
416 details_cycle_tab: KeyList(vec![KeyBinding::key(KeyCode::Tab)]),
417 details_prev_field: KeyList(vec![KeyBinding::char('w')]),
418 details_next_field: KeyList(vec![KeyBinding::char('s')]),
419 details_copy: KeyList(vec![KeyBinding::char('c')]),
420 details_view_parent: KeyList(vec![KeyBinding::char('u')]),
421 next_item: KeyList(vec![KeyBinding::key(KeyCode::Down), KeyBinding::char('j')]),
422 prev_item: KeyList(vec![KeyBinding::key(KeyCode::Up), KeyBinding::char('k')]),
423 copy_choose: KeyList(vec![KeyBinding::key(KeyCode::Enter)]),
424 copy_target_cmdline: KeyList(vec![KeyBinding::char('c')]),
425 copy_target_cmdline_full_env: KeyList(vec![KeyBinding::char('o')]),
426 copy_target_cmdline_stdio: KeyList(vec![KeyBinding::char('s')]),
427 copy_target_cmdline_fds: KeyList(vec![KeyBinding::char('f')]),
428 copy_target_env: KeyList(vec![KeyBinding::char('e')]),
429 copy_target_env_diff: KeyList(vec![KeyBinding::char('d')]),
430 copy_target_argv: KeyList(vec![KeyBinding::char('a')]),
431 copy_target_argv_joined: KeyList(vec![KeyBinding::char('w')]),
432 copy_target_filename: KeyList(vec![KeyBinding::char('n')]),
433 copy_target_syscall_result: KeyList(vec![KeyBinding::char('r')]),
434 copy_target_line: KeyList(vec![KeyBinding::char('l')]),
435 go_back: KeyList(vec![KeyBinding::char('q')]),
436 breakpoint_delete: KeyList(vec![
437 KeyBinding::key(KeyCode::Delete),
438 KeyBinding::char('d'),
439 ]),
440 breakpoint_toggle_active: KeyList(vec![KeyBinding::char(' ')]),
441 breakpoint_edit: KeyList(vec![KeyBinding::key(KeyCode::Enter), KeyBinding::char('e')]),
442 breakpoint_new: KeyList(vec![KeyBinding::char('n')]),
443 breakpoint_editor_save: KeyList(vec![KeyBinding::key(KeyCode::Enter)]),
444 breakpoint_editor_cancel: KeyList(vec![KeyBinding::new(
445 KeyCode::Char('c'),
446 KeyModifiers::CONTROL,
447 )]),
448 breakpoint_editor_toggle_stop: KeyList(vec![KeyBinding::new(
449 KeyCode::Char('s'),
450 KeyModifiers::ALT,
451 )]),
452 breakpoint_editor_toggle_active: KeyList(vec![KeyBinding::new(
453 KeyCode::Char('a'),
454 KeyModifiers::ALT,
455 )]),
456 hit_close: KeyList(vec![KeyBinding::char('q')]),
457 hit_detach: KeyList(vec![KeyBinding::char('d')]),
458 hit_resume: KeyList(vec![KeyBinding::char('r')]),
459 hit_edit_default_command: KeyList(vec![KeyBinding::char('e')]),
460 hit_run_default_command: KeyList(vec![KeyBinding::key(KeyCode::Enter)]),
461 hit_run_custom_command: KeyList(vec![KeyBinding::new(KeyCode::Enter, KeyModifiers::ALT)]),
462 hit_editor_save: KeyList(vec![KeyBinding::key(KeyCode::Enter)]),
463 hit_editor_cancel: KeyList(vec![
464 KeyBinding::key(KeyCode::Esc),
465 KeyBinding::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
466 ]),
467 hit_editor_clear: KeyList(vec![KeyBinding::ctrl('u')]),
468 terminal_toggle_scrollback: KeyList(vec![KeyBinding::ctrl('u')]),
469 terminal_scroll_up: KeyList(vec![KeyBinding::key(KeyCode::Up)]),
470 terminal_scroll_down: KeyList(vec![KeyBinding::key(KeyCode::Down)]),
471 terminal_page_up: KeyList(vec![KeyBinding::key(KeyCode::PageUp)]),
472 terminal_page_down: KeyList(vec![KeyBinding::key(KeyCode::PageDown)]),
473 terminal_scroll_top: KeyList(vec![KeyBinding::key(KeyCode::Home)]),
474 terminal_scroll_bottom: KeyList(vec![KeyBinding::key(KeyCode::End)]),
475 }
476 }
477}
478
479impl TuiKeyBindings {
480 pub fn from_config(config: Option<Box<TuiKeyBindingsConfig>>) -> Self {
481 let mut keys = Self::default();
482 if let Some(config) = config {
483 keys.apply_config(config);
484 }
485 keys
486 }
487
488 pub fn apply_config(&mut self, config: Box<TuiKeyBindingsConfig>) {
489 macro_rules! apply {
490 ($($field:ident),+ $(,)?) => {
491 $(
492 if let Some(value) = config.$field {
493 self.$field = value;
494 }
495 )+
496 };
497 }
498
499 apply!(
500 quit,
501 switch_pane,
502 switch_layout,
503 close_popup,
504 help,
505 page_down,
506 page_up,
507 page_left,
508 page_right,
509 scroll_left,
510 scroll_right,
511 scroll_top,
512 scroll_bottom,
513 scroll_start,
514 scroll_end,
515 event_grow_pane,
516 event_shrink_pane,
517 event_send_ctrl_s,
518 event_toggle_follow,
519 event_search,
520 event_toggle_env,
521 event_toggle_cwd,
522 event_view_details,
523 event_go_to_parent,
524 event_backtrace,
525 event_copy,
526 event_breakpoints,
527 event_hits,
528 query_execute,
529 query_cancel,
530 query_toggle_case,
531 query_toggle_regex,
532 query_next_match,
533 query_prev_match,
534 query_clear,
535 details_scroll_down,
536 details_scroll_up,
537 details_next_tab,
538 details_prev_tab,
539 details_cycle_tab,
540 details_prev_field,
541 details_next_field,
542 details_copy,
543 details_view_parent,
544 next_item,
545 prev_item,
546 copy_choose,
547 copy_target_cmdline,
548 copy_target_cmdline_full_env,
549 copy_target_cmdline_stdio,
550 copy_target_cmdline_fds,
551 copy_target_env,
552 copy_target_env_diff,
553 copy_target_argv,
554 copy_target_argv_joined,
555 copy_target_filename,
556 copy_target_syscall_result,
557 copy_target_line,
558 go_back,
559 breakpoint_delete,
560 breakpoint_toggle_active,
561 breakpoint_edit,
562 breakpoint_new,
563 breakpoint_editor_save,
564 breakpoint_editor_cancel,
565 breakpoint_editor_toggle_stop,
566 breakpoint_editor_toggle_active,
567 hit_close,
568 hit_detach,
569 hit_resume,
570 hit_edit_default_command,
571 hit_run_default_command,
572 hit_run_custom_command,
573 hit_editor_save,
574 hit_editor_cancel,
575 hit_editor_clear,
576 terminal_toggle_scrollback,
577 terminal_scroll_up,
578 terminal_scroll_down,
579 terminal_page_up,
580 terminal_page_down,
581 terminal_scroll_top,
582 terminal_scroll_bottom,
583 );
584 }
585}
586
587fn parse_key_binding(input: &str) -> Result<KeyBinding, String> {
588 let raw = input.trim();
589 if raw.is_empty() {
590 return Err("Key binding cannot be empty".into());
591 }
592 let mut modifiers = KeyModifiers::NONE;
593 let mut key_part: Option<&str> = None;
594 for part in raw.split('+') {
595 let part = part.trim();
596 if part.is_empty() {
597 continue;
598 }
599 match part.to_ascii_lowercase().as_str() {
600 "ctrl" | "control" | "ctl" => modifiers |= KeyModifiers::CONTROL,
601 "alt" | "option" => modifiers |= KeyModifiers::ALT,
602 "shift" => modifiers |= KeyModifiers::SHIFT,
603 "super" | "meta" | "cmd" | "command" | "win" => modifiers |= KeyModifiers::SUPER,
604 _ => {
605 if key_part.is_some() {
606 return Err(format!(
607 "Invalid key binding: multiple key codes in \"{input}\""
608 ));
609 }
610 key_part = Some(part);
611 }
612 }
613 }
614 let key_part = key_part.ok_or_else(|| format!("Invalid key binding \"{input}\""))?;
615 if modifiers == KeyModifiers::NONE
616 && key_part.len() == 1
617 && key_part
618 .chars()
619 .next()
620 .is_some_and(|ch| ch.is_ascii_uppercase())
621 {
622 modifiers |= KeyModifiers::SHIFT;
623 }
624 let code = parse_key_code(key_part, modifiers)?;
625
626 Ok(KeyBinding::new(code, modifiers))
627}
628
629fn parse_key_code(input: &str, modifiers: KeyModifiers) -> Result<KeyCode, String> {
630 let key = input.trim();
631 if key.is_empty() {
632 return Err("Key code cannot be empty".into());
633 }
634 let key_lower = key.to_ascii_lowercase();
635 let code = match key_lower.as_str() {
636 "enter" | "return" => KeyCode::Enter,
637 "esc" | "escape" => KeyCode::Esc,
638 "tab" => {
639 if modifiers.contains(KeyModifiers::SHIFT) {
640 KeyCode::BackTab
641 } else {
642 KeyCode::Tab
643 }
644 }
645 "backtab" | "back_tab" | "back-tab" => KeyCode::BackTab,
646 "backspace" | "bs" => KeyCode::Backspace,
647 "delete" | "del" => KeyCode::Delete,
648 "insert" | "ins" => KeyCode::Insert,
649 "home" => KeyCode::Home,
650 "end" => KeyCode::End,
651 "pageup" | "pgup" | "pg_up" | "page_up" => KeyCode::PageUp,
652 "pagedown" | "pgdn" | "pg_down" | "page_down" => KeyCode::PageDown,
653 "up" => KeyCode::Up,
654 "down" => KeyCode::Down,
655 "left" => KeyCode::Left,
656 "right" => KeyCode::Right,
657 "space" | "spacebar" => KeyCode::Char(' '),
658 _ if key.len() == 2 && key_lower.starts_with('f') => {
659 let n = key_lower[1..]
660 .parse::<u8>()
661 .map_err(|_| format!("Invalid function key \"{input}\". Use F1..F12."))?;
662 if (1..=12).contains(&n) {
663 KeyCode::F(n)
664 } else {
665 return Err(format!("Function key out of range in \"{input}\""));
666 }
667 }
668 _ if key.len() == 3 && key_lower.starts_with('f') => {
669 let n = key_lower[1..]
670 .parse::<u8>()
671 .map_err(|_| format!("Invalid function key \"{input}\". Use F1..F12."))?;
672 if (1..=12).contains(&n) {
673 KeyCode::F(n)
674 } else {
675 return Err(format!("Function key out of range in \"{input}\""));
676 }
677 }
678 _ => {
679 let mut chars = key.chars();
680 let ch = chars.next().ok_or_else(|| "Missing key code".to_string())?;
681 if chars.next().is_some() {
682 return Err(format!("Unknown key name \"{input}\""));
683 }
684 let ch = if ch.is_ascii_alphabetic() {
685 if modifiers.contains(KeyModifiers::SHIFT) {
686 ch.to_ascii_uppercase()
687 } else {
688 ch.to_ascii_lowercase()
689 }
690 } else {
691 ch
692 };
693 KeyCode::Char(ch)
694 }
695 };
696 Ok(code)
697}
698
699fn format_key_binding(code: KeyCode, modifiers: KeyModifiers) -> String {
700 let mut parts = Vec::new();
701 if modifiers.contains(KeyModifiers::CONTROL) {
702 parts.push("Ctrl".to_string());
703 }
704 if modifiers.contains(KeyModifiers::ALT) {
705 parts.push("Alt".to_string());
706 }
707 let show_shift = modifiers.contains(KeyModifiers::SHIFT)
708 && !matches!(code, KeyCode::BackTab)
709 && !(matches!(code, KeyCode::Char(ch) if ch.is_ascii_uppercase())
710 && modifiers == KeyModifiers::SHIFT);
711 if show_shift {
712 parts.push("Shift".to_string());
713 }
714 if modifiers.contains(KeyModifiers::SUPER) {
715 parts.push("Super".to_string());
716 }
717 let key = format_key_code(code);
718 if parts.is_empty() {
719 key
720 } else {
721 parts.push(key);
722 parts.join("+")
723 }
724}
725
726fn format_key_code(code: KeyCode) -> String {
727 match code {
728 KeyCode::Enter => "Enter".to_string(),
729 KeyCode::Esc => "Esc".to_string(),
730 KeyCode::Tab => "Tab".to_string(),
731 KeyCode::BackTab => "Shift+Tab".to_string(),
732 KeyCode::Backspace => "Backspace".to_string(),
733 KeyCode::Delete => "Del".to_string(),
734 KeyCode::Insert => "Ins".to_string(),
735 KeyCode::Home => "Home".to_string(),
736 KeyCode::End => "End".to_string(),
737 KeyCode::PageUp => "PgUp".to_string(),
738 KeyCode::PageDown => "PgDn".to_string(),
739 KeyCode::Up => "↑".to_string(),
740 KeyCode::Down => "↓".to_string(),
741 KeyCode::Left => "←".to_string(),
742 KeyCode::Right => "→".to_string(),
743 KeyCode::Char(' ') => "Space".to_string(),
744 KeyCode::Char(ch) => {
745 if ch.is_ascii_alphabetic() {
746 ch.to_ascii_uppercase().to_string()
747 } else {
748 ch.to_string()
749 }
750 }
751 KeyCode::F(n) => format!("F{n}"),
752 _ => format!("{code:?}"),
753 }
754}
755
756#[cfg(test)]
757mod tests {
758 use toml;
759
760 use super::*;
761
762 #[test]
763 fn test_parse_key_binding_ctrl_plus() {
764 let binding: KeyBinding = "Ctrl + S".parse().unwrap();
765 assert_eq!(binding.code, KeyCode::Char('s'));
766 assert_eq!(binding.modifiers, KeyModifiers::CONTROL);
767 }
768
769 #[test]
770 fn test_parse_key_binding_uppercase_without_shift() {
771 let binding: KeyBinding = "Q".parse().unwrap();
772 assert_eq!(binding.code, KeyCode::Char('Q'));
773 assert_eq!(binding.modifiers, KeyModifiers::SHIFT);
774 }
775
776 #[test]
777 fn test_parse_key_binding_shifted_letter() {
778 let binding: KeyBinding = "Shift+q".parse().unwrap();
779 assert_eq!(binding.code, KeyCode::Char('Q'));
780 assert_eq!(binding.modifiers, KeyModifiers::SHIFT);
781 }
782
783 #[test]
784 fn test_key_list_deserialize_single() {
785 #[derive(Deserialize)]
786 struct Wrapper {
787 keys: KeyList,
788 }
789 let list = toml::from_str::<Wrapper>(r#"keys = "Ctrl+F""#)
790 .unwrap()
791 .keys;
792 assert_eq!(list.0.len(), 1);
793 assert_eq!(list.0[0].code, KeyCode::Char('f'));
794 }
795
796 #[test]
797 fn test_key_list_deserialize_array() {
798 #[derive(Deserialize)]
799 struct Wrapper {
800 keys: KeyList,
801 }
802 let list = toml::from_str::<Wrapper>(r#"keys = ["Down", "J"]"#)
803 .unwrap()
804 .keys;
805 assert_eq!(list.0.len(), 2);
806 }
807
808 #[test]
809 fn test_configure_whitespace_joined_argv_copy_target() {
810 let config: TuiKeyBindingsConfig = toml::from_str(r#"copy_target_argv_joined = "x""#).unwrap();
811 let keys = TuiKeyBindings::from_config(Some(Box::new(config)));
812
813 assert_eq!(keys.copy_target_argv_joined.0, vec![KeyBinding::char('x')]);
814 }
815}