Skip to main content

tracexec_core/cli/
keys.rs

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}