use std::{ collections::BTreeMap, net::SocketAddr, path::{Path, PathBuf}, }; use crossterm::event::{Event, EventStream, KeyEventKind}; use futures::{FutureExt, StreamExt}; use jocalsend::{JocalEvent, JocalService, ReceiveRequest, error::Result}; use julid::Julid; use log::LevelFilter; use ratatui::{ Frame, widgets::{ListState, TableState, WidgetRef}, }; use ratatui_explorer::FileExplorer; use simsearch::{SearchOptions, SimSearch}; use tokio::sync::mpsc::UnboundedReceiver; use tui_input::Input; pub mod widgets; mod handle; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Peer { pub alias: String, pub fingerprint: String, pub addr: SocketAddr, } #[derive(Clone)] struct FileFinder { explorer: FileExplorer, fuzzy: SimSearch, working_dir: Option, input: Input, } fn searcher() -> SimSearch { SimSearch::new_with( SearchOptions::new() .stop_words(vec![std::path::MAIN_SEPARATOR_STR.to_string()]) .stop_whitespace(false), ) } pub struct App { pub service: JocalService, pub events: EventStream, pub peers: Vec, pub peer_state: ListState, pub receive_requests: BTreeMap, screen: Vec, 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_finder: FileFinder, text: Option, input: Input, } #[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(FileMode), Peers, Text, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FileMode { Picking, Fuzzy, } impl App { pub fn new(service: JocalService, event_listener: UnboundedReceiver) -> Self { App { service, event_listener, screen: vec![CurrentScreen::Main], file_finder: FileFinder::new().expect("could not create file explorer"), text: None, events: Default::default(), peers: Default::default(), peer_state: Default::default(), receive_requests: Default::default(), receiving_state: Default::default(), input: 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().fuse() => { if let Some(event) = transfer_event { log::trace!("got JocalEvent {event:?}"); match event { JocalEvent::ReceiveRequest { id, request } => { self.receive_requests.insert(id, request); } JocalEvent::Cancelled { session_id: id } | JocalEvent::ReceivedInbound(id) => { self.receive_requests.remove(&id); } JocalEvent::SendApproved(id) => log::info!("remote recipient approved outbound transfer {id}"), JocalEvent::SendDenied => log::warn!("outbound transfer request has been denied"), JocalEvent::SendSuccess { content, session: _session } => log::info!("successfully sent {content}"), JocalEvent::SendFailed { error } => log::error!("could not send content: {error}"), JocalEvent::Tick => {} } } } } Ok(()) } pub fn input(&mut self) -> &mut Input { &mut self.input } pub fn files(&mut self) -> &mut FileExplorer { &mut self.file_finder.explorer } pub fn text(&mut self) -> &mut Option { &mut self.text } pub fn screen(&self) -> CurrentScreen { *self.screen.last().unwrap() } pub fn screen_mut(&mut self) -> &mut CurrentScreen { self.screen.last_mut().unwrap() } pub fn draw(&mut self, frame: &mut Frame) { frame.render_widget(self, frame.area()); } } impl FileFinder { pub fn new() -> Result { Ok(Self { explorer: FileExplorer::new()?, fuzzy: searcher(), working_dir: None, input: Default::default(), }) } pub fn handle(&mut self, event: &Event) -> Result<()> { self.index(); Ok(self.explorer.handle(event)?) } pub fn cwd(&self) -> &Path { self.explorer.cwd() } pub fn set_cwd(&mut self, cwd: &Path) -> Result<()> { self.explorer.set_cwd(cwd)?; self.index(); Ok(()) } pub fn widget(&self) -> impl WidgetRef { self.explorer.widget() } pub fn reset_fuzzy(&mut self) { self.clear_fuzzy(); self.input.reset(); } fn clear_fuzzy(&mut self) { self.fuzzy = searcher(); } fn index(&mut self) { if let Some(owd) = self.working_dir.as_ref() && owd == self.cwd() { return; } self.working_dir = Some(self.cwd().to_path_buf()); self.clear_fuzzy(); for (i, f) in self.explorer.files().iter().enumerate() { self.fuzzy.insert(i, f.name()); } } } 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); }