supercode_frontend_tui/input/
key_hint.rs1use crossterm::event::KeyCode;
24use crossterm::event::KeyEvent;
25use crossterm::event::KeyEventKind;
26use crossterm::event::KeyModifiers;
27use ratatui::style::Style;
28use ratatui::text::Span;
29
30#[cfg(target_os = "macos")]
31const ALT_PREFIX: &str = "⌥ + ";
32#[cfg(not(target_os = "macos"))]
33const ALT_PREFIX: &str = "alt + ";
34const CTRL_PREFIX: &str = "ctrl + ";
35const SHIFT_PREFIX: &str = "shift + ";
36
37#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
47pub struct KeyBinding {
48 key: KeyCode,
49 modifiers: KeyModifiers,
50}
51
52impl KeyBinding {
53 pub const fn new(key: KeyCode, modifiers: KeyModifiers) -> Self {
54 Self { key, modifiers }
55 }
56
57 pub fn from_event(event: KeyEvent) -> Self {
58 let (key, modifiers) = normalize_key_parts(event.code, event.modifiers);
59 Self { key, modifiers }
60 }
61
62 pub fn is_press(&self, event: KeyEvent) -> bool {
63 normalize_key_parts(self.key, self.modifiers)
64 == normalize_key_parts(event.code, event.modifiers)
65 && (event.kind == KeyEventKind::Press || event.kind == KeyEventKind::Repeat)
66 }
67
68 pub const fn parts(&self) -> (KeyCode, KeyModifiers) {
69 (self.key, self.modifiers)
70 }
71
72 pub fn display_label(&self) -> String {
73 let modifiers = modifiers_to_string(self.modifiers);
74 let key = match self.key {
75 KeyCode::Enter => "enter".to_string(),
76 KeyCode::Char(' ') => "space".to_string(),
77 KeyCode::Up => "↑".to_string(),
78 KeyCode::Down => "↓".to_string(),
79 KeyCode::Left => "←".to_string(),
80 KeyCode::Right => "→".to_string(),
81 KeyCode::PageUp => "pgup".to_string(),
82 KeyCode::PageDown => "pgdn".to_string(),
83 _ => self.key.to_string().to_ascii_lowercase(),
84 };
85 format!("{modifiers}{key}")
86 }
87}
88
89pub fn normalize_key_parts(key: KeyCode, mut modifiers: KeyModifiers) -> (KeyCode, KeyModifiers) {
90 let KeyCode::Char(ch) = key else {
91 return (key, modifiers);
92 };
93 if modifiers.is_empty() {
94 if let Some(ctrl_char) = c0_control_char_to_ctrl_char(ch) {
95 return (KeyCode::Char(ctrl_char), KeyModifiers::CONTROL | modifiers);
96 }
97 }
98 if ch.is_ascii_uppercase() {
99 modifiers.insert(KeyModifiers::SHIFT);
100 return (KeyCode::Char(ch.to_ascii_lowercase()), modifiers);
101 }
102 (key, modifiers)
103}
104
105fn c0_control_char_to_ctrl_char(ch: char) -> Option<char> {
106 let code = u32::from(ch);
107 match code {
108 0x00 => Some(' '),
109 0x01..=0x1a => char::from_u32(code - 0x01 + u32::from('a')),
110 0x1c..=0x1f => char::from_u32(code - 0x1c + u32::from('4')),
111 _ => None,
112 }
113}
114
115pub trait KeyBindingListExt {
121 fn is_pressed(&self, event: KeyEvent) -> bool;
123}
124
125impl KeyBindingListExt for [KeyBinding] {
126 fn is_pressed(&self, event: KeyEvent) -> bool {
127 self.iter().any(|binding| binding.is_press(event))
128 }
129}
130
131pub fn is_plain_text_key_event(event: KeyEvent) -> bool {
140 matches!(
141 event,
142 KeyEvent {
143 code: KeyCode::Char(ch),
144 modifiers,
145 ..
146 } if !ch.is_ascii_control()
147 && !modifiers.contains(KeyModifiers::CONTROL)
148 && !modifiers.contains(KeyModifiers::ALT)
149 )
150}
151
152pub const fn plain(key: KeyCode) -> KeyBinding {
153 KeyBinding::new(key, KeyModifiers::NONE)
154}
155
156pub const fn alt(key: KeyCode) -> KeyBinding {
157 KeyBinding::new(key, KeyModifiers::ALT)
158}
159
160pub const fn shift(key: KeyCode) -> KeyBinding {
161 KeyBinding::new(key, KeyModifiers::SHIFT)
162}
163
164pub const fn ctrl(key: KeyCode) -> KeyBinding {
165 KeyBinding::new(key, KeyModifiers::CONTROL)
166}
167
168pub const fn ctrl_alt(key: KeyCode) -> KeyBinding {
169 KeyBinding::new(key, KeyModifiers::CONTROL.union(KeyModifiers::ALT))
170}
171
172fn modifiers_to_string(modifiers: KeyModifiers) -> String {
173 let mut result = String::new();
174 if modifiers.contains(KeyModifiers::CONTROL) {
175 result.push_str(CTRL_PREFIX);
176 }
177 if modifiers.contains(KeyModifiers::SHIFT) {
178 result.push_str(SHIFT_PREFIX);
179 }
180 if modifiers.contains(KeyModifiers::ALT) {
181 result.push_str(ALT_PREFIX);
182 }
183 result
184}
185
186impl From<KeyBinding> for Span<'static> {
187 fn from(binding: KeyBinding) -> Self {
188 (&binding).into()
189 }
190}
191impl From<&KeyBinding> for Span<'static> {
192 fn from(binding: &KeyBinding) -> Self {
193 Span::styled(binding.display_label(), key_hint_style())
194 }
195}
196
197fn key_hint_style() -> Style {
198 Style::default().dim()
199}
200
201pub fn has_ctrl_or_alt(mods: KeyModifiers) -> bool {
202 (mods.contains(KeyModifiers::CONTROL) || mods.contains(KeyModifiers::ALT)) && !is_altgr(mods)
203}
204
205#[cfg(windows)]
206#[inline]
207pub fn is_altgr(mods: KeyModifiers) -> bool {
208 mods.contains(KeyModifiers::ALT) && mods.contains(KeyModifiers::CONTROL)
209}
210
211#[cfg(not(windows))]
212#[inline]
213pub fn is_altgr(_mods: KeyModifiers) -> bool {
214 false
215}