From 358b96a7449db0134b529ffb38ac13465faec1a7 Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Sun, 3 Aug 2025 16:43:25 -0700 Subject: [PATCH] better peer selection --- src/app/mod.rs | 27 ++++++++++++++++++----- src/app/widgets.rs | 54 +++++++++++++++++++++++++++++++++++----------- src/lib.rs | 1 + src/main.rs | 16 ++++++++------ src/transfer.rs | 6 +++--- 5 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index d316bce..ca7379c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -6,20 +6,29 @@ 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::{ + Frame, + widgets::{ListState, TableState}, +}; use ratatui_explorer::FileExplorer; use tokio::sync::mpsc::UnboundedReceiver; pub mod widgets; -pub type Peers = BTreeMap; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Peer { + pub alias: String, + pub fingerprint: String, + pub addr: SocketAddr, +} pub struct App { pub service: JoecalService, pub screen: Vec, pub events: EventStream, // addr -> (alias, fingerprint) - pub peers: Peers, + pub peers: Vec, + pub peer_state: ListState, pub receive_requests: BTreeMap, receiving_state: TableState, // for getting messages back from the web server or web client about things we've done; the @@ -55,6 +64,7 @@ impl App { content: None, events: Default::default(), peers: Default::default(), + peer_state: Default::default(), receive_requests: Default::default(), receiving_state: Default::default(), } @@ -124,6 +134,8 @@ impl App { KeyCode::Char('q') => self.exit(), KeyCode::Tab => *s = SendingScreen::Files, KeyCode::Enter => self.send_content().await, + KeyCode::Up => self.peer_state.select_previous(), + KeyCode::Down => self.peer_state.select_next(), _ => {} }, SendingScreen::Text => {} @@ -234,11 +246,16 @@ impl App { error!("could not list directory {file:?}: {e}"); } - if let Some((_, (_, peer))) = self.peers.first_key_value() + if let Some(idx) = self.peer_state.selected() + && let Some(peer) = self.peers.get(idx) && file.is_file() { debug!("sending {file:?}"); - if let Err(e) = self.service.send_file(peer.to_owned(), file.clone()).await { + if let Err(e) = self + .service + .send_file(&peer.fingerprint, file.clone()) + .await + { error!("got error sending content: {e:?}"); } } diff --git a/src/app/widgets.rs b/src/app/widgets.rs index 95d9733..1846256 100644 --- a/src/app/widgets.rs +++ b/src/app/widgets.rs @@ -8,11 +8,14 @@ use ratatui::{ style::{Color, Style, Stylize}, symbols::border, text::{Line, Text, ToLine}, - widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Row, Table, TableState, Widget}, + widgets::{ + Block, Borders, List, ListItem, ListState, Padding, Paragraph, Row, Table, TableState, + Widget, + }, }; use tui_logger::{TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState}; -use super::{App, CurrentScreen, Peers, SendingScreen}; +use super::{App, CurrentScreen, Peer, SendingScreen}; static MAIN_MENU: LazyLock = LazyLock::new(|| { Line::from(vec![ @@ -126,7 +129,12 @@ impl Widget for &mut App { outer_frame(*current_screen, &MAIN_MENU, area, buf); logger(header_right.inner(header_margin), buf); let peers = PeersWidget { peers: &self.peers }; - peers.render(footer_right.inner(footer_margin), buf); + ratatui::widgets::StatefulWidget::render( + peers, + footer_right.inner(footer_margin), + buf, + &mut self.peer_state, + ); NetworkInfoWidget.render(footer_left.inner(footer_margin), buf); receive_requests( &rx_reqs, @@ -168,7 +176,12 @@ impl Widget for &mut App { .render(header_left.inner(header_margin), buf); logger(header_right.inner(header_margin), buf); - peers.render(bottom.inner(subscreen_margin), buf); + ratatui::widgets::StatefulWidget::render( + peers, + bottom.inner(subscreen_margin), + buf, + &mut self.peer_state, + ); } _ => { outer_frame(*current_screen, &MAIN_MENU, area, buf); @@ -261,19 +274,32 @@ fn receive_requests( #[derive(Debug, Clone)] pub struct PeersWidget<'p> { - pub peers: &'p Peers, + pub peers: &'p [Peer], } -impl<'p> Widget for PeersWidget<'p> { - fn render(self, area: Rect, buf: &mut Buffer) +impl<'p> ratatui::widgets::StatefulWidget for PeersWidget<'p> { + type State = ListState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) where Self: Sized, { - let mut items = Vec::with_capacity(self.peers.len()); - for (k, v) in self.peers.iter() { - let item = format!("{:?}: {} ({})", k, v.0, v.1); - let s = Line::from(item.yellow()); + if self.peers.is_empty() { + state.select(None); + } + if state.selected().is_none() { + state.select(Some(0)); + } + let mut items = Vec::with_capacity(self.peers.len()); + for Peer { + addr, + alias, + fingerprint, + } in self.peers.iter() + { + let item = format!("{:?}: {} ({})", addr, alias, fingerprint); + let s = Line::from(item.yellow()); let item = ListItem::new(s); items.push(item); } @@ -282,8 +308,10 @@ impl<'p> Widget for PeersWidget<'p> { .title(title) .borders(Borders::all()) .padding(Padding::uniform(1)); - let list = List::new(items).block(block); - list.render(area, buf); + let list = List::new(items) + .block(block) + .highlight_style(Style::new().bg(Color::Rgb(99, 99, 99))); + ratatui::widgets::StatefulWidget::render(list, area, buf, state); } } diff --git a/src/lib.rs b/src/lib.rs index cd3392b..88db4e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,6 +86,7 @@ impl JoecalService { socket.join_multicast_v4(MULTICAST_IP, Ipv4Addr::from_bits(0))?; let client = reqwest::ClientBuilder::new() + // localsend certs are self-signed .danger_accept_invalid_certs(true) .build()?; diff --git a/src/main.rs b/src/main.rs index f3fa933..be34164 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use tokio::{sync::mpsc::unbounded_channel, task::JoinSet}; use tui_logger::{LevelFilter, init_logger, set_env_filter_from_env}; mod app; -use app::{App, CurrentScreen}; +use app::{App, CurrentScreen, Peer}; fn main() -> error::Result<()> { let device = Device::default(); @@ -58,18 +58,22 @@ async fn start_and_run( app.peers.clear(); peers.iter().for_each(|(fingerprint, (addr, device))| { let alias = device.alias.clone(); - app.peers - .insert(addr.to_owned(), (alias, fingerprint.to_owned())); + let peer = Peer { + alias, + fingerprint: fingerprint.to_owned(), + addr: addr.to_owned(), + }; + app.peers.push(peer); }); - let mut stale_uploads = Vec::with_capacity(app.receive_requests.len()); + let mut stale_rx_requests = Vec::with_capacity(app.receive_requests.len()); let now = chrono::Utc::now().timestamp_millis() as u64; for (id, request) in app.receive_requests.iter() { if request.tx.is_closed() || (now - id.timestamp()) > 60_000 { - stale_uploads.push(*id); + stale_rx_requests.push(*id); } } - for id in stale_uploads { + for id in stale_rx_requests { app.receive_requests.remove(&id); } } diff --git a/src/transfer.rs b/src/transfer.rs index c2143e9..4c8fb94 100644 --- a/src/transfer.rs +++ b/src/transfer.rs @@ -55,10 +55,10 @@ pub struct PrepareUploadRequest { impl JoecalService { pub async fn prepare_upload( &self, - peer: String, + peer: &str, files: BTreeMap, ) -> Result { - let Some((addr, device)) = self.peers.lock().await.get(&peer).cloned() else { + let Some((addr, device)) = self.peers.lock().await.get(peer).cloned() else { return Err(LocalSendError::PeerNotFound); }; @@ -135,7 +135,7 @@ impl JoecalService { Ok(()) } - pub async fn send_file(&self, peer: String, file_path: PathBuf) -> Result<()> { + pub async fn send_file(&self, peer: &str, file_path: PathBuf) -> Result<()> { // Generate file metadata let file_metadata = FileMetadata::from_path(&file_path)?;