mirror of
https://github.com/MeexReay/bRAC.git
synced 2025-05-06 13:38:04 +03:00
gui init commit
This commit is contained in:
parent
bc89528be6
commit
dbc6323ab6
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -92,7 +92,6 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
|||||||
name = "bRAC"
|
name = "bRAC"
|
||||||
version = "0.1.3+2.0"
|
version = "0.1.3+2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"colored",
|
"colored",
|
||||||
|
@ -15,7 +15,6 @@ crossterm = { version = "0.29.0", optional = true }
|
|||||||
homedir = { version = "0.3.4", optional = true }
|
homedir = { version = "0.3.4", optional = true }
|
||||||
native-tls = { version = "0.2.14", optional = true }
|
native-tls = { version = "0.2.14", optional = true }
|
||||||
gtk4 = { version = "0.9.6", optional = true, features = [ "v4_10" ] }
|
gtk4 = { version = "0.9.6", optional = true, features = [ "v4_10" ] }
|
||||||
cfg-if = "1.0.0"
|
|
||||||
chrono = { version = "0.4.40", optional = true }
|
chrono = { version = "0.4.40", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
use std::{str::FromStr, sync::{Arc, RwLock}};
|
use std::str::FromStr;
|
||||||
#[allow(unused_imports)]
|
use std::{fs, path::PathBuf, thread, time::Duration};
|
||||||
use std::{env, fs, path::{Path, PathBuf}, thread, time::Duration};
|
|
||||||
use colored::Colorize;
|
|
||||||
use rand::random;
|
|
||||||
use serde_yml;
|
use serde_yml;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use crate::chat::ChatContext;
|
use super::gui::{ask_bool, ask_string, ask_string_option, ask_usize, show_message};
|
||||||
|
|
||||||
use super::util::get_input;
|
|
||||||
|
|
||||||
const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}";
|
const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}";
|
||||||
|
|
||||||
@ -41,31 +36,10 @@ fn default_update_time() -> usize { 50 }
|
|||||||
fn default_host() -> String { "meex.lol:11234".to_string() }
|
fn default_host() -> String { "meex.lol:11234".to_string() }
|
||||||
fn default_message_format() -> String { MESSAGE_FORMAT.to_string() }
|
fn default_message_format() -> String { MESSAGE_FORMAT.to_string() }
|
||||||
|
|
||||||
fn ask_usize(name: impl ToString, default: usize) -> usize {
|
|
||||||
get_input(format!("{} (default: {}) {} ", name.to_string().bold(), default, ">".bold()).bright_yellow())
|
|
||||||
.and_then(|o| o.parse().ok()).unwrap_or(default)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ask_string(name: impl ToString, default: impl ToString + Clone) -> String {
|
|
||||||
ask_string_option(name, default.clone()).unwrap_or(default.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ask_string_option(name: impl ToString, default: impl ToString) -> Option<String> {
|
|
||||||
let default = default.to_string();
|
|
||||||
get_input(format!("{} (default: {}) {} ", name.to_string().bold(), default, ">".bold()).bright_yellow())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ask_bool(name: impl ToString, default: bool) -> bool {
|
|
||||||
get_input(format!("{} (Y/N, default: {}) {} ", name.to_string().bold(), if default { "Y" } else { "N" }, ">".bold()).bright_yellow())
|
|
||||||
.map(|o| o.to_lowercase() != "n")
|
|
||||||
.unwrap_or(default)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn configure(path: PathBuf) -> Config {
|
pub fn configure(path: PathBuf) -> Config {
|
||||||
println!("{}", "To configure the client, please answer a few questions. It won't take long.".yellow());
|
show_message("Client setup", format!("To configure the client, please answer a few questions. It won't take long.
|
||||||
println!("{}", "You can reconfigure client in any moment via `bRAC --configure`".yellow());
|
You can reconfigure client in any moment via `bRAC --configure`
|
||||||
println!("{}", format!("Config stores in path `{}`", path.to_string_lossy()).yellow());
|
Config stores in path `{}`", path.to_string_lossy()));
|
||||||
println!();
|
|
||||||
|
|
||||||
let host = ask_string("Host", default_host());
|
let host = ask_string("Host", default_host());
|
||||||
let name = ask_string_option("Name", "ask every time");
|
let name = ask_string_option("Name", "ask every time");
|
||||||
@ -95,8 +69,7 @@ pub fn configure(path: PathBuf) -> Config {
|
|||||||
fs::create_dir_all(&path.parent().expect("Config save error")).expect("Config save error");
|
fs::create_dir_all(&path.parent().expect("Config save error")).expect("Config save error");
|
||||||
fs::write(&path, config_text).expect("Config save error");
|
fs::write(&path, config_text).expect("Config save error");
|
||||||
|
|
||||||
println!();
|
show_message("Config saved!", "You can reconfigure it in any moment via `bRAC --configure`");
|
||||||
println!("{}", "Config saved! You can reconfigure it in any moment via `bRAC --configure`".yellow());
|
|
||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
@ -210,43 +183,3 @@ pub struct Args {
|
|||||||
#[arg(short='u', long)]
|
#[arg(short='u', long)]
|
||||||
pub enable_chunked: bool,
|
pub enable_chunked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Context {
|
|
||||||
pub chat: Arc<RwLock<Option<Arc<ChatContext>>>>,
|
|
||||||
pub host: String,
|
|
||||||
pub name: String,
|
|
||||||
pub disable_formatting: bool,
|
|
||||||
pub disable_commands: bool,
|
|
||||||
pub disable_hiding_ip: bool,
|
|
||||||
pub message_format: String,
|
|
||||||
pub update_time: usize,
|
|
||||||
pub max_messages: usize,
|
|
||||||
pub enable_ip_viewing: bool,
|
|
||||||
pub enable_auth: bool,
|
|
||||||
pub enable_ssl: bool,
|
|
||||||
pub enable_chunked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Context {
|
|
||||||
pub fn new(config: &Config, args: &Args) -> Context {
|
|
||||||
Context {
|
|
||||||
chat: Arc::new(RwLock::new(None)),
|
|
||||||
message_format: args.message_format.clone().unwrap_or(config.message_format.clone()),
|
|
||||||
host: args.host.clone().unwrap_or(config.host.clone()),
|
|
||||||
name: args.name.clone().or(config.name.clone()).unwrap_or_else(|| ask_string("Name", format!("Anon#{:X}", random::<u16>()))),
|
|
||||||
disable_formatting: args.disable_formatting,
|
|
||||||
disable_commands: args.disable_commands,
|
|
||||||
disable_hiding_ip: args.disable_ip_hiding,
|
|
||||||
update_time: config.update_time,
|
|
||||||
max_messages: config.max_messages,
|
|
||||||
enable_ip_viewing: args.enable_users_ip_viewing || config.enable_ip_viewing,
|
|
||||||
enable_auth: args.enable_auth || config.enable_auth,
|
|
||||||
enable_ssl: args.enable_ssl || config.enable_ssl,
|
|
||||||
enable_chunked: args.enable_chunked || config.enable_chunked,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn chat(&self) -> Arc<ChatContext> {
|
|
||||||
self.chat.read().unwrap().clone().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
50
src/chat/ctx.rs
Normal file
50
src/chat/ctx.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use rand::random;
|
||||||
|
|
||||||
|
use super::{config::{Args, Config}, gui::ask_string, ChatContext};
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
pub chat: Arc<RwLock<Option<Arc<ChatContext>>>>,
|
||||||
|
pub host: String,
|
||||||
|
pub name: String,
|
||||||
|
pub disable_formatting: bool,
|
||||||
|
pub disable_commands: bool,
|
||||||
|
pub disable_hiding_ip: bool,
|
||||||
|
pub message_format: String,
|
||||||
|
pub update_time: usize,
|
||||||
|
pub max_messages: usize,
|
||||||
|
pub enable_ip_viewing: bool,
|
||||||
|
pub enable_auth: bool,
|
||||||
|
pub enable_ssl: bool,
|
||||||
|
pub enable_chunked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn new(config: &Config, args: &Args) -> Context {
|
||||||
|
Context {
|
||||||
|
chat: Arc::new(RwLock::new(None)),
|
||||||
|
message_format: args.message_format.clone().unwrap_or(config.message_format.clone()),
|
||||||
|
host: args.host.clone().unwrap_or(config.host.clone()),
|
||||||
|
name: args.name.clone()
|
||||||
|
.or(config.name.clone())
|
||||||
|
.unwrap_or_else(|| ask_string(
|
||||||
|
"Name",
|
||||||
|
format!("Anon#{:X}", random::<u16>())
|
||||||
|
)),
|
||||||
|
disable_formatting: args.disable_formatting,
|
||||||
|
disable_commands: args.disable_commands,
|
||||||
|
disable_hiding_ip: args.disable_ip_hiding,
|
||||||
|
update_time: config.update_time,
|
||||||
|
max_messages: config.max_messages,
|
||||||
|
enable_ip_viewing: args.enable_users_ip_viewing || config.enable_ip_viewing,
|
||||||
|
enable_auth: args.enable_auth || config.enable_auth,
|
||||||
|
enable_ssl: args.enable_ssl || config.enable_ssl,
|
||||||
|
enable_chunked: args.enable_chunked || config.enable_chunked,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chat(&self) -> Arc<ChatContext> {
|
||||||
|
self.chat.read().unwrap().clone().unwrap()
|
||||||
|
}
|
||||||
|
}
|
@ -18,10 +18,9 @@ use gtk4::{
|
|||||||
Button, Calendar, CssProvider, Entry, Fixed, Justification, Label, ListBox, Orientation, Overlay, Picture, ScrolledWindow, Settings
|
Button, Calendar, CssProvider, Entry, Fixed, Justification, Label, ListBox, Orientation, Overlay, Picture, ScrolledWindow, Settings
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::Context;
|
|
||||||
use crate::proto::{connect, read_messages};
|
use crate::proto::{connect, read_messages};
|
||||||
|
|
||||||
use super::{format_message, on_send_message, parse_message, set_chat, ChatStorage};
|
use super::{format_message, on_send_message, parse_message, set_chat, ChatStorage, ctx::Context};
|
||||||
|
|
||||||
pub struct ChatContext {
|
pub struct ChatContext {
|
||||||
pub messages: Arc<ChatStorage>,
|
pub messages: Arc<ChatStorage>,
|
||||||
@ -86,6 +85,28 @@ pub fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ask_usize(name: impl ToString, default: usize) -> usize {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ask_string(name: impl ToString, default: impl ToString + Clone) -> String {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ask_string_option(name: impl ToString, default: impl ToString) -> Option<String> {
|
||||||
|
let default = default.to_string();
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ask_bool(name: impl ToString, default: bool) -> bool {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_message(title: impl ToString, message: impl ToString) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
fn load_pixbuf(data: &[u8]) -> Pixbuf {
|
fn load_pixbuf(data: &[u8]) -> Pixbuf {
|
||||||
let loader = PixbufLoader::new();
|
let loader = PixbufLoader::new();
|
||||||
loader.write(data).unwrap();
|
loader.write(data).unwrap();
|
||||||
@ -179,7 +200,7 @@ fn build_menu(_: Arc<Context>, app: &Application) {
|
|||||||
.comments("better RAC client")
|
.comments("better RAC client")
|
||||||
.website("https://github.com/MeexReay/bRAC")
|
.website("https://github.com/MeexReay/bRAC")
|
||||||
.website_label("source code")
|
.website_label("source code")
|
||||||
.logo(&Texture::for_pixbuf(&load_pixbuf(include_bytes!("../../assets/icon.png"))))
|
.logo(&Texture::for_pixbuf(&load_pixbuf(include_bytes!("images/icon.png"))))
|
||||||
.build()
|
.build()
|
||||||
.present();
|
.present();
|
||||||
}
|
}
|
||||||
@ -222,17 +243,19 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
|
|||||||
let fixed = Fixed::new();
|
let fixed = Fixed::new();
|
||||||
fixed.set_can_target(false);
|
fixed.set_can_target(false);
|
||||||
|
|
||||||
let konata = Picture::for_pixbuf(&load_pixbuf(include_bytes!("../../assets/konata.png")));
|
let konata = Picture::for_pixbuf(&load_pixbuf(include_bytes!("images/konata.png")));
|
||||||
konata.set_size_request(174, 127);
|
konata.set_size_request(174, 127);
|
||||||
|
|
||||||
fixed.put(&konata, 325.0, 4.0);
|
fixed.put(&konata, 325.0, 4.0);
|
||||||
|
|
||||||
let logo = Picture::for_pixbuf(&load_pixbuf(include_bytes!("../../assets/logo.gif")));
|
let logo_gif = include_bytes!("images/logo.gif");
|
||||||
|
|
||||||
|
let logo = Picture::for_pixbuf(&load_pixbuf(logo_gif));
|
||||||
logo.set_size_request(152, 64);
|
logo.set_size_request(152, 64);
|
||||||
|
|
||||||
let logo_anim = PixbufAnimation::from_stream(
|
let logo_anim = PixbufAnimation::from_stream(
|
||||||
&MemoryInputStream::from_bytes(
|
&MemoryInputStream::from_bytes(
|
||||||
&glib::Bytes::from(include_bytes!("../../assets/logo.gif"))
|
&glib::Bytes::from(logo_gif)
|
||||||
),
|
),
|
||||||
None::<&gio::Cancellable>
|
None::<&gio::Cancellable>
|
||||||
).unwrap().iter(Some(SystemTime::now()));
|
).unwrap().iter(Some(SystemTime::now()));
|
||||||
@ -447,58 +470,24 @@ fn setup(ctx: Arc<Context>, ui: UiModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_css() {
|
fn load_css() {
|
||||||
|
let is_dark_theme = if let Some(settings) = Settings::default() {
|
||||||
|
settings.is_gtk_application_prefer_dark_theme() || settings.gtk_theme_name()
|
||||||
|
.map(|o| o.to_lowercase().contains("dark"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
let provider = CssProvider::new();
|
let provider = CssProvider::new();
|
||||||
provider.load_from_data(&format!(
|
provider.load_from_data(&format!(
|
||||||
"{}\n{}",
|
"{}\n{}",
|
||||||
if let Some(settings) = Settings::default() {
|
if is_dark_theme {
|
||||||
if settings.is_gtk_application_prefer_dark_theme() {
|
include_str!("styles/dark.css")
|
||||||
".message-content { color:rgb(255, 255, 255); }
|
|
||||||
.message-date { color:rgb(146, 146, 146); }
|
|
||||||
.message-ip { color:rgb(73, 73, 73); }"
|
|
||||||
} else {
|
} else {
|
||||||
".message-content { color:rgb(0, 0, 0); }
|
include_str!("styles/light.css")
|
||||||
.message-date { color:rgb(41, 41, 41); }
|
|
||||||
.message-ip { color:rgb(88, 88, 88); }"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
},
|
||||||
"
|
include_str!("styles/style.css")
|
||||||
.send-button, .send-text { border-radius: 0; }
|
));
|
||||||
.calendar {
|
|
||||||
transform: scale(0.6);
|
|
||||||
margin: -35px;
|
|
||||||
}
|
|
||||||
.widget_box {
|
|
||||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.20);
|
|
||||||
border-bottom: 2px solid rgba(0, 0, 0, 0.20);
|
|
||||||
min-height: 121px;
|
|
||||||
}
|
|
||||||
.time {
|
|
||||||
font-size: 20px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-name { font-weight: bold; }
|
|
||||||
|
|
||||||
.message-name-black { color: #2E2E2E; }
|
|
||||||
.message-name-bright-black { color: #555555; }
|
|
||||||
.message-name-red { color: #8B0000; }
|
|
||||||
.message-name-bright-red { color: #FF0000; }
|
|
||||||
.message-name-green { color: #006400; }
|
|
||||||
.message-name-bright-green { color: #00FF00; }
|
|
||||||
.message-name-yellow { color: #8B8B00; }
|
|
||||||
.message-name-bright-yellow { color: #FFFF00; }
|
|
||||||
.message-name-blue { color: #00008B; }
|
|
||||||
.message-name-bright-blue { color: #0000FF; }
|
|
||||||
.message-name-bright-magenta { color: #FF00FF; }
|
|
||||||
.message-name-magenta { color: #8B008B; }
|
|
||||||
.message-name-cyan { color: #008B8B; }
|
|
||||||
.message-name-bright-cyan { color: #00FFFF; }
|
|
||||||
.message-name-white { color: #A9A9A9; }
|
|
||||||
.message-name-bright-white { color: #FFFFFF; }
|
|
||||||
"));
|
|
||||||
|
|
||||||
gtk::style_context_add_provider_for_display(
|
gtk::style_context_add_provider_for_display(
|
||||||
&Display::default().expect("Could not connect to a display."),
|
&Display::default().expect("Could not connect to a display."),
|
BIN
src/chat/images/icon.png
Normal file
BIN
src/chat/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
src/chat/images/image.png
Normal file
BIN
src/chat/images/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
BIN
src/chat/images/konata.png
Normal file
BIN
src/chat/images/konata.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
src/chat/images/logo.gif
Normal file
BIN
src/chat/images/logo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@ -1,97 +0,0 @@
|
|||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::io::stdout;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use colored::Colorize;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
super::{
|
|
||||||
config::Context,
|
|
||||||
proto::{connect, read_messages},
|
|
||||||
util::get_input
|
|
||||||
}, format_message, on_send_message, ChatStorage, set_chat
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ChatContext {
|
|
||||||
pub messages: Arc<ChatStorage>,
|
|
||||||
pub registered: Arc<RwLock<Option<String>>>
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_console(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
|
|
||||||
let messages = ctx.chat().messages.messages();
|
|
||||||
|
|
||||||
let mut out = stdout().lock();
|
|
||||||
write!(
|
|
||||||
out,
|
|
||||||
"{}\n{}\n{} ",
|
|
||||||
"\n".repeat(ctx.max_messages - messages.len()),
|
|
||||||
messages
|
|
||||||
.into_iter()
|
|
||||||
.map(|o| o.white().blink().to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n"),
|
|
||||||
">".bright_yellow()
|
|
||||||
);
|
|
||||||
out.flush();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print_message(ctx: Arc<Context>, message: String) -> Result<(), Box<dyn Error>> {
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![message]);
|
|
||||||
update_console(ctx.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_main_loop(ctx: Arc<Context>) {
|
|
||||||
set_chat(ctx.clone(), ChatContext {
|
|
||||||
messages: Arc::new(ChatStorage::new()),
|
|
||||||
registered: Arc::new(RwLock::new(None)),
|
|
||||||
});
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match connect(&ctx.host, ctx.enable_ssl) {
|
|
||||||
Ok(mut stream) => {
|
|
||||||
match read_messages(
|
|
||||||
&mut stream,
|
|
||||||
ctx.max_messages,
|
|
||||||
ctx.chat().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.enable_ip_viewing, o)).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
if ctx.enable_chunked {
|
|
||||||
ctx.chat().messages.append_and_store(ctx.max_messages, messages.clone(), size);
|
|
||||||
} else {
|
|
||||||
ctx.chat().messages.update(ctx.max_messages, messages.clone(), size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let msg = format!("Read messages error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
let msg = format!("Connect error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = update_console(ctx.clone());
|
|
||||||
|
|
||||||
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.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,13 +10,20 @@ use crate::proto::{register_user, send_message_auth};
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
proto::{connect, read_messages, send_message, send_message_spoof_auth},
|
proto::{connect, read_messages, send_message, send_message_spoof_auth},
|
||||||
util::sanitize_text,
|
util::sanitize_text
|
||||||
config::Context
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use cfg_if::cfg_if;
|
|
||||||
|
use ctx::Context;
|
||||||
|
|
||||||
|
pub use gui::{
|
||||||
|
ChatContext,
|
||||||
|
print_message,
|
||||||
|
run_main_loop
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap();
|
pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap();
|
||||||
@ -31,18 +38,9 @@ lazy_static! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
cfg_if! {
|
pub mod gui;
|
||||||
if #[cfg(feature = "pretty_tui")] {
|
pub mod config;
|
||||||
mod pretty_tui;
|
pub mod ctx;
|
||||||
pub use pretty_tui::*;
|
|
||||||
} else if #[cfg(feature = "gtk_gui")] {
|
|
||||||
mod gtk_gui;
|
|
||||||
pub use gtk_gui::*;
|
|
||||||
} else {
|
|
||||||
mod minimal_tui;
|
|
||||||
pub use minimal_tui::*;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub struct ChatStorage {
|
pub struct ChatStorage {
|
@ -1,417 +0,0 @@
|
|||||||
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::{AtomicUsize, Ordering}, Arc, RwLock},
|
|
||||||
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, set_chat, ChatStorage
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
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.chat().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.chat().input.clone();
|
|
||||||
let messages = ctx.chat().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.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().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.chat().scroll.store(min(
|
|
||||||
ctx.chat().scroll.load(Ordering::SeqCst)+height,
|
|
||||||
ctx.chat().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.chat().scroll.store(max(
|
|
||||||
ctx.chat().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.chat().scroll.store(min(
|
|
||||||
ctx.chat().scroll.load(Ordering::SeqCst)+3,
|
|
||||||
ctx.chat().messages.messages().len()
|
|
||||||
), Ordering::SeqCst);
|
|
||||||
print_console(
|
|
||||||
ctx.clone(),
|
|
||||||
messages.messages(),
|
|
||||||
&input.read().unwrap()
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
MouseEventKind::ScrollDown => {
|
|
||||||
ctx.chat().scroll.store(max(ctx.chat().scroll.load(Ordering::SeqCst), 3)-3, Ordering::SeqCst);
|
|
||||||
print_console(
|
|
||||||
ctx.clone(),
|
|
||||||
messages.messages(),
|
|
||||||
&input.read().unwrap()
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
|
|
||||||
match read_messages(
|
|
||||||
&mut connect(&ctx.host, ctx.enable_ssl)?,
|
|
||||||
ctx.max_messages,
|
|
||||||
ctx.chat().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.enable_ip_viewing, o)).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
if ctx.enable_chunked {
|
|
||||||
ctx.chat().messages.append_and_store(ctx.max_messages, messages.clone(), size);
|
|
||||||
print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap())?;
|
|
||||||
} else {
|
|
||||||
ctx.chat().messages.update(ctx.max_messages, messages.clone(), size);
|
|
||||||
print_console(ctx.clone(), messages, &ctx.chat().input.read().unwrap())?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
let msg = format!("Read messages error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap())?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(ctx.update_time as u64));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_close() {
|
|
||||||
disable_raw_mode().unwrap();
|
|
||||||
execute!(stdout(), event::DisableMouseCapture).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub struct ChatContext {
|
|
||||||
pub messages: Arc<ChatStorage>,
|
|
||||||
pub input: Arc<RwLock<String>>,
|
|
||||||
pub registered: Arc<RwLock<Option<String>>>,
|
|
||||||
pub scroll: Arc<AtomicUsize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print_message(ctx: Arc<Context>, message: String) -> Result<(), Box<dyn Error>> {
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![message]);
|
|
||||||
print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_main_loop(ctx: Arc<Context>) {
|
|
||||||
set_chat(ctx.clone(), ChatContext {
|
|
||||||
messages: Arc::new(ChatStorage::new()),
|
|
||||||
input: Arc::new(RwLock::new(String::new())),
|
|
||||||
registered: Arc::new(RwLock::new(None)),
|
|
||||||
scroll: Arc::new(AtomicUsize::new(0)),
|
|
||||||
});
|
|
||||||
|
|
||||||
enable_raw_mode().unwrap();
|
|
||||||
execute!(stdout(), event::EnableMouseCapture).unwrap();
|
|
||||||
|
|
||||||
if let Err(e) = print_console(ctx.clone(), Vec::new(), &ctx.chat().input.read().unwrap()) {
|
|
||||||
let msg = format!("Print messages error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
let _ = print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().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.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
let _ = print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap());
|
|
||||||
thread::sleep(Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(e) = poll_events(ctx.clone()) {
|
|
||||||
let msg = format!("Poll events error: {}", e.to_string()).bright_red().to_string();
|
|
||||||
ctx.chat().messages.append(ctx.max_messages, vec![msg]);
|
|
||||||
let _ = print_console(ctx.clone(), ctx.chat().messages.messages(), &ctx.chat().input.read().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
3
src/chat/styles/dark.css
Normal file
3
src/chat/styles/dark.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.message-content { color:rgb(255, 255, 255); }
|
||||||
|
.message-date { color:rgb(146, 146, 146); }
|
||||||
|
.message-ip { color:rgb(73, 73, 73); }
|
38
src/chat/styles/light.css
Normal file
38
src/chat/styles/light.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
.message-content { color:rgb(0, 0, 0); }
|
||||||
|
.message-date { color:rgb(41, 41, 41); }
|
||||||
|
.message-ip { color:rgb(88, 88, 88); }
|
||||||
|
|
||||||
|
.send-button, .send-text { border-radius: 0; }
|
||||||
|
.calendar {
|
||||||
|
transform: scale(0.6);
|
||||||
|
margin: -35px;
|
||||||
|
}
|
||||||
|
.widget_box {
|
||||||
|
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.20);
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 0.20);
|
||||||
|
min-height: 121px;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-name { font-weight: bold; }
|
||||||
|
|
||||||
|
.message-name-black { color: #2E2E2E; }
|
||||||
|
.message-name-bright-black { color: #555555; }
|
||||||
|
.message-name-red { color: #8B0000; }
|
||||||
|
.message-name-bright-red { color: #FF0000; }
|
||||||
|
.message-name-green { color: #006400; }
|
||||||
|
.message-name-bright-green { color: #00FF00; }
|
||||||
|
.message-name-yellow { color: #8B8B00; }
|
||||||
|
.message-name-bright-yellow { color: #FFFF00; }
|
||||||
|
.message-name-blue { color: #00008B; }
|
||||||
|
.message-name-bright-blue { color: #0000FF; }
|
||||||
|
.message-name-bright-magenta { color: #FF00FF; }
|
||||||
|
.message-name-magenta { color: #8B008B; }
|
||||||
|
.message-name-cyan { color: #008B8B; }
|
||||||
|
.message-name-bright-cyan { color: #00FFFF; }
|
||||||
|
.message-name-white { color: #A9A9A9; }
|
||||||
|
.message-name-bright-white { color: #FFFFFF; }
|
36
src/chat/styles/style.css
Normal file
36
src/chat/styles/style.css
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
|
||||||
|
.send-button, .send-text { border-radius: 0; }
|
||||||
|
.calendar {
|
||||||
|
transform: scale(0.6);
|
||||||
|
margin: -35px;
|
||||||
|
}
|
||||||
|
.widget_box {
|
||||||
|
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.20);
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 0.20);
|
||||||
|
min-height: 121px;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-name { font-weight: bold; }
|
||||||
|
|
||||||
|
.message-name-black { color: #2E2E2E; }
|
||||||
|
.message-name-bright-black { color: #555555; }
|
||||||
|
.message-name-red { color: #8B0000; }
|
||||||
|
.message-name-bright-red { color: #FF0000; }
|
||||||
|
.message-name-green { color: #006400; }
|
||||||
|
.message-name-bright-green { color: #00FF00; }
|
||||||
|
.message-name-yellow { color: #8B8B00; }
|
||||||
|
.message-name-bright-yellow { color: #FFFF00; }
|
||||||
|
.message-name-blue { color: #00008B; }
|
||||||
|
.message-name-bright-blue { color: #0000FF; }
|
||||||
|
.message-name-bright-magenta { color: #FF00FF; }
|
||||||
|
.message-name-magenta { color: #8B008B; }
|
||||||
|
.message-name-cyan { color: #008B8B; }
|
||||||
|
.message-name-bright-cyan { color: #00FFFF; }
|
||||||
|
.message-name-white { color: #A9A9A9; }
|
||||||
|
.message-name-bright-white { color: #FFFFFF; }
|
@ -1,6 +1,5 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
pub mod config;
|
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod proto;
|
pub mod proto;
|
@ -1,9 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
use bRAC::config::{configure, get_config_path, load_config, Args, Context};
|
|
||||||
use bRAC::proto::{connect, read_messages, send_message};
|
use bRAC::proto::{connect, read_messages, send_message};
|
||||||
use bRAC::chat::run_main_loop;
|
use bRAC::chat::{config::{configure, get_config_path, load_config, Args}, ctx::Context, run_main_loop};
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
70
src/util.rs
70
src/util.rs
@ -1,5 +1,3 @@
|
|||||||
use std::{io::{stdin, stdout, BufRead, Write}, ops::Range};
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
@ -8,76 +6,8 @@ lazy_static! {
|
|||||||
static ref CONTROL_CHARS_REGEX: Regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap();
|
static ref CONTROL_CHARS_REGEX: Regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_matches(regex: &Regex, text: &str) -> Vec<Range<usize>> {
|
|
||||||
regex.find_iter(text).map(|mat| mat.range()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn char_index_to_byte_index(text: &str, char_index: usize) -> usize {
|
|
||||||
text.char_indices().skip(char_index).next().map(|o| o.0).unwrap_or(text.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn string_chunks(text: &str, width: usize) -> Vec<(String, usize)> {
|
|
||||||
let mut norm: Vec<bool> = vec![true; text.chars().count()];
|
|
||||||
|
|
||||||
for range in get_matches(&ANSI_REGEX, text) {
|
|
||||||
for i in range {
|
|
||||||
if let Some(index) = text.char_indices().position(|x| x.0 == i) {
|
|
||||||
norm[index] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for range in get_matches(&CONTROL_CHARS_REGEX, text) {
|
|
||||||
for i in range {
|
|
||||||
if let Some(index) = text.char_indices().position(|x| x.0 == i) {
|
|
||||||
norm[index] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut now_chunk = String::new();
|
|
||||||
let mut chunks = Vec::new();
|
|
||||||
let mut length = 0;
|
|
||||||
|
|
||||||
for (i, b) in norm.iter().enumerate() {
|
|
||||||
if *b {
|
|
||||||
length += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
now_chunk.push(text.chars().skip(i).next().unwrap());
|
|
||||||
|
|
||||||
if length == width {
|
|
||||||
chunks.push((now_chunk.clone(), length));
|
|
||||||
now_chunk.clear();
|
|
||||||
length = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !now_chunk.is_empty() {
|
|
||||||
chunks.push((now_chunk.clone(), length));
|
|
||||||
}
|
|
||||||
|
|
||||||
chunks
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sanitize_text(input: &str) -> String {
|
pub fn sanitize_text(input: &str) -> String {
|
||||||
let without_ansi = ANSI_REGEX.replace_all(input, "");
|
let without_ansi = ANSI_REGEX.replace_all(input, "");
|
||||||
let cleaned_text = CONTROL_CHARS_REGEX.replace_all(&without_ansi, "");
|
let cleaned_text = CONTROL_CHARS_REGEX.replace_all(&without_ansi, "");
|
||||||
cleaned_text.into_owned()
|
cleaned_text.into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_input(prompt: impl ToString) -> Option<String> {
|
|
||||||
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()?;
|
|
||||||
|
|
||||||
if input.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(input.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user