From 58213cf8c91aec596d83b78322d56c60f32a13db Mon Sep 17 00:00:00 2001 From: MeexReay Date: Sun, 13 Apr 2025 22:53:07 +0300 Subject: [PATCH] massive rewrite --- Cargo.lock | 110 +++++++++-- Cargo.toml | 17 +- README.md | 11 +- flake.lock | 68 +++---- flake.nix | 71 ++++--- src/chat.rs | 416 +++++----------------------------------- src/chat/minimal_tui.rs | 67 +++++++ src/chat/pretty_tui.rs | 377 ++++++++++++++++++++++++++++++++++++ src/config.rs | 15 +- src/lib.rs | 5 + src/proto.rs | 84 +++++--- src/util.rs | 9 +- 12 files changed, 772 insertions(+), 478 deletions(-) create mode 100644 src/chat/minimal_tui.rs create mode 100644 src/chat/pretty_tui.rs create mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 7bc4f60..92d3e9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "byteorder" @@ -124,9 +124,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.29" +version = "4.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" +checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" dependencies = [ "clap_builder", "clap_derive", @@ -134,9 +134,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.29" +version = "4.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" +checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" dependencies = [ "anstream", "anstyle", @@ -146,9 +146,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", @@ -177,6 +177,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -195,15 +204,17 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossterm" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ "bitflags", "crossterm_winapi", + "derive_more", + "document-features", "mio", "parking_lot", - "rustix", + "rustix 1.0.5", "signal-hook", "signal-hook-mio", "winapi", @@ -218,6 +229,36 @@ dependencies = [ "winapi", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -341,6 +382,18 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.12" @@ -377,9 +430,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -588,7 +641,20 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] @@ -638,18 +704,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -740,7 +806,7 @@ dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix", + "rustix 0.38.44", "windows-sys 0.59.0", ] @@ -750,6 +816,12 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 52bc13d..70ca0dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,16 @@ rand = "0.9.0" regex = "1.11.1" colored = "3.0.0" lazy_static = "1.5.0" -crossterm = "0.28.1" -serde = { version = "1.0.217", features = ["serde_derive"] } +crossterm = { version = "0.29.0", optional = true } +serde = { version = "1.0.219", features = ["serde_derive"] } serde_yml = "0.0.12" -homedir = "0.3.4" -clap = { version = "4.5.29", features = ["derive"] } -native-tls = "0.2.13" \ No newline at end of file +homedir = { version = "0.3.4", optional = true } +clap = { version = "4.5.36", features = ["derive"] } +native-tls = { version = "0.2.14", optional = true } + +[features] +default = ["ssl", "pretty", "homedir"] + +ssl = ["dep:native-tls"] +pretty = ["dep:crossterm"] +homedir = ["dep:homedir"] \ No newline at end of file diff --git a/README.md b/README.md index 033af78..2946e85 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,15 @@ better RAC client go to [releases](https://github.com/MeexReay/bRAC/releases/latest) and download file you need. its simple. +### from flake + +If you have Nix package manager installed, you can use: + +```bash +nix build github:MeexReay/bRAC # build binary (result/bin/bRAC) +nix run github:MeexReay/bRAC # run (builds and runs bRAC) +``` + ### build from source (you have to install [rust](https://www.rust-lang.org/tools/install) at first) @@ -35,7 +44,7 @@ go to [releases](https://github.com/MeexReay/bRAC/releases/latest) and download git clone https://github.com/MeexReay/bRAC.git cd bRAC cargo build --release # build release (target/release/bRAC) -cargo run # run (builds and runs bRAC itself) +cargo run --release # run (builds and runs bRAC itself) ``` ## default config diff --git a/flake.lock b/flake.lock index 5d31299..453d835 100644 --- a/flake.lock +++ b/flake.lock @@ -1,39 +1,54 @@ { "nodes": { - "flake-utils": { + "flake-parts": { "inputs": { - "systems": "systems" + "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "lastModified": 1743550720, + "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "c621e8422220273271f52058f618c94e405bb0f5", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1739055578, - "narHash": "sha256-2MhC2Bgd06uI1A0vkdNUyDYsMD0SLNGKtD8600mZ69A=", - "owner": "NixOS", + "lastModified": 1744463964, + "narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "a45fa362d887f4d4a7157d95c28ca9ce2899b70e", + "rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixos-24.11", + "owner": "nixos", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1743296961, + "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, "nixpkgs_2": { "locked": { "lastModified": 1736320768, @@ -52,7 +67,7 @@ }, "root": { "inputs": { - "flake-utils": "flake-utils", + "flake-parts": "flake-parts", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" } @@ -62,11 +77,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1739240901, - "narHash": "sha256-YDtl/9w71m5WcZvbEroYoWrjECDhzJZLZ8E68S3BYok=", + "lastModified": 1744513456, + "narHash": "sha256-NLVluTmK8d01Iz+WyarQhwFcXpHEwU7m5hH3YQQFJS0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "03473e2af8a4b490f4d2cdb2e4d3b75f82c8197c", + "rev": "730fd8e82799219754418483fabe1844262fd1e2", "type": "github" }, "original": { @@ -74,21 +89,6 @@ "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", diff --git a/flake.nix b/flake.nix index 1f44564..771375f 100644 --- a/flake.nix +++ b/flake.nix @@ -2,34 +2,59 @@ description = "bRAC - better RAC client"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; rust-overlay.url = "github:oxalica/rust-overlay"; - flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { - inherit system overlays; - }; - in { - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - rust-bin.stable.latest.default - pkg-config - ]; - }; + outputs = inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ "x86_64-linux" ]; + perSystem = { config, self', pkgs, lib, system, ... }: + let + runtimeDeps = with pkgs; [ pkg-config openssl ]; + buildDeps = with pkgs; [ pkg-config openssl ]; + devDeps = with pkgs; [ pkg-config openssl ]; - packages.default = pkgs.rustPlatform.buildRustPackage { - pname = "bRAC"; - version = "0.1.1+2.0"; - src = pkgs.lib.cleanSource ./.; + cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); + msrv = cargoToml.package.rust-version; - cargoLock = { - lockFile = ./Cargo.lock; + rustPackage = features: + (pkgs.makeRustPlatform { + cargo = pkgs.rust-bin.stable.latest.minimal; + rustc = pkgs.rust-bin.stable.latest.minimal; + }).buildRustPackage { + inherit (cargoToml.package) name version; + src = ./.; + cargoLock.lockFile = ./Cargo.lock; + buildFeatures = features; + buildInputs = runtimeDeps; + nativeBuildInputs = buildDeps; + }; + + mkDevShell = rustc: + pkgs.mkShell { + shellHook = '' + export RUST_SRC_PATH=${pkgs.rustPlatform.rustLibSrc} + ''; + buildInputs = runtimeDeps; + nativeBuildInputs = buildDeps ++ devDeps ++ [ rustc ]; + }; + in { + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ (import inputs.rust-overlay) ]; }; + + packages.default = self'.packages.bRAC; + devShells.default = self'.devShells.stable; + + packages.bRAC = (rustPackage "default"); + packages.bRAC-minimal = (rustPackage ""); + + devShells.nightly = (mkDevShell (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default))); + devShells.stable = (mkDevShell pkgs.rust-bin.stable.latest.default); + devShells.msrv = (mkDevShell pkgs.rust-bin.stable.${msrv}.default); }; - }); + }; } \ No newline at end of file diff --git a/src/chat.rs b/src/chat.rs index 20155fd..c0552d5 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,11 +1,29 @@ -use std::{cmp::{max, min}, error::Error, io::{stdout, Write}, sync::{atomic::{AtomicUsize, Ordering}, Arc, RwLock}, thread, time::{Duration, SystemTime, UNIX_EPOCH}}; +use std::{ + error::Error, io::{stdout, Write}, + sync::{atomic::{AtomicUsize, Ordering}, Arc, RwLock}, + time::{SystemTime, UNIX_EPOCH} +}; use colored::{Color, Colorize}; -use crossterm::{cursor::{MoveLeft, MoveRight}, event::{self, Event, KeyCode, KeyModifiers, MouseEventKind}, execute, terminal::{self, disable_raw_mode, enable_raw_mode}}; -use crate::{proto::{connect, send_message_auth}, util::{char_index_to_byte_index, string_chunks}, IP_REGEX}; +use super::{ + proto::{connect, read_messages, send_message, send_message_spoof_auth}, + IP_REGEX, + util::sanitize_text, + COLORED_USERNAMES, + DATE_REGEX, + config::Context +}; -use super::{proto::read_messages, util::sanitize_text, COLORED_USERNAMES, DATE_REGEX, config::Context, proto::send_message}; +#[cfg(not(feature = "pretty"))] +pub mod minimal_tui; +#[cfg(not(feature = "pretty"))] +pub use minimal_tui::run_main_loop; + +#[cfg(feature = "pretty")] +pub mod pretty_tui; +#[cfg(feature = "pretty")] +pub use pretty_tui::run_main_loop; pub struct ChatStorage { @@ -34,10 +52,14 @@ impl ChatStorage { *self.messages.write().unwrap() = messages; } - pub fn append(&self, messages: Vec, packet_size: usize) { + pub fn append_and_store(&self, messages: Vec, packet_size: usize) { self.packet_size.store(packet_size, Ordering::SeqCst); self.messages.write().unwrap().append(&mut messages.clone()); } + + pub fn append(&self, messages: Vec) { + self.messages.write().unwrap().append(&mut messages.clone()); + } } @@ -50,7 +72,7 @@ const HELP_MESSAGE: &str = "Help message:\r Press enter to close"; -fn on_command(ctx: Arc, command: &str) -> Result<(), Box> { +pub 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::>(); @@ -121,89 +143,7 @@ fn on_command(ctx: Arc, command: &str) -> Result<(), Box> { Ok(()) } - -pub fn print_console(ctx: Arc, messages: Vec, input: &str) -> Result<(), Box> { - let (width, height) = terminal::size()?; - let (width, height) = (width as usize, height as usize); - - let mut messages = messages - .into_iter() - .flat_map(|o| string_chunks(&o, width as usize - 1)) - .map(|o| (o.0.white().blink().to_string(), o.1)) - .collect::>(); - - let messages_size = if messages.len() >= height { - messages.len()-height - } else { - for _ in 0..height-messages.len() { - messages.insert(0, (String::new(), 0)); - } - 0 - }; - - let scroll = min(ctx.scroll.load(Ordering::SeqCst), messages_size); - let scroll_f = ((1f64 - scroll as f64 / (messages_size+1) as f64) * (height-2) as f64).round() as usize+1; - - let messages = if height < messages.len() { - if scroll < messages.len() - height { - messages[ - messages.len()-height-scroll.. - messages.len()-scroll - ].to_vec() - } else { - if scroll < messages.len() { - messages[ - 0.. - messages.len()-scroll - ].to_vec() - } else { - vec![] - } - } - } else { - messages - }; - - let formatted_messages = if ctx.disable_formatting { - messages - .into_iter() - .map(|(i, _)| i) - .collect::>() - } else { - messages - .into_iter() - .enumerate() - .map(|(i, (s, l))| { - format!("{}{}{}", - s, - " ".repeat(width - 1 - l), - if i == scroll_f { - "▐".bright_yellow() - } else { - "▕".yellow() - } - ) - }) - .collect::>() - - }; - - let text = format!( - "{}\r\n{} {}", - formatted_messages.join("\r\n"), - ">".bright_yellow(), - input - ); - - let mut out = stdout().lock(); - write!(out, "{}", text)?; - out.flush()?; - - Ok(()) -} - - -fn prepare_message(context: Arc, message: &str) -> String { +pub fn prepare_message(context: Arc, message: &str) -> String { format!("{}{}{}", if !context.disable_hiding_ip { "\r\x07" @@ -229,8 +169,28 @@ fn prepare_message(context: Arc, message: &str) -> String { ) } +pub fn on_send_message(ctx: Arc, message: &str) -> Result<(), Box> { + if message.starts_with("/") && !ctx.disable_commands { + on_command(ctx.clone(), &message)?; + } else { + let message = prepare_message( + ctx.clone(), + &ctx.message_format + .replace("{name}", &ctx.name) + .replace("{text}", &message) + ); -fn format_message(ctx: Arc, message: String) -> Option { + if ctx.enable_auth { + send_message_spoof_auth(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?; + } else { + send_message(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?; + } + } + + Ok(()) +} + +pub fn format_message(ctx: Arc, message: String) -> Option { let message = sanitize_text(&message); let date = DATE_REGEX.captures(&message)?; @@ -282,7 +242,6 @@ 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) { @@ -290,279 +249,4 @@ fn find_username_color(message: &str) -> Option<(String, String, Color)> { } } None -} - - -fn replace_input(cursor: usize, len: usize, text: &str) { - let spaces = if text.chars().count() < len { - len-text.chars().count() - } else { - 0 - }; - write!(stdout(), - "{}{}{}{}", - MoveLeft(1).to_string().repeat(cursor), - text, - " ".repeat(spaces), - MoveLeft(1).to_string().repeat(spaces) - ).unwrap(); - stdout().lock().flush().unwrap(); -} - -fn replace_input_left(cursor: usize, len: usize, text: &str, left: usize) { - let spaces = if text.chars().count() < len { - len-text.chars().count() - } else { - 0 - }; - write!(stdout(), - "{}{}{}{}", - MoveLeft(1).to_string().repeat(cursor), - text, - " ".repeat(spaces), - MoveLeft(1).to_string().repeat(len-left) - ).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 } - - let event = match event::read() { - Ok(i) => i, - Err(_) => { continue }, - }; - - match event { - Event::Key(event) => { - match event.code { - KeyCode::Enter => { - let message = input.read().unwrap().clone(); - - if !message.is_empty() { - replace_input(cursor, message.chars().count(), ""); - input.write().unwrap().clear(); - - cursor = 0; - - history.push(String::new()); - history_cursor = history.len()-1; - - if message.starts_with("/") && !ctx.disable_commands { - on_command(ctx.clone(), &message)?; - } else { - let message = prepare_message( - ctx.clone(), - &ctx.message_format - .replace("{name}", &ctx.name) - .replace("{text}", &message) - ); - - if ctx.enable_auth { - send_message_auth(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?; - } else { - send_message(&mut connect(&ctx.host, ctx.enable_ssl)?, &message)?; - } - } - } else { - print_console( - ctx.clone(), - messages.messages(), - "" - )?; - } - } - KeyCode::Backspace => { - if cursor == 0 || !(0..=history[history_cursor].len()).contains(&(cursor)) { - continue - } - let len = input.read().unwrap().chars().count(); - let i = char_index_to_byte_index(&history[history_cursor], cursor-1); - history[history_cursor].remove(i); - *input.write().unwrap() = history[history_cursor].clone(); - replace_input_left(cursor, len, &history[history_cursor], cursor-1); - cursor -= 1; - } - KeyCode::Delete => { - if cursor == 0 || !(0..history[history_cursor].len()).contains(&(cursor)) { - continue - } - let len = input.read().unwrap().chars().count(); - let i = char_index_to_byte_index(&history[history_cursor], cursor); - history[history_cursor].remove(i); - *input.write().unwrap() = history[history_cursor].clone(); - replace_input_left(cursor, len, &history[history_cursor], cursor); - } - KeyCode::Esc => { - on_close(); - break; - } - KeyCode::Up | KeyCode::Down => { - history_cursor = if event.code == KeyCode::Up { - max(history_cursor, 1) - 1 - } else { - min(history_cursor + 1, history.len() - 1) - }; - let len = input.read().unwrap().chars().count(); - *input.write().unwrap() = history[history_cursor].clone(); - replace_input(cursor, len, &history[history_cursor]); - cursor = history[history_cursor].chars().count(); - } - KeyCode::PageUp => { - let height = terminal::size().unwrap().1 as usize; - ctx.scroll.store(min(ctx.scroll.load(Ordering::SeqCst)+height, ctx.messages.messages().len()), Ordering::SeqCst); - print_console( - ctx.clone(), - messages.messages(), - &input.read().unwrap() - )?; - } - KeyCode::PageDown => { - let height = terminal::size().unwrap().1 as usize; - ctx.scroll.store(max(ctx.scroll.load(Ordering::SeqCst), height)-height, Ordering::SeqCst); - print_console( - ctx.clone(), - messages.messages(), - &input.read().unwrap() - )?; - } - KeyCode::Left => { - if cursor > 0 { - cursor -= 1; - write!(stdout(), "{}", MoveLeft(1).to_string(), ).unwrap(); - stdout().lock().flush().unwrap(); - } - } - KeyCode::Right => { - if cursor < history[history_cursor].len() { - cursor += 1; - write!(stdout(), "{}", MoveRight(1).to_string(), ).unwrap(); - stdout().lock().flush().unwrap(); - } - } - KeyCode::Char(c) => { - if event.modifiers.contains(KeyModifiers::CONTROL) && "zxcZXCячсЯЧС".contains(c) { - on_close(); - break; - } - let i = char_index_to_byte_index(&history[history_cursor], cursor); - history[history_cursor].insert(i, c); - input.write().unwrap().insert(i, c); - write!(stdout(), "{}{}", - history[history_cursor][i..].to_string(), - MoveLeft(1).to_string().repeat(history[history_cursor].chars().count()-cursor-1) - ).unwrap(); - stdout().lock().flush().unwrap(); - cursor += 1; - } - _ => {} - } - }, - Event::Paste(data) => { - let i = char_index_to_byte_index(&history[history_cursor], cursor); - history[history_cursor].insert_str(i, &data); - input.write().unwrap().insert_str(i, &data); - write!(stdout(), "{}{}", - history[history_cursor][cursor..].to_string(), - MoveLeft(1).to_string().repeat(history[history_cursor].len()-cursor-1) - ).unwrap(); - stdout().lock().flush().unwrap(); - cursor += data.len(); - }, - Event::Resize(_, _) => { - print_console( - ctx.clone(), - messages.messages(), - &input.read().unwrap() - )?; - }, - Event::Mouse(data) => { - match data.kind { - MouseEventKind::ScrollUp => { - ctx.scroll.store(min(ctx.scroll.load(Ordering::SeqCst)+3, ctx.messages.messages().len()), Ordering::SeqCst); - print_console( - ctx.clone(), - messages.messages(), - &input.read().unwrap() - )?; - }, - MouseEventKind::ScrollDown => { - ctx.scroll.store(max(ctx.scroll.load(Ordering::SeqCst), 3)-3, Ordering::SeqCst); - print_console( - ctx.clone(), - messages.messages(), - &input.read().unwrap() - )?; - }, - _ => {} - } - } - _ => {} - } - } - - Ok(()) -} - -pub fn recv_tick(ctx: Arc) -> Result<(), Box> { - match read_messages( - &mut connect(&ctx.host, ctx.enable_ssl)?, - ctx.max_messages, - ctx.messages.packet_size(), - !ctx.enable_ssl, - ctx.enable_chunked - ) { - Ok(Some((messages, size))) => { - let messages: Vec = if ctx.disable_formatting { - messages - } else { - messages.into_iter().flat_map(|o| format_message(ctx.clone(), o)).collect() - }; - - if ctx.enable_chunked { - ctx.messages.append(messages.clone(), size); - print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap())?; - } else { - ctx.messages.update(messages.clone(), size); - print_console(ctx.clone(), messages, &ctx.input.read().unwrap())?; - } - } - Err(e) => { - println!("{:?}", e); - } - _ => {} - } - thread::sleep(Duration::from_millis(ctx.update_time as u64)); - Ok(()) -} - -pub fn on_close() { - disable_raw_mode().unwrap(); - execute!(stdout(), event::DisableMouseCapture).unwrap(); -} - -pub fn run_main_loop(ctx: Arc) { - enable_raw_mode().unwrap(); - execute!(stdout(), event::EnableMouseCapture).unwrap(); - - 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/chat/minimal_tui.rs b/src/chat/minimal_tui.rs new file mode 100644 index 0000000..65d25f1 --- /dev/null +++ b/src/chat/minimal_tui.rs @@ -0,0 +1,67 @@ +use std::sync::Arc; + +use colored::Colorize; + +use super::{ + super::{ + config::Context, + proto::{connect, read_messages}, + util::get_input + }, format_message, on_send_message +}; + +pub fn run_main_loop(ctx: Arc) { + loop { + match connect(&ctx.host, ctx.enable_ssl) { + Ok(mut stream) => { + match read_messages( + &mut stream, + ctx.max_messages, + ctx.messages.packet_size(), + !ctx.enable_ssl, + ctx.enable_chunked + ) { + Ok(Some((messages, size))) => { + let messages: Vec = if ctx.disable_formatting { + messages + } else { + messages.into_iter().flat_map(|o| format_message(ctx.clone(), o)).collect() + }; + + if ctx.enable_chunked { + ctx.messages.append_and_store(messages.clone(), size); + } else { + ctx.messages.update(messages.clone(), size); + } + } + Err(e) => { + let msg = format!("Read messages error: {}", e.to_string()).bright_red().to_string(); + ctx.messages.append(vec![msg]); + } + _ => {} + } + }, + Err(e) => { + let msg = format!("Connect error: {}", e.to_string()).bright_red().to_string(); + ctx.messages.append(vec![msg]); + } + } + + println!( + "{}\n{} ", + ctx.messages.messages() + .into_iter() + .map(|o| o.white().blink().to_string()) + .collect::>() + .join("\n"), + ">".bright_yellow() + ); + + if let Some(message) = get_input("") { + if let Err(e) = on_send_message(ctx.clone(), &message) { + let msg = format!("Send message error: {}", e.to_string()).bright_red().to_string(); + ctx.messages.append(vec![msg]); + } + } + } +} \ No newline at end of file diff --git a/src/chat/pretty_tui.rs b/src/chat/pretty_tui.rs new file mode 100644 index 0000000..9a3d210 --- /dev/null +++ b/src/chat/pretty_tui.rs @@ -0,0 +1,377 @@ +use crossterm::{ + cursor::{MoveLeft, MoveRight}, + event::{self, Event, KeyCode, KeyModifiers, MouseEventKind}, + execute, + terminal::{self, disable_raw_mode, enable_raw_mode} +}; + +use colored::Colorize; + +use std::{ + cmp::{max, min}, + error::Error, io::{stdout, Write}, + sync::{atomic::Ordering, Arc}, + thread, + time::Duration +}; + +use super::{ + super::{ + config::Context, proto::{connect, read_messages}, util::{char_index_to_byte_index, string_chunks} + }, format_message, on_send_message +}; + + +pub fn print_console(ctx: Arc, messages: Vec, input: &str) -> Result<(), Box> { + let (width, height) = terminal::size()?; + let (width, height) = (width as usize, height as usize); + + let mut messages = messages + .into_iter() + .flat_map(|o| string_chunks(&o, width as usize - 1)) + .map(|o| (o.0.white().blink().to_string(), o.1)) + .collect::>(); + + let messages_size = if messages.len() >= height { + messages.len()-height + } else { + for _ in 0..height-messages.len() { + messages.insert(0, (String::new(), 0)); + } + 0 + }; + + let scroll = min(ctx.scroll.load(Ordering::SeqCst), messages_size); + let scroll_f = ((1f64 - scroll as f64 / (messages_size+1) as f64) * (height-2) as f64).round() as usize+1; + + let messages = if height < messages.len() { + if scroll < messages.len() - height { + messages[ + messages.len()-height-scroll.. + messages.len()-scroll + ].to_vec() + } else { + if scroll < messages.len() { + messages[ + 0.. + messages.len()-scroll + ].to_vec() + } else { + vec![] + } + } + } else { + messages + }; + + let formatted_messages = if ctx.disable_formatting { + messages + .into_iter() + .map(|(i, _)| i) + .collect::>() + } else { + messages + .into_iter() + .enumerate() + .map(|(i, (s, l))| { + format!("{}{}{}", + s, + " ".repeat(width - 1 - l), + if i == scroll_f { + "▐".bright_yellow() + } else { + "▕".yellow() + } + ) + }) + .collect::>() + + }; + + let text = format!( + "{}\r\n{} {}", + formatted_messages.join("\r\n"), + ">".bright_yellow(), + input + ); + + let mut out = stdout().lock(); + write!(out, "{}", text)?; + out.flush()?; + + Ok(()) +} + + + +fn replace_input(cursor: usize, len: usize, text: &str) { + let spaces = if text.chars().count() < len { + len-text.chars().count() + } else { + 0 + }; + write!(stdout(), + "{}{}{}{}", + MoveLeft(1).to_string().repeat(cursor), + text, + " ".repeat(spaces), + MoveLeft(1).to_string().repeat(spaces) + ).unwrap(); + stdout().lock().flush().unwrap(); +} + +fn replace_input_left(cursor: usize, len: usize, text: &str, left: usize) { + let spaces = if text.chars().count() < len { + len-text.chars().count() + } else { + 0 + }; + write!(stdout(), + "{}{}{}{}", + MoveLeft(1).to_string().repeat(cursor), + text, + " ".repeat(spaces), + MoveLeft(1).to_string().repeat(len-left) + ).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 } + + let event = match event::read() { + Ok(i) => i, + Err(_) => { continue }, + }; + + match event { + Event::Key(event) => { + match event.code { + KeyCode::Enter => { + let message = input.read().unwrap().clone(); + + if !message.is_empty() { + replace_input(cursor, message.chars().count(), ""); + input.write().unwrap().clear(); + + cursor = 0; + + history.push(String::new()); + history_cursor = history.len()-1; + + if let Err(e) = on_send_message(ctx.clone(), &message) { + let msg = format!("Send message error: {}", e.to_string()).bright_red().to_string(); + ctx.messages.append(vec![msg]); + print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap())?; + } + } else { + print_console( + ctx.clone(), + messages.messages(), + "" + )?; + } + } + KeyCode::Backspace => { + if cursor == 0 || !(0..=history[history_cursor].len()).contains(&(cursor)) { + continue + } + let len = input.read().unwrap().chars().count(); + let i = char_index_to_byte_index(&history[history_cursor], cursor-1); + history[history_cursor].remove(i); + *input.write().unwrap() = history[history_cursor].clone(); + replace_input_left(cursor, len, &history[history_cursor], cursor-1); + cursor -= 1; + } + KeyCode::Delete => { + if cursor == 0 || !(0..history[history_cursor].len()).contains(&(cursor)) { + continue + } + let len = input.read().unwrap().chars().count(); + let i = char_index_to_byte_index(&history[history_cursor], cursor); + history[history_cursor].remove(i); + *input.write().unwrap() = history[history_cursor].clone(); + replace_input_left(cursor, len, &history[history_cursor], cursor); + } + KeyCode::Esc => { + on_close(); + break; + } + KeyCode::Up | KeyCode::Down => { + history_cursor = if event.code == KeyCode::Up { + max(history_cursor, 1) - 1 + } else { + min(history_cursor + 1, history.len() - 1) + }; + let len = input.read().unwrap().chars().count(); + *input.write().unwrap() = history[history_cursor].clone(); + replace_input(cursor, len, &history[history_cursor]); + cursor = history[history_cursor].chars().count(); + } + KeyCode::PageUp => { + let height = terminal::size().unwrap().1 as usize; + ctx.scroll.store(min(ctx.scroll.load(Ordering::SeqCst)+height, ctx.messages.messages().len()), Ordering::SeqCst); + print_console( + ctx.clone(), + messages.messages(), + &input.read().unwrap() + )?; + } + KeyCode::PageDown => { + let height = terminal::size().unwrap().1 as usize; + ctx.scroll.store(max(ctx.scroll.load(Ordering::SeqCst), height)-height, Ordering::SeqCst); + print_console( + ctx.clone(), + messages.messages(), + &input.read().unwrap() + )?; + } + KeyCode::Left => { + if cursor > 0 { + cursor -= 1; + write!(stdout(), "{}", MoveLeft(1).to_string(), ).unwrap(); + stdout().lock().flush().unwrap(); + } + } + KeyCode::Right => { + if cursor < history[history_cursor].len() { + cursor += 1; + write!(stdout(), "{}", MoveRight(1).to_string(), ).unwrap(); + stdout().lock().flush().unwrap(); + } + } + KeyCode::Char(c) => { + if event.modifiers.contains(KeyModifiers::CONTROL) && "zxcZXCячсЯЧС".contains(c) { + on_close(); + break; + } + let i = char_index_to_byte_index(&history[history_cursor], cursor); + history[history_cursor].insert(i, c); + input.write().unwrap().insert(i, c); + write!(stdout(), "{}{}", + history[history_cursor][i..].to_string(), + MoveLeft(1).to_string().repeat(history[history_cursor].chars().count()-cursor-1) + ).unwrap(); + stdout().lock().flush().unwrap(); + cursor += 1; + } + _ => {} + } + }, + Event::Paste(data) => { + let i = char_index_to_byte_index(&history[history_cursor], cursor); + history[history_cursor].insert_str(i, &data); + input.write().unwrap().insert_str(i, &data); + write!(stdout(), "{}{}", + history[history_cursor][cursor..].to_string(), + MoveLeft(1).to_string().repeat(history[history_cursor].len()-cursor-1) + ).unwrap(); + stdout().lock().flush().unwrap(); + cursor += data.len(); + }, + Event::Resize(_, _) => { + print_console( + ctx.clone(), + messages.messages(), + &input.read().unwrap() + )?; + }, + Event::Mouse(data) => { + match data.kind { + MouseEventKind::ScrollUp => { + ctx.scroll.store(min(ctx.scroll.load(Ordering::SeqCst)+3, ctx.messages.messages().len()), Ordering::SeqCst); + print_console( + ctx.clone(), + messages.messages(), + &input.read().unwrap() + )?; + }, + MouseEventKind::ScrollDown => { + ctx.scroll.store(max(ctx.scroll.load(Ordering::SeqCst), 3)-3, Ordering::SeqCst); + print_console( + ctx.clone(), + messages.messages(), + &input.read().unwrap() + )?; + }, + _ => {} + } + } + _ => {} + } + } + + Ok(()) +} + +pub fn recv_tick(ctx: Arc) -> Result<(), Box> { + match read_messages( + &mut connect(&ctx.host, ctx.enable_ssl)?, + ctx.max_messages, + ctx.messages.packet_size(), + !ctx.enable_ssl, + ctx.enable_chunked + ) { + Ok(Some((messages, size))) => { + let messages: Vec = if ctx.disable_formatting { + messages + } else { + messages.into_iter().flat_map(|o| format_message(ctx.clone(), o)).collect() + }; + + if ctx.enable_chunked { + ctx.messages.append_and_store(messages.clone(), size); + print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap())?; + } else { + ctx.messages.update(messages.clone(), size); + print_console(ctx.clone(), messages, &ctx.input.read().unwrap())?; + } + } + Err(e) => { + let msg = format!("Read messages error: {}", e.to_string()).bright_red().to_string(); + ctx.messages.append(vec![msg]); + print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap())?; + } + _ => {} + } + thread::sleep(Duration::from_millis(ctx.update_time as u64)); + Ok(()) +} + +pub fn on_close() { + disable_raw_mode().unwrap(); + execute!(stdout(), event::DisableMouseCapture).unwrap(); +} + +pub fn run_main_loop(ctx: Arc) { + enable_raw_mode().unwrap(); + execute!(stdout(), event::EnableMouseCapture).unwrap(); + + thread::spawn({ + let ctx = ctx.clone(); + + move || { + loop { + if let Err(e) = recv_tick(ctx.clone()) { + let msg = format!("Print messages error: {}", e.to_string()).bright_red().to_string(); + ctx.messages.append(vec![msg]); + let _ = print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap()); + } + } + } + }); + + if let Err(e) = poll_events(ctx.clone()) { + let msg = format!("Poll events error: {}", e.to_string()).bright_red().to_string(); + ctx.messages.append(vec![msg]); + let _ = print_console(ctx.clone(), ctx.messages.messages(), &ctx.input.read().unwrap()); + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 7238eb3..3e437c7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,7 @@ -use std::sync::{atomic::AtomicUsize, Arc, RwLock}; +use std::{str::FromStr, sync::{atomic::AtomicUsize, Arc, RwLock}}; #[allow(unused_imports)] use std::{env, fs, path::{Path, PathBuf}, thread, time::Duration}; use colored::Colorize; -use homedir::my_home; use rand::random; use serde_yml; use clap::Parser; @@ -114,18 +113,26 @@ pub fn load_config(path: PathBuf) -> Config { } pub fn get_config_path() -> PathBuf { + let home_dir = PathBuf::from_str(".").ok(); + + #[cfg(feature = "homedir")] + let home_dir = { + use homedir::my_home; + my_home().ok().flatten() + }; + #[allow(unused_variables)] let config_path = Path::new("config.yml").to_path_buf(); #[cfg(target_os = "linux")] let config_path = { - let home_dir = my_home().ok().flatten().expect("Config find path error"); + let home_dir = home_dir.expect("Config find path error"); home_dir.join(".config").join("bRAC").join("config.yml") }; #[cfg(target_os = "macos")] let config_path = { - let home_dir = my_home().ok().flatten().expect("Config find path error"); + let home_dir = home_dir.expect("Config find path error"); home_dir.join(".config").join("bRAC").join("config.yml") }; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ea610d6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +#[warn(non_snake_case)] + +mod proto; + +pub use proto::*; \ No newline at end of file diff --git a/src/proto.rs b/src/proto.rs index 4238436..480e455 100644 --- a/src/proto.rs +++ b/src/proto.rs @@ -1,10 +1,12 @@ use std::{error::Error, fmt::Debug, io::{Read, Write}, net::TcpStream}; -use native_tls::TlsConnector; - pub trait RacStream: Read + Write + Unpin + Send + Sync + Debug {} impl RacStream for T {} +/// Create RAC connection (also you can just TcpStream::connect) +/// +/// host - host string, example: "example.com:12345", "example.com" (default port is 42666) +/// ssl - wrap with ssl client, write false if you dont know what it is pub fn connect(host: &str, ssl: bool) -> Result, Box> { let host = if host.contains(":") { host.to_string() @@ -12,27 +14,59 @@ pub fn connect(host: &str, ssl: bool) -> Result, Box Result<(), Box> { +/// Send message +/// +/// stream - any stream that can be written to +/// message - message text +pub fn send_message(stream: &mut impl Write, message: &str) -> Result<(), Box> { stream.write_all(format!("\x01{message}").as_bytes())?; Ok(()) } -pub fn send_message_auth(stream: &mut (impl Read + Write), message: &str) -> Result<(), Box> { +/// Register user +/// +/// stream - any stream that can be written to +/// name - user name +/// password - user password +pub fn register_user(stream: &mut impl Write, name: &str, password: &str) -> Result<(), Box> { + stream.write_all(format!("\x03{name}\n{password}").as_bytes())?; + Ok(()) +} + +/// Send message with auth +/// +/// stream - any stream that can be written to +/// message - message text +/// name - user name +/// password - user password +pub fn send_message_auth(stream: &mut impl Write, name: &str, password: &str, message: &str) -> Result<(), Box> { + Ok(stream.write_all(format!("\x02{name}\n{password}\n{message}").as_bytes())?) +} + +/// Send message with fake auth +/// +/// im rly bored to explain all of this so if you want to know just check sources +pub fn send_message_spoof_auth(stream: &mut (impl Write + Read), message: &str) -> Result<(), Box> { let Some((name, message)) = message.split_once("> ") else { return send_message(stream, message) }; stream.write_all(format!("\x02{name}\n{name}\n{message}").as_bytes())?; @@ -42,18 +76,14 @@ pub fn send_message_auth(stream: &mut (impl Read + Write), message: &str) -> Res let name = format!("\x1f{name}"); register_user(stream, &name, &name)?; let message = format!("{name}> {message}"); - send_message_auth(stream, &message) + send_message_spoof_auth(stream, &message) } else { Ok(()) } } -pub fn register_user(stream: &mut (impl Read + Write), name: &str, password: &str) -> Result<(), Box> { - stream.write_all(format!("\x03{name}\n{password}").as_bytes())?; - Ok(()) -} - -fn skip_null(stream: &mut impl Read) -> Result, Box> { +/// Skip null bytes and return first non-null byte +pub fn skip_null(stream: &mut impl Read) -> Result, Box> { loop { let mut buf = vec![0; 1]; stream.read_exact(&mut buf)?; @@ -63,6 +93,14 @@ fn skip_null(stream: &mut impl Read) -> Result, Box> { } } +/// Read messages +/// +/// max_messages - max messages in list +/// last_size - last returned packet size +/// start_null - start with skipping null bytes +/// chunked - is chunked reading enabled +/// +/// returns (messages, packet size) pub fn read_messages( stream: &mut (impl Read + Write), max_messages: usize, diff --git a/src/util.rs b/src/util.rs index 56eebf5..aa3ba33 100644 --- a/src/util.rs +++ b/src/util.rs @@ -65,9 +65,12 @@ pub fn sanitize_text(input: &str) -> String { } 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 prompt = prompt.to_string(); + if !prompt.is_empty() { + 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()?;