diff --git a/client/Cargo.toml b/client/Cargo.toml index bba9e7b..948dde1 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,3 +9,10 @@ tokio = { version = "1.35.1", features = ["full"] } # Async runtime tokio-tungstenite = { version = "0.21.0", features = [ "native-tls", ] } # Async WebSocket + +# Terminal output +ratatui = "0.25" +crossterm = "0.27" + +# Error handling +anyhow = "1.0.76" diff --git a/client/src/main.rs b/client/src/main.rs index 7c6fe63..c072865 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,14 +1,20 @@ +use std::io::BufRead; + use futures_util::{stream::SplitStream, SinkExt, StreamExt}; use tokio::{ - io::{AsyncBufReadExt, BufReader}, + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::TcpStream, sync::mpsc, task::JoinHandle, }; use tokio_tungstenite::{connect_async, tungstenite::protocol::Message, WebSocketStream}; +mod tui; + #[tokio::main] async fn main() { + // tui::init().expect("Failed to draw TUI"); + let url = "wss://echo.websocket.events"; let (ws_stream, _) = connect_async(url).await.expect("Failed to connect"); @@ -17,23 +23,24 @@ async fn main() { let (mut write, read) = ws_stream.split(); let (write_tx, mut write_rx) = mpsc::unbounded_channel(); - let write_thread: JoinHandle> = - tokio::spawn(async move { - while let Some(msg) = write_rx.recv().await { - write.send(msg).await?; - } - Ok(()) - }); + let write_thread: JoinHandle> = tokio::spawn(async move { + while let Some(msg) = write_rx.recv().await { + write.send(msg).await?; + } + Ok(()) + }); let input = tokio::spawn(async move { - let mut lines = BufReader::new(tokio::io::stdin()).lines(); - while let Some(line) = lines.next_line().await.expect("Terminal got closed") { + let lines = std::io::BufReader::new(std::io::stdin()).lines(); + for line in lines { + println!("Sending: {}", line.as_ref().unwrap()); + let line = line.expect("Failed to read line"); write_tx.send(Message::text(line.trim())).unwrap(); } println!("Terminal got closed, exiting..."); }); - let output = tokio::spawn(print_messages(read)); + let output = tokio::spawn(recieve(read)); tokio::select! { Ok(e) = output => println!("Websocket read thread exited: {:?}", e.err()), @@ -41,11 +48,14 @@ async fn main() { _ = input => println!("stdin thread exited"), _ = tokio::signal::ctrl_c() => println!("Ctrl-C received"), } + println!("Exiting..."); + tokio::io::stdout().flush().await.unwrap(); + println!("Exited"); } -async fn print_messages( - mut read: SplitStream>>, -) -> Result<(), tokio_tungstenite::tungstenite::Error> { +type WebsocketStream = SplitStream>>; + +async fn recieve(mut read: WebsocketStream) -> anyhow::Result<()> { while let Some(message) = read.next().await { let msg = message?; if msg.is_close() { diff --git a/client/src/tui.rs b/client/src/tui.rs new file mode 100644 index 0000000..3d23be0 --- /dev/null +++ b/client/src/tui.rs @@ -0,0 +1,51 @@ +use crossterm::{ + event::{self, KeyCode, KeyEventKind}, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, +}; +use ratatui::{ + prelude::{CrosstermBackend, Stylize, Terminal}, + widgets::Paragraph, +}; +use std::io::{stdout, Result, Stdout}; + +pub fn init() -> Result<()> { + initialize_panic_handler(); + stdout().execute(EnterAlternateScreen)?; + enable_raw_mode()?; + let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; + terminal.clear()?; + + loop { + terminal.draw(|frame| { + let area = frame.size(); + frame.render_widget( + Paragraph::new("Hello Ratatui! (press 'q' to quit)") + .white() + .on_blue(), + area, + ); + })?; + + if event::poll(std::time::Duration::from_millis(16))? { + if let event::Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { + break; + } + } + } + } + + stdout().execute(LeaveAlternateScreen)?; + disable_raw_mode()?; + Ok(()) +} + +pub fn initialize_panic_handler() { + let original_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap(); + crossterm::terminal::disable_raw_mode().unwrap(); + original_hook(panic_info); + })); +}