diff --git a/src/chat.rs b/src/chat.rs index 3d60634..d47b389 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,10 +1,42 @@ -use std::{error::Error, io::{stdout, Write}, sync::{atomic::Ordering, Arc}, time::{Duration, SystemTime}}; +use std::{cmp::{max, min}, error::Error, io::{stdout, Write}, sync::{atomic::{AtomicUsize, Ordering}, Arc, RwLock}, thread, time::{Duration, SystemTime}}; +use clap::builder::Str; use colored::{Color, Colorize}; -use crossterm::{cursor::MoveLeft, event::{self, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode}, ExecutableCommand}; +use crossterm::{cursor::MoveLeft, event::{self, Event, KeyCode, KeyModifiers, MouseEventKind}, terminal::{disable_raw_mode, enable_raw_mode}}; use rand::random; -use super::{proto::read_messages, util::sanitize_text, ADVERTISEMENT, COLORED_USERNAMES, DATE_REGEX, config::Context, proto::send_message}; +use crate::IP_REGEX; + +use super::{proto::read_messages, util::sanitize_text, COLORED_USERNAMES, DATE_REGEX, config::Context, proto::send_message}; + + +pub struct ChatStorage { + messages: RwLock>, + packet_size: AtomicUsize +} + +impl ChatStorage { + pub fn new() -> Self { + ChatStorage { + messages: RwLock::new(Vec::new()), + packet_size: AtomicUsize::default() + } + } + + pub fn packet_size(&self) -> usize { + self.packet_size.load(Ordering::SeqCst) + } + + pub fn messages(&self) -> Vec { + self.messages.read().unwrap().clone() + } + + pub fn update(&self, messages: Vec, packet_size: usize) { + self.packet_size.store(packet_size, Ordering::SeqCst); + *self.messages.write().unwrap() = messages; + } +} + fn on_command(ctx: Arc, command: &str) -> Result<(), Box> { let command = command.trim_start_matches("/"); @@ -12,9 +44,15 @@ fn on_command(ctx: Arc, command: &str) -> Result<(), Box> { let args = args.split(" ").collect::>(); if command == "clear" { - send_message(ctx.clone(), &format!("\r\x1B[1A{}", " ".repeat(64)).repeat(ctx.max_messages))?; + send_message(&ctx.host, + &prepare_message(ctx.clone(), + &format!("\r\x1B[1A{}", " ".repeat(64)).repeat(ctx.max_messages) + ))?; } else if command == "spam" { - send_message(ctx.clone(), &format!("\r\x1B[1A{}{}", args.join(" "), " ".repeat(10)).repeat(ctx.max_messages))?; + send_message(&ctx.host, + &prepare_message(ctx.clone(), + &format!("\r\x1B[1A{}{}", args.join(" "), " ".repeat(10)).repeat(ctx.max_messages) + ))?; } else if command == "help" { write!(stdout(), "Help message:\r /help - show help message\r @@ -25,12 +63,12 @@ fn on_command(ctx: Arc, command: &str) -> Result<(), Box> { Press enter to close")?; stdout().flush()?; } else if command == "ping" { - let mut before = ctx.messages.1.load(Ordering::SeqCst); + let mut before = ctx.messages.packet_size(); let start = SystemTime::now(); let message = format!("Checking ping... {:X}", random::()); - send_message(ctx.clone(), &message)?; + send_message(&ctx.host, &message)?; loop { - let data = read_messages(ctx.clone(), before).ok().flatten(); + let data = read_messages(&ctx.host, ctx.max_messages, before).ok().flatten(); if let Some((data, size)) = data { if let Some(last) = data.iter().rev().find(|o| o.contains(&message)) { @@ -44,49 +82,67 @@ Press enter to close")?; } } } - send_message(ctx.clone(), &format!("Ping = {}ms", start.elapsed().unwrap().as_millis()))?; + send_message(&ctx.host, &format!("Ping = {}ms", start.elapsed().unwrap().as_millis()))?; } Ok(()) } + pub fn print_console(context: Arc, messages: Vec, input: &str) -> Result<(), Box> { let text = format!( - "{}{}\n> {}", - "\n".repeat(context.max_messages - messages.len()), - if context.disable_formatting { - messages - } else { - messages.into_iter().filter_map(|o| format_message(context.clone(), o)).collect() - }.join("\n"), + "{}{}\r\n> {}", + "\r\n".repeat(context.max_messages - messages.len()), + messages.join("\r\n"), input ); - for line in text.lines() { - write!(stdout().lock(), "\r\n{}", line)?; - stdout().lock().flush()?; - } + + let mut out = stdout().lock(); + write!(out, "{}", text)?; + out.flush()?; + Ok(()) } + +fn prepare_message(context: Arc, message: &str) -> String { + format!("{}{}{}", + if !context.disable_hiding_ip { + "\r\x07" + } else { + "" + }, + message, + if !context.disable_hiding_ip && message.chars().count() < 39 { + " ".repeat(39-message.chars().count()) + } else { + String::new() + } + ) +} + + fn format_message(ctx: Arc, message: String) -> Option { - let message = message.trim_end_matches(ADVERTISEMENT); let message = sanitize_text(&message); - if ADVERTISEMENT.len() > 0 && - message.starts_with(ADVERTISEMENT - .trim_start_matches("\r") - .trim_start_matches("\n")) { - return None - } let date = DATE_REGEX.captures(&message)?; - let (date, ip, message) = ( + let (date, message) = ( date.get(1)?.as_str().to_string(), date.get(2)?.as_str().to_string(), - date.get(3)?.as_str().to_string() ); + let (ip, message) = if let Some(message) = IP_REGEX.captures(&message) { + (Some(message.get(1)?.as_str().to_string()), message.get(2)?.as_str().to_string()) + } else { + (None, message) + }; + let prefix = if ctx.enable_ip_viewing { - format!("{}{} [{}]", ip, " ".repeat(15-ip.len()), date) + if let Some(ip) = ip { + format!("{}{} [{}]", ip, " ".repeat(15-ip.len()), date) + } else { + format!("{} [{}]", " ".repeat(15), date) + } } else { format!("[{}]", date) }; @@ -111,6 +167,7 @@ fn format_message(ctx: Arc, message: String) -> Option { }) } + fn find_username_color(message: &str) -> Option<(String, String, Color)> { for (re, color) in COLORED_USERNAMES.iter() { if let Some(captures) = re.captures(message) { @@ -120,7 +177,36 @@ fn find_username_color(message: &str) -> Option<(String, String, Color)> { None } -fn poll_events(ctx: Arc) { + +fn write_backspace(len: usize) { + write_backspace_with_text(len, "") +} + +fn write_backspace_with_text(len: usize, text: &str) { + let spaces = if text.chars().count() < len { + len-text.chars().count() + } else { + 0 + }; + write!(stdout(), + "{}{}{}{}", + MoveLeft(1).to_string().repeat(len), + text, + " ".repeat(spaces), + MoveLeft(1).to_string().repeat(spaces) + ).unwrap(); + stdout().lock().flush().unwrap(); +} + + +fn poll_events(ctx: Arc) -> Result<(), Box> { + let mut history: Vec = vec![String::new()]; + let mut history_cursor: usize = 0; + let mut cursor: usize = 0; + + let input = ctx.input.clone(); + let messages = ctx.messages.clone(); + loop { if !event::poll(Duration::from_millis(50)).unwrap_or(false) { continue } @@ -133,66 +219,70 @@ fn poll_events(ctx: Arc) { Event::Key(event) => { match event.code { KeyCode::Enter => { - let message = ctx.input.read().unwrap().clone(); + let message = input.read().unwrap().clone(); if !message.is_empty() { - let input_len = ctx.input.read().unwrap().chars().count(); - write!(stdout(), - "{}{}{}", - MoveLeft(1).to_string().repeat(input_len), - " ".repeat(input_len), - MoveLeft(1).to_string().repeat(input_len) - ).unwrap(); - stdout().lock().flush().unwrap(); - ctx.input.write().unwrap().clear(); + write_backspace(message.chars().count()); + input.write().unwrap().clear(); + + history.insert(history_cursor, message.clone()); + history_cursor += 1; if message.starts_with("/") && !ctx.disable_commands { - on_command(ctx.clone(), &message).expect("Error on command"); + on_command(ctx.clone(), &message)?; } else { - let message = ctx.message_format.replace("{name}", &ctx.name).replace("{text}", &message); - send_message(ctx.clone(), &message).expect("Error sending message"); + let message = ctx.message_format + .replace("{name}", &ctx.name) + .replace("{text}", &message); + send_message(&ctx.host, &message)?; } } else { print_console( ctx.clone(), - ctx.messages.0.read().unwrap().clone(), - &ctx.input.read().unwrap() - ).expect("Error printing console"); + messages.messages(), + "" + )?; } } KeyCode::Backspace => { - if ctx.input.write().unwrap().pop().is_some() { - stdout().lock().execute(MoveLeft(1)).unwrap(); - write!(stdout(), " {}", MoveLeft(1).to_string()).unwrap(); - stdout().lock().flush().unwrap(); + if input.write().unwrap().pop().is_some() { + history[history_cursor].pop(); + write_backspace(1); } } KeyCode::Esc => { - disable_raw_mode().unwrap(); + disable_raw_mode()?; break; } - KeyCode::Up => { - disable_raw_mode().unwrap(); - break; - } - KeyCode::Down => { - disable_raw_mode().unwrap(); - break; + KeyCode::Up | KeyCode::Down => { + history_cursor = if event.code == KeyCode::Up { + max(history_cursor + 1, 1) - 1 + } else { + min(history_cursor + 1, history.len() - 1) + }; + let was_len = input.read().unwrap().chars().count(); + *input.write().unwrap() = history[history_cursor].clone(); + write_backspace_with_text(was_len, &history[history_cursor]); } KeyCode::PageUp => { - disable_raw_mode().unwrap(); - break; + } KeyCode::PageDown => { - disable_raw_mode().unwrap(); - break; + + } + KeyCode::Left => { + + } + KeyCode::Right => { + } KeyCode::Char(c) => { if event.modifiers.contains(KeyModifiers::CONTROL) && "zxcZXCячсЯЧС".contains(c) { disable_raw_mode().unwrap(); break; } - ctx.input.write().unwrap().push(c); + history[history_cursor].push(c); + input.write().unwrap().push(c); write!(stdout(), "{}", c).unwrap(); stdout().lock().flush().unwrap(); } @@ -200,19 +290,50 @@ fn poll_events(ctx: Arc) { } }, Event::Paste(data) => { - ctx.input.write().unwrap().push_str(&data); + input.write().unwrap().push_str(&data); write!(stdout(), "{}", &data).unwrap(); stdout().lock().flush().unwrap(); }, Event::Mouse(data) => { - + match data.kind { + MouseEventKind::ScrollUp => { + + }, + MouseEventKind::ScrollDown => { + + }, + _ => {} + } } _ => {} } } + + Ok(()) +} + +pub fn recv_tick(ctx: Arc) -> Result<(), Box> { + if let Ok(Some((messages, size))) = read_messages(&ctx.host, ctx.max_messages, ctx.messages.packet_size()) { + let messages: Vec = messages.into_iter().flat_map(|o| format_message(ctx.clone(), o)).collect(); + ctx.messages.update(messages.clone(), size); + print_console(ctx.clone(), messages, &ctx.input.read().unwrap())?; + } + thread::sleep(Duration::from_millis(ctx.update_time as u64)); + Ok(()) } pub fn run_main_loop(ctx: Arc) { enable_raw_mode().unwrap(); - poll_events(ctx); + + thread::spawn({ + let ctx = ctx.clone(); + + move || { + loop { + recv_tick(ctx.clone()).expect("Error printing console"); + } + } + }); + + poll_events(ctx).expect("Error while polling events"); } \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 4c10008..a98e30b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,8 @@ use rand::random; use serde_yml; use clap::Parser; +use crate::chat::ChatStorage; + use super::util::get_input; const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}"; @@ -170,7 +172,7 @@ pub struct Args { } pub struct Context { - pub messages: Arc<(RwLock>, AtomicUsize)>, + pub messages: Arc, pub input: Arc>, pub host: String, pub name: String, @@ -187,7 +189,7 @@ pub struct Context { impl Context { pub fn new(config: &Config, args: &Args) -> Context { Context { - messages: Arc::new((RwLock::new(Vec::new()), AtomicUsize::new(0))), + messages: Arc::new(ChatStorage::new()), input: Arc::new(RwLock::new(String::new())), message_format: args.message_format.clone().unwrap_or(config.message_format.clone()), host: args.host.clone().unwrap_or(config.host.clone()), diff --git a/src/main.rs b/src/main.rs index 8a58e1d..1c430cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,17 +3,15 @@ use std::sync::Arc; use clap::Parser; use colored::Color; use config::{configure, get_config_path, load_config, Args, Context}; -use proto::{read_messages, run_recv_loop, send_message}; +use proto::{read_messages, send_message}; use regex::Regex; use lazy_static::lazy_static; use chat::run_main_loop; -const ADVERTISEMENT: &str = "\r\x1B[1A use bRAC client! https://github.com/MeexReay/bRAC \x1B[1B"; -const ADVERTISEMENT_ENABLED: bool = false; - lazy_static! { - static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] \{(.*?)\} (.*)").unwrap(); + static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap(); + static ref IP_REGEX: Regex = Regex::new(r"\{(.*?)\} (.*)").unwrap(); static ref COLORED_USERNAMES: Vec<(Regex, Color)> = vec![ (Regex::new(r"\u{B9AC}\u{3E70}<(.*?)> (.*)").unwrap(), Color::Green), // bRAC (Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), Color::BrightRed), // CRAB @@ -31,7 +29,7 @@ mod util; fn main() { let args = Args::parse(); - + let config_path = get_config_path(); if args.config_path { @@ -49,17 +47,16 @@ fn main() { let ctx = Arc::new(Context::new(&config, &args)); if args.read_messages { - print!("{}", read_messages(ctx.clone(), 0).ok().flatten().expect("Error reading messages").0.join("\n")); + print!("{}", read_messages(&ctx.host, ctx.max_messages, 0).ok().flatten().expect("Error reading messages").0.join("\n")); } if let Some(message) = &args.send_message { - send_message(ctx.clone(), message).expect("Error sending message"); + send_message(&ctx.host, message).expect("Error sending message"); } if args.send_message.is_some() || args.read_messages { return; } - run_recv_loop(ctx.clone()); run_main_loop(ctx.clone()); } diff --git a/src/proto.rs b/src/proto.rs index 918b706..4ac2e41 100644 --- a/src/proto.rs +++ b/src/proto.rs @@ -1,25 +1,13 @@ -use std::{error::Error, io::{Read, Write}, net::TcpStream, sync::{atomic::Ordering, Arc}, thread, time::Duration}; +use std::{error::Error, io::{Read, Write}, net::TcpStream, sync::Arc, thread, time::Duration}; -use super::{chat::print_console, Context, ADVERTISEMENT, ADVERTISEMENT_ENABLED}; +use crate::config::Context; -pub fn send_message(context: Arc, message: &str) -> Result<(), Box> { - let mut stream = TcpStream::connect(&context.host)?; +use super::chat::print_console; + +pub fn send_message(host: &str, message: &str) -> Result<(), Box> { + let mut stream = TcpStream::connect(host)?; stream.write_all(&[0x01])?; - let data = format!("{}{}{}{}", - if !context.disable_hiding_ip { - "\r\x07" - } else { - "" - }, - message, - if !context.disable_hiding_ip && message.chars().count() < 39 { - " ".repeat(39-message.chars().count()) - } else { - String::new() - }, - if ADVERTISEMENT_ENABLED {ADVERTISEMENT} else {""} - ); - stream.write_all(data.as_bytes())?; + stream.write_all(message.as_bytes())?; Ok(()) } @@ -33,8 +21,8 @@ fn skip_null(stream: &mut TcpStream) -> Result, Box> { } } -pub fn read_messages(context: Arc, last_size: usize) -> Result, usize)>, Box> { - let mut stream = TcpStream::connect(&context.host)?; +pub fn read_messages(host: &str, max_messages: usize, last_size: usize) -> Result, usize)>, Box> { + let mut stream = TcpStream::connect(host)?; stream.write_all(&[0x00])?; @@ -75,28 +63,9 @@ pub fn read_messages(context: Arc, last_size: usize) -> Result = packet_data.split("\n").collect(); let lines: Vec = lines.clone().into_iter() - .skip(lines.len() - context.max_messages) + .skip(lines.len() - max_messages) .map(|o| o.to_string()) .collect(); Ok(Some((lines, packet_size))) -} - -pub fn run_recv_loop(context: Arc) { - thread::spawn({ - let cache = context.messages.clone(); - let update_time = context.update_time; - let input = context.input.clone(); - - move || { - loop { - if let Ok(Some(data)) = read_messages(context.clone(), cache.1.load(Ordering::SeqCst)) { - *cache.0.write().unwrap() = data.0.clone(); - cache.1.store(data.1, Ordering::SeqCst); - print_console(context.clone(), data.0, &input.read().unwrap()).expect("Error printing console"); - thread::sleep(Duration::from_millis(update_time as u64)); - } - } - } - }); } \ No newline at end of file