| Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago | 1 | //! Key names, and the two word motions the entry needs. |
| 2 | //! |
| 3 | //! A key crosses this ABI as a string — `"ctrl+u"`, `"page-down"`, `"a"` — |
| 4 | //! because that is the only shape jolt can match on, and because a name is |
| 5 | //! something a test can type. The same names glimmer-tui's `k/match?` takes. |
| 6 | |
| 7 | /// Where the word before `at` starts. |
| 8 | pub fn word_left(text: &[char], at: usize) -> usize { |
| 9 | let mut i = at.min(text.len()); |
| 10 | while i > 0 && text[i - 1].is_whitespace() { |
| 11 | i -= 1; |
| 12 | } |
| 13 | while i > 0 && !text[i - 1].is_whitespace() { |
| 14 | i -= 1; |
| 15 | } |
| 16 | i |
| 17 | } |
| 18 | |
| 19 | /// Where the word after `at` ends. |
| 20 | pub fn word_right(text: &[char], at: usize) -> usize { |
| 21 | let mut i = at.min(text.len()); |
| 22 | while i < text.len() && text[i].is_whitespace() { |
| 23 | i += 1; |
| 24 | } |
| 25 | while i < text.len() && !text[i].is_whitespace() { |
| 26 | i += 1; |
| 27 | } |
| 28 | i |
| 29 | } |
| 30 | |
| 31 | #[cfg(feature = "terminal")] |
| 32 | mod terminal { |
| 33 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; |
| 34 | |
| 35 | /// The name for one key event, or `None` for a key with no name — a bare |
| 36 | /// modifier press, or a media key nothing here would do anything with. |
| 37 | pub fn name(event: KeyEvent) -> Option<String> { |
| 38 | let mods = event.modifiers; |
| 39 | let base = match event.code { |
| 40 | KeyCode::Char(' ') => "space".to_owned(), |
| 41 | KeyCode::Char(c) => { |
| 42 | // Shift is already in the character the terminal sent; saying |
| 43 | // it twice would make `A` into `shift+a`, which nothing wants |
| 44 | // to match on. |
| 45 | let mut name = c.to_string(); |
| 46 | if mods.contains(KeyModifiers::CONTROL) || mods.contains(KeyModifiers::ALT) { |
| 47 | name = c.to_lowercase().to_string(); |
| 48 | } |
| 49 | name |
| 50 | } |
| 51 | KeyCode::Enter => "enter".into(), |
| 52 | KeyCode::Tab => "tab".into(), |
| 53 | KeyCode::BackTab => return Some("shift+tab".into()), |
| 54 | KeyCode::Backspace => "backspace".into(), |
| 55 | KeyCode::Delete => "delete".into(), |
| 56 | KeyCode::Insert => "insert".into(), |
| 57 | KeyCode::Esc => "esc".into(), |
| 58 | KeyCode::Up => "up".into(), |
| 59 | KeyCode::Down => "down".into(), |
| 60 | KeyCode::Left => "left".into(), |
| 61 | KeyCode::Right => "right".into(), |
| 62 | KeyCode::Home => "home".into(), |
| 63 | KeyCode::End => "end".into(), |
| 64 | KeyCode::PageUp => "page-up".into(), |
| 65 | KeyCode::PageDown => "page-down".into(), |
| 66 | KeyCode::F(n) => format!("f{n}"), |
| 67 | _ => return None, |
| 68 | }; |
| 69 | let mut out = String::new(); |
| 70 | if mods.contains(KeyModifiers::CONTROL) { |
| 71 | out.push_str("ctrl+"); |
| 72 | } |
| 73 | if mods.contains(KeyModifiers::ALT) { |
| 74 | out.push_str("alt+"); |
| 75 | } |
| 76 | if mods.contains(KeyModifiers::SHIFT) && !matches!(event.code, KeyCode::Char(_)) { |
| 77 | out.push_str("shift+"); |
| 78 | } |
| 79 | out.push_str(&base); |
| 80 | Some(out) |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | #[cfg(feature = "terminal")] |
| 85 | pub use terminal::name; |
| 86 | |
| 87 | #[cfg(test)] |
| 88 | mod tests { |
| 89 | use super::*; |
| 90 | |
| 91 | fn chars(text: &str) -> Vec<char> { |
| 92 | text.chars().collect() |
| 93 | } |
| 94 | |
| 95 | #[test] |
| 96 | fn word_motions_step_over_the_gap_and_then_the_word() { |
| 97 | let text = chars("one two three"); |
| 98 | assert_eq!(word_left(&text, 13), 8); |
| 99 | assert_eq!(word_left(&text, 8), 4); |
| 100 | assert_eq!(word_left(&text, 0), 0); |
| 101 | assert_eq!(word_right(&text, 0), 3); |
| 102 | assert_eq!(word_right(&text, 3), 7); |
| 103 | assert_eq!(word_right(&text, 13), 13); |
| 104 | } |
| 105 | } |