diff --git a/Cargo.lock b/Cargo.lock index 4289692..c49edac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 5046b86..ece7aaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/application/handlers.rs b/src/application/handlers.rs index dc79cc4..ef77757 100644 --- a/src/application/handlers.rs +++ b/src/application/handlers.rs @@ -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) { + 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() { diff --git a/src/lib.rs b/src/lib.rs index f7ee22e..de78c7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/main.rs b/src/main.rs index 82d1f4f..bf74d9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(); } }