//! Key names, and the two word motions the entry needs. //! //! A key crosses this ABI as a string — `"ctrl+u"`, `"page-down"`, `"a"` — //! because that is the only shape jolt can match on, and because a name is //! something a test can type. The same names glimmer-tui's `k/match?` takes. /// Where the word before `at` starts. pub fn word_left(text: &[char], at: usize) -> usize { let mut i = at.min(text.len()); while i > 0 && text[i - 1].is_whitespace() { i -= 1; } while i > 0 && !text[i - 1].is_whitespace() { i -= 1; } i } /// Where the word after `at` ends. pub fn word_right(text: &[char], at: usize) -> usize { let mut i = at.min(text.len()); while i < text.len() && text[i].is_whitespace() { i += 1; } while i < text.len() && !text[i].is_whitespace() { i += 1; } i } #[cfg(feature = "terminal")] mod terminal { use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; /// The name for one key event, or `None` for a key with no name — a bare /// modifier press, or a media key nothing here would do anything with. pub fn name(event: KeyEvent) -> Option { let mods = event.modifiers; let base = match event.code { KeyCode::Char(' ') => "space".to_owned(), KeyCode::Char(c) => { // Shift is already in the character the terminal sent; saying // it twice would make `A` into `shift+a`, which nothing wants // to match on. let mut name = c.to_string(); if mods.contains(KeyModifiers::CONTROL) || mods.contains(KeyModifiers::ALT) { name = c.to_lowercase().to_string(); } name } KeyCode::Enter => "enter".into(), KeyCode::Tab => "tab".into(), KeyCode::BackTab => return Some("shift+tab".into()), KeyCode::Backspace => "backspace".into(), KeyCode::Delete => "delete".into(), KeyCode::Insert => "insert".into(), KeyCode::Esc => "esc".into(), KeyCode::Up => "up".into(), KeyCode::Down => "down".into(), KeyCode::Left => "left".into(), KeyCode::Right => "right".into(), KeyCode::Home => "home".into(), KeyCode::End => "end".into(), KeyCode::PageUp => "page-up".into(), KeyCode::PageDown => "page-down".into(), KeyCode::F(n) => format!("f{n}"), _ => return None, }; let mut out = String::new(); if mods.contains(KeyModifiers::CONTROL) { out.push_str("ctrl+"); } if mods.contains(KeyModifiers::ALT) { out.push_str("alt+"); } if mods.contains(KeyModifiers::SHIFT) && !matches!(event.code, KeyCode::Char(_)) { out.push_str("shift+"); } out.push_str(&base); Some(out) } } #[cfg(feature = "terminal")] pub use terminal::name; #[cfg(test)] mod tests { use super::*; fn chars(text: &str) -> Vec { text.chars().collect() } #[test] fn word_motions_step_over_the_gap_and_then_the_word() { let text = chars("one two three"); assert_eq!(word_left(&text, 13), 8); assert_eq!(word_left(&text, 8), 4); assert_eq!(word_left(&text, 0), 0); assert_eq!(word_right(&text, 0), 3); assert_eq!(word_right(&text, 3), 7); assert_eq!(word_right(&text, 13), 13); } }