From b4f8f46d73d39976f9090c5abb179b06e58fd522 Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 11 Dec 2023 14:58:48 +0100 Subject: [PATCH] Cranberry TUI - init --- Cargo.toml | 10 +-- cranberry.zip | Bin 0 -> 5714 bytes src/app.rs | 52 ++++++++++++ src/command.rs | 4 - src/main.rs | 206 ++++++++------------------------------------- src/net_handler.rs | 63 ++++++++++++++ src/tui.rs | 74 ++++++++++++++++ 7 files changed, 227 insertions(+), 182 deletions(-) create mode 100644 cranberry.zip create mode 100644 src/app.rs delete mode 100644 src/command.rs create mode 100644 src/net_handler.rs create mode 100644 src/tui.rs diff --git a/Cargo.toml b/Cargo.toml index 694fcbc..440d057 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,11 @@ strip = true opt-level = 'z' [dependencies] +ansi-to-tui = "3.1.0" +better-panic = "0.3.0" clap = { version = "4.4.11", features = ["derive"] } -color-eyre = "0.6.2" -fastrand = "2.0.1" +crossterm = "0.27.0" owo-colors = "3.5.0" -rayon = "1.8.0" -rustyline = "13.0.0" +ratatui = "0.24.0" serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" -tokio = { version = "1.35.0", features = ["full"] } - diff --git a/cranberry.zip b/cranberry.zip new file mode 100644 index 0000000000000000000000000000000000000000..f6881cb7d7d0e2303c42cc92e42a45621e289329 GIT binary patch literal 5714 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 X0w4-#cUB_+fPQxk-evE2PXYWFxFixe literal 0 HcmV?d00001 diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..41dfe1f --- /dev/null +++ b/src/app.rs @@ -0,0 +1,52 @@ +use crate::cli::Args; +use crate::net_handler::*; +use crate::tui::ui; +use clap::Parser; +use crossterm::event; +use crossterm::event::{Event, KeyCode}; +use ratatui::backend::Backend; +use ratatui::Terminal; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; +use std::thread::spawn; +use std::time::Duration; + +#[derive(Default)] +pub struct App { + pub input: String, + pub cursor_pos: usize, + pub messages: Vec, + pub message_queue: Vec, + pub input_history: Vec, + pub debug_messages: Vec, +} + +impl App { + pub fn run(self, term: &mut Terminal) { + let app = Arc::new(RwLock::new(self)); + let cli_args = Args::parse(); + let stream = + TcpStream::connect((cli_args.addr, cli_args.port)).expect("Failed to open stream"); + let stream_r = stream.try_clone().unwrap(); + let stream_w = stream.try_clone().unwrap(); + let c1 = app.clone(); + let c2 = c1.clone(); + spawn(|| handler_c2s(c1, stream_r)); + spawn(|| handler_s2c(c2, stream_w)); + loop { + term.draw(|f| ui(f, app.clone())).expect("Error drawing"); + if !event::poll(Duration::from_millis(100)).expect("Failed to poll event") { + continue; + } + if let Event::Key(key) = event::read().expect("Failed to read event") { + match key.code { + KeyCode::Enter => app.write().unwrap().send(), + KeyCode::Esc => return, + KeyCode::Char(c) => app.write().unwrap().enter(c), + KeyCode::Backspace => app.write().unwrap().delete(), + _ => {} + } + } + } + } +} diff --git a/src/command.rs b/src/command.rs deleted file mode 100644 index 13c61f8..0000000 --- a/src/command.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub struct Command<'a> { - pub name: &'a str, - pub handler: fn(Vec) -> Vec, -} diff --git a/src/main.rs b/src/main.rs index 22f0482..a959b70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,179 +1,41 @@ -#![allow(deprecated)] -#![warn( - clippy::all, - clippy::nursery, - clippy::pedantic, -)] - -use std::io::Write; -use crate::command::Command; -use clap::Parser; -use owo_colors::OwoColorize; -use serde_json::Value; -use std::process::exit; -use std::thread::sleep_ms; -use std::time::Duration; -use rayon::prelude::*; -use rustyline::DefaultEditor; -use rustyline::error::ReadlineError; -use tokio::io::{split, AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}; -use tokio::net::TcpStream; -use tokio::time::sleep; - +mod app; mod cli; -mod command; +mod net_handler; +mod tui; -async fn s2c_t(mut r_server: ReadHalf) { - loop { - let mut buff = [0u8; 1]; - let mut str_buf = String::new(); - let mut wraps = 0; - loop { - let n_bytes = r_server.read(&mut buff).await.expect("Failed to read from stream"); - if n_bytes == 0 { - println!("Server closed connection, exiting"); - exit(0); - } - match buff[0] as char { - '{' => { - wraps += 1; - str_buf.push('{'); - } - '}' => { - wraps -= 1; - str_buf.push('}'); - } - c => str_buf.push(c) - } - if wraps == 0 { - break - } - //dbg!(wraps, &str_buf); - } - let msg: Value = match serde_json::from_str(&str_buf) { - Ok(ok) => ok, - Err(e) => { - println!("{} error desering packet ({str_buf}): {e}", "[err]".red()); - continue; - } - }; - match msg["message_type"].as_str() { - Some("system_message") => println!( - "{} {}", - "[sys]".bright_green(), - msg["message"]["content"].as_str().unwrap() - ), - Some("stbchat_backend") => {} - Some("user_message") => println!( - "{} {} ({}) -> {}", - "[msg]".bright_blue(), - msg["username"].as_str().unwrap(), - msg["nickname"].as_str().unwrap(), - msg["message"]["content"].as_str().unwrap() - ), - None => unreachable!(), - m => println!( - "{} Unimplemented packet {} - full packet: {}", - "[uimp]".red(), - m.unwrap(), - str_buf - ), - } - } -} - - -async fn c2s_t(mut w_server: WriteHalf) { - let cmds = vec![ - Command { - name: "randchars", - handler: |a| { - let num_chars = a.first().map_or(600, |s| s.parse().unwrap_or(600)); - let num_messages = a.get(1).map_or(1, |s| s.parse().unwrap_or(1)); - let mut res = vec![]; - for _ in 0..num_messages { - let mut bytes = String::new(); - for _ in 0..num_chars { - bytes.push(fastrand::char(..)); - } - res.push(bytes); - } +use crate::cli::Args; +use better_panic::Settings; +use clap::Parser; +use crossterm::execute; +use crossterm::terminal::*; +use ratatui::prelude::*; +use std::io; +use std::io::stdout; +use crate::app::App; - res - }, - }, - Command { - name: "loginspam", - handler: |a| { - if a.len() < 2 { - println!("Not enough args - 2 required"); - return vec![]; - } - let args = cli::Args::parse(); - let user = a.first().unwrap(); - let pass = a.get(1).unwrap(); - let logins: u64 = a.get(2).unwrap_or(&"10".to_string()).parse().unwrap_or(10); - (0..logins).into_par_iter().for_each(|_| { - let mut stream = std::net::TcpStream::connect((args.addr.clone(), args.port)).unwrap(); - sleep_ms(230); - stream.write_all(user.as_bytes()).unwrap(); - sleep_ms(230); - stream.write_all(pass.as_bytes()).unwrap(); - sleep_ms(300); - }); - vec![] - }, - }, - ]; - let mut rl = DefaultEditor::new().unwrap(); - loop { - let line = match rl.readline("") { - Ok(l) => { - rl.add_history_entry(&l).unwrap(); - l - }, - Err(ReadlineError::Eof) => exit(0), - Err(ReadlineError::Interrupted) => { - w_server.write_all("/exit".as_bytes()).await.expect("Failed to write to stream"); - sleep(Duration::from_millis(400)).await; - exit(0); - }, - Err(why) => panic!("{why}") - }; - let f = line.split(' ').next().unwrap(); - let cmd_filt = cmds.iter().filter(|c| c.name == f); - for cmd in cmd_filt.clone() { - let args: Vec = line.split(' ').map(String::from).skip(1).collect(); - for mut m in (cmd.handler)(args) { - while m.len() > 4096 { - m.pop(); - } - w_server - .write_all(m.as_bytes()) - .await - .expect("Failed to write to stream"); - sleep(Duration::from_millis(50)).await; - } - } - if cmd_filt.count() > 0 { - continue; - } - let read_str = line.trim_end_matches('\n'); - w_server - .write_all(read_str.as_bytes()) - .await - .expect("Failed to write to stream"); - } +pub fn initialize_panic_handler() { + std::panic::set_hook(Box::new(|panic_info| { + execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap(); + disable_raw_mode().unwrap(); + Settings::auto() + .most_recent_first(false) + .lineno_suffix(true) + .create_panic_handler()(panic_info); + })); } -#[tokio::main] -async fn main() -> color_eyre::Result<()> { - color_eyre::install().unwrap(); - rayon::ThreadPoolBuilder::new().num_threads(5).build_global().unwrap(); - let args = cli::Args::parse(); - let stream = TcpStream::connect((args.addr, args.port)).await?; - let halves = split(stream); - tokio::spawn(s2c_t(halves.0)); - tokio::spawn(c2s_t(halves.1)).await.expect("Join error"); +fn main() -> io::Result<()> { + initialize_panic_handler(); + enable_raw_mode()?; + Args::parse(); + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + terminal.clear()?; + let app = App::default(); + app.run(&mut terminal); + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + disable_raw_mode()?; Ok(()) } diff --git a/src/net_handler.rs b/src/net_handler.rs new file mode 100644 index 0000000..b009b0d --- /dev/null +++ b/src/net_handler.rs @@ -0,0 +1,63 @@ +use crossterm::style::Stylize; +use owo_colors::OwoColorize; +use serde_json::Value; +use std::io::Write; +use std::net::TcpStream; +use std::sync::{Arc, RwLock}; +use crate::app::App; + +pub fn handler_s2c(app: Arc>, stream: TcpStream) { + let deser = serde_json::Deserializer::from_reader(stream); + let messages_iter = deser.into_iter::(); + for message in messages_iter { + match message { + Err(e) => app.write().unwrap().messages.push(format!( + "{} Error deserializing packet - {}", + "[err]".red(), + e + )), + Ok(msg) => { + let to_push = match msg["message_type"].as_str() { + Some("system_message") => Some(format!( + "{} {}", + "[sys]".bright_green(), + msg["message"]["content"].as_str().unwrap() + )), + Some("stbchat_backend") => None, + Some("user_message") => Some(format!( + "{} {} ({}) -> {}", + "[msg]".bright_blue(), + msg["username"].as_str().unwrap(), + msg["nickname"].as_str().unwrap(), + msg["message"]["content"].as_str().unwrap() + )), + None => None, + m => { + println!("{} Unimplemented packet {}", "[uimp]".red(), m.unwrap()); + None + } + }; + if let Some(text) = to_push { + app.write().unwrap().messages.push(text); + } + } + } + } +} + +pub fn handler_c2s(app: Arc>, mut stream: TcpStream) { + loop { + let state = app.read().unwrap(); + let msgs = state.message_queue.clone(); + drop(state); + match msgs.last() { + Some(msg) => { + stream + .write_all(msg.as_bytes()) + .expect("Failed to write to stream"); + app.write().unwrap().message_queue.pop(); + } + None => {} + } + } +} diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..87d086b --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,74 @@ +use ansi_to_tui::IntoText; +use ratatui::prelude::*; +use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph}; +use std::sync::{Arc, RwLock}; +use crate::app::App; + +pub fn ui(frame: &mut Frame, app: Arc>) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Min(3), + ]) + .split(frame.size()); + let text = Text::from(Line::from("Press ESC to exit")); + let esc_info = Paragraph::new(text); + frame.render_widget(esc_info, chunks[0]); + + let state = app.read().unwrap(); + let input = Paragraph::new(state.input.as_str()) + .block(Block::default().borders(Borders::ALL).title("Message")); + frame.render_widget(input, chunks[1]); + + let messages: Vec = state + .messages + .iter() + .rev() + .map(|m| { + let line = m.into_text().unwrap(); + ListItem::new(line) + }) + .collect(); + let messages = + List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages")); + frame.render_widget(messages, chunks[2]); +} + + + +impl App { + pub fn move_cursor_left(&mut self) { + let moved = self.cursor_pos.saturating_sub(1); + self.cursor_pos = moved.clamp(0, self.input.len()); + } + pub fn move_cursor_right(&mut self) { + let moved = self.cursor_pos + 1; + self.cursor_pos = moved.clamp(0, self.input.len()); + } + + pub fn enter(&mut self, ch: char) { + self.input.insert(self.cursor_pos, ch); + self.move_cursor_right(); + } + + pub fn delete(&mut self) { + if self.cursor_pos == 0 { + return; + } + let char_to_delete_pos = self.cursor_pos - 1; + self.input.remove(char_to_delete_pos); + self.move_cursor_left(); + } + + pub fn reset_cursor(&mut self) { + self.cursor_pos = 0; + } + + pub fn send(&mut self) { + self.message_queue.push(self.input.clone()); + self.input = String::new(); + self.reset_cursor(); + } +}