better peer selection

This commit is contained in:
Joe Ardent 2025-08-03 16:43:25 -07:00
parent 2ff3e3971d
commit 358b96a744
5 changed files with 77 additions and 27 deletions

View file

@ -6,20 +6,29 @@ use futures::{FutureExt, StreamExt};
use joecalsend::{JoecalService, ReceiveDialog, ReceiveRequest, TransferEvent, error::Result}; use joecalsend::{JoecalService, ReceiveDialog, ReceiveRequest, TransferEvent, error::Result};
use julid::Julid; use julid::Julid;
use log::{LevelFilter, debug, error, warn}; use log::{LevelFilter, debug, error, warn};
use ratatui::{Frame, widgets::TableState}; use ratatui::{
Frame,
widgets::{ListState, TableState},
};
use ratatui_explorer::FileExplorer; use ratatui_explorer::FileExplorer;
use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedReceiver;
pub mod widgets; pub mod widgets;
pub type Peers = BTreeMap<SocketAddr, (String, String)>; #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Peer {
pub alias: String,
pub fingerprint: String,
pub addr: SocketAddr,
}
pub struct App { pub struct App {
pub service: JoecalService, pub service: JoecalService,
pub screen: Vec<CurrentScreen>, pub screen: Vec<CurrentScreen>,
pub events: EventStream, pub events: EventStream,
// addr -> (alias, fingerprint) // addr -> (alias, fingerprint)
pub peers: Peers, pub peers: Vec<Peer>,
pub peer_state: ListState,
pub receive_requests: BTreeMap<Julid, ReceiveRequest>, pub receive_requests: BTreeMap<Julid, ReceiveRequest>,
receiving_state: TableState, receiving_state: TableState,
// for getting messages back from the web server or web client about things we've done; the // 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, content: None,
events: Default::default(), events: Default::default(),
peers: Default::default(), peers: Default::default(),
peer_state: Default::default(),
receive_requests: Default::default(), receive_requests: Default::default(),
receiving_state: Default::default(), receiving_state: Default::default(),
} }
@ -124,6 +134,8 @@ impl App {
KeyCode::Char('q') => self.exit(), KeyCode::Char('q') => self.exit(),
KeyCode::Tab => *s = SendingScreen::Files, KeyCode::Tab => *s = SendingScreen::Files,
KeyCode::Enter => self.send_content().await, KeyCode::Enter => self.send_content().await,
KeyCode::Up => self.peer_state.select_previous(),
KeyCode::Down => self.peer_state.select_next(),
_ => {} _ => {}
}, },
SendingScreen::Text => {} SendingScreen::Text => {}
@ -234,11 +246,16 @@ impl App {
error!("could not list directory {file:?}: {e}"); 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() && file.is_file()
{ {
debug!("sending {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:?}"); error!("got error sending content: {e:?}");
} }
} }

View file

