mirror of
https://github.com/MeexReay/bRAC.git
synced 2025-05-06 13:38:04 +03:00
massive rewrite
This commit is contained in:
parent
036648b5e4
commit
58213cf8c9
110
Cargo.lock
generated
110
Cargo.lock
generated
@ -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"
|
||||
|
17
Cargo.toml
17
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"
|
||||
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"]
|
11
README.md
11
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
|
||||
|
68
flake.lock
generated
68
flake.lock
generated
@ -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",
|
||||
|
71
flake.nix
71
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);
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
416
src/chat.rs
416
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<String>, packet_size: usize) {
|
||||
pub fn append_and_store(&self, messages: Vec<String>, 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<String>) {
|
||||
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<Context>, command: &str) -> Result<(), Box<dyn Error>> {
|
||||
pub fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>> {
|
||||
let command = command.trim_start_matches("/");
|
||||
let (command, args) = command.split_once(" ").unwrap_or((&command, ""));
|
||||
let args = args.split(" ").collect::<Vec<&str>>();
|
||||
@ -121,89 +143,7 @@ fn on_command(ctx: Arc<Context>, command: &str) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn print_console(ctx: Arc<Context>, messages: Vec<String>, input: &str) -> Result<(), Box<dyn Error>> {
|
||||
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::<Vec<(String, usize)>>();
|
||||
|
||||
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::<Vec<String>>()
|
||||
} else {
|
||||
messages
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (s, l))| {
|
||||
format!("{}{}{}",
|
||||
s,
|
||||
" ".repeat(width - 1 - l),
|
||||
if i == scroll_f {
|
||||
"▐".bright_yellow()
|
||||
} else {
|
||||
"▕".yellow()
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
|
||||
};
|
||||
|
||||
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<Context>, message: &str) -> String {
|
||||
pub fn prepare_message(context: Arc<Context>, message: &str) -> String {
|
||||
format!("{}{}{}",
|
||||
if !context.disable_hiding_ip {
|
||||
"\r\x07"
|
||||
@ -229,8 +169,28 @@ fn prepare_message(context: Arc<Context>, message: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn Error>> {
|
||||
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<Context>, message: String) -> Option<String> {
|
||||
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<Context>, message: String) -> Option<String> {
|
||||
let message = sanitize_text(&message);
|
||||
|
||||
let date = DATE_REGEX.captures(&message)?;
|
||||
@ -282,7 +242,6 @@ fn format_message(ctx: Arc<Context>, message: String) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fn find_username_color(message: &str) -> Option<(String, String, Color)> {
|
||||
for (re, color) in COLORED_USERNAMES.iter() {
|
||||
if let Some(captures) = re.captures(message) {
|
||||
@ -291,278 +250,3 @@ 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<Context>) -> Result<(), Box<dyn Error>> {
|
||||
let mut history: Vec<String> = 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<Context>) -> Result<(), Box<dyn Error>> {
|
||||
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<String> = 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<Context>) {
|
||||
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");
|
||||
}
|
67
src/chat/minimal_tui.rs
Normal file
67
src/chat/minimal_tui.rs
Normal file
@ -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<Context>) {
|
||||
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<String> = 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::<Vec<String>>()
|
||||
.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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
377
src/chat/pretty_tui.rs
Normal file
377
src/chat/pretty_tui.rs
Normal file
@ -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<Context>, messages: Vec<String>, input: &str) -> Result<(), Box<dyn Error>> {
|
||||
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::<Vec<(String, usize)>>();
|
||||
|
||||
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::<Vec<String>>()
|
||||
} else {
|
||||
messages
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (s, l))| {
|
||||
format!("{}{}{}",
|
||||
s,
|
||||
" ".repeat(width - 1 - l),
|
||||
if i == scroll_f {
|
||||
"▐".bright_yellow()
|
||||
} else {
|
||||
"▕".yellow()
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
|
||||
};
|
||||
|
||||
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<Context>) -> Result<(), Box<dyn Error>> {
|
||||
let mut history: Vec<String> = 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<Context>) -> Result<(), Box<dyn Error>> {
|
||||
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<String> = 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<Context>) {
|
||||
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());
|
||||
}
|
||||
}
|
@ -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")
|
||||
};
|
||||
|
||||
|
5
src/lib.rs
Normal file
5
src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
||||
#[warn(non_snake_case)]
|
||||
|
||||
mod proto;
|
||||
|
||||
pub use proto::*;
|
84
src/proto.rs
84
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<T: Read + Write + Unpin + Send + Sync + Debug> 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<dyn RacStream>, Box<dyn Error>> {
|
||||
let host = if host.contains(":") {
|
||||
host.to_string()
|
||||
@ -12,27 +14,59 @@ pub fn connect(host: &str, ssl: bool) -> Result<Box<dyn RacStream>, Box<dyn Erro
|
||||
format!("{host}:42666")
|
||||
};
|
||||
|
||||
Ok(if ssl {
|
||||
let ip: String = host.split_once(":")
|
||||
.map(|o| o.0.to_string())
|
||||
.unwrap_or(host.clone());
|
||||
#[cfg(feature = "ssl")]
|
||||
{
|
||||
use native_tls::TlsConnector;
|
||||
|
||||
Box::new(TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()?
|
||||
.connect(&ip, connect(&host, false)?)?)
|
||||
} else {
|
||||
Box::new(TcpStream::connect(host)?)
|
||||
})
|
||||
if ssl {
|
||||
let ip: String = host.split_once(":")
|
||||
.map(|o| o.0.to_string())
|
||||
.unwrap_or(host.clone());
|
||||
|
||||
return Ok(Box::new(TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()?
|
||||
.connect(&ip, connect(&host, false)?)?))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Box::new(TcpStream::connect(host)?))
|
||||
}
|
||||
|
||||
pub fn send_message(stream: &mut (impl Read + Write), message: &str) -> Result<(), Box<dyn Error>> {
|
||||
/// 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<dyn Error>> {
|
||||
stream.write_all(format!("\x01{message}").as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_message_auth(stream: &mut (impl Read + Write), message: &str) -> Result<(), Box<dyn Error>> {
|
||||
/// 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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
stream.write_all(format!("\x03{name}\n{password}").as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
/// Skip null bytes and return first non-null byte
|
||||
pub fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
loop {
|
||||
let mut buf = vec![0; 1];
|
||||
stream.read_exact(&mut buf)?;
|
||||
@ -63,6 +93,14 @@ fn skip_null(stream: &mut impl Read) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
|
@ -65,9 +65,12 @@ pub fn sanitize_text(input: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn get_input(prompt: impl ToString) -> Option<String> {
|
||||
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()?;
|
||||
|
Loading…
Reference in New Issue
Block a user