diff --git a/Cargo.lock b/Cargo.lock index 9b00118..12ff815 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,7 +92,6 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" name = "bRAC" version = "0.1.3+2.0" dependencies = [ - "cfg-if", "chrono", "clap", "colored", diff --git a/Cargo.toml b/Cargo.toml index 1c20556..f6e1ef9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ crossterm = { version = "0.29.0", optional = true } homedir = { version = "0.3.4", optional = true } native-tls = { version = "0.2.14", optional = true } gtk4 = { version = "0.9.6", optional = true, features = [ "v4_10" ] } -cfg-if = "1.0.0" chrono = { version = "0.4.40", optional = true } [features] diff --git a/src/config.rs b/src/chat/config.rs similarity index 61% rename from src/config.rs rename to src/chat/config.rs index 7a6aed2..1437dc6 100644 --- a/src/config.rs +++ b/src/chat/config.rs @@ -1,14 +1,9 @@ -use std::{str::FromStr, sync::{Arc, RwLock}}; -#[allow(unused_imports)] -use std::{env, fs, path::{Path, PathBuf}, thread, time::Duration}; -use colored::Colorize; -use rand::random; +use std::str::FromStr; +use std::{fs, path::PathBuf, thread, time::Duration}; use serde_yml; use clap::Parser; -use crate::chat::ChatContext; - -use super::util::get_input; +use super::gui::{ask_bool, ask_string, ask_string_option, ask_usize, show_message}; 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_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 { - 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 { - println!("{}", "To configure the client, please answer a few questions. It won't take long.".yellow()); - println!("{}", "You can reconfigure client in any moment via `bRAC --configure`".yellow()); - println!("{}", format!("Config stores in path `{}`", path.to_string_lossy()).yellow()); - println!(); + show_message("Client setup", format!("To configure the client, please answer a few questions. It won't take long. +You can reconfigure client in any moment via `bRAC --configure` +Config stores in path `{}`", path.to_string_lossy())); let host = ask_string("Host", default_host()); 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::write(&path, config_text).expect("Config save error"); - println!(); - println!("{}", "Config saved! You can reconfigure it in any moment via `bRAC --configure`".yellow()); + show_message("Config saved!", "You can reconfigure it in any moment via `bRAC --configure`"); config } @@ -209,44 +182,4 @@ pub struct Args { /// Enable chunked reading #[arg(short='u', long)] pub enable_chunked: bool, -} - -pub struct Context { - pub chat: Arc>>>, - pub host: String, - pub name: String, - pub disable_formatting: bool, - pub disable_commands: bool, - pub disable_hiding_ip: bool, - pub message_format: String, - pub update_time: usize, - pub max_messages: usize, - pub enable_ip_viewing: bool, - pub 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::()))), - 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 { - self.chat.read().unwrap().clone().unwrap() - } } \ No newline at end of file diff --git a/src/chat/ctx.rs b/src/chat/ctx.rs new file mode 100644 index 0000000..a6beb48 --- /dev/null +++ b/src/chat/ctx.rs @@ -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>>>, + 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::()) + )), + 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 { + self.chat.read().unwrap().clone().unwrap() + } +} \ No newline at end of file diff --git a/src/chat/gtk_gui.rs b/src/chat/gui.rs similarity index 89% rename from src/chat/gtk_gui.rs rename to src/chat/gui.rs index d7b8ed1..abd7328 100644 --- a/src/chat/gtk_gui.rs +++ b/src/chat/gui.rs @@ -18,10 +18,9 @@ use gtk4::{ Button, Calendar, CssProvider, Entry, Fixed, Justification, Label, ListBox, Orientation, Overlay, Picture, ScrolledWindow, Settings }; -use crate::config::Context; 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 messages: Arc, @@ -86,6 +85,28 @@ pub fn recv_tick(ctx: Arc) -> Result<(), Box> { 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 { + 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 { let loader = PixbufLoader::new(); loader.write(data).unwrap(); @@ -179,7 +200,7 @@ fn build_menu(_: Arc, app: &Application) { .comments("better RAC client") .website("https://github.com/MeexReay/bRAC") .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() .present(); } @@ -222,17 +243,19 @@ fn build_ui(ctx: Arc, app: &Application) -> UiModel { let fixed = Fixed::new(); 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); 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); let logo_anim = PixbufAnimation::from_stream( &MemoryInputStream::from_bytes( - &glib::Bytes::from(include_bytes!("../../assets/logo.gif")) + &glib::Bytes::from(logo_gif) ), None::<&gio::Cancellable> ).unwrap().iter(Some(SystemTime::now())); @@ -447,58 +470,24 @@ fn setup(ctx: Arc, ui: UiModel) { } 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(); provider.load_from_data(&format!( "{}\n{}", - if let Some(settings) = Settings::default() { - if settings.is_gtk_application_prefer_dark_theme() { - ".message-content { color:rgb(255, 255, 255); } - .message-date { color:rgb(146, 146, 146); } - .message-ip { color:rgb(73, 73, 73); }" - } else { - ".message-content { color:rgb(0, 0, 0); } - .message-date { color:rgb(41, 41, 41); } - .message-ip { color:rgb(88, 88, 88); }" - } + if is_dark_theme { + include_str!("styles/dark.css") } else { - "" + include_str!("styles/light.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; } - ")); + include_str!("styles/style.css") + )); gtk::style_context_add_provider_for_display( &Display::default().expect("Could not connect to a display."), diff --git a/src/chat/images/icon.png b/src/chat/images/icon.png new file mode 100644 index 0000000..7e1fd4e Binary files /dev/null and b/src/chat/images/icon.png differ diff --git a/src/chat/images/image.png b/src/chat/images/image.png new file mode 100644 index 0000000..99109d0 Binary files /dev/null and b/src/chat/images/image.png differ diff --git a/src/chat/images/konata.png b/src/chat/images/konata.png new file mode 100644 index 0000000..e29a4cb Binary files /dev/null and b/src/chat/images/konata.png differ diff --git a/src/chat/images/logo.gif b/src/chat/images/logo.gif new file mode 100644 index 0000000..930fdc1 Binary files /dev/null and b/src/chat/images/logo.gif differ diff --git a/src/chat/minimal_tui.rs b/src/chat/minimal_tui.rs deleted file mode 100644 index b0aba8f..0000000 --- a/src/chat/minimal_tui.rs +++ /dev/null @@ -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, - pub registered: Arc>> -} - -fn update_console(ctx: Arc) -> Result<(), Box> { - 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::>() - .join("\n"), - ">".bright_yellow() - ); - out.flush(); - - Ok(()) -} - -pub fn print_message(ctx: Arc, message: String) -> Result<(), Box> { - ctx.chat().messages.append(ctx.max_messages, vec![message]); - update_console(ctx.clone()) -} - -pub fn run_main_loop(ctx: Arc) { - 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 = 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]); - } - } - } -} \ No newline at end of file diff --git a/src/chat.rs b/src/chat/mod.rs similarity index 96% rename from src/chat.rs rename to src/chat/mod.rs index 7e49457..032f3e6 100644 --- a/src/chat.rs +++ b/src/chat/mod.rs @@ -10,13 +10,20 @@ use crate::proto::{register_user, send_message_auth}; use super::{ proto::{connect, read_messages, send_message, send_message_spoof_auth}, - util::sanitize_text, - config::Context + util::sanitize_text }; use lazy_static::lazy_static; use regex::Regex; -use cfg_if::cfg_if; + +use ctx::Context; + +pub use gui::{ + ChatContext, + print_message, + run_main_loop +}; + lazy_static! { pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap(); @@ -31,18 +38,9 @@ lazy_static! { } -cfg_if! { - if #[cfg(feature = "pretty_tui")] { - mod pretty_tui; - 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 mod gui; +pub mod config; +pub mod ctx; pub struct ChatStorage { diff --git a/src/chat/pretty_tui.rs b/src/chat/pretty_tui.rs deleted file mode 100644 index fe31362..0000000 --- a/src/chat/pretty_tui.rs +++ /dev/null @@ -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, messages: Vec, input: &str) -> Result<(), Box> { - let (width, height) = terminal::size()?; - let (width, height) = (width as usize, height as usize); - - let mut messages = messages - .into_iter() - .flat_map(|o| string_chunks(&o, width as usize - 1)) - .map(|o| (o.0.white().blink().to_string(), o.1)) - .collect::>(); - - let messages_size = if messages.len() >= height { - messages.len()-height - } else { - for _ in 0..height-messages.len() { - messages.insert(0, (String::new(), 0)); - } - 0 - }; - - let scroll = min(ctx.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::>() - } else { - messages - .into_iter() - .enumerate() - .map(|(i, (s, l))| { - format!("{}{}{}", - s, - " ".repeat(width - 1 - l), - if i == scroll_f { - "▐".bright_yellow() - } else { - "▕".yellow() - } - ) - }) - .collect::>() - - }; - - let text = format!( - "{}\r\n{} {}", - formatted_messages.join("\r\n"), - ">".bright_yellow(), - input - ); - - let mut out = stdout().lock(); - write!(out, "{}", text)?; - out.flush()?; - - Ok(()) -} - - - -fn replace_input(cursor: usize, len: usize, text: &str) { - let spaces = if text.chars().count() < len { - len-text.chars().count() - } else { - 0 - }; - write!(stdout(), - "{}{}{}{}", - MoveLeft(1).to_string().repeat(cursor), - text, - " ".repeat(spaces), - MoveLeft(1).to_string().repeat(spaces) - ).unwrap(); - stdout().lock().flush().unwrap(); -} - -fn replace_input_left(cursor: usize, len: usize, text: &str, left: usize) { - let spaces = if text.chars().count() < len { - len-text.chars().count() - } else { - 0 - }; - write!(stdout(), - "{}{}{}{}", - MoveLeft(1).to_string().repeat(cursor), - text, - " ".repeat(spaces), - MoveLeft(1).to_string().repeat(len-left) - ).unwrap(); - stdout().lock().flush().unwrap(); -} - -fn poll_events(ctx: Arc) -> Result<(), Box> { - let mut history: Vec = vec![String::new()]; - let mut history_cursor: usize = 0; - let mut cursor: usize = 0; - - let input = ctx.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) -> Result<(), Box> { - 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 = 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, - pub input: Arc>, - pub registered: Arc>>, - pub scroll: Arc, -} - -pub fn print_message(ctx: Arc, message: String) -> Result<(), Box> { - 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) { - 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()); - } -} \ No newline at end of file diff --git a/src/chat/styles/dark.css b/src/chat/styles/dark.css new file mode 100644 index 0000000..9dca5ee --- /dev/null +++ b/src/chat/styles/dark.css @@ -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); } \ No newline at end of file diff --git a/src/chat/styles/light.css b/src/chat/styles/light.css new file mode 100644 index 0000000..8c00fea --- /dev/null +++ b/src/chat/styles/light.css @@ -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; } \ No newline at end of file diff --git a/src/chat/styles/style.css b/src/chat/styles/style.css new file mode 100644 index 0000000..eeb4211 --- /dev/null +++ b/src/chat/styles/style.css @@ -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; } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 1d108fb..8e581b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ #![allow(non_snake_case)] -pub mod config; pub mod chat; pub mod util; pub mod proto; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d842638..8cc7d13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,8 @@ 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::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() { diff --git a/src/util.rs b/src/util.rs index aa3ba33..ef55b39 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,3 @@ -use std::{io::{stdin, stdout, BufRead, Write}, ops::Range}; - use lazy_static::lazy_static; use regex::Regex; @@ -8,76 +6,8 @@ lazy_static! { static ref CONTROL_CHARS_REGEX: Regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap(); } -fn get_matches(regex: &Regex, text: &str) -> Vec> { - 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 = 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 { let without_ansi = ANSI_REGEX.replace_all(input, ""); let cleaned_text = CONTROL_CHARS_REGEX.replace_all(&without_ansi, ""); cleaned_text.into_owned() -} - -pub fn get_input(prompt: impl ToString) -> Option { - let 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()) - } } \ No newline at end of file