From 71931457b27aaedb90194550fab076d1548d2ed5 Mon Sep 17 00:00:00 2001 From: Meisterlala <6453306+Meisterlala@users.noreply.github.com> Date: Thu, 28 Mar 2024 17:28:56 +0100 Subject: [PATCH] improvements --- client/src/app.rs | 99 +++++++++++++++++++++++++++++----------- client/src/input.rs | 6 ++- client/src/lib.rs | 1 + client/src/main.rs | 29 ++++++++++-- client/src/model.rs | 39 ++++++++-------- client/src/tui.rs | 52 +++++++++++++-------- client/src/websocket.rs | 10 ++-- server/src/connection.rs | 16 +++++-- server/src/main.rs | 22 ++++++--- 9 files changed, 187 insertions(+), 87 deletions(-) diff --git a/client/src/app.rs b/client/src/app.rs index 625ad9c..fd4d589 100644 --- a/client/src/app.rs +++ b/client/src/app.rs @@ -1,5 +1,5 @@ use futures_util::{select, FutureExt}; -use log::info; +use log::{error, info}; use crate::{ input::EventHandler, @@ -11,6 +11,7 @@ use crate::{ pub struct Application { pub url: String, + pub user_name: String, pub input: EventHandler, pub tui: TUI, pub model: Model, @@ -18,24 +19,29 @@ pub struct Application { } impl Application { - pub fn new(ws_url: &str) -> Self { - let mut t = TUI::new(); - TUI::initialize_panic_handler(); - t.enter().unwrap(); - + pub fn new(ws_url: &str, user_name: &str) -> Self { Self { url: ws_url.to_string(), + user_name: user_name.to_string(), tui: TUI::new(), input: EventHandler::new(), - model: Model::default(), + model: Model { + url: ws_url.to_string(), + username: user_name.to_string(), + ..Default::default() + }, ws: Websocket::loopback(), } } - pub async fn run(mut self) -> anyhow::Result<()> { + pub async fn run(mut self) -> bool { info!("Starting Application"); - self.ws = Websocket::connect(&self.url).await.unwrap(); + // Wait for server start + self.ws = Self::wait_for_websocket(&self.url).await; + + TUI::initialize_panic_handler(); + self.tui.enter().unwrap(); let render_thread = tokio::spawn(async move { loop { @@ -49,16 +55,24 @@ impl Application { msg } msg = self.ws.recieve().fuse() => { - Event::ReciveMessage(ChatMessage { - username: "other person".to_string(), - message: msg.unwrap(), - }) + // Cant recive message, restart + if msg.is_err() { + error!("Failed to recive message, restarting"); + Event::Restart + // Deserialize message + } else if let Some(msg) = ChatMessage::deserialize(&msg.unwrap()) { + Event::ReciveMessage(msg) + } else { + continue; + } } }; match event { - Event::Quit => { - break; + Event::Quit | Event::Restart => { + TUI::exit().unwrap(); + + return event; } other => { self.update(other); @@ -68,19 +82,29 @@ impl Application { }); tokio::select! { - _ = render_thread => info!("Render thread exited"), + e = render_thread => { + match e { + Ok(Event::Quit) => { + TUI::exit().unwrap(); + return false; + } + Ok(Event::Restart) => { + TUI::exit().unwrap(); + info!("Connection lost"); + return true; + } + _ => {info!("Render thread exited")} + } + }, } - info!("Exiting Application"); - Ok(()) + info!("Exiting UI"); + false } pub fn update(&mut self, event: Event) { match event { Event::Input(c) => match c { - 'c' => { - self.model.counter += 1; - } _ => { self.model.text_area.push(c); } @@ -89,6 +113,9 @@ impl Application { Event::Quit => { unreachable!("Quit event should be handled in run()"); } + Event::Restart => { + unreachable!("Restart event should be handled in run()"); + } Event::Resize { width, height } => { self.tui.resize(width, height); } @@ -96,17 +123,35 @@ impl Application { self.model.messages.push(msg); } Event::Send => { - let txt = self.model.text_area.clone(); + if self.model.text_area.is_empty() { + return; + } + let msg = ChatMessage { + username: self.user_name.clone(), + message: self.model.text_area.clone(), + }; + self.model.text_area.clear(); - self.update(Event::ReciveMessage(ChatMessage { - username: "me".to_string(), - message: txt.clone(), - })); - self.ws.send(txt).unwrap(); + self.ws.send(msg.serialize()).unwrap(); } Event::Backspace => { self.model.text_area.pop(); } }; } + + async fn wait_for_websocket(url: &str) -> Websocket { + loop { + let connection = Websocket::connect(url).await; + match connection { + Ok(c) => { + return c; + } + Err(e) => { + error!("Failed to connect to server: {}", e); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + } + } + } } diff --git a/client/src/input.rs b/client/src/input.rs index 7c0abb5..a245f38 100644 --- a/client/src/input.rs +++ b/client/src/input.rs @@ -23,7 +23,7 @@ impl EventHandler { match event.await { Some(Ok(event)) => { if let Some(message) = Self::handle_event(event) { - events_tx.send(message).unwrap(); + if events_tx.send(message).is_ok() {} } } Some(Err(e)) => error!("Error: {:?}\r", e), @@ -54,6 +54,10 @@ impl EventHandler { kind: KeyEventKind::Press, .. }) => Self::handle_key(code), + CEvent::Key(KeyEvent { + kind: KeyEventKind::Release, + .. + }) => None, e => { debug!("Unhandled event: {:?}", e); None diff --git a/client/src/lib.rs b/client/src/lib.rs index 9dfa99c..e44382f 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -10,6 +10,7 @@ pub enum Event { Input(char), Refresh, Quit, + Restart, Resize { width: u16, height: u16 }, Send, Backspace, diff --git a/client/src/main.rs b/client/src/main.rs index e91909c..661f768 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,5 +1,5 @@ use chat_client::{app::Application, tui::TUI}; -use log::info; +use log::{error, info}; #[tokio::main] async fn main() { @@ -8,9 +8,30 @@ async fn main() { // Setup env_logger to write to stderr env_logger::init(); - let app = Application::new("ws://127.0.0.1:9001"); - app.run().await.unwrap(); + // Get Name + let mut name = String::new(); + println!("Enter your name: "); + std::io::stdin().read_line(&mut name).unwrap(); + let name = match name.trim() { + "" => "Anonymous", + name => name, + }; + + // Get Server Address + let mut address = String::new(); + println!( + "Enter server address and port (leave empty for default value 'ws://127.0.0.1:9001'): " + ); + std::io::stdin().read_line(&mut address).unwrap(); + let address = match address.trim() { + "" => "ws://127.0.0.1:9001", + address => address, + }; + + // Run until the application returns false + while Application::new(address, name).run().await { + error!("Application Disconnected. Press any key to reconnect"); + } - info!("Exiting"); TUI::exit().expect("Failed to reset terminal"); } diff --git a/client/src/model.rs b/client/src/model.rs index 5826174..fa5831c 100644 --- a/client/src/model.rs +++ b/client/src/model.rs @@ -1,5 +1,6 @@ pub struct Model { - pub counter: u32, + pub url: String, + pub username: String, pub text_area: String, pub messages: Vec, } @@ -9,29 +10,27 @@ pub struct ChatMessage { pub message: String, } +impl ChatMessage { + pub fn serialize(&self) -> String { + format!("{}: {}", self.username, self.message) + } + + // Security issue: a username can contain ": ", which would break the deserialization + pub fn deserialize(s: &str) -> Option { + let mut parts = s.splitn(2, ": "); + let username = parts.next()?.to_string(); + let message = parts.next()?.to_string(); + Some(Self { username, message }) + } +} + impl Default for Model { fn default() -> Self { Self { - counter: 0, + username: String::new(), + url: String::new(), text_area: String::new(), - messages: vec![ - ChatMessage { - username: "test".to_string(), - message: "wow you are really cool".to_string(), - }, - ChatMessage { - username: "test2".to_string(), - message: "Thank you!".to_string(), - }, - ChatMessage { - username: "test".to_string(), - message: "How was your day".to_string(), - }, - ChatMessage { - username: "test2".to_string(), - message: "It was good".to_string(), - }, - ], + messages: vec![], } } } diff --git a/client/src/tui.rs b/client/src/tui.rs index 9d8cd4e..1369389 100644 --- a/client/src/tui.rs +++ b/client/src/tui.rs @@ -6,6 +6,7 @@ use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, prelude::{CrosstermBackend, Stylize, Terminal as RatatuiTerminal}, symbols::block, + text::{Line, Span}, widgets::{Block, Borders, Paragraph, Wrap}, Frame, }; @@ -50,15 +51,18 @@ impl TUI { fn draw(frame: &mut Frame, model: &Model) { let area = frame.size(); frame.render_widget( - Paragraph::new(format!("Counter: {}", model.counter)) - .white() - .on_blue(), + Paragraph::new(format!( + "Chat Client, logged into {} as {}", + model.url, model.username + )) + .white() + .on_blue(), area, ); let layout = Layout::default() .direction(Direction::Vertical) - .constraints(vec![Constraint::Percentage(80), Constraint::Percentage(20)]) + .constraints(vec![Constraint::Percentage(90), Constraint::Min(3)]) .split(frame.size()); let t = &model.text_area; @@ -87,24 +91,32 @@ impl TUI { .messages .iter() .rev() - .take((layout[0].height - 2) as usize); + .take((layout[0].height - 1) as usize); let mut y = layout[0].bottom() - 1; for message in messages { - for line in message.message.lines() { - frame.render_widget( - Paragraph::new(format!("{}: {}", message.username, line)) - .white() - .on_blue() - .wrap(Wrap { trim: true }), - Rect { - x: layout[0].left() + 1, - y, - width: layout[0].width - 2, - height: 1, - }, - ); - y -= 1; - } + let line = Line::from(vec![ + if message.username == model.username { + Span::styled(&message.username, ratatui::style::Style::default().bold()) + } else { + Span::styled(&message.username, ratatui::style::Style::default()) + }, + ": ".into(), + (&message.message).into(), + ]); + + frame.render_widget( + Paragraph::new(line) + .white() + .on_blue() + .wrap(Wrap { trim: true }), + Rect { + x: layout[0].left() + 1, + y, + width: layout[0].width - 2, + height: 1, + }, + ); + y -= 1; } } diff --git a/client/src/websocket.rs b/client/src/websocket.rs index 8e80374..410369e 100644 --- a/client/src/websocket.rs +++ b/client/src/websocket.rs @@ -62,11 +62,9 @@ impl Websocket { } pub async fn recieve(&mut self) -> anyhow::Result { - let msg = self.read.recv().await.unwrap(); - Ok(msg) - } - - pub fn status(&self) -> Result<()> { - Ok(()) + match self.read.recv().await { + Some(msg) => Ok(msg), + None => Err(anyhow::anyhow!("Failed to recieve message")), + } } } diff --git a/server/src/connection.rs b/server/src/connection.rs index d2d5ac9..ff921c3 100644 --- a/server/src/connection.rs +++ b/server/src/connection.rs @@ -14,23 +14,29 @@ impl Connection { let (tx_write, mut rx_write) = tokio::sync::mpsc::unbounded_channel(); let t = tokio::spawn(async move { + let connected_to = stream.get_ref().peer_addr().unwrap().to_string(); let (mut ws_write, mut ws_read) = stream.split(); loop { tokio::select! { + _ = tokio::signal::ctrl_c() => { + debug!("Ctrl-C recieved, dropping connection to {}", connected_to); + ws_write.send(tokio_tungstenite::tungstenite::Message::Close(None)).await.unwrap(); + break; + } Some(msg) = rx_write.recv() => { - debug!("Sending message: {}", msg); + debug!("<{}> Sending message: {}", connected_to, msg); ws_write.send(tokio_tungstenite::tungstenite::Message::Text(msg)).await.unwrap(); } Some(msg) = ws_read.next() => { match msg { Ok(msg) => { let msg = msg.into_text().expect("Failed to convert message to text"); - debug!("Recieved message: {}", msg); + debug!("<{}> Recieved message: {}", connected_to, msg); tx_read.send(msg).unwrap(); } Err(e) => { - error!("Error reading from websocket: {}", e); + error!("<{}> Error reading from websocket: {}", connected_to, e); break; } } @@ -55,6 +61,10 @@ impl Connection { ws_write.send(msg).await.unwrap(); } } + ws_write + .send(tokio_tungstenite::tungstenite::Message::Close(None)) + .await + .unwrap(); debug!("Loopback closed") }); diff --git a/server/src/main.rs b/server/src/main.rs index 9a5a419..71a2813 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,19 +1,29 @@ -use std::net::TcpListener; - use chat_server::websocket::Websocket; use futures_util::FutureExt; use log::info; #[tokio::main] async fn main() { - env_logger::init(); + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + // Get Port + let mut port = String::new(); + println!("Enter server port (leave empty for default value '9001'): "); + std::io::stdin().read_line(&mut port).unwrap(); + let adress = match port.trim() { + "" => "127.0.0.1:9001".to_string(), + port => format!("127.0.0.1:{}", port), + }; - let ws = Websocket::new("127.0.0.1:9001"); + let ws = Websocket::new(&adress); - info!("Strted"); + info!("Started"); tokio::select! { - _ = tokio::signal::ctrl_c() => {info!("Ctrl-C recieved")}, + _ = tokio::signal::ctrl_c() => { + info!("Ctrl-C recieved"); + } r = ws.serve().fuse() => {info!("Websocket task ended with: {:?}", r)}, } + info!("Exiting"); } -- 2.51.2