diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 948dfe3..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,16 +0,0 @@ - -[target.i686-unknown-linux-gnu] -linker = "i686-unknown-linux-gnu-gcc" -ar = "i686-unknown-linux-gnu-ar" - -[target.x86_64-unknown-linux-gnu] -linker = "x86_64-unknown-linux-gnu-gcc" -ar = "x86_64-unknown-linux-gnu-ar" - -# [target.i686-pc-windows-gnu] -# linker = "i686-w64-mingw32-gcc" -# ar = "i686-w64-mingw32-ar" - -# [target.x86_64-pc-windows-gnu] -# linker = "x86_64-w64-mingw32-gcc" -# ar = "x86_64-w64-mingw32-ar" \ No newline at end of file diff --git a/.gitignore b/.gitignore index c41cc9e..ae4d0ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/target \ No newline at end of file +/target +/result \ No newline at end of file diff --git a/readme.md b/README.md similarity index 100% rename from readme.md rename to README.md diff --git a/build_all.bat b/build_all.bat deleted file mode 100644 index 5efbc86..0000000 --- a/build_all.bat +++ /dev/null @@ -1,13 +0,0 @@ -@echo off -REM cargo build --release --target x86_64-unknown-linux-gnu -REM echo x86_64-unknown-linux-gnu built -REM cargo build --release --target i686-unknown-linux-gnu -REM echo i686-unknown-linux-gnu built -REM cargo build --release --target x86_64-pc-windows-gnu -REM echo x86_64-pc-windows-gnu built -REM cargo build --release --target i686-pc-windows-gnu -REM echo i686-pc-windows-gnu built -cargo build --release --target x86_64-pc-windows-msvc -echo x86_64-pc-windows-msvc built -cargo build --release --target i686-pc-windows-msvc -echo i686-pc-windows-msvc built \ No newline at end of file diff --git a/build_all.sh b/build_all.sh deleted file mode 100644 index 10d2eef..0000000 --- a/build_all.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -cargo build --release --target x86_64-unknown-linux-gnu -echo x86_64-unknown-linux-gnu built -cargo build --release --target i686-unknown-linux-gnu -echo i686-unknown-linux-gnu built -cargo build --release --target x86_64-pc-windows-gnu -echo x86_64-pc-windows-gnu built -# cargo build --release --target i686-pc-windows-gnu -# echo i686-pc-windows-gnu built -# cargo build --release --target x86_64-pc-windows-msvc -# echo x86_64-pc-windows-msvc built -# cargo build --release --target i686-pc-windows-msvc -# echo i686-pc-windows-msvc built \ No newline at end of file diff --git a/colored_usernames.md b/docs/colored_usernames.md similarity index 100% rename from colored_usernames.md rename to docs/colored_usernames.md diff --git a/docs/rac_protocol.md b/docs/rac_protocol.md new file mode 100644 index 0000000..4d94406 --- /dev/null +++ b/docs/rac_protocol.md @@ -0,0 +1 @@ +todo \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5d31299 --- /dev/null +++ b/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1739055578, + "narHash": "sha256-2MhC2Bgd06uI1A0vkdNUyDYsMD0SLNGKtD8600mZ69A=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a45fa362d887f4d4a7157d95c28ca9ce2899b70e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1736320768, + "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1739240901, + "narHash": "sha256-YDtl/9w71m5WcZvbEroYoWrjECDhzJZLZ8E68S3BYok=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "03473e2af8a4b490f4d2cdb2e4d3b75f82c8197c", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2927a54 --- /dev/null +++ b/flake.nix @@ -0,0 +1,66 @@ +{ + description = "Cargo project with cross-compilation support"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }: + flake-utils.lib.eachSystem [ + "x86_64-linux" + "x86_64-darwin" + "x86_64-windows" + "i686-linux" + "i686-windows" + "aarch64-darwin" + ] (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + crossSystem = (import ).systems.examples.gnu64 // { + rust.rustcTarget = { + "x86_64-linux" = "x86_64-unknown-linux-gnu"; + "x86_64-darwin" = "x86_64-apple-darwin"; + "x86_64-windows" = "x86_64-pc-windows-gnu"; + "i686-linux" = "i686-unknown-linux-gnu"; + "i686-windows" = "i686-pc-windows-gnu"; + "aarch64-darwin" = "aarch64-apple-darwin"; + }.${system}; # here invalid target format + }; + }; + + exeSuffix = if pkgs.stdenv.hostPlatform.isWindows then ".exe" else ""; + in { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + rust-bin.stable.latest.default + pkg-config + (if stdenv.isDarwin then darwin.libiconv else null) + ]; + }; + + packages.default = pkgs.rustPlatform.buildRustPackage { + pname = "my-cargo-project"; + version = "0.1.0"; + src = pkgs.lib.cleanSource ./.; + + cargoLock = { + lockFile = ./Cargo.lock; + }; + + meta = with pkgs.lib; { + description = "My Rust project"; + license = licenses.mit; + maintainers = with maintainers; [ ]; + platforms = platforms.all; + }; + + postInstall = '' + mv $out/bin/bRAC $out/bin/bRAC-${system}${exeSuffix} + ''; + }; + }); +} \ No newline at end of file diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 501d316..0000000 --- a/shell.nix +++ /dev/null @@ -1,39 +0,0 @@ -{ pkgs ? import {} }: - -let - # Переопределение для mingw32 - mingw32WithDwarf = pkgs.pkgsCross.mingw32.buildPackages.gcc.overrideAttrs (oldAttrs: { - configureFlags = [ - "--disable-sjlj-exceptions" - "--enable-dwarf2" - ]; - }); -in - -pkgs.mkShell { - buildInputs = with pkgs; [ - rustup - gcc_multi - pkg-config - zlib - openssl - - # Добавляем кросс-компиляторы - pkgsCross.gnu32.buildPackages.gcc - pkgsCross.gnu32.buildPackages.binutils - pkgsCross.gnu64.buildPackages.gcc - pkgsCross.gnu64.buildPackages.binutils - - # Переопределённый MinGW для 32-бит Windows - mingw32WithDwarf - - # Необходимые библиотеки для Windows - pkgsCross.mingw32.windows.pthreads - pkgsCross.mingw32.windows.mcfgthreads - - # 64-битный MinGW и необходимые библиотеки - pkgsCross.mingwW64.buildPackages.gcc - pkgsCross.mingwW64.windows.pthreads - pkgsCross.mingwW64.windows.mcfgthreads - ]; -} \ No newline at end of file diff --git a/src/term.rs b/src/chat.rs similarity index 68% rename from src/term.rs rename to src/chat.rs index a6b59d9..3d60634 100644 --- a/src/term.rs +++ b/src/chat.rs @@ -1,10 +1,54 @@ -use std::{error::Error, io::{stdout, Write}, sync::Arc, time::Duration}; +use std::{error::Error, io::{stdout, Write}, sync::{atomic::Ordering, Arc}, time::{Duration, SystemTime}}; use colored::{Color, Colorize}; use crossterm::{cursor::MoveLeft, event::{self, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode}, ExecutableCommand}; -use regex::Regex; +use rand::random; -use super::{on_command, rac::send_message, Context, ADVERTISEMENT, COLORED_USERNAMES, DATE_REGEX}; +use super::{proto::read_messages, util::sanitize_text, ADVERTISEMENT, COLORED_USERNAMES, DATE_REGEX, config::Context, proto::send_message}; + +fn on_command(ctx: Arc, command: &str) -> Result<(), Box> { + let command = command.trim_start_matches("/"); + let (command, args) = command.split_once(" ").unwrap_or((&command, "")); + let args = args.split(" ").collect::>(); + + if command == "clear" { + send_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))?; + } else if command == "help" { + write!(stdout(), "Help message:\r +/help - show help message\r +/clear - clear console\r +/spam *args - spam console with text\r +/ping - check server ping\r +\r +Press enter to close")?; + stdout().flush()?; + } else if command == "ping" { + let mut before = ctx.messages.1.load(Ordering::SeqCst); + let start = SystemTime::now(); + let message = format!("Checking ping... {:X}", random::()); + send_message(ctx.clone(), &message)?; + loop { + let data = read_messages(ctx.clone(), before).ok().flatten(); + + if let Some((data, size)) = data { + if let Some(last) = data.iter().rev().find(|o| o.contains(&message)) { + if last.contains(&message) { + break; + } else { + before = size; + } + } else { + before = size; + } + } + } + send_message(ctx.clone(), &format!("Ping = {}ms", start.elapsed().unwrap().as_millis()))?; + } + + Ok(()) +} pub fn print_console(context: Arc, messages: Vec, input: &str) -> Result<(), Box> { let text = format!( @@ -67,14 +111,6 @@ fn format_message(ctx: Arc, message: String) -> Option { }) } -fn sanitize_text(input: &str) -> String { - let ansi_regex = Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap(); - let control_chars_regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap(); - let without_ansi = ansi_regex.replace_all(input, ""); - let cleaned_text = control_chars_regex.replace_all(&without_ansi, ""); - cleaned_text.into_owned() -} - fn find_username_color(message: &str) -> Option<(String, String, Color)> { for (re, color) in COLORED_USERNAMES.iter() { if let Some(captures) = re.captures(message) { @@ -135,6 +171,22 @@ fn poll_events(ctx: Arc) { disable_raw_mode().unwrap(); break; } + KeyCode::Up => { + disable_raw_mode().unwrap(); + break; + } + KeyCode::Down => { + disable_raw_mode().unwrap(); + break; + } + KeyCode::PageUp => { + disable_raw_mode().unwrap(); + break; + } + KeyCode::PageDown => { + disable_raw_mode().unwrap(); + break; + } KeyCode::Char(c) => { if event.modifiers.contains(KeyModifiers::CONTROL) && "zxcZXCячсЯЧС".contains(c) { disable_raw_mode().unwrap(); @@ -151,6 +203,9 @@ fn poll_events(ctx: Arc) { ctx.input.write().unwrap().push_str(&data); write!(stdout(), "{}", &data).unwrap(); stdout().lock().flush().unwrap(); + }, + Event::Mouse(data) => { + } _ => {} } diff --git a/src/config.rs b/src/config.rs index e554eda..4c10008 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,12 @@ +use std::sync::{atomic::AtomicUsize, Arc, RwLock}; #[allow(unused_imports)] use std::{env, fs, path::{Path, PathBuf}, thread, time::Duration}; use homedir::my_home; +use rand::random; use serde_yml; +use clap::Parser; -use super::get_input; +use super::util::get_input; const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}"; @@ -25,23 +28,49 @@ pub struct Config { pub disable_ip_hiding: bool, } -fn default_max_messages() -> usize { 100 } +fn default_max_messages() -> usize { 1000 } fn default_update_time() -> usize { 50 } fn default_host() -> String { "meex.lol:11234".to_string() } fn default_message_format() -> String { MESSAGE_FORMAT.to_string() } +fn ask_usize(name: impl ToString, default: usize) -> usize { + get_input(format!("{} (default: {}) > ", name.to_string(), default)) + .and_then(|o| o.parse().ok()).unwrap_or(default) +} + +fn ask_string(name: impl ToString, default: impl ToString + Clone) -> String { + ask_string_option(name, default.clone()).unwrap_or(default.to_string()) +} + +fn ask_string_option(name: impl ToString, default: impl ToString) -> Option { + let default = default.to_string(); + get_input(format!("{} (default: {}) > ", name.to_string(), default)) +} + +fn ask_bool(name: impl ToString, default: bool) -> bool { + get_input(format!("{} (Y/N, default: {}) > ", name.to_string(), default)) + .map(|o| o.to_lowercase() != "n") + .unwrap_or(default) +} + pub fn configure(path: PathBuf) -> Config { - let host = get_input("Host (default: meex.lol:11234) > ").unwrap_or("meex.lol:11234".to_string()); - let name = get_input("Name (default: ask every time) > "); - let update_time = get_input("Update interval (default: 50) > ").map(|o| o.parse().ok()).flatten().unwrap_or(50); - let max_messages = get_input("Max messages (default: 100) > ").map(|o| o.parse().ok()).flatten().unwrap_or(100); - let enable_ip_viewing = get_input("Enable users IP viewing? (Y/N, default: N) > ").map(|o| o.to_lowercase() != "n").unwrap_or(false); - let disable_ip_hiding = get_input("Enable your IP viewing? (Y/N, default: N) > ").map(|o| o.to_lowercase() != "n").unwrap_or(false); + println!("To configure the client, please answer a few questions. It won't take long."); + println!("You can reconfigure client in any moment via `bRAC --configure`"); + println!("Config stores in path `{}`", path.to_string_lossy()); + println!(); + + let host = ask_string("Host", default_host()); + let name = ask_string_option("Name", "ask every time"); + let update_time = ask_usize("Update interval", default_update_time()); + let max_messages = ask_usize("Max messages", default_max_messages()); + let message_format = ask_string("Message format", default_message_format()); + let enable_ip_viewing = ask_bool("Enable users IP viewing?", false); + let disable_ip_hiding = ask_bool("Enable your IP viewing?", false); let config = Config { host, name, - message_format: MESSAGE_FORMAT.to_string(), + message_format, update_time, max_messages, enable_ip_viewing, @@ -90,4 +119,92 @@ pub fn get_config_path() -> PathBuf { }; config_path +} + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct Args { + /// Print config path + #[arg(short='p', long)] + pub config_path: bool, + + /// Use specified host + #[arg(short='H', long)] + pub host: Option, + + /// Use specified name + #[arg(short='n', long)] + pub name: Option, + + /// Use specified message format + #[arg(short='F', long)] + pub message_format: Option, + + /// Print unformatted messages from chat and exit + #[arg(short='r', long)] + pub read_messages: bool, + + /// Send unformatted message to chat and exit + #[arg(short='s', long, value_name="MESSAGE")] + pub send_message: Option, + + /// Disable message formatting and sanitizing + #[arg(short='f', long)] + pub disable_formatting: bool, + + /// Disable slash commands + #[arg(short='c', long)] + pub disable_commands: bool, + + /// Disable ip hiding + #[arg(short='i', long)] + pub disable_ip_hiding: bool, + + /// Enable users IP viewing + #[arg(short='v', long)] + pub enable_users_ip_viewing: bool, + + /// Configure client + #[arg(short='C', long)] + pub configure: bool, +} + +pub struct Context { + pub messages: Arc<(RwLock>, AtomicUsize)>, + pub input: Arc>, + pub host: String, + pub name: String, + pub disable_formatting: bool, + pub disable_commands: bool, + pub disable_hiding_ip: bool, + pub message_format: String, + pub update_time: usize, + pub max_messages: usize, + pub enable_ip_viewing: bool, + pub scroll: Arc +} + +impl Context { + pub fn new(config: &Config, args: &Args) -> Context { + Context { + messages: Arc::new((RwLock::new(Vec::new()), AtomicUsize::new(0))), + 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()), + name: match args.name.clone().or(config.name.clone()) { + Some(i) => i, + None => { + let anon_name = format!("Anon#{:X}", random::()); + get_input(&format!("Name (default: {}) > ", anon_name)).unwrap_or(anon_name) + }, + }, + disable_formatting: args.disable_formatting, + disable_commands: args.disable_commands, + disable_hiding_ip: args.disable_ip_hiding, + update_time: config.update_time, + max_messages: config.max_messages, + enable_ip_viewing: args.enable_users_ip_viewing || config.enable_ip_viewing, + scroll: Arc::new(AtomicUsize::new(0)) + } + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 1247c43..8a58e1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,12 @@ -use std::{ - error::Error, io::{stdin, stdout, BufRead, Write}, sync::{atomic::{AtomicUsize, Ordering}, Arc, RwLock}, time::SystemTime -}; +use std::sync::Arc; +use clap::Parser; use colored::Color; -use config::{configure, get_config_path, load_config}; -use rac::{read_messages, run_recv_loop, send_message}; -use rand::random; +use config::{configure, get_config_path, load_config, Args, Context}; +use proto::{read_messages, run_recv_loop, send_message}; use regex::Regex; use lazy_static::lazy_static; -use term::run_main_loop; -use clap::Parser; +use chat::run_main_loop; const ADVERTISEMENT: &str = "\r\x1B[1A use bRAC client! https://github.com/MeexReay/bRAC \x1B[1B"; @@ -18,141 +15,18 @@ const ADVERTISEMENT_ENABLED: bool = false; lazy_static! { static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] \{(.*?)\} (.*)").unwrap(); static ref COLORED_USERNAMES: Vec<(Regex, Color)> = vec![ - (Regex::new(r"\u{B9AC}\u{3E70}<(.*?)> (.*)").unwrap(), Color::Green), - (Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), Color::BrightRed), - (Regex::new(r"(.*?): (.*)").unwrap(), Color::Magenta), - (Regex::new(r"<(.*?)> (.*)").unwrap(), Color::Cyan), + (Regex::new(r"\u{B9AC}\u{3E70}<(.*?)> (.*)").unwrap(), Color::Green), // bRAC + (Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), Color::BrightRed), // CRAB + (Regex::new(r"\u{00B0}\u{0298}<(.*?)> (.*)").unwrap(), Color::Magenta), // Mefidroniy + (Regex::new(r"<(.*?)> (.*)").unwrap(), Color::Cyan), // clRAC ]; } mod config; -mod term; -mod rac; - - -fn get_input(prompt: &str) -> Option { - let mut out = stdout().lock(); - out.write_all(prompt.as_bytes()).ok()?; - out.flush().ok()?; - let input = stdin().lock().lines().next() - .map(|o| o.ok()) - .flatten()?; - - if input.is_empty() { - None - } else { - Some(input.to_string()) - } -} - -fn on_command(ctx: Arc, command: &str) -> Result<(), Box> { - let command = command.trim_start_matches("/"); - let (command, args) = command.split_once(" ").unwrap_or((&command, "")); - let args = args.split(" ").collect::>(); - - if command == "clear" { - send_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))?; - } else if command == "help" { - write!(stdout(), "Help message:\r -/help - show help message\r -/clear - clear console\r -/spam *args - spam console with text\r -/ping - check server ping\r -\r -Press enter to close")?; - stdout().flush()?; - } else if command == "ping" { - let mut before = ctx.messages.1.load(Ordering::SeqCst); - let start = SystemTime::now(); - let message = format!("Checking ping... {:X}", random::()); - send_message(ctx.clone(), &message)?; - loop { - let data = read_messages(ctx.clone(), before).ok().flatten(); - - if let Some((data, size)) = data { - if let Some(last) = data.iter().rev().find(|o| o.contains(&message)) { - if last.contains(&message) { - break; - } else { - before = size; - } - } else { - before = size; - } - } - } - send_message(ctx.clone(), &format!("Ping = {}ms", start.elapsed().unwrap().as_millis()))?; - } - - Ok(()) -} - - -#[derive(Parser, Debug)] -#[command(version, about, long_about = None)] -struct Args { - /// Print config path - #[arg(short='p', long)] - config_path: bool, - - /// Use specified host - #[arg(short='H', long)] - host: Option, - - /// Use specified name - #[arg(short='n', long)] - name: Option, - - /// Use specified message format - #[arg(short='F', long)] - message_format: Option, - - /// Print unformatted messages from chat and exit - #[arg(short='r', long)] - read_messages: bool, - - /// Send unformatted message to chat and exit - #[arg(short='s', long, value_name="MESSAGE")] - send_message: Option, - - /// Disable message formatting and sanitizing - #[arg(short='f', long)] - disable_formatting: bool, - - /// Disable slash commands - #[arg(short='c', long)] - disable_commands: bool, - - /// Disable ip hiding - #[arg(short='i', long)] - disable_ip_hiding: bool, - - /// Enable users IP viewing - #[arg(short='v', long)] - enable_users_ip_viewing: bool, - - /// Configure client - #[arg(short='C', long)] - configure: bool, -} - - -struct Context { - messages: Arc<(RwLock>, AtomicUsize)>, - input: Arc>, - host: String, - name: String, - disable_formatting: bool, - disable_commands: bool, - disable_hiding_ip: bool, - message_format: String, - update_time: usize, - max_messages: usize, - enable_ip_viewing: bool -} +mod chat; +mod proto; +mod util; fn main() { @@ -169,46 +43,23 @@ fn main() { configure(config_path); return; } + + let config = load_config(config_path); - let context = { - let config = load_config(config_path); - - Context { - messages: Arc::new((RwLock::new(Vec::new()), AtomicUsize::new(0))), - 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()), - name: match args.name.clone().or(config.name.clone()) { - Some(i) => i, - None => { - let anon_name = format!("Anon#{:X}", random::()); - get_input(&format!("Name (default: {}) > ", anon_name)).unwrap_or(anon_name) - }, - }, - disable_formatting: args.disable_formatting, - disable_commands: args.disable_commands, - disable_hiding_ip: args.disable_ip_hiding, - update_time: config.update_time, - max_messages: config.max_messages, - enable_ip_viewing: args.enable_users_ip_viewing || config.enable_ip_viewing - } - }; - - let context = Arc::new(context); + let ctx = Arc::new(Context::new(&config, &args)); if args.read_messages { - print!("{}", read_messages(context.clone(), 0).ok().flatten().expect("Error reading messages").0.join("\n")); + print!("{}", read_messages(ctx.clone(), 0).ok().flatten().expect("Error reading messages").0.join("\n")); } if let Some(message) = &args.send_message { - send_message(context.clone(), message).expect("Error sending message"); + send_message(ctx.clone(), message).expect("Error sending message"); } if args.send_message.is_some() || args.read_messages { return; } - run_recv_loop(context.clone()); - run_main_loop(context.clone()); + run_recv_loop(ctx.clone()); + run_main_loop(ctx.clone()); } diff --git a/src/rac.rs b/src/proto.rs similarity index 97% rename from src/rac.rs rename to src/proto.rs index 42557d8..918b706 100644 --- a/src/rac.rs +++ b/src/proto.rs @@ -1,6 +1,6 @@ use std::{error::Error, io::{Read, Write}, net::TcpStream, sync::{atomic::Ordering, Arc}, thread, time::Duration}; -use super::{term::print_console, Context, ADVERTISEMENT, ADVERTISEMENT_ENABLED}; +use super::{chat::print_console, Context, ADVERTISEMENT, ADVERTISEMENT_ENABLED}; pub fn send_message(context: Arc, message: &str) -> Result<(), Box> { let mut stream = TcpStream::connect(&context.host)?; diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..f6a6ad8 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,27 @@ +use std::io::{stdin, stdout, BufRead, Write}; + +use regex::Regex; + + +pub fn sanitize_text(input: &str) -> String { + let ansi_regex = Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap(); + let control_chars_regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap(); + let without_ansi = ansi_regex.replace_all(input, ""); + let cleaned_text = control_chars_regex.replace_all(&without_ansi, ""); + cleaned_text.into_owned() +} + +pub fn get_input(prompt: impl ToString) -> Option { + let mut out = stdout().lock(); + out.write_all(prompt.to_string().as_bytes()).ok()?; + out.flush().ok()?; + let input = stdin().lock().lines().next() + .map(|o| o.ok()) + .flatten()?; + + if input.is_empty() { + None + } else { + Some(input.to_string()) + } +} \ No newline at end of file