@ -8,11 +8,14 @@ use ratatui::{
style::{Color, Style, Stylize}, style::{Color, Style, Stylize},
symbols::border, symbols::border,
text::{Line, Text, ToLine}, 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 tui_logger::{TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState};
use super::{App, CurrentScreen, Peers, SendingScreen}; use super::{App, CurrentScreen, Peer, SendingScreen};
static MAIN_MENU: LazyLock<Line> = LazyLock::new(|| { static MAIN_MENU: LazyLock<Line> = LazyLock::new(|| {
Line::from(vec![ Line::from(vec![
@ -126,7 +129,12 @@ impl Widget for &mut App {
outer_frame(*current_screen, &MAIN_MENU, area, buf); outer_frame(*current_screen, &MAIN_MENU, area, buf);
logger(header_right.inner(header_margin), buf); logger(header_right.inner(header_margin), buf);
let peers = PeersWidget { peers: &self.peers }; 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); NetworkInfoWidget.render(footer_left.inner(footer_margin), buf);
receive_requests( receive_requests(
&rx_reqs, &rx_reqs,
@ -168,7 +176,12 @@ impl Widget for &mut App {
.render(header_left.inner(header_margin), buf); .render(header_left.inner(header_margin), buf);
logger(header_right.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); outer_frame(*current_screen, &MAIN_MENU, area, buf);
@ -261,19 +274,32 @@ fn receive_requests(
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PeersWidget<'p> { pub struct PeersWidget<'p> {
pub peers: &'p Peers, pub peers: &'p [Peer],
} }
impl<'p> Widget for PeersWidget<'p> { impl<'p> ratatui::widgets::StatefulWidget for PeersWidget<'p> {
fn render(self, area: Rect, buf: &mut Buffer) type State = ListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where where
Self: Sized, Self: Sized,
{ {
let mut items = Vec::with_capacity(self.peers.len()); if self.peers.is_empty() {
for (k, v) in self.peers.iter() { state.select(None);
let item = format!("{:?}: {} ({})", k, v.0, v.1); }
let s = Line::from(item.yellow()); 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); let item = ListItem::new(s);
items.push(item); items.push(item);
} }
@ -282,8 +308,10 @@ impl<'p> Widget for PeersWidget<'p> {
.title(title) .title(title)
.borders(Borders::all()) .borders(Borders::all())
.padding(Padding::uniform(1)); .padding(Padding::uniform(1));
let list = List::new(items).block(block); let list = List::new(items)
list.render(area, buf); .block(block)
.highlight_style(Style::new().bg(Color::Rgb(99, 99, 99)));
ratatui::widgets::StatefulWidget::render(list, area, buf, state);
} }
} }

View file

@ -86,6 +86,7 @@ impl JoecalService {
socket.join_multicast_v4(MULTICAST_IP, Ipv4Addr::from_bits(0))?; socket.join_multicast_v4(MULTICAST_IP, Ipv4Addr::from_bits(0))?;
let client = reqwest::ClientBuilder::new() let client = reqwest::ClientBuilder::new()
// localsend certs are self-signed
.danger_accept_invalid_certs(true) .danger_accept_invalid_certs(true)
.build()?; .build()?;

View file

@ -5,7 +5,7 @@ use tokio::{sync::mpsc::unbounded_channel, task::JoinSet};
use tui_logger::{LevelFilter, init_logger, set_env_filter_from_env}; use tui_logger::{LevelFilter, init_logger, set_env_filter_from_env};
mod app; mod app;
use app::{App, CurrentScreen}; use app::{App, CurrentScreen, Peer};
fn main() -> error::Result<()> { fn main() -> error::Result<()> {
let device = Device::default(); let device = Device::default();
@ -58,18 +58,22 @@ async fn start_and_run(
app.peers.clear(); app.peers.clear();
peers.iter().for_each(|(fingerprint, (addr, device))| { peers.iter().for_each(|(fingerprint, (addr, device))| {
let alias = device.alias.clone(); let alias = device.alias.clone();
app.peers let peer = Peer {
.insert(addr.to_owned(), (alias, fingerprint.to_owned())); 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; let now = chrono::Utc::now().timestamp_millis() as u64;
for (id, request) in app.receive_requests.iter() { for (id, request) in app.receive_requests.iter() {
if request.tx.is_closed() || (now - id.timestamp()) > 60_000 { 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); app.receive_requests.remove(&id);
} }
} }

View file

@ -55,10 +55,10 @@ pub struct PrepareUploadRequest {
impl JoecalService { impl JoecalService {
pub async fn prepare_upload( pub async fn prepare_upload(
&self, &self,
peer: String, peer: &str,
files: BTreeMap<String, FileMetadata>, files: BTreeMap<String, FileMetadata>,
) -> Result<PrepareUploadResponse> { ) -> Result<PrepareUploadResponse> {
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); return Err(LocalSendError::PeerNotFound);
}; };
@ -135,7 +135,7 @@ impl JoecalService {
Ok(()) 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 // Generate file metadata
let file_metadata = FileMetadata::from_path(&file_path)?; let file_metadata = FileMetadata::from_path(&file_path)?;