From 9b1734f8c04d2e3eac62114f71f9bc93c94be43f Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Fri, 15 Aug 2025 14:57:27 -0700 Subject: [PATCH] move event handling code into submodule of app --- src/app/handle.rs | 277 ++++++++++++++++++++++++++++++++++++++++++++++ src/app/mod.rs | 275 +-------------------------------------------- 2 files changed, 283 insertions(+), 269 deletions(-) create mode 100644 src/app/handle.rs diff --git a/src/app/handle.rs b/src/app/handle.rs new file mode 100644 index 0000000..084175e --- /dev/null +++ b/src/app/handle.rs @@ -0,0 +1,277 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use jocalsend::ReceiveDialog; +use log::{debug, error, warn}; +use tui_input::backend::crossterm::EventHandler; + +use crate::app::{App, CurrentScreen, FileMode, SendingScreen}; + +impl App { + pub(super) async fn handle_key_event( + &mut self, + key_event: KeyEvent, + event: crossterm::event::Event, + ) { + let code = key_event.code; + let mode = self.screen.last_mut().unwrap(); + match mode { + CurrentScreen::Main + | CurrentScreen::Logging + | CurrentScreen::Receiving + | CurrentScreen::Sending(SendingScreen::Files(FileMode::Picking)) + | CurrentScreen::Sending(SendingScreen::Peers) => match code { + KeyCode::Char('q') => self.exit().await, + KeyCode::Char('s') => self.send(), + KeyCode::Char('r') => self.recv(), + KeyCode::Char('l') => self.logs(), + KeyCode::Char('m') => self.main(), + KeyCode::Esc => self.pop(), + _ => match mode { + CurrentScreen::Main => { + if let KeyCode::Char('d') = code { + self.service.refresh_peers().await + } + } + CurrentScreen::Logging => match code { + KeyCode::Left => super::change_log_level(-1), + KeyCode::Right => super::change_log_level(1), + _ => {} + }, + CurrentScreen::Receiving => match code { + KeyCode::Up => self.receiving_state.select_previous(), + KeyCode::Down => self.receiving_state.select_next(), + KeyCode::Char('a') => self.accept(), + KeyCode::Char('d') => self.deny(), + _ => {} + }, + CurrentScreen::Sending(sending_screen) => match sending_screen { + // we can only be in picking mode + SendingScreen::Files(fmode) => match code { + KeyCode::Char('t') => *sending_screen = SendingScreen::Text, + KeyCode::Tab => *sending_screen = SendingScreen::Peers, + KeyCode::Enter => self.chdir_or_send_file().await, + KeyCode::Char('/') => { + *fmode = FileMode::Fuzzy; + } + _ => self.file_finder.handle(&event).unwrap_or_default(), + }, + SendingScreen::Peers => match code { + KeyCode::Tab => { + *sending_screen = SendingScreen::Files(FileMode::Picking) + } + KeyCode::Char('t') => *sending_screen = SendingScreen::Text, + KeyCode::Enter => self.send_content().await, + KeyCode::Up => self.peer_state.select_previous(), + KeyCode::Down => self.peer_state.select_next(), + _ => {} + }, + SendingScreen::Text => unreachable!(), + }, + CurrentScreen::Stopping => unreachable!(), + }, + }, + // we only need to deal with sending text now or doing fuzzy matching + CurrentScreen::Sending(sending_screen) => match sending_screen { + SendingScreen::Text => match code { + KeyCode::Tab => *sending_screen = SendingScreen::Peers, + KeyCode::Enter => self.send_text().await, + KeyCode::Esc => { + self.text = None; + self.input.reset(); + *sending_screen = SendingScreen::Files(FileMode::Picking); + } + _ => { + if let Some(changed) = self.input.handle_event(&event) + && changed.value + { + if self.input.value().is_empty() { + self.text = None; + } else { + self.text = Some(self.input.to_string()); + } + } + } + }, + SendingScreen::Files(fmode) => { + if *fmode == FileMode::Fuzzy { + match code { + KeyCode::Tab => *sending_screen = SendingScreen::Peers, + KeyCode::Enter => self.chdir_or_send_file().await, + KeyCode::Esc => { + self.file_finder.reset_fuzzy(); + *fmode = FileMode::Picking; + } + KeyCode::Up | KeyCode::Down => { + if let Err(e) = self.file_finder.handle(&event) { + log::error!("error selecting file: {e:?}"); + } + } + _ => { + self.file_finder.index(); + if let Some(changed) = self.file_finder.input.handle_event(&event) + && changed.value + { + let id = self + .file_finder + .fuzzy + .search(self.file_finder.input.value()) + .first() + .copied() + .unwrap_or(0); + self.file_finder.explorer.set_selected_idx(id); + } + } + } + } + } + SendingScreen::Peers => unreachable!(), + }, + CurrentScreen::Stopping => {} + } + } + + pub async fn exit(&mut self) { + self.screen.push(CurrentScreen::Stopping); + self.service.stop().await; + } + + pub fn send(&mut self) { + let last = self.screen.last(); + match last { + Some(CurrentScreen::Sending(_)) => {} + _ => self + .screen + .push(CurrentScreen::Sending(SendingScreen::Files( + FileMode::Picking, + ))), + } + } + + pub fn recv(&mut self) { + let last = self.screen.last(); + match last { + Some(CurrentScreen::Receiving) => {} + _ => self.screen.push(CurrentScreen::Receiving), + } + } + + pub fn logs(&mut self) { + let last = self.screen.last(); + match last { + Some(CurrentScreen::Logging) => {} + _ => self.screen.push(CurrentScreen::Logging), + } + } + + pub fn pop(&mut self) { + self.screen.pop(); + if self.screen.last().is_none() { + self.screen.push(CurrentScreen::Main); + } + } + + pub fn main(&mut self) { + let last = self.screen.last(); + match last { + Some(CurrentScreen::Main) => {} + _ => self.screen.push(CurrentScreen::Main), + } + } + + // accept a content receive request + fn accept(&mut self) { + let Some(idx) = self.receiving_state.selected() else { + return; + }; + // keys are sorted, so we can use the table selection index + let keys: Vec<_> = self.receive_requests.keys().collect(); + let Some(key) = keys.get(idx) else { + warn!("could not get id from selection index {idx}"); + return; + }; + let Some(req) = self.receive_requests.get(key) else { + return; + }; + if let Err(e) = req.tx.send(ReceiveDialog::Approve) { + error!("got error sending upload confirmation: {e:?}"); + }; + } + + // reject an content receive request + fn deny(&mut self) { + let Some(idx) = self.receiving_state.selected() else { + return; + }; + // keys are sorted, so we can use the table selection index + let keys: Vec<_> = self.receive_requests.keys().cloned().collect(); + let Some(key) = keys.get(idx) else { + warn!("could not get id from selection index {idx}"); + return; + }; + let Some(req) = self.receive_requests.get(key).cloned() else { + return; + }; + if let Err(e) = req.tx.send(ReceiveDialog::Deny) { + error!("got error sending upload confirmation: {e:?}"); + }; + self.receive_requests.remove(key); + } + + async fn chdir_or_send_file(&mut self) { + let file = self.file_finder.explorer.current().path().clone(); + if file.is_dir() + && let Err(e) = self.file_finder.set_cwd(&file) + { + error!("could not list directory {file:?}: {e}"); + return; + } else if file.is_dir() { + return; + } + + let Some(peer_idx) = self.peer_state.selected() else { + warn!("no peer selected to send to"); + return; + }; + let Some(peer) = self.peers.get(peer_idx) else { + warn!("invalid peer index {peer_idx}"); + return; + }; + + if file.is_file() { + debug!("sending {file:?}"); + if let Err(e) = self.service.send_file(&peer.fingerprint, file).await { + error!("got error sending content: {e:?}"); + } + } + } + + // send content to selected peer, or change directories in the file explorer + async fn send_text(&mut self) { + debug!("sending text"); + + let Some(peer_idx) = self.peer_state.selected() else { + debug!("no peer selected to send to"); + return; + }; + let Some(peer) = self.peers.get(peer_idx) else { + warn!("invalid peer index {peer_idx}"); + return; + }; + + let Some(text) = &self.text else { + debug!("no text to send"); + return; + }; + + if let Err(e) = self.service.send_text(&peer.fingerprint, text).await { + error!("got error sending \"{text}\" to {}: {e:?}", peer.alias); + } + } + + async fn send_content(&mut self) { + if self.text.is_some() { + self.send_text().await; + } else { + self.chdir_or_send_file().await; + } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 4ba9afb..7a45290 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -4,11 +4,11 @@ use std::{ path::{Path, PathBuf}, }; -use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind}; +use crossterm::event::{Event, EventStream, KeyEventKind}; use futures::{FutureExt, StreamExt}; -use jocalsend::{JocalEvent, JocalService, ReceiveDialog, ReceiveRequest, error::Result}; +use jocalsend::{JocalEvent, JocalService, ReceiveRequest, error::Result}; use julid::Julid; -use log::{LevelFilter, debug, error, warn}; +use log::LevelFilter; use ratatui::{ Frame, widgets::{ListState, TableState, WidgetRef}, @@ -16,10 +16,12 @@ use ratatui::{ use ratatui_explorer::FileExplorer; use simsearch::{SearchOptions, SimSearch}; use tokio::sync::mpsc::UnboundedReceiver; -use tui_input::{Input, backend::crossterm::EventHandler}; +use tui_input::Input; pub mod widgets; +mod handle; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Peer { pub alias: String, @@ -154,274 +156,9 @@ impl App { self.screen.last_mut().unwrap() } - async fn handle_key_event(&mut self, key_event: KeyEvent, event: crossterm::event::Event) { - let code = key_event.code; - let mode = self.screen.last_mut().unwrap(); - match mode { - CurrentScreen::Main - | CurrentScreen::Logging - | CurrentScreen::Receiving - | CurrentScreen::Sending(SendingScreen::Files(FileMode::Picking)) - | CurrentScreen::Sending(SendingScreen::Peers) => match code { - KeyCode::Char('q') => self.exit().await, - KeyCode::Char('s') => self.send(), - KeyCode::Char('r') => self.recv(), - KeyCode::Char('l') => self.logs(), - KeyCode::Char('m') => self.main(), - KeyCode::Esc => self.pop(), - _ => match mode { - CurrentScreen::Main => { - if let KeyCode::Char('d') = code { - self.service.refresh_peers().await - } - } - CurrentScreen::Logging => match code { - KeyCode::Left => change_log_level(-1), - KeyCode::Right => change_log_level(1), - _ => {} - }, - CurrentScreen::Receiving => match code { - KeyCode::Up => self.receiving_state.select_previous(), - KeyCode::Down => self.receiving_state.select_next(), - KeyCode::Char('a') => self.accept(), - KeyCode::Char('d') => self.deny(), - _ => {} - }, - CurrentScreen::Sending(sending_screen) => match sending_screen { - // we can only be in picking mode - SendingScreen::Files(fmode) => match code { - KeyCode::Char('t') => *sending_screen = SendingScreen::Text, - KeyCode::Tab => *sending_screen = SendingScreen::Peers, - KeyCode::Enter => self.chdir_or_send_file().await, - KeyCode::Char('/') => { - *fmode = FileMode::Fuzzy; - } - _ => self.file_finder.handle(&event).unwrap_or_default(), - }, - SendingScreen::Peers => match code { - KeyCode::Tab => { - *sending_screen = SendingScreen::Files(FileMode::Picking) - } - KeyCode::Char('t') => *sending_screen = SendingScreen::Text, - KeyCode::Enter => self.send_content().await, - KeyCode::Up => self.peer_state.select_previous(), - KeyCode::Down => self.peer_state.select_next(), - _ => {} - }, - SendingScreen::Text => unreachable!(), - }, - CurrentScreen::Stopping => unreachable!(), - }, - }, - // we only need to deal with sending text now or doing fuzzy matching - CurrentScreen::Sending(sending_screen) => match sending_screen { - SendingScreen::Text => match code { - KeyCode::Tab => *sending_screen = SendingScreen::Peers, - KeyCode::Enter => self.send_text().await, - KeyCode::Esc => { - self.text = None; - self.input.reset(); - *sending_screen = SendingScreen::Files(FileMode::Picking); - } - _ => { - if let Some(changed) = self.input.handle_event(&event) - && changed.value - { - if self.input.value().is_empty() { - self.text = None; - } else { - self.text = Some(self.input.to_string()); - } - } - } - }, - SendingScreen::Files(fmode) => { - if *fmode == FileMode::Fuzzy { - match code { - KeyCode::Tab => *sending_screen = SendingScreen::Peers, - KeyCode::Enter => self.chdir_or_send_file().await, - KeyCode::Esc => { - self.file_finder.reset_fuzzy(); - *fmode = FileMode::Picking; - } - KeyCode::Up | KeyCode::Down => { - if let Err(e) = self.file_finder.handle(&event) { - log::error!("error selecting file: {e:?}"); - } - } - _ => { - self.file_finder.index(); - if let Some(changed) = self.file_finder.input.handle_event(&event) - && changed.value - { - let id = self - .file_finder - .fuzzy - .search(self.file_finder.input.value()) - .first() - .copied() - .unwrap_or(0); - self.file_finder.explorer.set_selected_idx(id); - } - } - } - } - } - SendingScreen::Peers => unreachable!(), - }, - CurrentScreen::Stopping => {} - } - } - pub fn draw(&mut self, frame: &mut Frame) { frame.render_widget(self, frame.area()); } - - pub async fn exit(&mut self) { - self.screen.push(CurrentScreen::Stopping); - self.service.stop().await; - } - - pub fn send(&mut self) { - let last = self.screen.last(); - match last { - Some(CurrentScreen::Sending(_)) => {} - _ => self - .screen - .push(CurrentScreen::Sending(SendingScreen::Files( - FileMode::Picking, - ))), - } - } - - pub fn recv(&mut self) { - let last = self.screen.last(); - match last { - Some(CurrentScreen::Receiving) => {} - _ => self.screen.push(CurrentScreen::Receiving), - } - } - - pub fn logs(&mut self) { - let last = self.screen.last(); - match last { - Some(CurrentScreen::Logging) => {} - _ => self.screen.push(CurrentScreen::Logging), - } - } - - pub fn pop(&mut self) { - self.screen.pop(); - if self.screen.last().is_none() { - self.screen.push(CurrentScreen::Main); - } - } - - pub fn main(&mut self) { - let last = self.screen.last(); - match last { - Some(CurrentScreen::Main) => {} - _ => self.screen.push(CurrentScreen::Main), - } - } - - // accept a content receive request - fn accept(&mut self) { - let Some(idx) = self.receiving_state.selected() else { - return; - }; - // keys are sorted, so we can use the table selection index - let keys: Vec<_> = self.receive_requests.keys().collect(); - let Some(key) = keys.get(idx) else { - warn!("could not get id from selection index {idx}"); - return; - }; - let Some(req) = self.receive_requests.get(key) else { - return; - }; - if let Err(e) = req.tx.send(ReceiveDialog::Approve) { - error!("got error sending upload confirmation: {e:?}"); - }; - } - - // reject an content receive request - fn deny(&mut self) { - let Some(idx) = self.receiving_state.selected() else { - return; - }; - // keys are sorted, so we can use the table selection index - let keys: Vec<_> = self.receive_requests.keys().cloned().collect(); - let Some(key) = keys.get(idx) else { - warn!("could not get id from selection index {idx}"); - return; - }; - let Some(req) = self.receive_requests.get(key).cloned() else { - return; - }; - if let Err(e) = req.tx.send(ReceiveDialog::Deny) { - error!("got error sending upload confirmation: {e:?}"); - }; - self.receive_requests.remove(key); - } - - async fn chdir_or_send_file(&mut self) { - let file = self.file_finder.explorer.current().path().clone(); - if file.is_dir() - && let Err(e) = self.file_finder.set_cwd(&file) - { - error!("could not list directory {file:?}: {e}"); - return; - } else if file.is_dir() { - return; - } - - let Some(peer_idx) = self.peer_state.selected() else { - warn!("no peer selected to send to"); - return; - }; - let Some(peer) = self.peers.get(peer_idx) else { - warn!("invalid peer index {peer_idx}"); - return; - }; - - if file.is_file() { - debug!("sending {file:?}"); - if let Err(e) = self.service.send_file(&peer.fingerprint, file).await { - error!("got error sending content: {e:?}"); - } - } - } - - // send content to selected peer, or change directories in the file explorer - async fn send_text(&mut self) { - debug!("sending text"); - - let Some(peer_idx) = self.peer_state.selected() else { - debug!("no peer selected to send to"); - return; - }; - let Some(peer) = self.peers.get(peer_idx) else { - warn!("invalid peer index {peer_idx}"); - return; - }; - - let Some(text) = &self.text else { - debug!("no text to send"); - return; - }; - - if let Err(e) = self.service.send_text(&peer.fingerprint, text).await { - error!("got error sending \"{text}\" to {}: {e:?}", peer.alias); - } - } - - async fn send_content(&mut self) { - if self.text.is_some() { - self.send_text().await; - } else { - self.chdir_or_send_file().await; - } - } } impl FileFinder {