refactor: update key handler

This commit is contained in:
Kristofers Solo 2025-07-11 16:32:57 +03:00
parent ae0fc2bbf5
commit d336160b5b
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
4 changed files with 94 additions and 55 deletions

View File

@ -15,3 +15,5 @@ jobs:
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
run: cargo test --verbose run: cargo test --verbose
- name: Run clippy
run: cargo clippy --verbose

View File

@ -1,6 +1,7 @@
use crate::app::{App, action::Action}; use crate::app::{App, action::Action};
use color_eyre::Result; use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use thiserror::Error;
use tracing::{debug, info}; use tracing::{debug, info};
#[tracing::instrument(name = "Handling input", skip(app))] #[tracing::instrument(name = "Handling input", skip(app))]
@ -102,56 +103,91 @@ pub async fn update(app: &mut App<'_>, action: Action) -> Result<()> {
/// Check if a [`KeyEvent`] matches a configured keybind string /// Check if a [`KeyEvent`] matches a configured keybind string
fn matches_keybind(event: &KeyEvent, config_key: &str) -> bool { fn matches_keybind(event: &KeyEvent, config_key: &str) -> bool {
let (modifiers, key_code) = parse_keybind(config_key); parse_keybind(config_key)
let Some(key_code) = key_code else { .map(|parsed_ev| parsed_ev == *event)
return false; .unwrap_or(false)
};
event.code == key_code && event.modifiers == modifiers
} }
fn parse_keybind(key_str: &str) -> (KeyModifiers, Option<KeyCode>) { #[derive(Debug, Error)]
pub enum ParseKeybingError {
/// No “main” key was found (e.g. the user only wrote modifiers).
#[error("no main key was found in input")]
NoKeyCode,
/// An unrecognized token was encountered.
#[error("unrecognized token `{0}`")]
UnknownPart(String),
}
fn parse_keybind(key_str: &str) -> Result<KeyEvent, ParseKeybingError> {
let mut modifiers = KeyModifiers::NONE; let mut modifiers = KeyModifiers::NONE;
let mut key_code = None; let mut key_code = None;
for part in key_str.split('+') { for raw in key_str.split('+') {
match part.trim().to_lowercase().as_str() { let part = raw.trim();
"ctrl" => modifiers.insert(KeyModifiers::CONTROL), if part.is_empty() {
"alt" => modifiers.insert(KeyModifiers::ALT), continue;
"shift" => modifiers.insert(KeyModifiers::SHIFT), }
key @ ("esc" | "enter" | "left" | "right" | "up" | "down" | "tab" | "backspace" let low = part.to_lowercase();
| "delete" | "home" | "end" | "pageup" | "pagedown" | "null" | "insert") => { match low.as_str() {
key_code = Some(match key { // modifiers
"esc" => KeyCode::Esc, "ctrl" | "control" => modifiers |= KeyModifiers::CONTROL,
"enter" => KeyCode::Enter, "shift" => modifiers |= KeyModifiers::SHIFT,
"left" => KeyCode::Left, "alt" | "option" => modifiers |= KeyModifiers::ALT,
"right" => KeyCode::Right,
"up" => KeyCode::Up, // named keys
"down" => KeyCode::Down, "enter" => key_code = Some(KeyCode::Enter),
"tab" => KeyCode::Tab, "tab" => key_code = Some(KeyCode::Tab),
"backspace" => KeyCode::Backspace, "backspace" => key_code = Some(KeyCode::Backspace),
"delete" => KeyCode::Delete, "delete" => key_code = Some(KeyCode::Delete),
"home" => KeyCode::Home, "insert" => key_code = Some(KeyCode::Insert),
"end" => KeyCode::End, "home" => key_code = Some(KeyCode::Home),
"pageup" => KeyCode::PageUp, "end" => key_code = Some(KeyCode::End),
"pagedown" => KeyCode::PageDown, "pageup" | "page_up" => key_code = Some(KeyCode::PageUp),
"null" => KeyCode::Null, "pagedown" | "page_down" => key_code = Some(KeyCode::PageDown),
"insert" => KeyCode::Insert, "up" => key_code = Some(KeyCode::Up),
_ => unreachable!(), "down" => key_code = Some(KeyCode::Down),
}); "left" => key_code = Some(KeyCode::Left),
} "right" => key_code = Some(KeyCode::Right),
f_key if f_key.starts_with('f') => { "esc" | "escape" => key_code = Some(KeyCode::Esc),
if let Ok(num) = f_key[1..].parse::<u8>() { "space" => key_code = Some(KeyCode::Char(' ')),
key_code = Some(KeyCode::F(num)); "null" => key_code = Some(KeyCode::Null),
// symbol names
"plus" => key_code = Some(KeyCode::Char('+')),
"minus" => key_code = Some(KeyCode::Char('-')),
"equals" | "equal" => key_code = Some(KeyCode::Char('=')),
"comma" => key_code = Some(KeyCode::Char(',')),
"dot" | "period" => key_code = Some(KeyCode::Char('.')),
"semicolon" => key_code = Some(KeyCode::Char(';')),
"slash" | "forward_slash" => key_code = Some(KeyCode::Char('/')),
"backslash" => key_code = Some(KeyCode::Char('\\')),
"tilde" => key_code = Some(KeyCode::Char('~')),
"grave" | "backtick" => key_code = Some(KeyCode::Char('`')),
"quote" => key_code = Some(KeyCode::Char('"')),
"apostrophe" => key_code = Some(KeyCode::Char('\'')),
// function keys F1...F<N>
f if f.starts_with('f') && f.len() > 1 => {
let num_str = &f[1..];
match num_str.parse::<u8>() {
Ok(n) => key_code = Some(KeyCode::F(n)),
Err(_) => return Err(ParseKeybingError::UnknownPart(part.to_owned())),
} }
} }
single_char if single_char.len() == 1 => {
if let Some(c) = single_char.chars().next() { // singlecharacter fallback
key_code = Some(KeyCode::Char(c)); _ if low.len() == 1 => {
if let Some(ch) = low.trim().chars().next() {
key_code = Some(KeyCode::Char(ch));
} }
} }
_ => return (modifiers, None),
// unknown token
other => return Err(ParseKeybingError::UnknownPart(other.to_owned())),
} }
} }
(modifiers, key_code)
key_code
.map(|kc| KeyEvent::new(kc, modifiers))
.ok_or(ParseKeybingError::NoKeyCode)
} }

View File

@ -0,0 +1,7 @@
pub mod app;
pub mod config;
pub mod event;
pub mod handler;
pub mod telemetry;
pub mod tui;
pub mod ui;

View File

@ -1,21 +1,15 @@
pub mod app;
pub mod config;
pub mod event;
pub mod handler;
pub mod telemetry;
pub mod tui;
pub mod ui;
use app::App;
use color_eyre::Result; use color_eyre::Result;
use config::Config;
use event::{Event, EventHandler};
use handler::{get_action, update};
use ratatui::{Terminal, backend::CrosstermBackend}; use ratatui::{Terminal, backend::CrosstermBackend};
use std::io; use std::io;
use telemetry::setup_logger;
use tracing::{debug, trace}; use tracing::{debug, trace};
use tui::Tui; use traxor::{
app::App,
config::Config,
event::{Event, EventHandler},
handler::{get_action, update},
telemetry::setup_logger,
tui::Tui,
};
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {