Compare commits

..

3 commits

Author SHA1 Message Date
Joe Ardent
75677faeae fix alarm in shutdown 2025-07-15 16:32:00 -07:00
Joe Ardent
a2c6f7f8e7 update to ratatui 0.30 2025-07-15 16:27:19 -07:00
Joe Ardent
cac0e4f6e3 shutdown more reliably 2025-07-15 15:46:13 -07:00
6 changed files with 808 additions and 146 deletions

787
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@ mime = "0.3"
mime_guess = "2"
native-dialog = "0.9"
network-interface = { version = "2", features = ["serde"] }
ratatui = "0.29"
ratatui = "0.30.0-alpha.5"
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View file

@ -1,13 +1,8 @@
use std::{
collections::{BTreeMap, VecDeque},
io,
net::SocketAddr,
time::Duration,
};
use std::{collections::BTreeMap, io, net::SocketAddr, sync::OnceLock, time::Duration};
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind};
use futures::{FutureExt, StreamExt};
use joecalsend::JoecalState;
use joecalsend::{Config, JoecalState, Listeners, models::Device};
use ratatui::{
DefaultTerminal,
buffer::Buffer,
@ -17,13 +12,14 @@ use ratatui::{
text::{Line, Text},
widgets::{Block, Paragraph, Widget},
};
use tokio::task::JoinSet;
pub mod ui;
pub type Peers = BTreeMap<SocketAddr, (String, String)>;
pub struct App {
pub state: JoecalState,
pub state: OnceLock<JoecalState>,
pub screen: Vec<CurrentScreen>,
pub events: EventStream,
// addr -> (alias, fingerprint)
@ -39,16 +35,29 @@ pub enum CurrentScreen {
}
impl App {
pub fn new(state: JoecalState) -> Self {
pub fn new() -> Self {
App {
state,
state: Default::default(),
screen: vec![CurrentScreen::Main],
peers: Default::default(),
events: Default::default(),
}
}
pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
#[tokio::main]
pub async fn run(
&mut self,
terminal: &mut DefaultTerminal,
config: Config,
device: Device,
) -> io::Result<()> {
let state = JoecalState::new(device)
.await
.expect("Could not create JoecalState");
let mut handles = JoinSet::new();
state.start(&config, &mut handles).await;
self.state.get_or_init(|| state);
loop {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events().await?;
@ -56,25 +65,25 @@ impl App {
if let Some(&top) = self.screen.last()
&& top == CurrentScreen::Stopping
{
self.state.stop().await;
self.state.get().unwrap().stop().await;
break;
}
let peers = self.state.peers.lock().await;
let peers = self.state.get().unwrap().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));
peers.iter().for_each(|(fingerprint, (addr, device))| {
let alias = device.alias.clone();
self.peers
.insert(addr.to_owned(), (alias, fingerprint.to_owned()));
});
}
shutdown(&mut handles).await;
Ok(())
}
async fn handle_events(&mut self) -> io::Result<()> {
let mut tick = tokio::time::interval(Duration::from_millis(100));
tokio::select! {
event = self.events.next().fuse() => {
if let Some(Ok(evt)) = event {
@ -88,7 +97,7 @@ impl App {
}
}
}
_ = tick.tick() => {}
_ = tokio::time::sleep(Duration::from_millis(200)) => {}
}
Ok(())
@ -109,18 +118,48 @@ impl App {
}
fn send(&mut self) {
self.screen.push(CurrentScreen::Sending);
let last = self.screen.last();
match last {
Some(CurrentScreen::Sending) => {}
_ => self.screen.push(CurrentScreen::Sending),
}
}
fn recv(&mut self) {
self.screen.push(CurrentScreen::Receiving);
let last = self.screen.last();
match last {
Some(CurrentScreen::Receiving) => {}
_ => self.screen.push(CurrentScreen::Receiving),
}
}
fn pop(&mut self) {
self.screen.pop();
if self.screen.last().is_none() {
self.screen.push(CurrentScreen::Main);
} else {
self.screen.pop();
}
}
}
async fn shutdown(handles: &mut JoinSet<Listeners>) {
let mut alarm = tokio::time::interval(tokio::time::Duration::from_secs(5));
alarm.tick().await;
loop {
tokio::select! {
join_result = handles.join_next() => {
match join_result {
Some(handle) => match handle {
Ok(h) => println!("Stopped {h:?}"),
Err(e) => println!("Got error {e:?}"),
}
None => break,
}
}
_ = alarm.tick() => {
println!("Exit timeout reached, aborting all unjoined tasks");
handles.abort_all();
break;
},
}
}
}

View file

@ -1,6 +1,6 @@
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
layout::{Constraint, Direction, Layout, Margin, Rect},
style::Stylize,
text::Line,
widgets::{Block, Borders, List, ListItem, Padding},
@ -34,8 +34,8 @@ impl App {
.cloned()
.unwrap();
network_info(frame, footer_left);
peers(&self.peers, frame, footer_right);
network_info(frame, footer_left.inner(Margin::new(1, 1)));
peers(&self.peers, frame, footer_right.inner(Margin::new(1, 1)));
// draw the main frame last
frame.render_widget(self, frame.area());
}
@ -70,7 +70,7 @@ fn network_info(frame: &mut Frame, area: Rect) {
)
.yellow()
.into();
let http: Line = format!(" HTTP address:\t\t{:?}", joecalsend::LISTENING_SOCKET_ADDR)
let http: Line = format!(" HTTP address:\t{:?}", joecalsend::LISTENING_SOCKET_ADDR)
.yellow()
.into();
let items = [

View file

@ -8,7 +8,6 @@ use std::{
collections::HashMap,
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
sync::{Arc, OnceLock},
time::Duration,
};
use models::Device;
@ -108,22 +107,14 @@ impl JoecalState {
}
pub async fn stop(&self) {
loop {
let mut rstate = self.running_state.lock().await;
*rstate = RunningState::Stopping;
if self
.stop_tx
.get()
.expect("Could not get stop signal transmitter")
.send(())
.await
.is_ok()
{
break;
} else {
tokio::time::sleep(Duration::from_millis(777)).await;
}
}
let mut rstate = self.running_state.lock().await;
*rstate = RunningState::Stopping;
let _ = self
.stop_tx
.get()
.expect("Could not get stop signal transmitter")
.send(())
.await;
}
pub async fn refresh_peers(&self) {

View file

@ -1,15 +1,13 @@
#![feature(slice_as_array)]
use frontend::App;
use joecalsend::{Config, JoecalState, error, models::Device};
use joecalsend::{Config, error, models::Device};
use local_ip_address::local_ip;
use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig, V4IfAddr};
use tokio::task::JoinSet;
mod frontend;
#[tokio::main]
async fn main() -> error::Result<()> {
fn main() -> error::Result<()> {
let device = Device::default();
dbg!(&device);
@ -18,7 +16,7 @@ async fn main() -> error::Result<()> {
};
// for enumerating subnet peers when multicast fails (https://github.com/localsend/protocol?tab=readme-ov-file#32-http-legacy-mode)
let mut network_ip = ip;
let mut _network_ip = ip;
let nifs = NetworkInterface::show().unwrap();
for addr in nifs.into_iter().flat_map(|i| i.addr) {
if let Addr::V4(V4IfAddr {
@ -28,43 +26,14 @@ async fn main() -> error::Result<()> {
}) = addr
&& ip == ifip
{
network_ip = ip & netmask;
_network_ip = ip & netmask;
break;
}
}
let state = JoecalState::new(device)
.await
.expect("Could not create application session");
let config = Config::default();
let mut handles = JoinSet::new();
state.start(&config, &mut handles).await;
let mut app = App::new(state.clone());
let mut terminal = ratatui::init();
let result = app.run(&mut terminal).await;
ratatui::restore();
let mut alarm = tokio::time::interval(tokio::time::Duration::from_secs(5));
alarm.tick().await;
let mut app = App::new();
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,
}
}
_ = alarm.tick() => {
println!("Exit timeout reached, aborting all unjoined tasks");
handles.abort_all();
break;
},
}
}
Ok(result?)
Ok(ratatui::run(|terminal| app.run(terminal, config, device))?)
}