traxor/src/handler.rs
2025-07-14 14:08:00 +03:00

197 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::app::{App, action::Action};
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use thiserror::Error;
use tracing::{debug, info};
#[tracing::instrument(name = "Handling input", skip(app))]
async fn handle_input(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option<Action>> {
match key_event.code {
KeyCode::Enter => Ok(Some(Action::Submit)),
KeyCode::Tab => {
app.complete_input().await?;
Ok(None)
}
KeyCode::Char(c) => {
app.input.push(c);
app.cursor_position = app.input.len();
Ok(None)
}
KeyCode::Backspace => {
app.input.pop();
app.cursor_position = app.input.len();
Ok(None)
}
KeyCode::Esc => Ok(Some(Action::Cancel)),
_ => Ok(None),
}
}
/// Handles the key events of [`App`].
///
/// # Errors
///
/// TODO: add error types
#[tracing::instrument(name = "Getting action", skip(app))]
pub async fn get_action(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option<Action>> {
if app.input_mode {
return handle_input(key_event, app).await;
}
debug!("handling key event: {:?}", key_event);
let keybinds = &app.config.keybinds;
let actions = [
(Action::Quit, &keybinds.quit),
(Action::NextTab, &keybinds.next_tab),
(Action::PrevTab, &keybinds.prev_tab),
(Action::NextTorrent, &keybinds.next_torrent),
(Action::PrevTorrent, &keybinds.prev_torrent),
(Action::SwitchTab(0), &keybinds.switch_tab_1),
(Action::SwitchTab(1), &keybinds.switch_tab_2),
(Action::SwitchTab(2), &keybinds.switch_tab_3),
(Action::ToggleTorrent, &keybinds.toggle_torrent),
(Action::ToggleAll, &keybinds.toggle_all),
(Action::Delete(false), &keybinds.delete),
(Action::Delete(true), &keybinds.delete_force),
(Action::Select, &keybinds.select),
(Action::ToggleHelp, &keybinds.toggle_help),
(Action::Move, &keybinds.move_torrent),
];
for (action, keybind) in actions {
if matches_keybind(&key_event, keybind) {
return Ok(Some(action));
}
}
Ok(None)
}
/// Handles the updates of [`App`].
///
/// # Errors
///
/// TODO: add error types
#[tracing::instrument(name = "Update", skip(app))]
pub async fn update(app: &mut App<'_>, action: Action) -> Result<()> {
info!("updating app with action: {}", action);
match action {
Action::Quit => app.quit(),
Action::NextTab => app.next_tab(),
Action::PrevTab => app.prev_tab(),
Action::NextTorrent => app.next(),
Action::PrevTorrent => app.previous(),
Action::SwitchTab(x) => app.switch_tab(x as usize),
Action::ToggleHelp => app.toggle_help(),
Action::ToggleTorrent => app.toggle_torrents().await?,
Action::ToggleAll => app.torrents.toggle_all().await?,
Action::PauseAll => app.torrents.stop_all().await?,
Action::StartAll => app.torrents.start_all().await?,
Action::Move => app.prepare_move_action(),
Action::Delete(x) => app.delete(x).await?,
Action::Rename => unimplemented!(),
Action::Select => app.select(),
Action::Submit => app.move_torrent().await?,
Action::Cancel => {
app.input.clear();
app.input_mode = false;
}
}
Ok(())
}
/// Check if a [`KeyEvent`] matches a configured keybind string
fn matches_keybind(event: &KeyEvent, config_key: &str) -> bool {
parse_keybind(config_key)
.map(|parsed_ev| parsed_ev == *event)
.unwrap_or(false)
}
#[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 key_code = None;
for raw in key_str.split('+') {
let part = raw.trim();
if part.is_empty() {
if raw.contains(' ') {
key_code = Some(KeyCode::Char(' '));
}
continue;
}
let low = part.to_lowercase();
match low.as_str() {
// modifiers
"ctrl" | "control" => modifiers |= KeyModifiers::CONTROL,
"shift" => modifiers |= KeyModifiers::SHIFT,
"alt" | "option" => modifiers |= KeyModifiers::ALT,
// named keys
"enter" => key_code = Some(KeyCode::Enter),
"tab" => key_code = Some(KeyCode::Tab),
"backspace" => key_code = Some(KeyCode::Backspace),
"delete" => key_code = Some(KeyCode::Delete),
"insert" => key_code = Some(KeyCode::Insert),
"home" => key_code = Some(KeyCode::Home),
"end" => key_code = Some(KeyCode::End),
"pageup" | "page_up" => key_code = Some(KeyCode::PageUp),
"pagedown" | "page_down" => key_code = Some(KeyCode::PageDown),
"up" => key_code = Some(KeyCode::Up),
"down" => key_code = Some(KeyCode::Down),
"left" => key_code = Some(KeyCode::Left),
"right" => key_code = Some(KeyCode::Right),
"esc" | "escape" => key_code = Some(KeyCode::Esc),
"space" => key_code = Some(KeyCode::Char(' ')),
"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())),
}
}
// singlecharacter fallback
_ if part.len() == 1 => {
if let Some(ch) = part.chars().next() {
key_code = Some(KeyCode::Char(ch));
}
}
// unknown token
other => return Err(ParseKeybingError::UnknownPart(other.to_owned())),
}
}
key_code
.map(|kc| KeyEvent::new(kc, modifiers))
.ok_or(ParseKeybingError::NoKeyCode)
}