feat: wire evil-keys dispatcher into event loop and action handlers

Replace 3 hardcoded keys with full trie-based dispatch. Dispatcher created
in main.rs, handle_action() implements all 18 AppAction variants including
GotoFirst/GotoLast per focus pane, NextTab/PrevTab cycling, and HalfPage scroll.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Alexander
2026-05-10 10:59:43 +02:00
parent f859c40eb1
commit 1c1dadf5cd
5 changed files with 277 additions and 22 deletions
Generated
+147 -5
View File
@@ -151,6 +151,21 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "2.11.1"
@@ -329,6 +344,15 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "evil-keys"
version = "0.1.0"
dependencies = [
"crossterm",
"indexmap 2.14.0",
"proptest",
]
[[package]]
name = "eyre"
version = "0.6.12"
@@ -413,6 +437,18 @@ dependencies = [
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi 5.3.0",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.2"
@@ -421,7 +457,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"r-efi 6.0.0",
"wasip2",
"wasip3",
]
@@ -780,6 +816,15 @@ dependencies = [
"libc",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.37.3"
@@ -900,6 +945,25 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proptest"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
dependencies = [
"bit-set",
"bit-vec",
"bitflags",
"num-traits",
"rand 0.9.4",
"rand_chacha 0.9.0",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
"tempfile",
"unarray",
]
[[package]]
name = "prost"
version = "0.13.5"
@@ -952,6 +1016,12 @@ dependencies = [
"prost",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.45"
@@ -961,6 +1031,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
@@ -974,8 +1050,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
]
[[package]]
@@ -985,7 +1071,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
]
[[package]]
@@ -997,6 +1093,24 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_xorshift"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
dependencies = [
"rand_core 0.9.5",
]
[[package]]
name = "ratatui"
version = "0.29.0"
@@ -1094,6 +1208,18 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rusty-fork"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
dependencies = [
"fnv",
"quick-error",
"tempfile",
"wait-timeout",
]
[[package]]
name = "ryu"
version = "1.0.23"
@@ -1424,7 +1550,7 @@ dependencies = [
"indexmap 1.9.3",
"pin-project",
"pin-project-lite",
"rand",
"rand 0.8.6",
"slab",
"tokio",
"tokio-util",
@@ -1524,6 +1650,7 @@ version = "0.1.0"
dependencies = [
"color-eyre",
"crossterm",
"evil-keys",
"insta",
"nix",
"prost",
@@ -1535,6 +1662,12 @@ dependencies = [
"tonic-build",
]
[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -1588,6 +1721,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]]
name = "want"
version = "0.3.1"
+4
View File
@@ -1,3 +1,6 @@
[workspace]
members = ["crates/evil-keys"]
[package]
name = "ui-agregator"
version = "0.1.0"
@@ -10,6 +13,7 @@ color-eyre = "0.6"
nix = { version = "0.29", features = ["fs"] }
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
evil-keys = { path = "crates/evil-keys" }
tonic = "0.12"
prost = "0.13"
+98 -8
View File
@@ -2,16 +2,100 @@
use crossterm::event::MouseButton;
use ratatui::widgets::ListState;
use tokio::sync::mpsc::Sender;
use crate::application::app_state::App;
use crate::data::{Artist, Track};
use crate::domain::conversions::{convert_album, convert_artist, convert_track};
use crate::domain::navigation::Tab;
use crate::grpc::GrpcResponse;
use crate::domain::navigation::{ModalKind, Tab};
use crate::grpc::{GrpcRequest, GrpcResponse};
use crate::input::AppAction;
use crate::ui::library::{LibraryFocus, LibraryState};
use crate::ui::notifications::NotifKind;
impl App {
pub fn handle_action(&mut self, action: AppAction, count: usize, tx: &Sender<GrpcRequest>) {
match action {
AppAction::MoveDown => {
for _ in 0..count {
self.library.move_down();
}
}
AppAction::MoveUp => {
for _ in 0..count {
self.library.move_up();
}
}
AppAction::FocusLeft => self.library.focus_left(),
AppAction::FocusRight => self.library.focus_right(),
AppAction::CycleFocus => self.library.cycle_focus(),
AppAction::GotoFirst => match self.library.focus {
LibraryFocus::Artists => self.library.artist_state.select(Some(0)),
LibraryFocus::Albums => self.library.album_state.select(Some(0)),
LibraryFocus::Tracks => self.library.track_state.select(Some(0)),
},
AppAction::GotoLast => match self.library.focus {
LibraryFocus::Artists => {
let last = self.library.artists.len().saturating_sub(1);
self.library.artist_state.select(Some(last));
}
LibraryFocus::Albums => {
if let Some(artist) = self.library.selected_artist() {
let last = artist.albums.len().saturating_sub(1);
self.library.album_state.select(Some(last));
}
}
LibraryFocus::Tracks => {
let last = self.library.tracks.len().saturating_sub(1);
self.library.track_state.select(Some(last));
}
},
AppAction::HalfPageDown => {
for _ in 0..15 {
self.library.move_down();
}
}
AppAction::HalfPageUp => {
for _ in 0..15 {
self.library.move_up();
}
}
AppAction::NextTab => {
let idx = self.tab.index();
let next = (idx + 1) % Tab::ALL.len();
self.tab = Tab::ALL[next];
}
AppAction::PrevTab => {
let idx = self.tab.index();
let prev = if idx == 0 {
Tab::ALL.len() - 1
} else {
idx - 1
};
self.tab = Tab::ALL[prev];
}
AppAction::GotoTab(i) => {
if let Some(tab) = Tab::ALL.get(i) {
self.tab = *tab;
}
}
AppAction::Quit => self.running = false,
AppAction::Refresh => {
self.library.clear_cache();
let _ = tx.try_send(GrpcRequest::GetArtists);
}
AppAction::ShowHelp => self.modal = Some(ModalKind::Help),
AppAction::ToggleNotifications => {
self.notifications_open = !self.notifications_open;
if self.notifications_open {
self.notifications_scroll = 0;
self.notifications_expanded = None;
}
}
AppAction::Escape => self.handle_escape(),
}
}
pub fn handle_escape(&mut self) {
if self.notifications_open {
self.notifications_open = false;
@@ -138,8 +222,10 @@ impl App {
let albums = self.library.albums_inner_area;
let tracks = self.library.tracks_inner_area;
if x >= artists.x && x < artists.x + artists.width
&& y >= artists.y && y < artists.y + artists.height
if x >= artists.x
&& x < artists.x + artists.width
&& y >= artists.y
&& y < artists.y + artists.height
{
let row = (y - artists.y) as usize;
if row < self.library.artists.len() {
@@ -148,8 +234,10 @@ impl App {
self.library.track_state.select(Some(0));
self.library.focus = LibraryFocus::Artists;
}
} else if x >= albums.x && x < albums.x + albums.width
&& y >= albums.y && y < albums.y + albums.height
} else if x >= albums.x
&& x < albums.x + albums.width
&& y >= albums.y
&& y < albums.y + albums.height
{
let row = (y - albums.y) as usize;
if let Some(artist) = self.library.selected_artist()
@@ -159,8 +247,10 @@ impl App {
self.library.track_state.select(Some(0));
self.library.focus = LibraryFocus::Albums;
}
} else if x >= tracks.x && x < tracks.x + tracks.width
&& y >= tracks.y && y < tracks.y + tracks.height
} else if x >= tracks.x
&& x < tracks.x + tracks.width
&& y >= tracks.y
&& y < tracks.y + tracks.height
{
let row = (y - tracks.y) as usize;
if row < self.library.tracks.len() {
+1
View File
@@ -5,6 +5,7 @@ pub mod data;
pub mod domain;
pub mod grpc;
pub mod infrastructure;
pub mod input;
pub mod presentation;
pub mod proto;
pub mod theme;
+27 -9
View File
@@ -11,11 +11,13 @@ use crossterm::{
},
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use evil_keys::{DispatchResult, Dispatcher};
use ratatui::prelude::*;
use ui_agregator::app::App;
use ui_agregator::config::Config;
use ui_agregator::grpc::{GrpcRequest, spawn_grpc_worker};
use ui_agregator::input::{build_insert_keymap, build_normal_keymap};
const TICK_RATE: Duration = Duration::from_millis(100);
@@ -59,6 +61,16 @@ async fn run() -> Result<()> {
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut app = App::new();
let mut dispatcher = Dispatcher::new();
dispatcher
.add_mode("normal", build_normal_keymap())
.unwrap();
dispatcher
.add_mode("insert", build_insert_keymap())
.unwrap();
dispatcher.set_active("normal").unwrap();
dispatcher.set_timeout(Duration::from_millis(1000));
let (grpc_tx, mut grpc_rx) = spawn_grpc_worker(config.grpc_addr());
if grpc_tx.try_send(GrpcRequest::GetArtists).is_err() {
@@ -78,18 +90,23 @@ async fn run() -> Result<()> {
if event::poll(TICK_RATE)? {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => {
if key.modifiers.contains(KeyModifiers::CONTROL)
Event::Key(key) => {
if key.kind == KeyEventKind::Press
&& key.modifiers.contains(KeyModifiers::CONTROL)
&& key.code == KeyCode::Char('c')
{
app.running = false;
} else if key.code == KeyCode::Esc {
app.handle_escape();
} else if key.code == KeyCode::Char('r')
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
app.library.clear_cache();
let _ = grpc_tx.try_send(GrpcRequest::GetArtists);
} else {
match dispatcher.dispatch(key) {
DispatchResult::Matched { action, count } => {
app.handle_action(action, count, &grpc_tx);
}
DispatchResult::Pending
| DispatchResult::Cancelled
| DispatchResult::CountAccumulated
| DispatchResult::Ignored
| DispatchResult::NotFound => {}
}
}
}
Event::Mouse(mouse) => match mouse.kind {
@@ -107,6 +124,7 @@ async fn run() -> Result<()> {
_ => {}
}
} else {
dispatcher.check_timeout();
app.handle_tick();
}
}