diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 0e31d16..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "berublan.vscode-log-viewer" - ] -} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 56984be..b9f1c77 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -45,7 +45,7 @@ "args": [], "cwd": "${workspaceFolder}", "env": { - "RUST_LOG": "chat_client=debug", + "RUST_LOG": "debug", "RUST_BACKTRACE": "1", }, "stdio": [ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index fd9a93c..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "logViewer.watch": [ - { - "title": "Client", - "pattern": "client.log" - }, - { - "title": "Server", - "pattern": "server.log" - } - ] -} \ No newline at end of file diff --git a/client/src/app.rs b/client/src/app.rs index 3d4f142..92a5a56 100644 --- a/client/src/app.rs +++ b/client/src/app.rs @@ -1,11 +1,17 @@ -use std::sync::{atomic, Mutex}; +use std::{ + collections::VecDeque, + sync::{atomic, Arc, Mutex}, +}; +use futures_util::{select, FutureExt}; use log::info; use ratatui::widgets::RenderDirection; +use tokio::sync::mpsc; +use tokio_tungstenite::tungstenite::Message; use crate::{ input::EventHandler, - model::Model, + model::{ChatMessage, Model}, tui::{TUIMessage, TUI}, websocket::Websocket, Event, @@ -16,6 +22,7 @@ pub struct Application { pub input: EventHandler, pub tui: TUI, pub model: Model, + pub ws: Websocket, } impl Application { @@ -29,17 +36,14 @@ impl Application { tui: TUI::new(), input: EventHandler::new(), model: Model::default(), + ws: Websocket::loopback(), } } pub async fn run(mut self) -> anyhow::Result<()> { - let mut ws = Websocket::new(&self.url).await; + info!("Starting Application"); - let echo_thread = tokio::spawn(async move { - while let Ok(msg) = ws.recieve().await { - info!("Received: {}", msg.trim()); - } - }); + self.ws = Websocket::connect(&self.url).await.unwrap(); let render_thread = tokio::spawn(async move { loop { @@ -48,7 +52,18 @@ impl Application { None }); - let event = self.input.next().await; + let event = select! { + msg = self.input.next().fuse() => { + msg + } + msg = self.ws.recieve().fuse() => { + Event::ReciveMessage(ChatMessage { + username: "other person".to_string(), + message: msg.unwrap(), + }) + } + }; + match event { Event::Quit => { break; @@ -61,7 +76,6 @@ impl Application { }); tokio::select! { - _ = echo_thread => info!("Echo thread exited"), _ = render_thread => info!("Render thread exited"), } @@ -73,12 +87,10 @@ impl Application { match event { Event::Input(c) => match c { 'c' => { - self.model - .counter - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + self.model.counter += 1; } _ => { - self.model.text_area.lock().unwrap().push(c); + self.model.text_area.push(c); } }, Event::Refresh => {} @@ -88,9 +100,20 @@ impl Application { Event::Resize { width, height } => { self.tui.resize(width, height); } - Event::Send => {} + Event::ReciveMessage(msg) => { + self.model.messages.push(msg); + } + Event::Send => { + let txt = 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(); + } Event::Backspace => { - self.model.text_area.lock().unwrap().pop(); + self.model.text_area.pop(); } }; } diff --git a/client/src/lib.rs b/client/src/lib.rs index 3e33222..9dfa99c 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,3 +1,5 @@ +use model::ChatMessage; + pub mod app; pub mod input; pub mod model; @@ -11,4 +13,5 @@ pub enum Event { Resize { width: u16, height: u16 }, Send, Backspace, + ReciveMessage(ChatMessage), } diff --git a/client/src/model.rs b/client/src/model.rs index 9a2d686..2f9379d 100644 --- a/client/src/model.rs +++ b/client/src/model.rs @@ -1,8 +1,10 @@ use std::sync::{atomic, Mutex}; +use tokio_tungstenite::tungstenite::Message; + pub struct Model { - pub counter: atomic::AtomicU32, - pub text_area: Mutex, + pub counter: u32, + pub text_area: String, pub messages: Vec, } @@ -11,14 +13,29 @@ pub struct ChatMessage { pub message: String, } - - impl Default for Model { fn default() -> Self { Self { - counter: atomic::AtomicU32::new(0), - text_area: Mutex::new(String::new()), - messages: Vec::new(), + counter: 0, + 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(), + }, + ], } } } diff --git a/client/src/tui.rs b/client/src/tui.rs index 609901e..d7b19e9 100644 --- a/client/src/tui.rs +++ b/client/src/tui.rs @@ -37,12 +37,14 @@ impl TUI { } pub fn resize(&mut self, width: u16, height: u16) { - self.terminal.resize(Rect { - x: 0, - y: 0, - width, - height, - }); + self.terminal + .resize(Rect { + x: 0, + y: 0, + width, + height, + }) + .unwrap(); } fn draw(frame: &mut Frame, model: &Model) { @@ -50,7 +52,7 @@ impl TUI { frame.render_widget( Paragraph::new(format!( "Counter: {}", - model.counter.load(std::sync::atomic::Ordering::SeqCst) + model.counter )) .white() .on_blue(), @@ -62,7 +64,7 @@ impl TUI { .constraints(vec![Constraint::Percentage(80), Constraint::Percentage(20)]) .split(frame.size()); - let t = &model.text_area.lock().unwrap(); + let t = &model.text_area; let avaliable_space = (layout[1].height - 2) * (layout[1].width - 2); let text = if avaliable_space < t.len() as u16 { @@ -82,6 +84,31 @@ impl TUI { .block(block), layout[1], ); + + // Render all messages line by line alligned to bottom + let messages = model + .messages + .iter() + .rev() + .take((layout[0].height - 2) 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; + } + } } /// Enter raw mode and the alternate screen diff --git a/client/src/websocket.rs b/client/src/websocket.rs index d9a0f02..410b799 100644 --- a/client/src/websocket.rs +++ b/client/src/websocket.rs @@ -5,51 +5,59 @@ use tokio::net::TcpStream; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; pub struct Websocket { - pub url: String, - socket: WebSocketStream>, + pub read: tokio::sync::mpsc::UnboundedReceiver, + pub write: tokio::sync::mpsc::UnboundedSender, } impl Websocket { - pub async fn new(url: &str) -> Self { - let (ws_stream, response) = tokio_tungstenite::connect_async(url) - .await - .unwrap_or_else(|_| panic!("Failed to connect to websocket: {}", url)); - - info!("WebSocket handshake has been successfully completed"); - + pub fn loopback() -> Self { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); Self { - url: url.to_string(), - socket: ws_stream, + read: rx, + write: tx, } } - pub async fn send(&mut self, msg: String) -> anyhow::Result<()> { - self.socket - .send(tokio_tungstenite::tungstenite::Message::Text(msg)) - .await?; - Ok(()) - } + pub async fn connect(url: &str) -> Result { + let (ws_stream, response) = tokio_tungstenite::connect_async(url).await?; - pub async fn recieve(&mut self) -> anyhow::Result { - let msg = self.socket.next().await; - let msg = msg.expect("Failed to recieve message"); - let msg = msg.expect("Failed to recieve message"); - let msg = msg.into_text().expect("Failed to convert message to text"); - Ok(msg) - } + info!("WebSocket handshake has been successfully completed"); - async fn reconnect(&mut self) -> anyhow::Result<()> { - let (ws_stream, response) = tokio_tungstenite::connect_async(&self.url) - .await - .expect(&format!("Failed to connect to websocket: {}", self.url)); + let (tx_read, rx_read) = tokio::sync::mpsc::unbounded_channel(); + let (tx_write, mut rx_write) = tokio::sync::mpsc::unbounded_channel(); - info!("WebSocket handshake has been successfully completed"); + tokio::spawn(async move { + let (mut ws_write, mut ws_read) = ws_stream.split(); - self.socket = ws_stream; + loop { + tokio::select! { + Some(msg) = rx_write.recv() => { + ws_write.send(tokio_tungstenite::tungstenite::Message::Text(msg)).await.unwrap(); + } + Some(msg) = ws_read.next() => { + let msg = msg.unwrap().into_text().expect("Failed to convert message to text"); + tx_read.send(msg).unwrap(); + } + } + } + }); + + Ok(Self { + read: rx_read, + write: tx_write, + }) + } + pub fn send(&mut self, msg: String) -> anyhow::Result<()> { + self.write.send(msg).unwrap(); Ok(()) } + pub async fn recieve(&mut self) -> anyhow::Result { + let msg = self.read.recv().await.unwrap(); + Ok(msg) + } + pub fn status(&self) -> Result<()> { Ok(()) }