From dd52430508aa1b9b6deb7583a750fbda9677dc53 Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Tue, 8 Jul 2025 21:51:44 -0700 Subject: [PATCH] move frontend stuff to frontend module, really nicely shutdown --- src/frontend/mod.rs | 138 ++++++++++++++++++++++++++++++++++ src/{ => frontend}/ui.rs | 4 +- src/main.rs | 158 +++++---------------------------------- 3 files changed, 158 insertions(+), 142 deletions(-) create mode 100644 src/frontend/mod.rs rename src/{ => frontend}/ui.rs (97%) diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs new file mode 100644 index 0000000..8890791 --- /dev/null +++ b/src/frontend/mod.rs @@ -0,0 +1,138 @@ +use std::{collections::BTreeMap, io, net::SocketAddr}; + +use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind}; +use futures::{FutureExt, StreamExt}; +use joecalsend::JoecalState; +use ratatui::{ + DefaultTerminal, + buffer::Buffer, + layout::Rect, + style::Stylize, + symbols::border, + text::{Line, Text}, + widgets::{Block, Paragraph, Widget}, +}; + +pub mod ui; + +pub type Peers = BTreeMap; + +pub struct App { + pub state: JoecalState, + pub screen: CurrentScreen, + pub events: EventStream, + // addr -> (alias, fingerprint) + pub peers: Peers, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CurrentScreen { + Main, + Sending, + Receiving, + Stopping, +} + +impl App { + pub fn new(state: JoecalState) -> Self { + App { + state, + screen: CurrentScreen::Main, + peers: Default::default(), + events: Default::default(), + } + } + + pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { + loop { + terminal.draw(|frame| self.draw(frame))?; + self.handle_events().await?; + + if self.screen == CurrentScreen::Stopping { + self.state.stop().await; + break; + } + + let peers = self.state.peers.lock().await; + self.peers.clear(); + peers.iter().for_each(|(k, v)| { + // k is fingerprint, v is addr, device + let addr = v.0; + let alias = v.1.alias.clone(); + let fingerprint = k.clone(); + self.peers.insert(addr, (alias, fingerprint)); + }); + } + Ok(()) + } + + async fn handle_events(&mut self) -> io::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), + Event::Mouse(_) => {} + Event::Resize(_, _) => {} + _ => {} + } + } + } + _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => {} + } + + Ok(()) + } + + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Char('q') => self.exit(), + KeyCode::Char('s') => self.send(), + KeyCode::Char('r') => self.recv(), + KeyCode::Char('d') => {} + _ => {} + } + } + + fn exit(&mut self) { + self.screen = CurrentScreen::Stopping; + } + + fn send(&mut self) { + self.screen = CurrentScreen::Sending; + } + + fn recv(&mut self) { + self.screen = CurrentScreen::Receiving; + } +} + +impl Widget for &App { + fn render(self, area: Rect, buf: &mut Buffer) { + let title = Line::from(" Joecalsend ".bold()); + let instructions = Line::from(vec![ + " Send ".into(), + "".blue().bold(), + " Receive ".into(), + "".blue().bold(), + " Discover ".into(), + "".blue().bold(), + " Quit ".into(), + " ".blue().bold(), + ]); + let block = Block::bordered() + .title(title.centered()) + .title_bottom(instructions.centered()) + .border_set(border::THICK); + + let current_screen = format!("{:?}", self.screen); + let text = Text::from(Line::from(current_screen.yellow())); + + Paragraph::new(text) + .centered() + .block(block) + .render(area, buf); + } +} diff --git a/src/ui.rs b/src/frontend/ui.rs similarity index 97% rename from src/ui.rs rename to src/frontend/ui.rs index 48e76f8..69ed93a 100644 --- a/src/ui.rs +++ b/src/frontend/ui.rs @@ -6,7 +6,7 @@ use ratatui::{ widgets::{Block, Borders, List, ListItem}, }; -use crate::App; +use crate::{App, frontend::Peers}; // helper function to create a centered rect using up certain percentage of the // available rect `r` @@ -56,7 +56,7 @@ impl App { } } -fn peers(peers: &crate::Peers, frame: &mut Frame, area: Rect) { +fn peers(peers: &Peers, frame: &mut Frame, area: Rect) { let mut items = Vec::with_capacity(peers.len()); for (k, v) in peers.iter() { let item = format!("{:?}: {} ({})", k, v.0, v.1); diff --git a/src/main.rs b/src/main.rs index b8b993b..af9e854 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,12 @@ #![feature(slice_as_array)] -use std::{collections::BTreeMap, io, net::SocketAddr}; -use futures::{FutureExt, StreamExt}; +use frontend::App; use joecalsend::{Config, JoecalState, error, models::Device}; use local_ip_address::local_ip; use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig, V4IfAddr}; -use ratatui::{ - DefaultTerminal, - buffer::Buffer, - crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind}, - layout::Rect, - style::Stylize, - symbols::border, - text::{Line, Text}, - widgets::{Block, Paragraph, Widget}, -}; use tokio::task::JoinSet; -mod ui; +mod frontend; #[tokio::main] async fn main() -> error::Result<()> { @@ -50,139 +39,28 @@ async fn main() -> error::Result<()> { let config = Config::default(); let mut handles = JoinSet::new(); state.start(&config, &mut handles).await; - let mut app = App::new(state.clone()).await; + let mut app = App::new(state.clone()); let mut terminal = ratatui::init(); let result = app.run(&mut terminal).await; ratatui::restore(); - while let Some(handle) = handles.join_next().await { - match handle { - Ok(h) => println!("Stopped {h:?}"), - Err(e) => println!("Got error {e:?}"), + loop { + tokio::select! { + handle = handles.join_next() => { + match handle { + Some(handle) => match handle { + Ok(h) => println!("Stopped {h:?}"), + Err(e) => println!("Got error {e:?}"), + } + None => break, + } + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => { + handles.abort_all(); + break; + }, } } Ok(result?) } - -pub type Peers = BTreeMap; - -pub struct App { - state: JoecalState, - screen: CurrentScreen, - // addr -> (alias, fingerprint) - peers: Peers, - events: EventStream, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum CurrentScreen { - Main, - Sending, - Receiving, - Stopping, -} - -impl App { - pub async fn new(state: JoecalState) -> Self { - App { - state, - screen: CurrentScreen::Main, - peers: Default::default(), - events: Default::default(), - } - } - - pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { - loop { - terminal.draw(|frame| self.draw(frame))?; - self.handle_events().await?; - - if self.screen == CurrentScreen::Stopping { - self.state.stop().await; - break; - } - - let peers = self.state.peers.lock().await; - self.peers.clear(); - peers.iter().for_each(|(k, v)| { - // k is fingerprint, v is addr, device - let addr = v.0; - let alias = v.1.alias.clone(); - let fingerprint = k.clone(); - self.peers.insert(addr, (alias, fingerprint)); - }); - } - Ok(()) - } - - async fn handle_events(&mut self) -> io::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), - Event::Mouse(_) => {} - Event::Resize(_, _) => {} - _ => {} - } - } - } - _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => {} - } - - Ok(()) - } - - fn handle_key_event(&mut self, key_event: KeyEvent) { - match key_event.code { - KeyCode::Char('q') => self.exit(), - KeyCode::Char('s') => self.send(), - KeyCode::Char('r') => self.recv(), - KeyCode::Char('d') => {} - _ => {} - } - } - - fn exit(&mut self) { - self.screen = CurrentScreen::Stopping; - } - - fn send(&mut self) { - self.screen = CurrentScreen::Sending; - } - - fn recv(&mut self) { - self.screen = CurrentScreen::Receiving; - } -} - -impl Widget for &App { - fn render(self, area: Rect, buf: &mut Buffer) { - let title = Line::from(" Joecalsend ".bold()); - let instructions = Line::from(vec![ - " Send ".into(), - "".blue().bold(), - " Receive ".into(), - "".blue().bold(), - " Discover ".into(), - "".blue().bold(), - " Quit ".into(), - " ".blue().bold(), - ]); - let block = Block::bordered() - .title(title.centered()) - .title_bottom(instructions.centered()) - .border_set(border::THICK); - - let current_screen = format!("{:?}", self.screen); - let text = Text::from(Line::from(current_screen.yellow())); - - Paragraph::new(text) - .centered() - .block(block) - .render(area, buf); - } -}