use std::{collections::BTreeMap, net::SocketAddr, time::Duration}; use axum::body::Bytes; use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind}; use futures::{FutureExt, StreamExt}; use joecalsend::{JoecalService, ReceiveDialog, ReceiveRequest, TransferEvent, error::Result}; use julid::Julid; use log::{LevelFilter, debug, error, warn}; use ratatui::{Frame, widgets::TableState}; use ratatui_explorer::FileExplorer; use tokio::sync::mpsc::UnboundedReceiver; pub mod widgets; pub type Peers = BTreeMap; pub struct App { pub service: JoecalService, pub screen: Vec, pub events: EventStream, // addr -> (alias, fingerprint) pub peers: Peers, pub receive_requests: BTreeMap, receiving_state: TableState, // for getting messages back from the web server or web client about things we've done; the // other end is held by the service event_listener: UnboundedReceiver, file_picker: FileExplorer, content: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CurrentScreen { Main, Sending(SendingScreen), Receiving, Stopping, Logging, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SendingScreen { Files, Peers, Text, } impl App { pub fn new(service: JoecalService, event_listener: UnboundedReceiver) -> Self { App { service, event_listener, screen: vec![CurrentScreen::Main], file_picker: FileExplorer::new().expect("could not create file explorer"), content: None, events: Default::default(), peers: Default::default(), receive_requests: Default::default(), receiving_state: Default::default(), } } pub async fn handle_events(&mut self) -> Result<()> { tokio::select! { event = self.events.next().fuse() => { if let Some(Ok(evt)) = event { match evt { Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_event(key, evt).await, Event::Mouse(_) => {} Event::Resize(_, _) => {} _ => {} } } } transfer_event = self.event_listener.recv() => { if let Some(event) = transfer_event { debug!("got transferr event {event:?}"); match event { TransferEvent::ReceiveRequest { id, request } => { self.receive_requests.insert(id, request); } TransferEvent::Cancelled(id) | TransferEvent::Received(id) => { self.receive_requests.remove(&id); } } } } _ = tokio::time::sleep(Duration::from_millis(200)) => {} } Ok(()) } pub async fn handle_key_event(&mut self, key_event: KeyEvent, event: crossterm::event::Event) { match self.screen.last_mut().unwrap() { CurrentScreen::Logging => match key_event.code { KeyCode::Esc => self.pop(), KeyCode::Left => change_log_level(-1), KeyCode::Right => change_log_level(1), KeyCode::Char('q') => self.exit(), _ => {} }, CurrentScreen::Receiving => match key_event.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(), KeyCode::Esc => self.pop(), KeyCode::Char('q') => self.exit(), _ => {} }, CurrentScreen::Sending(s) => match s { SendingScreen::Files => match key_event.code { KeyCode::Esc => self.pop(), KeyCode::Char('q') => self.exit(), KeyCode::Tab => *s = SendingScreen::Peers, KeyCode::Enter => self.send_content().await, _ => self.file_picker.handle(&event).unwrap_or_default(), }, SendingScreen::Peers => match key_event.code { KeyCode::Esc => self.pop(), KeyCode::Char('q') => self.exit(), KeyCode::Tab => *s = SendingScreen::Files, KeyCode::Enter => self.send_content().await, _ => {} }, SendingScreen::Text => {} }, _ => match key_event.code { KeyCode::Char('q') => self.exit(), KeyCode::Char('s') => self.send(), KeyCode::Char('r') => self.recv(), KeyCode::Char('l') => self.logs(), KeyCode::Esc => self.pop(), _ => {} }, } } pub fn draw(&mut self, frame: &mut Frame) { frame.render_widget(self, frame.area()); } pub fn exit(&mut self) { self.screen.push(CurrentScreen::Stopping); } pub fn send(&mut self) { let last = self.screen.last(); match last { Some(CurrentScreen::Sending(_)) => {} _ => self .screen .push(CurrentScreen::Sending(SendingScreen::Files)), } } 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); } } // 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); } // send content to selected peer, or change directories in the file explorer async fn send_content(&mut self) { debug!("sending content"); if let Some(_bytes) = self.content.to_owned() { // self.service.send_bytes(session_id, content_id, token, body) warn!("entering text is not yet supported"); } else { let file = self.file_picker.current().path().clone(); if file.is_dir() && let Err(e) = self.file_picker.set_cwd(&file) { error!("could not list directory {file:?}: {e}"); } if let Some((_, (_, peer))) = self.peers.first_key_value() && file.is_file() { debug!("sending {file:?}"); if let Err(e) = self.service.send_file(peer.to_owned(), file.clone()).await { error!("got error sending content: {e:?}"); } } } } } fn change_log_level(delta: isize) { let level = log::max_level() as isize; let max = log::LevelFilter::max() as isize; let level = (level + delta).clamp(0, max) as usize; // levelfilter is repr(usize) so this is safe let level = unsafe { std::mem::transmute::(level) }; log::set_max_level(level); }