bhcli

"Strange's fork of n0tr1v's bhcli (onion)"
git clone https://git.dasho.dev/Strange/bhcli.git
Log | Files | Refs | README

main.rs (119703B)


      1 mod bhc;
      2 mod lechatphp;
      3 mod util;
      4 
      5 use crate::lechatphp::LoginErr;
      6 use anyhow::{anyhow, Context};
      7 use chrono::{Datelike, NaiveDateTime, Utc};
      8 use clap::Parser;
      9 use clipboard::ClipboardContext;
     10 use clipboard::ClipboardProvider;
     11 use colors_transform::{Color, Rgb};
     12 use crossbeam_channel::{self, after, select};
     13 use crossterm::event;
     14 use crossterm::event::Event as CEvent;
     15 use crossterm::event::{MouseEvent, MouseEventKind};
     16 use crossterm::{
     17     event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent, KeyModifiers},
     18     execute,
     19     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
     20 };
     21 use lazy_static::lazy_static;
     22 use linkify::LinkFinder;
     23 use log::LevelFilter;
     24 use log4rs::append::file::FileAppender;
     25 use log4rs::encode::pattern::PatternEncoder;
     26 use rand::distributions::Alphanumeric;
     27 use rand::{thread_rng, Rng};
     28 use regex::Regex;
     29 use reqwest::blocking::multipart;
     30 use reqwest::blocking::Client;
     31 use reqwest::redirect::Policy;
     32 use rodio::{source::Source, Decoder, OutputStream};
     33 use select::document::Document;
     34 use select::predicate::{Attr, Name};
     35 use serde_derive::{Deserialize, Serialize};
     36 use std::collections::HashMap;
     37 use std::io::Cursor;
     38 use std::io::{self, Write};
     39 use std::process::Command;
     40 use std::sync::Mutex;
     41 use std::sync::{Arc, MutexGuard};
     42 use std::thread;
     43 use std::time::Duration;
     44 use std::time::Instant;
     45 use tokio::process::Command as TokioCommand;
     46 use tui::layout::Rect;
     47 use tui::style::Color as tuiColor;
     48 use tui::{
     49     backend::CrosstermBackend,
     50     layout::{Constraint, Direction, Layout},
     51     style::{Modifier, Style},
     52     text::{Span, Spans, Text},
     53     widgets::{Block, Borders, List, ListItem, Paragraph},
     54     Frame, Terminal,
     55 };
     56 use unicode_width::UnicodeWidthStr;
     57 use util::StatefulList;
     58 
     59 const LANG: &str = "en";
     60 const SEND_TO_ALL: &str = "s *";
     61 const SEND_TO_MEMBERS: &str = "s ?";
     62 const SEND_TO_STAFFS: &str = "s %";
     63 const SEND_TO_ADMINS: &str = "s _";
     64 const SOUND1: &[u8] = include_bytes!("sound1.mp3");
     65 const SERVER_DOWN_500_ERR: &str = "500 Internal Server Error, server down";
     66 const SERVER_DOWN_ERR: &str = "502 Bad Gateway, server down";
     67 const KICKED_ERR: &str = "You have been kicked";
     68 const REG_ERR: &str = "This nickname is a registered member";
     69 const NICKNAME_ERR: &str = "Invalid nickname";
     70 const CAPTCHA_WG_ERR: &str = "Wrong Captcha";
     71 const CAPTCHA_FAILED_SOLVE_ERR: &str = "Failed solve captcha";
     72 const CAPTCHA_USED_ERR: &str = "Captcha already used or timed out";
     73 const UNKNOWN_ERR: &str = "Unknown error";
     74 
     75 lazy_static! {
     76     static ref META_REFRESH_RGX: Regex = Regex::new(r#"url='([^']+)'"#).unwrap();
     77     static ref SESSION_RGX: Regex = Regex::new(r#"session=([^&]+)"#).unwrap();
     78     static ref COLOR_RGX: Regex = Regex::new(r#"color:\s*([#\w]+)\s*;"#).unwrap();
     79     static ref COLOR1_RGX: Regex = Regex::new(r#"^#([0-9A-Fa-f]{6})$"#).unwrap();
     80     static ref PM_RGX: Regex = Regex::new(r#"^/pm ([^\s]+) (.*)"#).unwrap();
     81     static ref KICK_RGX: Regex = Regex::new(r#"^/(?:kick|k) ([^\s]+)\s?(.*)"#).unwrap();
     82     static ref IGNORE_RGX: Regex = Regex::new(r#"^/ignore ([^\s]+)"#).unwrap();
     83     static ref UNIGNORE_RGX: Regex = Regex::new(r#"^/unignore ([^\s]+)"#).unwrap();
     84     static ref DLX_RGX: Regex = Regex::new(r#"^/dl([\d]+)$"#).unwrap();
     85     static ref UPLOAD_RGX: Regex = Regex::new(r#"^/u\s([^\s]+)\s?(?:@([^\s]+)\s)?(.*)$"#).unwrap();
     86     static ref FIND_RGX: Regex = Regex::new(r#"^/f\s(.*)$"#).unwrap();
     87     static ref NEW_NICKNAME_RGX: Regex = Regex::new(r#"^/nick\s(.*)$"#).unwrap();
     88     static ref NEW_COLOR_RGX: Regex = Regex::new(r#"^/color\s(.*)$"#).unwrap();
     89 }
     90 
     91 fn default_empty_str() -> String {
     92     "".to_string()
     93 }
     94 
     95 #[derive(Debug, Serialize, Deserialize)]
     96 struct Profile {
     97     username: String,
     98     password: String,
     99     #[serde(default = "default_empty_str")]
    100     url: String,
    101     #[serde(default = "default_empty_str")]
    102     date_format: String,
    103     #[serde(default = "default_empty_str")]
    104     page_php: String,
    105     #[serde(default = "default_empty_str")]
    106     members_tag: String,
    107     #[serde(default = "default_empty_str")]
    108     keepalive_send_to: String,
    109 }
    110 
    111 #[derive(Debug, Serialize, Deserialize)]
    112 struct LogLevel {
    113     level: String,
    114 }
    115 
    116 impl Default for LogLevel {
    117     fn default() -> Self {
    118         LogLevel {
    119             level: "error".to_string(),
    120         }
    121     }
    122 }
    123 
    124 #[derive(Default, Debug, Serialize, Deserialize)]
    125 struct MyConfig {
    126     profiles: HashMap<String, Profile>,
    127     log_level: LogLevel,
    128     commands: HashMap<String, String>,
    129 }
    130 
    131 #[derive(Parser)]
    132 #[command(name = "bhcli")]
    133 #[command(author = "Strange <StrangeGuy6228@protonmail.com>")]
    134 #[command(version = "0.1.0")]
    135 
    136 struct Opts {
    137     #[arg(long, env = "DKF_API_KEY")]
    138     dkf_api_key: Option<String>,
    139     #[arg(short, long, env = "BHC_USERNAME")]
    140     username: Option<String>,
    141     #[arg(short, long, env = "BHC_PASSWORD")]
    142     password: Option<String>,
    143     #[arg(short, long, env = "BHC_MANUAL_CAPTCHA")]
    144     manual_captcha: bool,
    145     #[arg(short, long, env = "BHC_GUEST_COLOR")]
    146     guest_color: Option<String>,
    147     #[arg(short, long, env = "BHC_REFRESH_RATE", default_value = "5")]
    148     refresh_rate: u64,
    149     #[arg(long, env = "BHC_MAX_LOGIN_RETRY", default_value = "5")]
    150     max_login_retry: isize,
    151     #[arg(long)]
    152     url: Option<String>,
    153     #[arg(long)]
    154     page_php: Option<String>,
    155     #[arg(long)]
    156     datetime_fmt: Option<String>,
    157     #[arg(long)]
    158     members_tag: Option<String>,
    159     #[arg(short, long)]
    160     dan: bool,
    161     #[arg(
    162         short,
    163         long,
    164         env = "BHC_PROXY_URL",
    165         default_value = "socks5h://127.0.0.1:9050"
    166     )]
    167     socks_proxy_url: String,
    168     #[arg(long)]
    169     no_proxy: bool,
    170     #[arg(long, env = "DNMX_USERNAME")]
    171     dnmx_username: Option<String>,
    172     #[arg(long, env = "DNMX_PASSWORD")]
    173     dnmx_password: Option<String>,
    174     #[arg(short = 'c', long, default_value = "default")]
    175     profile: String,
    176 
    177     //Strange
    178     #[arg(long, default_value = "0")]
    179     keepalive_send_to: Option<String>,
    180 
    181     #[arg(long)]
    182     session: Option<String>,
    183 
    184     #[arg(long)]
    185     sxiv: bool,
    186 
    187     #[arg(short = 'l', long, env = "LOG_LEVEL")]
    188     log_level: Option<String>,
    189 }
    190 
    191 struct LeChatPHPConfig {
    192     url: String,
    193     datetime_fmt: String,
    194     page_php: String,
    195     keepalive_send_to: String,
    196     members_tag: String,
    197     staffs_tag: String,
    198 }
    199 
    200 impl LeChatPHPConfig {
    201     fn new_black_hat_chat_config() -> Self {
    202         Self {
    203             url: "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion".to_owned(),
    204             datetime_fmt: "%m-%d %H:%M:%S".to_owned(),
    205             page_php: "chat.php".to_owned(),
    206             keepalive_send_to: "0".to_owned(),
    207             members_tag: "[M] ".to_owned(),
    208             staffs_tag: "[Staff] ".to_owned(),
    209         }
    210     }
    211 }
    212 
    213 struct BaseClient {
    214     username: String,
    215     password: String,
    216 }
    217 
    218 struct LeChatPHPClient {
    219     base_client: BaseClient,
    220     guest_color: String,
    221     client: Client,
    222     session: Option<String>,
    223     config: LeChatPHPConfig,
    224     last_key_event: Option<KeyCode>,
    225     manual_captcha: bool,
    226     sxiv: bool,
    227     refresh_rate: u64,
    228     max_login_retry: isize,
    229 
    230     is_muted: Arc<Mutex<bool>>,
    231     show_sys: bool,
    232     display_guest_view: bool,
    233     display_member_view: bool,
    234     display_hidden_msgs: bool,
    235     tx: crossbeam_channel::Sender<PostType>,
    236     rx: Arc<Mutex<crossbeam_channel::Receiver<PostType>>>,
    237 
    238     color_tx: crossbeam_channel::Sender<()>,
    239     color_rx: Arc<Mutex<crossbeam_channel::Receiver<()>>>,
    240 }
    241 
    242 impl LeChatPHPClient {
    243     fn run_forever(&mut self) {
    244         let max_retry = self.max_login_retry;
    245         let mut attempt = 0;
    246         loop {
    247             match self.login() {
    248                 Err(e) => match e {
    249                     LoginErr::KickedErr
    250                     | LoginErr::RegErr
    251                     | LoginErr::NicknameErr
    252                     | LoginErr::UnknownErr => {
    253                         log::error!("{}", e);
    254                         println!("Login error: {}", e); // Print error message
    255                         break;
    256                     }
    257                     LoginErr::CaptchaFailedSolveErr => {
    258                         log::error!("{}", e);
    259                         println!("Captcha failed to solve: {}", e); // Print error message
    260                         continue;
    261                     }
    262                     LoginErr::CaptchaWgErr | LoginErr::CaptchaUsedErr => {}
    263                     LoginErr::ServerDownErr | LoginErr::ServerDown500Err => {
    264                         log::error!("{}", e);
    265                         println!("Server is down: {}", e); // Print error message
    266                     }
    267                     LoginErr::Reqwest(err) => {
    268                         if err.is_connect() {
    269                             log::error!("{}\nIs tor proxy enabled ?", err);
    270                             println!("Connection error: {}\nIs tor proxy enabled ?", err); // Print error message
    271                             break;
    272                         } else if err.is_timeout() {
    273                             log::error!("timeout: {}", err);
    274                             println!("Timeout error: {}", err); // Print error message
    275                         } else {
    276                             log::error!("{}", err);
    277                             println!("Reqwest error: {}", err); // Print error message
    278                         }
    279                     }
    280                 },
    281 
    282                 Ok(()) => {
    283                     attempt = 0;
    284                     match self.get_msgs() {
    285                         Ok(ExitSignal::NeedLogin) => {}
    286                         Ok(ExitSignal::Terminate) => return,
    287                         Err(e) => log::error!("{:?}", e),
    288                     }
    289                 }
    290             }
    291             attempt += 1;
    292             if max_retry > 0 && attempt > max_retry {
    293                 break;
    294             }
    295             self.session = None;
    296             let retry_in = Duration::from_secs(2);
    297             let mut msg = format!("retry login in {:?}, attempt: {}", retry_in, attempt);
    298             if max_retry > 0 {
    299                 msg += &format!("/{}", max_retry);
    300             }
    301             println!("{}", msg);
    302             thread::sleep(retry_in);
    303         }
    304     }
    305 
    306     fn start_keepalive_thread(
    307         &self,
    308         exit_rx: crossbeam_channel::Receiver<ExitSignal>,
    309         last_post_rx: crossbeam_channel::Receiver<()>,
    310     ) -> thread::JoinHandle<()> {
    311         let tx = self.tx.clone();
    312         let send_to = self.config.keepalive_send_to.clone();
    313         thread::spawn(move || loop {
    314             let clb = || {
    315                 tx.send(PostType::Post(
    316                     "<keepalive>".to_owned(),
    317                     Some(send_to.clone()),
    318                 ))
    319                 .unwrap();
    320                 tx.send(PostType::DeleteLast).unwrap();
    321             };
    322             let timeout = after(Duration::from_secs(60 * 75));
    323             select! {
    324                 // Whenever we send a message to chat server,
    325                 // we will receive a message on this channel
    326                 // and reset the timer for next keepalive.
    327                 recv(&last_post_rx) -> _ => {},
    328                 recv(&exit_rx) -> _ => return,
    329                 recv(&timeout) -> _ => clb(),
    330             }
    331         })
    332     }
    333 
    334     // Thread that POST to chat server
    335     fn start_post_msg_thread(
    336         &self,
    337         exit_rx: crossbeam_channel::Receiver<ExitSignal>,
    338         last_post_tx: crossbeam_channel::Sender<()>,
    339     ) -> thread::JoinHandle<()> {
    340         let client = self.client.clone();
    341         let rx = Arc::clone(&self.rx);
    342         let full_url = format!("{}/{}", &self.config.url, &self.config.page_php);
    343         let session = self.session.clone().unwrap();
    344         let url = format!("{}?action=post&session={}", &full_url, &session);
    345         thread::spawn(move || loop {
    346             // select! macro fucks all the LSP, therefore the code gymnastic here
    347             let clb = |v: Result<PostType, crossbeam_channel::RecvError>| match v {
    348                 Ok(post_type_recv) => post_msg(
    349                     &client,
    350                     post_type_recv,
    351                     &full_url,
    352                     session.clone(),
    353                     &url,
    354                     &last_post_tx,
    355                 ),
    356                 Err(_) => return,
    357             };
    358             let rx = rx.lock().unwrap();
    359             select! {
    360                 recv(&exit_rx) -> _ => return,
    361                 recv(&rx) -> v => clb(v),
    362             }
    363         })
    364     }
    365 
    366     // Thread that update messages every "refresh_rate"
    367     fn start_get_msgs_thread(
    368         &self,
    369         sig: &Arc<Mutex<Sig>>,
    370         messages: &Arc<Mutex<Vec<Message>>>,
    371         users: &Arc<Mutex<Users>>,
    372         messages_updated_tx: crossbeam_channel::Sender<()>,
    373     ) -> thread::JoinHandle<()> {
    374         let client = self.client.clone();
    375         let messages = Arc::clone(messages);
    376         let users = Arc::clone(users);
    377         let session = self.session.clone().unwrap();
    378         let username = self.base_client.username.clone();
    379         let refresh_rate = self.refresh_rate;
    380         let base_url = self.config.url.clone();
    381         let page_php = self.config.page_php.clone();
    382         let datetime_fmt = self.config.datetime_fmt.clone();
    383         let is_muted = Arc::clone(&self.is_muted);
    384         let exit_rx = sig.lock().unwrap().clone();
    385         let sig = Arc::clone(sig);
    386         let members_tag = self.config.members_tag.clone();
    387 
    388         use std::panic::{catch_unwind, AssertUnwindSafe};
    389 
    390         thread::spawn(move || {
    391             let result = catch_unwind(AssertUnwindSafe(|| loop {
    392                 let (_stream, stream_handle) = OutputStream::try_default().unwrap();
    393                 let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap();
    394                 let mut should_notify = false;
    395 
    396                 if let Err(err) = get_msgs(
    397                     &client,
    398                     &base_url,
    399                     &page_php,
    400                     &session,
    401                     &username,
    402                     &users,
    403                     &sig,
    404                     &messages_updated_tx,
    405                     &members_tag,
    406                     &datetime_fmt,
    407                     &messages,
    408                     &mut should_notify,
    409                 ) {
    410                     log::error!("{}", err);
    411                 }
    412 
    413                 let muted = { *is_muted.lock().unwrap() };
    414                 if should_notify && !muted {
    415                     if let Err(err) = stream_handle.play_raw(source.convert_samples()) {
    416                         log::error!("{}", err);
    417                     }
    418                 }
    419 
    420                 let timeout = after(Duration::from_secs(refresh_rate));
    421                 select! {
    422                     recv(&exit_rx) -> _ => return,
    423                     recv(&timeout) -> _ => {},
    424                 }
    425             }));
    426 
    427             if let Err(panic_err) = result {
    428                 log::error!("❌ get_msgs thread panicked!");
    429                 if let Some(msg) = panic_err.downcast_ref::<&str>() {
    430                     log::error!("panic message: {}", msg);
    431                 } else if let Some(msg) = panic_err.downcast_ref::<String>() {
    432                     log::error!("panic message: {}", msg);
    433                 } else {
    434                     log::error!("Unknown panic type");
    435                 }
    436             }
    437         })
    438     }
    439 
    440     fn get_msgs(&mut self) -> anyhow::Result<ExitSignal> {
    441         let terminate_signal: ExitSignal;
    442 
    443         let messages: Arc<Mutex<Vec<Message>>> = Arc::new(Mutex::new(Vec::new()));
    444         let users: Arc<Mutex<Users>> = Arc::new(Mutex::new(Users::default()));
    445 
    446         // Create default app state
    447         let mut app = App::default();
    448 
    449         // Each threads gets a clone of the receiver.
    450         // When someone calls ".signal", all threads receive it,
    451         // and knows that they have to terminate.
    452         let sig = Arc::new(Mutex::new(Sig::new()));
    453 
    454         let (messages_updated_tx, messages_updated_rx) = crossbeam_channel::unbounded();
    455         let (last_post_tx, last_post_rx) = crossbeam_channel::unbounded();
    456 
    457         let h1 = self.start_keepalive_thread(sig.lock().unwrap().clone(), last_post_rx);
    458         let h2 = self.start_post_msg_thread(sig.lock().unwrap().clone(), last_post_tx);
    459         let h3 = self.start_get_msgs_thread(&sig, &messages, &users, messages_updated_tx);
    460 
    461         // Terminal initialization
    462         let mut stdout = io::stdout();
    463         enable_raw_mode().unwrap();
    464         execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    465         let backend = CrosstermBackend::new(stdout);
    466         let mut terminal = Terminal::new(backend)?;
    467 
    468         // Setup event handlers
    469         let (events, h4) = Events::with_config(Config {
    470             messages_updated_rx,
    471             exit_rx: sig.lock().unwrap().clone(),
    472             tick_rate: Duration::from_millis(250),
    473         });
    474 
    475         loop {
    476             app.is_muted = *self.is_muted.lock().unwrap();
    477             app.show_sys = self.show_sys;
    478             app.display_guest_view = self.display_guest_view;
    479             app.display_member_view = self.display_member_view;
    480             app.display_hidden_msgs = self.display_hidden_msgs;
    481             app.members_tag = self.config.members_tag.clone();
    482             app.staffs_tag = self.config.staffs_tag.clone();
    483 
    484             // process()
    485             // Draw UI
    486             terminal.draw(|f| {
    487                 draw_terminal_frame(f, &mut app, &messages, &users, &self.base_client.username);
    488             })?;
    489 
    490             // Handle input
    491             match self.handle_input(&events, &mut app, &messages, &users) {
    492                 Err(ExitSignal::Terminate) => {
    493                     terminate_signal = ExitSignal::Terminate;
    494                     sig.lock().unwrap().signal(&terminate_signal);
    495                     break;
    496                 }
    497                 Err(ExitSignal::NeedLogin) => {
    498                     terminate_signal = ExitSignal::NeedLogin;
    499                     sig.lock().unwrap().signal(&terminate_signal);
    500                     break;
    501                 }
    502                 Ok(_) => continue,
    503             };
    504         }
    505 
    506         // Cleanup before leaving
    507         disable_raw_mode()?;
    508         execute!(
    509             terminal.backend_mut(),
    510             LeaveAlternateScreen,
    511             DisableMouseCapture
    512         )?;
    513         terminal.show_cursor()?;
    514         terminal.clear()?;
    515         terminal.set_cursor(0, 0)?;
    516 
    517         h1.join().unwrap();
    518         h2.join().unwrap();
    519         h3.join().unwrap();
    520         h4.join().unwrap();
    521 
    522         Ok(terminate_signal)
    523     }
    524 
    525     fn post_msg(&self, post_type: PostType) -> anyhow::Result<()> {
    526         self.tx.send(post_type)?;
    527         Ok(())
    528     }
    529 
    530     fn login(&mut self) -> Result<(), LoginErr> {
    531         // If we provided a session, skip login process
    532         if self.session.is_some() {
    533             // println!("Session in params: {:?}", self.session);
    534             return Ok(());
    535         }
    536         // println!("self.session is not Some");
    537         // println!("self.sxiv = {:?}", self.sxiv);
    538         self.session = Some(lechatphp::login(
    539             &self.client,
    540             &self.config.url,
    541             &self.config.page_php,
    542             &self.base_client.username,
    543             &self.base_client.password,
    544             &self.guest_color,
    545             self.manual_captcha,
    546             self.sxiv,
    547         )?);
    548         Ok(())
    549     }
    550 
    551     fn logout(&mut self) -> anyhow::Result<()> {
    552         if let Some(session) = &self.session {
    553             lechatphp::logout(
    554                 &self.client,
    555                 &self.config.url,
    556                 &self.config.page_php,
    557                 session,
    558             )?;
    559             self.session = None;
    560         }
    561         Ok(())
    562     }
    563 
    564     fn start_cycle(&self, color_only: bool) {
    565         let username = self.base_client.username.clone();
    566         let tx = self.tx.clone();
    567         let color_rx = Arc::clone(&self.color_rx);
    568         thread::spawn(move || {
    569             let mut idx = 0;
    570             let colors = [
    571                 "#ff3366", "#ff6633", "#FFCC33", "#33FF66", "#33FFCC", "#33CCFF", "#3366FF",
    572                 "#6633FF", "#CC33FF", "#efefef",
    573             ];
    574             loop {
    575                 let color_rx = color_rx.lock().unwrap();
    576                 let timeout = after(Duration::from_millis(5200));
    577                 select! {
    578                     recv(&color_rx) -> _ => break,
    579                     recv(&timeout) -> _ => {}
    580                 }
    581                 idx = (idx + 1) % colors.len();
    582                 let color = colors[idx].to_owned();
    583                 if !color_only {
    584                     let name = format!("{}{}", username, random_string(14));
    585                     log::error!("New name : {}", name);
    586                     tx.send(PostType::Profile(color, name)).unwrap();
    587                 } else {
    588                     tx.send(PostType::NewColor(color)).unwrap();
    589                 }
    590                 // tx.send(PostType::Post("!up".to_owned(), Some(username.clone())))
    591                 //     .unwrap();
    592                 // tx.send(PostType::DeleteLast).unwrap();
    593             }
    594             let msg = PostType::Profile("#90ee90".to_owned(), username);
    595             tx.send(msg).unwrap();
    596         });
    597     }
    598 
    599     fn handle_input(
    600         &mut self,
    601         events: &Events,
    602         app: &mut App,
    603         messages: &Arc<Mutex<Vec<Message>>>,
    604         users: &Arc<Mutex<Users>>,
    605     ) -> Result<(), ExitSignal> {
    606         match events.next() {
    607             Ok(Event::NeedLogin) => return Err(ExitSignal::NeedLogin),
    608             Ok(Event::Terminate) => return Err(ExitSignal::Terminate),
    609             Ok(Event::Input(evt)) => self.handle_event(app, messages, users, evt),
    610             _ => Ok(()),
    611         }
    612     }
    613 
    614     fn handle_event(
    615         &mut self,
    616         app: &mut App,
    617         messages: &Arc<Mutex<Vec<Message>>>,
    618         users: &Arc<Mutex<Users>>,
    619         event: event::Event,
    620     ) -> Result<(), ExitSignal> {
    621         match event {
    622             event::Event::Resize(_cols, _rows) => Ok(()),
    623             event::Event::FocusGained => Ok(()),
    624             event::Event::FocusLost => Ok(()),
    625             event::Event::Paste(_) => Ok(()),
    626             event::Event::Key(key_event) => self.handle_key_event(app, messages, users, key_event),
    627             event::Event::Mouse(mouse_event) => self.handle_mouse_event(app, mouse_event),
    628         }
    629     }
    630 
    631     fn handle_key_event(
    632         &mut self,
    633         app: &mut App,
    634         messages: &Arc<Mutex<Vec<Message>>>,
    635         users: &Arc<Mutex<Users>>,
    636         key_event: KeyEvent,
    637     ) -> Result<(), ExitSignal> {
    638         if app.input_mode != InputMode::Normal {
    639             self.last_key_event = None;
    640         }
    641         match app.input_mode {
    642             InputMode::LongMessage => {
    643                 self.handle_long_message_mode_key_event(app, key_event, messages)
    644             }
    645             InputMode::Normal => self.handle_normal_mode_key_event(app, key_event, messages),
    646             InputMode::Editing | InputMode::EditingErr => {
    647                 self.handle_editing_mode_key_event(app, key_event, users)
    648             }
    649         }
    650     }
    651 
    652     fn handle_long_message_mode_key_event(
    653         &mut self,
    654         app: &mut App,
    655         key_event: KeyEvent,
    656         messages: &Arc<Mutex<Vec<Message>>>,
    657     ) -> Result<(), ExitSignal> {
    658         match key_event {
    659             KeyEvent {
    660                 code: KeyCode::Enter,
    661                 modifiers: KeyModifiers::NONE,
    662                 ..
    663             }
    664             | KeyEvent {
    665                 code: KeyCode::Esc,
    666                 modifiers: KeyModifiers::NONE,
    667                 ..
    668             } => self.handle_long_message_mode_key_event_esc(app),
    669             KeyEvent {
    670                 code: KeyCode::Char('d'),
    671                 modifiers: KeyModifiers::CONTROL,
    672                 ..
    673             } => self.handle_long_message_mode_key_event_ctrl_d(app, messages),
    674             _ => {}
    675         }
    676         Ok(())
    677     }
    678 
    679     fn handle_normal_mode_key_event(
    680         &mut self,
    681         app: &mut App,
    682         key_event: KeyEvent,
    683         messages: &Arc<Mutex<Vec<Message>>>,
    684     ) -> Result<(), ExitSignal> {
    685         match key_event {
    686             KeyEvent {
    687                 code: KeyCode::Char('/'),
    688                 modifiers: KeyModifiers::NONE,
    689                 ..
    690             } => self.handle_normal_mode_key_event_slash(app),
    691             KeyEvent {
    692                 code: KeyCode::Char('j'),
    693                 modifiers: KeyModifiers::NONE,
    694                 ..
    695             }
    696             | KeyEvent {
    697                 code: KeyCode::Down,
    698                 modifiers: KeyModifiers::NONE,
    699                 ..
    700             } => self.handle_normal_mode_key_event_down(app),
    701             KeyEvent {
    702                 code: KeyCode::Char('J'),
    703                 modifiers: KeyModifiers::SHIFT,
    704                 ..
    705             } => self.handle_normal_mode_key_event_j(app, 5),
    706             KeyEvent {
    707                 code: KeyCode::Char('k'),
    708                 modifiers: KeyModifiers::NONE,
    709                 ..
    710             }
    711             | KeyEvent {
    712                 code: KeyCode::Up,
    713                 modifiers: KeyModifiers::NONE,
    714                 ..
    715             } => self.handle_normal_mode_key_event_up(app),
    716             KeyEvent {
    717                 code: KeyCode::Char('K'),
    718                 modifiers: KeyModifiers::SHIFT,
    719                 ..
    720             } => self.handle_normal_mode_key_event_k(app, 5),
    721             KeyEvent {
    722                 code: KeyCode::Enter,
    723                 modifiers: KeyModifiers::NONE,
    724                 ..
    725             } => self.handle_normal_mode_key_event_enter(app, messages),
    726             KeyEvent {
    727                 code: KeyCode::Backspace,
    728                 modifiers: KeyModifiers::NONE,
    729                 ..
    730             } => self.handle_normal_mode_key_event_backspace(app, messages),
    731             KeyEvent {
    732                 code: KeyCode::Char('y'),
    733                 modifiers: KeyModifiers::NONE,
    734                 ..
    735             }
    736             | KeyEvent {
    737                 code: KeyCode::Char('c'),
    738                 modifiers: KeyModifiers::CONTROL,
    739                 ..
    740             } => self.handle_normal_mode_key_event_yank(app),
    741             KeyEvent {
    742                 code: KeyCode::Char('Y'),
    743                 modifiers: KeyModifiers::SHIFT,
    744                 ..
    745             } => self.handle_normal_mode_key_event_yank_link(app),
    746 
    747             KeyEvent {
    748                 code: KeyCode::Char('s'),
    749                 modifiers: KeyModifiers::NONE,
    750                 ..
    751             } => {
    752                 let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
    753                 if let Some(session) = &self.session {
    754                     ctx.set_contents(session.clone()).unwrap();
    755                 } else {
    756                     log::error!("Session was empty !")
    757                 }
    758             }
    759 
    760             //Strange
    761             KeyEvent {
    762                 code: KeyCode::Char('d'),
    763                 modifiers: KeyModifiers::NONE,
    764                 ..
    765             } => {
    766                 let mut app = app.clone();
    767                 let config_url = self.config.url.clone();
    768                 let members_tag = self.config.members_tag.clone();
    769                 log::info!("Calling handle_normal_mode_key_event_download_link");
    770                 tokio::spawn(async move {
    771                     Self::handle_normal_mode_key_event_download_link(
    772                         &mut app,
    773                         config_url,
    774                         members_tag,
    775                         false,
    776                     )
    777                     .await;
    778                 });
    779             }
    780             KeyEvent {
    781                 code: KeyCode::Char('D'),
    782                 modifiers: KeyModifiers::SHIFT,
    783                 ..
    784             } => {
    785                 let mut app = app.clone();
    786                 let config_url = self.config.url.clone();
    787                 let members_tag = self.config.members_tag.clone();
    788                 log::info!("Calling handle_normal_mode_key_event_download_link");
    789                 tokio::spawn(async move {
    790                     Self::handle_normal_mode_key_event_download_link(
    791                         &mut app,
    792                         config_url,
    793                         members_tag,
    794                         true,
    795                     )
    796                     .await;
    797                 });
    798             }
    799 
    800             KeyEvent {
    801                 code: KeyCode::Char('m'),
    802                 modifiers: KeyModifiers::NONE,
    803                 ..
    804             } => self.handle_normal_mode_key_event_toggle_mute(),
    805             KeyEvent {
    806                 code: KeyCode::Char('S'),
    807                 modifiers: KeyModifiers::SHIFT,
    808                 ..
    809             } => self.handle_normal_mode_key_event_toggle_sys(),
    810             KeyEvent {
    811                 code: KeyCode::Char('M'),
    812                 modifiers: KeyModifiers::SHIFT,
    813                 ..
    814             } => self.handle_normal_mode_key_event_toggle_member_view(),
    815             KeyEvent {
    816                 code: KeyCode::Char('G'),
    817                 modifiers: KeyModifiers::SHIFT,
    818                 ..
    819             } => self.handle_normal_mode_key_event_toggle_guest_view(),
    820             KeyEvent {
    821                 code: KeyCode::Char('H'),
    822                 modifiers: KeyModifiers::SHIFT,
    823                 ..
    824             } => self.handle_normal_mode_key_event_toggle_hidden(),
    825             KeyEvent {
    826                 code: KeyCode::Char('i'),
    827                 modifiers: KeyModifiers::NONE,
    828                 ..
    829             } => self.handle_normal_mode_key_event_input_mode(app),
    830             KeyEvent {
    831                 code: KeyCode::Char('Q'),
    832                 modifiers: KeyModifiers::SHIFT,
    833                 ..
    834             } => self.handle_normal_mode_key_event_logout()?,
    835             KeyEvent {
    836                 code: KeyCode::Char('q'),
    837                 modifiers: KeyModifiers::NONE,
    838                 ..
    839             } => self.handle_normal_mode_key_event_exit()?,
    840             KeyEvent {
    841                 code: KeyCode::Char('t'),
    842                 modifiers: KeyModifiers::NONE,
    843                 ..
    844             } => self.handle_normal_mode_key_event_tag(app),
    845             KeyEvent {
    846                 code: KeyCode::Char('p'),
    847                 modifiers: KeyModifiers::NONE,
    848                 ..
    849             } => self.handle_normal_mode_key_event_pm(app),
    850             KeyEvent {
    851                 code: KeyCode::Char('k'),
    852                 modifiers: KeyModifiers::CONTROL,
    853                 ..
    854             } => self.handle_normal_mode_key_event_kick(app),
    855             KeyEvent {
    856                 code: KeyCode::Char('w'),
    857                 modifiers: KeyModifiers::CONTROL,
    858                 ..
    859             } => self.handle_normal_mode_key_event_warn(app),
    860             KeyEvent {
    861                 code: KeyCode::Char('T'),
    862                 modifiers: KeyModifiers::SHIFT,
    863                 ..
    864             } => {
    865                 self.handle_normal_mode_key_event_translate(app, messages);
    866             }
    867             KeyEvent {
    868                 code: KeyCode::Char('u'),
    869                 modifiers: KeyModifiers::CONTROL,
    870                 ..
    871             }
    872             | KeyEvent {
    873                 code: KeyCode::PageUp,
    874                 modifiers: KeyModifiers::NONE,
    875                 ..
    876             } => self.handle_normal_mode_key_event_page_up(app),
    877             KeyEvent {
    878                 code: KeyCode::Char('d'),
    879                 modifiers: KeyModifiers::CONTROL,
    880                 ..
    881             }
    882             | KeyEvent {
    883                 code: KeyCode::PageDown,
    884                 modifiers: KeyModifiers::NONE,
    885                 ..
    886             } => self.handle_normal_mode_key_event_page_down(app),
    887             KeyEvent {
    888                 code: KeyCode::Esc,
    889                 modifiers: KeyModifiers::NONE,
    890                 ..
    891             } => self.handle_normal_mode_key_event_esc(app),
    892             KeyEvent {
    893                 code: KeyCode::Char('u'),
    894                 modifiers: KeyModifiers::SHIFT,
    895                 ..
    896             } => self.handle_normal_mode_key_event_shift_u(app),
    897             KeyEvent {
    898                 code: KeyCode::Char('g'),
    899                 modifiers: KeyModifiers::NONE,
    900                 ..
    901             } => self.handle_normal_mode_key_event_g(app),
    902             _ => {}
    903         }
    904         self.last_key_event = Some(key_event.code);
    905         Ok(())
    906     }
    907 
    908     fn handle_editing_mode_key_event(
    909         &mut self,
    910         app: &mut App,
    911         key_event: KeyEvent,
    912         users: &Arc<Mutex<Users>>,
    913     ) -> Result<(), ExitSignal> {
    914         app.input_mode = InputMode::Editing;
    915         match key_event {
    916             KeyEvent {
    917                 code: KeyCode::Enter,
    918                 modifiers: KeyModifiers::NONE,
    919                 ..
    920             } => self.handle_editing_mode_key_event_enter(app)?,
    921             KeyEvent {
    922                 code: KeyCode::Tab,
    923                 modifiers: KeyModifiers::NONE,
    924                 ..
    925             } => self.handle_editing_mode_key_event_tab(app, users),
    926             KeyEvent {
    927                 code: KeyCode::Char('c'),
    928                 modifiers: KeyModifiers::CONTROL,
    929                 ..
    930             } => self.handle_editing_mode_key_event_ctrl_c(app),
    931             KeyEvent {
    932                 code: KeyCode::Char('a'),
    933                 modifiers: KeyModifiers::CONTROL,
    934                 ..
    935             } => self.handle_editing_mode_key_event_ctrl_a(app),
    936             KeyEvent {
    937                 code: KeyCode::Char('e'),
    938                 modifiers: KeyModifiers::CONTROL,
    939                 ..
    940             } => self.handle_editing_mode_key_event_ctrl_e(app),
    941             KeyEvent {
    942                 code: KeyCode::Char('f'),
    943                 modifiers: KeyModifiers::CONTROL,
    944                 ..
    945             } => self.handle_editing_mode_key_event_ctrl_f(app),
    946             KeyEvent {
    947                 code: KeyCode::Char('b'),
    948                 modifiers: KeyModifiers::CONTROL,
    949                 ..
    950             } => self.handle_editing_mode_key_event_ctrl_b(app),
    951             KeyEvent {
    952                 code: KeyCode::Char('v'),
    953                 modifiers: KeyModifiers::CONTROL,
    954                 ..
    955             } => self.handle_editing_mode_key_event_ctrl_v(app),
    956             KeyEvent {
    957                 code: KeyCode::Left,
    958                 modifiers: KeyModifiers::NONE,
    959                 ..
    960             } => self.handle_editing_mode_key_event_left(app),
    961             KeyEvent {
    962                 code: KeyCode::Right,
    963                 modifiers: KeyModifiers::NONE,
    964                 ..
    965             } => self.handle_editing_mode_key_event_right(app),
    966             KeyEvent {
    967                 code: KeyCode::Down,
    968                 modifiers: KeyModifiers::NONE,
    969                 ..
    970             } => self.handle_editing_mode_key_event_down(app),
    971             KeyEvent {
    972                 code: KeyCode::Char(c),
    973                 modifiers: KeyModifiers::NONE,
    974                 ..
    975             }
    976             | KeyEvent {
    977                 code: KeyCode::Char(c),
    978                 modifiers: KeyModifiers::SHIFT,
    979                 ..
    980             } => self.handle_editing_mode_key_event_shift_c(app, c),
    981             KeyEvent {
    982                 code: KeyCode::Backspace,
    983                 modifiers: KeyModifiers::NONE,
    984                 ..
    985             } => self.handle_editing_mode_key_event_backspace(app),
    986             KeyEvent {
    987                 code: KeyCode::Delete,
    988                 modifiers: KeyModifiers::NONE,
    989                 ..
    990             } => self.handle_editing_mode_key_event_delete(app),
    991             KeyEvent {
    992                 code: KeyCode::Esc,
    993                 modifiers: KeyModifiers::NONE,
    994                 ..
    995             } => self.handle_editing_mode_key_event_esc(app),
    996             _ => {}
    997         }
    998         Ok(())
    999     }
   1000 
   1001     fn handle_long_message_mode_key_event_esc(&mut self, app: &mut App) {
   1002         app.long_message = None;
   1003         app.input_mode = InputMode::Normal;
   1004     }
   1005 
   1006     fn handle_long_message_mode_key_event_ctrl_d(
   1007         &mut self,
   1008         app: &mut App,
   1009         messages: &Arc<Mutex<Vec<Message>>>,
   1010     ) {
   1011         if let Some(idx) = app.items.state.selected() {
   1012             if let Some(item) = app.items.items.get(idx) {
   1013                 self.post_msg(PostType::Clean(item.date.to_owned(), item.text.text()))
   1014                     .unwrap();
   1015                 let mut messages = messages.lock().unwrap();
   1016                 if let Some(pos) = messages
   1017                     .iter()
   1018                     .position(|m| m.date == item.date && m.text == item.text)
   1019                 {
   1020                     messages[pos].hide = !messages[pos].hide;
   1021                 }
   1022                 app.long_message = None;
   1023                 app.input_mode = InputMode::Normal;
   1024             }
   1025         }
   1026     }
   1027 
   1028     fn handle_normal_mode_key_event_up(&mut self, app: &mut App) {
   1029         app.items.previous()
   1030     }
   1031 
   1032     fn handle_normal_mode_key_event_down(&mut self, app: &mut App) {
   1033         app.items.next()
   1034     }
   1035 
   1036     fn handle_normal_mode_key_event_j(&mut self, app: &mut App, lines: usize) {
   1037         for _ in 0..lines {
   1038             app.items.next(); // Move to the next item
   1039         }
   1040     }
   1041 
   1042     fn handle_normal_mode_key_event_k(&mut self, app: &mut App, lines: usize) {
   1043         for _ in 0..lines {
   1044             app.items.previous(); // Move to the next item
   1045         }
   1046     }
   1047 
   1048     fn handle_normal_mode_key_event_slash(&mut self, app: &mut App) {
   1049         app.items.unselect();
   1050         app.input = "/".to_owned();
   1051         app.input_idx = app.input.width();
   1052         app.input_mode = InputMode::Editing;
   1053     }
   1054 
   1055     fn handle_normal_mode_key_event_enter(
   1056         &mut self,
   1057         app: &mut App,
   1058         messages: &Arc<Mutex<Vec<Message>>>,
   1059     ) {
   1060         if let Some(idx) = app.items.state.selected() {
   1061             if let Some(item) = app.items.items.get(idx) {
   1062                 // If we have a filter, <enter> will "jump" to the message
   1063                 if !app.filter.is_empty() {
   1064                     let idx = messages
   1065                         .lock()
   1066                         .unwrap()
   1067                         .iter()
   1068                         .enumerate()
   1069                         .find(|(_, e)| e.date == item.date)
   1070                         .map(|(i, _)| i);
   1071                     app.clear_filter();
   1072                     app.items.state.select(idx);
   1073                     return;
   1074                 }
   1075                 app.long_message = Some(item.clone());
   1076                 app.input_mode = InputMode::LongMessage;
   1077             }
   1078         }
   1079     }
   1080 
   1081     fn handle_normal_mode_key_event_backspace(
   1082         &mut self,
   1083         app: &mut App,
   1084         messages: &Arc<Mutex<Vec<Message>>>,
   1085     ) {
   1086         if let Some(idx) = app.items.state.selected() {
   1087             if let Some(item) = app.items.items.get(idx) {
   1088                 let mut messages = messages.lock().unwrap();
   1089                 if let Some(pos) = messages
   1090                     .iter()
   1091                     .position(|m| m.date == item.date && m.text == item.text)
   1092                 {
   1093                     if item.deleted {
   1094                         messages.remove(pos);
   1095                     } else {
   1096                         messages[pos].hide = !messages[pos].hide;
   1097                     }
   1098                 }
   1099             }
   1100         }
   1101     }
   1102 
   1103     fn handle_normal_mode_key_event_yank(&mut self, app: &mut App) {
   1104         if let Some(idx) = app.items.state.selected() {
   1105             if let Some(item) = app.items.items.get(idx) {
   1106                 if let Some(upload_link) = &item.upload_link {
   1107                     let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   1108                     let mut out = format!("{}{}", self.config.url, upload_link);
   1109                     if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag) {
   1110                         out = format!("{} {}", msg, out);
   1111                     }
   1112                     ctx.set_contents(out).unwrap();
   1113                 } else if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag)
   1114                 {
   1115                     let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   1116                     ctx.set_contents(msg).unwrap();
   1117                 }
   1118             }
   1119         }
   1120     }
   1121 
   1122     fn handle_normal_mode_key_event_yank_link(&mut self, app: &mut App) {
   1123         if let Some(idx) = app.items.state.selected() {
   1124             if let Some(item) = app.items.items.get(idx) {
   1125                 if let Some(upload_link) = &item.upload_link {
   1126                     let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   1127                     let out = format!("{}{}", self.config.url, upload_link);
   1128                     ctx.set_contents(out).unwrap();
   1129                 } else if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag)
   1130                 {
   1131                     let finder = LinkFinder::new();
   1132                     let links: Vec<_> = finder.links(msg.as_str()).collect();
   1133                     if let Some(link) = links.get(0) {
   1134                         let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   1135                         ctx.set_contents(link.as_str().to_owned()).unwrap();
   1136                     }
   1137                 }
   1138             }
   1139         }
   1140     }
   1141 
   1142     //Strange
   1143     async fn handle_normal_mode_key_event_download_link(
   1144         app: &mut App,
   1145         config_url: String,
   1146         members_tag: String,
   1147         show_image: bool,
   1148     ) {
   1149         log::debug!("handle_normal_mode_key_event_download_link called");
   1150         if let Some(idx) = app.items.state.selected() {
   1151             if let Some(item) = app.items.items.get(idx) {
   1152                 if let Some(upload_link) = &item.upload_link {
   1153                     let url = format!("{}{}", config_url, upload_link);
   1154                     let filename = if let Some((username, _, full_text)) =
   1155                         get_message(&item.text, &members_tag)
   1156                     {
   1157                         if let Some(start) = full_text.find('[') {
   1158                             if let Some(end) = full_text[start..].find(']') {
   1159                                 let inside_brackets = &full_text[start + 1..start + end];
   1160                                 let sanitized = inside_brackets.replace(' ', "_");
   1161                                 format!("{}_{}", username, sanitized)
   1162                             } else {
   1163                                 item.text.text()
   1164                             }
   1165                         } else {
   1166                             item.text.text()
   1167                         }
   1168                     } else {
   1169                         item.text.text()
   1170                     };
   1171 
   1172                     let output = TokioCommand::new("curl")
   1173                         .args([
   1174                             "--socks5",
   1175                             "localhost:9050",
   1176                             "--socks5-hostname",
   1177                             "localhost:9050",
   1178                             &url,
   1179                             "-o",
   1180                             &filename,
   1181                         ])
   1182                         .output()
   1183                         .await;
   1184 
   1185                     match output {
   1186                         Ok(output) => {
   1187                             if output.status.success() {
   1188                                 if show_image {
   1189                                     let _ =
   1190                                         TokioCommand::new("sxiv").arg("-a").arg(&filename).spawn();
   1191                                 }
   1192                             } else {
   1193                                 log::error!("Curl command failed with status: {:?}", output.status);
   1194                                 log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
   1195                                 log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
   1196                             }
   1197                         }
   1198                         Err(e) => {
   1199                             log::error!("Failed to execute curl command: {}", e);
   1200                         }
   1201                     }
   1202                 } else {
   1203                     let msg = if let Some((_, _, msg)) = get_message(&item.text, &members_tag) {
   1204                         msg.to_string()
   1205                     } else {
   1206                         item.text.text()
   1207                     };
   1208 
   1209                     let finder = LinkFinder::new();
   1210                     let links: Vec<_> = finder.links(msg.as_str()).collect();
   1211                     if let Some(link) = links.first() {
   1212                         let url = link.as_str();
   1213                         let output = TokioCommand::new("curl")
   1214                             .args([
   1215                                 "--socks5",
   1216                                 "localhost:9050",
   1217                                 "--socks5-hostname",
   1218                                 "localhost:9050",
   1219                                 url,
   1220                                 "-O",
   1221                             ])
   1222                             .output()
   1223                             .await;
   1224 
   1225                         match output {
   1226                             Ok(output) => {
   1227                                 if !output.status.success() {
   1228                                     log::error!(
   1229                                         "Curl command failed with status: {:?}",
   1230                                         output.status
   1231                                     );
   1232                                     log::error!(
   1233                                         "stdout: {}",
   1234                                         String::from_utf8_lossy(&output.stdout)
   1235                                     );
   1236                                     log::error!(
   1237                                         "stderr: {}",
   1238                                         String::from_utf8_lossy(&output.stderr)
   1239                                     );
   1240                                 }
   1241                             }
   1242                             Err(e) => {
   1243                                 log::error!("Failed to execute curl command: {}", e);
   1244                             }
   1245                         }
   1246                     }
   1247                 }
   1248             }
   1249         }
   1250     }
   1251 
   1252     fn handle_normal_mode_key_event_toggle_mute(&mut self) {
   1253         let mut is_muted = self.is_muted.lock().unwrap();
   1254         *is_muted = !*is_muted;
   1255     }
   1256 
   1257     fn handle_normal_mode_key_event_toggle_sys(&mut self) {
   1258         self.show_sys = !self.show_sys;
   1259     }
   1260 
   1261     fn handle_normal_mode_key_event_toggle_guest_view(&mut self) {
   1262         self.display_guest_view = !self.display_guest_view;
   1263     }
   1264 
   1265     fn handle_normal_mode_key_event_toggle_member_view(&mut self) {
   1266         self.display_member_view = !self.display_member_view;
   1267     }
   1268 
   1269     fn handle_normal_mode_key_event_g(&mut self, app: &mut App) {
   1270         // Handle "gg" key combination
   1271         if self.last_key_event == Some(KeyCode::Char('g')) {
   1272             app.items.select_top();
   1273             self.last_key_event = None;
   1274         }
   1275     }
   1276 
   1277     fn handle_normal_mode_key_event_toggle_hidden(&mut self) {
   1278         self.display_hidden_msgs = !self.display_hidden_msgs;
   1279     }
   1280 
   1281     fn handle_normal_mode_key_event_input_mode(&mut self, app: &mut App) {
   1282         app.input_mode = InputMode::Editing;
   1283         app.items.unselect();
   1284     }
   1285 
   1286     fn handle_normal_mode_key_event_logout(&mut self) -> Result<(), ExitSignal> {
   1287         self.logout().unwrap();
   1288         return Err(ExitSignal::Terminate);
   1289     }
   1290 
   1291     fn handle_normal_mode_key_event_exit(&mut self) -> Result<(), ExitSignal> {
   1292         return Err(ExitSignal::Terminate);
   1293     }
   1294 
   1295     fn handle_normal_mode_key_event_tag(&mut self, app: &mut App) {
   1296         if let Some(idx) = app.items.state.selected() {
   1297             let text = &app.items.items.get(idx).unwrap().text;
   1298             if let Some(username) =
   1299                 get_username(&self.base_client.username, &text, &self.config.members_tag)
   1300             {
   1301                 if text.text().starts_with(&app.members_tag) {
   1302                     app.input = format!("/m @{} ", username);
   1303                 } else {
   1304                     app.input = format!("@{} ", username);
   1305                 }
   1306                 app.input_idx = app.input.width();
   1307                 app.input_mode = InputMode::Editing;
   1308                 app.items.unselect();
   1309             }
   1310         }
   1311     }
   1312 
   1313     fn handle_normal_mode_key_event_pm(&mut self, app: &mut App) {
   1314         if let Some(idx) = app.items.state.selected() {
   1315             if let Some(username) = get_username(
   1316                 &self.base_client.username,
   1317                 &app.items.items.get(idx).unwrap().text,
   1318                 &self.config.members_tag,
   1319             ) {
   1320                 app.input = format!("/pm {} ", username);
   1321                 app.input_idx = app.input.width();
   1322                 app.input_mode = InputMode::Editing;
   1323                 app.items.unselect();
   1324             }
   1325         }
   1326     }
   1327 
   1328     fn handle_normal_mode_key_event_kick(&mut self, app: &mut App) {
   1329         if let Some(idx) = app.items.state.selected() {
   1330             if let Some(username) = get_username(
   1331                 &self.base_client.username,
   1332                 &app.items.items.get(idx).unwrap().text,
   1333                 &self.config.members_tag,
   1334             ) {
   1335                 app.input = format!("/kick {} ", username);
   1336                 app.input_idx = app.input.width();
   1337                 app.input_mode = InputMode::Editing;
   1338                 app.items.unselect();
   1339             }
   1340         }
   1341     }
   1342 
   1343     //Strange
   1344     fn handle_normal_mode_key_event_translate(
   1345         &mut self,
   1346         app: &mut App,
   1347         messages: &Arc<Mutex<Vec<Message>>>,
   1348     ) {
   1349         log::info!("translate running");
   1350         if let Some(idx) = app.items.state.selected() {
   1351             let mut message_lock = messages.lock().unwrap();
   1352             if let Some(message) = message_lock.get_mut(idx) {
   1353                 let input_text = message.text.text(); // convert StyledText -> &str
   1354                 let output = Command::new("trans").arg("-b").arg(input_text).output();
   1355 
   1356                 match output {
   1357                     Ok(output) => {
   1358                         if output.status.success() {
   1359                             if let Ok(new_text) = String::from_utf8(output.stdout) {
   1360                                 message.text = StyledText::Text(new_text.trim().to_owned());
   1361                                 log::info!("Translation successful: {}", new_text);
   1362                             } else {
   1363                                 log::error!("Failed to decode translation output as UTF-8");
   1364                             }
   1365                         } else {
   1366                             log::error!("Translation command failed: {:?}", output.status);
   1367                             log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
   1368                         }
   1369                     }
   1370                     Err(e) => {
   1371                         log::error!("Failed to execute translation command: {}", e);
   1372                     }
   1373                 }
   1374             }
   1375         }
   1376     }
   1377 
   1378     //Strange
   1379     fn handle_normal_mode_key_event_warn(&mut self, app: &mut App) {
   1380         if let Some(idx) = app.items.state.selected() {
   1381             if let Some(username) = get_username(
   1382                 &self.base_client.username,
   1383                 &app.items.items.get(idx).unwrap().text,
   1384                 &self.config.members_tag,
   1385             ) {
   1386                 app.input = format!("!warn @{} ", username);
   1387                 app.input_idx = app.input.width();
   1388                 app.input_mode = InputMode::Editing;
   1389                 app.items.unselect();
   1390             }
   1391         }
   1392     }
   1393     fn handle_normal_mode_key_event_page_up(&mut self, app: &mut App) {
   1394         if let Some(idx) = app.items.state.selected() {
   1395             app.items.state.select(idx.checked_sub(10).or(Some(0)));
   1396         } else {
   1397             app.items.next();
   1398         }
   1399     }
   1400 
   1401     fn handle_normal_mode_key_event_page_down(&mut self, app: &mut App) {
   1402         if let Some(idx) = app.items.state.selected() {
   1403             let wanted_idx = idx + 10;
   1404             let max_idx = app.items.items.len() - 1;
   1405             let new_idx = std::cmp::min(wanted_idx, max_idx);
   1406             app.items.state.select(Some(new_idx));
   1407         } else {
   1408             app.items.next();
   1409         }
   1410     }
   1411 
   1412     fn handle_normal_mode_key_event_esc(&mut self, app: &mut App) {
   1413         app.items.unselect();
   1414     }
   1415 
   1416     fn handle_normal_mode_key_event_shift_u(&mut self, app: &mut App) {
   1417         app.items.state.select(Some(0));
   1418     }
   1419 
   1420     fn handle_editing_mode_key_event_enter(&mut self, app: &mut App) -> Result<(), ExitSignal> {
   1421         if FIND_RGX.is_match(&app.input) {
   1422             return Ok(());
   1423         }
   1424 
   1425         let input: String = app.input.drain(..).collect();
   1426         app.input_idx = 0;
   1427 
   1428         // Iterate over commands and execute associated actions
   1429         for (command, action) in &app.commands.commands {
   1430             // log::error!("command :{} action :{}", command, action);
   1431             let expected_input = format!("!{}", command);
   1432             if input == expected_input {
   1433                 // Execute the action by posting a message
   1434                 self.post_msg(PostType::Post(action.clone(), None)).unwrap();
   1435                 // Return Ok(()) if the action is executed successfully
   1436                 return Ok(());
   1437             }
   1438         }
   1439 
   1440         if input == "/dl" {
   1441             // Delete last message
   1442             self.post_msg(PostType::DeleteLast).unwrap();
   1443         } else if let Some(captures) = DLX_RGX.captures(&input) {
   1444             // Delete the last X messages
   1445             let x: usize = captures.get(1).unwrap().as_str().parse().unwrap();
   1446             for _ in 0..x {
   1447                 self.post_msg(PostType::DeleteLast).unwrap();
   1448             }
   1449         } else if input == "/dall" {
   1450             // Delete all messages
   1451             self.post_msg(PostType::DeleteAll).unwrap();
   1452         } else if input == "/cycles" {
   1453             self.color_tx.send(()).unwrap();
   1454         } else if input == "/cycle1" {
   1455             self.start_cycle(true);
   1456         } else if input == "/cycle2" {
   1457             self.start_cycle(false);
   1458         } else if input == "/kall" {
   1459             // Kick all guests
   1460             let username = "s _".to_owned();
   1461             let msg = "".to_owned();
   1462             self.post_msg(PostType::Kick(msg, username)).unwrap();
   1463         } else if input.starts_with("/m ") {
   1464             // Send message to "members" section
   1465             let msg = remove_prefix(&input, "/m ").to_owned();
   1466             let to = Some(SEND_TO_MEMBERS.to_owned());
   1467             self.post_msg(PostType::Post(msg, to)).unwrap();
   1468             app.input = "/m ".to_owned();
   1469             app.input_idx = app.input.width()
   1470         } else if input.starts_with("/a ") {
   1471             // Send message to "admin" section
   1472             let msg = remove_prefix(&input, "/a ").to_owned();
   1473             let to = Some(SEND_TO_ADMINS.to_owned());
   1474             self.post_msg(PostType::Post(msg, to)).unwrap();
   1475             app.input = "/a ".to_owned();
   1476             app.input_idx = app.input.width()
   1477         } else if input.starts_with("/s ") {
   1478             // Send message to "staff" section
   1479             let msg = remove_prefix(&input, "/s ").to_owned();
   1480             let to = Some(SEND_TO_STAFFS.to_owned());
   1481             self.post_msg(PostType::Post(msg, to)).unwrap();
   1482             app.input = "/s ".to_owned();
   1483             app.input_idx = app.input.width()
   1484         } else if let Some(captures) = PM_RGX.captures(&input) {
   1485             // Send PM message
   1486             let username = &captures[1];
   1487             let msg = captures[2].to_owned();
   1488             let to = Some(username.to_owned());
   1489             self.post_msg(PostType::Post(msg, to)).unwrap();
   1490             app.input = format!("/pm {} ", username);
   1491             app.input_idx = app.input.width()
   1492         } else if let Some(captures) = NEW_NICKNAME_RGX.captures(&input) {
   1493             // Change nickname
   1494             let new_nickname = captures[1].to_owned();
   1495             self.post_msg(PostType::NewNickname(new_nickname)).unwrap();
   1496         } else if let Some(captures) = NEW_COLOR_RGX.captures(&input) {
   1497             // Change color
   1498             let new_color = captures[1].to_owned();
   1499             self.post_msg(PostType::NewColor(new_color)).unwrap();
   1500         } else if let Some(captures) = KICK_RGX.captures(&input) {
   1501             // Kick a user
   1502             let username = captures[1].to_owned();
   1503             let msg = captures[2].to_owned();
   1504             self.post_msg(PostType::Kick(msg, username)).unwrap();
   1505         } else if let Some(captures) = IGNORE_RGX.captures(&input) {
   1506             // Ignore a user
   1507             let username = captures[1].to_owned();
   1508             self.post_msg(PostType::Ignore(username)).unwrap();
   1509         } else if let Some(captures) = UNIGNORE_RGX.captures(&input) {
   1510             // Unignore a user
   1511             let username = captures[1].to_owned();
   1512             self.post_msg(PostType::Unignore(username)).unwrap();
   1513         } else if let Some(captures) = UPLOAD_RGX.captures(&input) {
   1514             // Upload a file
   1515             let file_path = captures[1].to_owned();
   1516             let send_to = match captures.get(2) {
   1517                 Some(to_match) => match to_match.as_str() {
   1518                     "members" => SEND_TO_MEMBERS,
   1519                     "staffs" => SEND_TO_STAFFS,
   1520                     "admins" => SEND_TO_ADMINS,
   1521                     _ => SEND_TO_ALL,
   1522                 },
   1523                 None => SEND_TO_ALL,
   1524             }
   1525             .to_owned();
   1526             let msg = match captures.get(3) {
   1527                 Some(msg_match) => msg_match.as_str().to_owned(),
   1528                 None => "".to_owned(),
   1529             };
   1530             self.post_msg(PostType::Upload(file_path, send_to, msg))
   1531                 .unwrap();
   1532         } else if input.starts_with("!warn") {
   1533             // Strange
   1534             let msg: String = input
   1535                 .find('@')
   1536                 .map(|index| input[index..].to_string())
   1537                 .unwrap_or_else(String::new);
   1538 
   1539             let end_msg = format!(
   1540                 "This is your warning - {}, will be kicked next  !rules",
   1541                 msg
   1542             );
   1543             // log::error!("The Strange end_msg is :{}", end_msg);
   1544             self.post_msg(PostType::Post(end_msg, None)).unwrap();
   1545         } else {
   1546             if input.starts_with("/") && !input.starts_with("/me ") {
   1547                 app.input_idx = input.len();
   1548                 app.input = input;
   1549                 app.input_mode = InputMode::EditingErr;
   1550             } else {
   1551                 // Send normal message
   1552                 self.post_msg(PostType::Post(input, None)).unwrap();
   1553             }
   1554         }
   1555         Ok(())
   1556     }
   1557 
   1558     fn handle_editing_mode_key_event_tab(&mut self, app: &mut App, users: &Arc<Mutex<Users>>) {
   1559         let (p1, p2) = app.input.split_at(app.input_idx);
   1560         if p2 == "" || p2.chars().nth(0) == Some(' ') {
   1561             let mut parts: Vec<&str> = p1.split(" ").collect();
   1562             if let Some(user_prefix) = parts.pop() {
   1563                 let mut should_autocomplete = false;
   1564                 let mut prefix = "";
   1565                 if parts.len() == 1
   1566                     && ((parts[0] == "/kick" || parts[0] == "/k")
   1567                         || parts[0] == "/pm"
   1568                         || parts[0] == "/ignore"
   1569                         || parts[0] == "/unignore")
   1570                 {
   1571                     should_autocomplete = true;
   1572                 } else if user_prefix.starts_with("@") {
   1573                     should_autocomplete = true;
   1574                     prefix = "@";
   1575                 }
   1576                 if should_autocomplete {
   1577                     let user_prefix_norm = remove_prefix(user_prefix, prefix);
   1578                     let user_prefix_norm_len = user_prefix_norm.len();
   1579                     if let Some(name) = autocomplete_username(users, user_prefix_norm) {
   1580                         let complete_name = format!("{}{}", prefix, name);
   1581                         parts.push(complete_name.as_str());
   1582                         let p2 = p2.trim_start();
   1583                         if p2 != "" {
   1584                             parts.push(p2);
   1585                         }
   1586                         app.input = parts.join(" ");
   1587                         app.input_idx += name.len() - user_prefix_norm_len;
   1588                     }
   1589                 }
   1590             }
   1591         }
   1592     }
   1593 
   1594     fn handle_editing_mode_key_event_ctrl_c(&mut self, app: &mut App) {
   1595         app.clear_filter();
   1596         app.input = "".to_owned();
   1597         app.input_idx = 0;
   1598         app.input_mode = InputMode::Normal;
   1599     }
   1600 
   1601     fn handle_editing_mode_key_event_ctrl_a(&mut self, app: &mut App) {
   1602         app.input_idx = 0;
   1603     }
   1604 
   1605     fn handle_editing_mode_key_event_ctrl_e(&mut self, app: &mut App) {
   1606         app.input_idx = app.input.width();
   1607     }
   1608 
   1609     fn handle_editing_mode_key_event_ctrl_f(&mut self, app: &mut App) {
   1610         if let Some(idx) = app.input.chars().skip(app.input_idx).position(|c| c == ' ') {
   1611             app.input_idx = std::cmp::min(app.input_idx + idx + 1, app.input.width());
   1612         } else {
   1613             app.input_idx = app.input.width();
   1614         }
   1615     }
   1616 
   1617     fn handle_editing_mode_key_event_ctrl_b(&mut self, app: &mut App) {
   1618         if let Some(idx) = app.input_idx.checked_sub(2) {
   1619             let tmp = app
   1620                 .input
   1621                 .chars()
   1622                 .take(idx)
   1623                 .collect::<String>()
   1624                 .chars()
   1625                 .rev()
   1626                 .collect::<String>();
   1627             if let Some(idx) = tmp.chars().position(|c| c == ' ') {
   1628                 app.input_idx = std::cmp::max(tmp.width() - idx, 0);
   1629             } else {
   1630                 app.input_idx = 0;
   1631             }
   1632         }
   1633     }
   1634 
   1635     fn handle_editing_mode_key_event_ctrl_v(&mut self, app: &mut App) {
   1636         let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   1637         if let Ok(clipboard) = ctx.get_contents() {
   1638             let byte_position = byte_pos(&app.input, app.input_idx).unwrap();
   1639             app.input.insert_str(byte_position, &clipboard);
   1640             app.input_idx += clipboard.width();
   1641         }
   1642     }
   1643 
   1644     fn handle_editing_mode_key_event_left(&mut self, app: &mut App) {
   1645         if app.input_idx > 0 {
   1646             app.input_idx -= 1;
   1647         }
   1648     }
   1649 
   1650     fn handle_editing_mode_key_event_right(&mut self, app: &mut App) {
   1651         if app.input_idx < app.input.width() {
   1652             app.input_idx += 1;
   1653         }
   1654     }
   1655 
   1656     fn handle_editing_mode_key_event_down(&mut self, app: &mut App) {
   1657         app.input_mode = InputMode::Normal;
   1658         app.items.next();
   1659     }
   1660 
   1661     fn handle_editing_mode_key_event_shift_c(&mut self, app: &mut App, c: char) {
   1662         let byte_position = byte_pos(&app.input, app.input_idx).unwrap();
   1663         app.input.insert(byte_position, c);
   1664 
   1665         app.input_idx += 1;
   1666         app.update_filter();
   1667     }
   1668 
   1669     fn handle_editing_mode_key_event_backspace(&mut self, app: &mut App) {
   1670         if app.input_idx > 0 {
   1671             app.input_idx -= 1;
   1672             app.input = remove_at(&app.input, app.input_idx);
   1673             app.update_filter();
   1674         }
   1675     }
   1676 
   1677     fn handle_editing_mode_key_event_delete(&mut self, app: &mut App) {
   1678         if app.input_idx > 0 && app.input_idx == app.input.width() {
   1679             app.input_idx -= 1;
   1680         }
   1681         app.input = remove_at(&app.input, app.input_idx);
   1682         app.update_filter();
   1683     }
   1684 
   1685     fn handle_editing_mode_key_event_esc(&mut self, app: &mut App) {
   1686         app.input_mode = InputMode::Normal;
   1687     }
   1688 
   1689     fn handle_mouse_event(
   1690         &mut self,
   1691         app: &mut App,
   1692         mouse_event: MouseEvent,
   1693     ) -> Result<(), ExitSignal> {
   1694         match mouse_event.kind {
   1695             MouseEventKind::ScrollDown => app.items.next(),
   1696             MouseEventKind::ScrollUp => app.items.previous(),
   1697             _ => {}
   1698         }
   1699         Ok(())
   1700     }
   1701 }
   1702 
   1703 // Give a char index, return the byte position
   1704 fn byte_pos(v: &str, idx: usize) -> Option<usize> {
   1705     let mut b = 0;
   1706     let mut chars = v.chars();
   1707     for _ in 0..idx {
   1708         if let Some(c) = chars.next() {
   1709             b += c.len_utf8();
   1710         } else {
   1711             return None;
   1712         }
   1713     }
   1714     Some(b)
   1715 }
   1716 
   1717 // Remove the character at idx (utf-8 aware)
   1718 fn remove_at(v: &str, idx: usize) -> String {
   1719     v.chars()
   1720         .enumerate()
   1721         .flat_map(|(i, c)| {
   1722             if i == idx {
   1723                 return None;
   1724             }
   1725             Some(c)
   1726         })
   1727         .collect::<String>()
   1728 }
   1729 
   1730 // Autocomplete any username
   1731 fn autocomplete_username(users: &Arc<Mutex<Users>>, prefix: &str) -> Option<String> {
   1732     let users = users.lock().unwrap();
   1733     let all_users = users.all();
   1734     let prefix_lower = prefix.to_lowercase();
   1735     let filtered = all_users
   1736         .iter()
   1737         .find(|(_, name)| name.to_lowercase().starts_with(&prefix_lower));
   1738     Some(filtered?.1.to_owned())
   1739 }
   1740 
   1741 fn set_profile_base_info(
   1742     client: &Client,
   1743     full_url: &str,
   1744     params: &mut Vec<(&str, String)>,
   1745 ) -> anyhow::Result<()> {
   1746     params.extend(vec![("action", "profile".to_owned())]);
   1747     let profile_resp = client.post(full_url).form(&params).send()?;
   1748     let profile_resp_txt = profile_resp.text().unwrap();
   1749     let doc = Document::from(profile_resp_txt.as_str());
   1750     let bold = doc.find(Attr("id", "bold")).next().unwrap();
   1751     let italic = doc.find(Attr("id", "italic")).next().unwrap();
   1752     let small = doc.find(Attr("id", "small")).next().unwrap();
   1753     if bold.attr("checked").is_some() {
   1754         params.push(("bold", "on".to_owned()));
   1755     }
   1756     if italic.attr("checked").is_some() {
   1757         params.push(("italic", "on".to_owned()));
   1758     }
   1759     if small.attr("checked").is_some() {
   1760         params.push(("small", "on".to_owned()));
   1761     }
   1762     let font_select = doc.find(Attr("name", "font")).next().unwrap();
   1763     let font = font_select.find(Name("option")).find_map(|el| {
   1764         if el.attr("selected").is_some() {
   1765             return Some(el.attr("value").unwrap());
   1766         }
   1767         None
   1768     });
   1769     params.push(("font", font.unwrap_or("").to_owned()));
   1770     Ok(())
   1771 }
   1772 
   1773 enum RetryErr {
   1774     Retry,
   1775     Exit,
   1776 }
   1777 
   1778 fn retry_fn<F>(mut clb: F)
   1779 where
   1780     F: FnMut() -> anyhow::Result<RetryErr>,
   1781 {
   1782     loop {
   1783         match clb() {
   1784             Ok(RetryErr::Retry) => continue,
   1785             Ok(RetryErr::Exit) => return,
   1786             Err(err) => {
   1787                 log::error!("{}", err);
   1788                 continue;
   1789             }
   1790         }
   1791     }
   1792 }
   1793 
   1794 fn post_msg(
   1795     client: &Client,
   1796     post_type_recv: PostType,
   1797     full_url: &str,
   1798     session: String,
   1799     url: &str,
   1800     last_post_tx: &crossbeam_channel::Sender<()>,
   1801 ) {
   1802     let mut should_reset_keepalive_timer = false;
   1803     retry_fn(|| -> anyhow::Result<RetryErr> {
   1804         let post_type = post_type_recv.clone();
   1805         let resp_text = client.get(url).send()?.text()?;
   1806         let doc = Document::from(resp_text.as_str());
   1807         let nc = doc
   1808             .find(Attr("name", "nc"))
   1809             .next()
   1810             .context("nc not found")?;
   1811         let nc_value = nc.attr("value").context("nc value not found")?.to_owned();
   1812         let postid = doc
   1813             .find(Attr("name", "postid"))
   1814             .next()
   1815             .context("failed to get postid")?;
   1816         let postid_value = postid
   1817             .attr("value")
   1818             .context("failed to get postid value")?
   1819             .to_owned();
   1820         let mut params: Vec<(&str, String)> = vec![
   1821             ("lang", LANG.to_owned()),
   1822             ("nc", nc_value.to_owned()),
   1823             ("session", session.clone()),
   1824         ];
   1825 
   1826         if let PostType::Clean(date, text) = post_type {
   1827             if let Err(e) = delete_message(&client, full_url, &mut params, date, text) {
   1828                 log::error!("failed to delete message: {:?}", e);
   1829                 return Ok(RetryErr::Retry);
   1830             }
   1831             return Ok(RetryErr::Exit);
   1832         }
   1833 
   1834         let mut req = client.post(full_url);
   1835         let mut form: Option<multipart::Form> = None;
   1836 
   1837         match post_type {
   1838             PostType::Post(msg, send_to) => {
   1839                 should_reset_keepalive_timer = true;
   1840                 params.extend(vec![
   1841                     ("action", "post".to_owned()),
   1842                     ("postid", postid_value.to_owned()),
   1843                     ("multi", "on".to_owned()),
   1844                     ("message", msg),
   1845                     ("sendto", send_to.unwrap_or(SEND_TO_ALL.to_owned())),
   1846                 ]);
   1847             }
   1848             PostType::NewNickname(new_nickname) => {
   1849                 set_profile_base_info(client, full_url, &mut params)?;
   1850                 params.extend(vec![
   1851                     ("do", "save".to_owned()),
   1852                     ("timestamps", "on".to_owned()),
   1853                     ("newnickname", new_nickname),
   1854                 ]);
   1855             }
   1856             PostType::NewColor(new_color) => {
   1857                 set_profile_base_info(client, full_url, &mut params)?;
   1858                 params.extend(vec![
   1859                     ("do", "save".to_owned()),
   1860                     ("timestamps", "on".to_owned()),
   1861                     ("colour", new_color),
   1862                 ]);
   1863             }
   1864             PostType::Ignore(username) => {
   1865                 set_profile_base_info(client, full_url, &mut params)?;
   1866                 params.extend(vec![
   1867                     ("do", "save".to_owned()),
   1868                     ("timestamps", "on".to_owned()),
   1869                     ("ignore", username),
   1870                 ]);
   1871             }
   1872             PostType::Unignore(username) => {
   1873                 set_profile_base_info(client, full_url, &mut params)?;
   1874                 params.extend(vec![
   1875                     ("do", "save".to_owned()),
   1876                     ("timestamps", "on".to_owned()),
   1877                     ("unignore", username),
   1878                 ]);
   1879             }
   1880             PostType::Profile(new_color, new_nickname) => {
   1881                 set_profile_base_info(client, full_url, &mut params)?;
   1882                 params.extend(vec![
   1883                     ("do", "save".to_owned()),
   1884                     ("timestamps", "on".to_owned()),
   1885                     ("colour", new_color),
   1886                     ("newnickname", new_nickname),
   1887                 ]);
   1888             }
   1889             PostType::Kick(msg, send_to) => {
   1890                 params.extend(vec![
   1891                     ("action", "post".to_owned()),
   1892                     ("postid", postid_value.to_owned()),
   1893                     ("message", msg),
   1894                     ("sendto", send_to),
   1895                     ("kick", "kick".to_owned()),
   1896                     ("what", "purge".to_owned()),
   1897                 ]);
   1898             }
   1899             PostType::DeleteLast | PostType::DeleteAll => {
   1900                 params.extend(vec![("action", "delete".to_owned())]);
   1901                 if let PostType::DeleteAll = post_type {
   1902                     params.extend(vec![
   1903                         ("sendto", SEND_TO_ALL.to_owned()),
   1904                         ("confirm", "yes".to_owned()),
   1905                         ("what", "all".to_owned()),
   1906                     ]);
   1907                 } else {
   1908                     params.extend(vec![("sendto", "".to_owned()), ("what", "last".to_owned())]);
   1909                 }
   1910             }
   1911             PostType::Upload(file_path, send_to, msg) => {
   1912                 form = Some(
   1913                     match multipart::Form::new()
   1914                         .text("lang", LANG.to_owned())
   1915                         .text("nc", nc_value.to_owned())
   1916                         .text("session", session.clone())
   1917                         .text("action", "post".to_owned())
   1918                         .text("postid", postid_value.to_owned())
   1919                         .text("message", msg)
   1920                         .text("sendto", send_to.to_owned())
   1921                         .text("what", "purge".to_owned())
   1922                         .file("file", file_path)
   1923                     {
   1924                         Ok(f) => f,
   1925                         Err(e) => {
   1926                             log::error!("{:?}", e);
   1927                             return Ok(RetryErr::Exit);
   1928                         }
   1929                     },
   1930                 );
   1931             }
   1932             PostType::Clean(_, _) => {}
   1933         }
   1934 
   1935         if let Some(form_content) = form {
   1936             req = req.multipart(form_content);
   1937         } else {
   1938             req = req.form(&params);
   1939         }
   1940         if let Err(err) = req.send() {
   1941             log::error!("{:?}", err.to_string());
   1942             if err.is_timeout() {
   1943                 return Ok(RetryErr::Retry);
   1944             }
   1945         }
   1946         return Ok(RetryErr::Exit);
   1947     });
   1948     if should_reset_keepalive_timer {
   1949         last_post_tx.send(()).unwrap();
   1950     }
   1951 }
   1952 
   1953 fn parse_date(date: &str, datetime_fmt: &str) -> NaiveDateTime {
   1954     let now = Utc::now();
   1955     let date_fmt = format!("%Y-{}", datetime_fmt);
   1956     NaiveDateTime::parse_from_str(
   1957         format!("{}-{}", now.year(), date).as_str(),
   1958         date_fmt.as_str(),
   1959     )
   1960     .unwrap()
   1961 }
   1962 
   1963 fn get_msgs(
   1964     client: &Client,
   1965     base_url: &str,
   1966     page_php: &str,
   1967     session: &str,
   1968     username: &str,
   1969     users: &Arc<Mutex<Users>>,
   1970     sig: &Arc<Mutex<Sig>>,
   1971     messages_updated_tx: &crossbeam_channel::Sender<()>,
   1972     members_tag: &str,
   1973     datetime_fmt: &str,
   1974     messages: &Arc<Mutex<Vec<Message>>>,
   1975     should_notify: &mut bool,
   1976 ) -> anyhow::Result<()> {
   1977     let url = format!(
   1978         "{}/{}?action=view&session={}&lang={}",
   1979         base_url, page_php, session, LANG
   1980     );
   1981     let resp_text = client.get(url).send()?.text()?;
   1982     let resp_text = resp_text.replace("<br>", "\n");
   1983     let doc = Document::from(resp_text.as_str());
   1984     let new_messages = match extract_messages(&doc) {
   1985         Ok(messages) => messages,
   1986         Err(_) => {
   1987             // Failed to get messages, probably need re-login
   1988             sig.lock().unwrap().signal(&ExitSignal::NeedLogin);
   1989             return Ok(());
   1990         }
   1991     };
   1992     {
   1993         let messages = messages.lock().unwrap();
   1994         process_new_messages(
   1995             &new_messages,
   1996             &messages,
   1997             datetime_fmt,
   1998             members_tag,
   1999             username,
   2000             should_notify,
   2001         );
   2002         // Build messages vector. Tag deleted messages.
   2003         update_messages(new_messages, messages, datetime_fmt);
   2004         // Notify new messages has arrived.
   2005         // This ensure that we redraw the messages on the screen right away.
   2006         // Otherwise, the screen would not redraw until a keyboard event occurs.
   2007         messages_updated_tx.send(()).unwrap();
   2008     }
   2009     {
   2010         let mut users = users.lock().unwrap();
   2011         *users = extract_users(&doc);
   2012     }
   2013     Ok(())
   2014 }
   2015 
   2016 fn process_new_messages(
   2017     new_messages: &Vec<Message>,
   2018     messages: &MutexGuard<Vec<Message>>,
   2019     datetime_fmt: &str,
   2020     members_tag: &str,
   2021     username: &str,
   2022     should_notify: &mut bool,
   2023 ) {
   2024     if let Some(last_known_msg) = messages.first() {
   2025         let last_known_msg_parsed_dt = parse_date(&last_known_msg.date, datetime_fmt);
   2026         let filtered = new_messages.iter().filter(|new_msg| {
   2027             last_known_msg_parsed_dt <= parse_date(&new_msg.date, datetime_fmt)
   2028                 && !(new_msg.date == last_known_msg.date && last_known_msg.text == new_msg.text)
   2029         });
   2030         for new_msg in filtered {
   2031             if let Some((_, to_opt, msg)) = get_message(&new_msg.text, members_tag) {
   2032                 // Process new messages
   2033 
   2034                 // Notify when tagged
   2035                 if msg.contains(format!("@{}", &username).as_str()) {
   2036                     *should_notify = true;
   2037                 }
   2038                 // Notify when PM is received
   2039                 if let Some(to) = to_opt {
   2040                     if to == username && msg != "!up" {
   2041                         *should_notify = true;
   2042                     }
   2043                 }
   2044             }
   2045         }
   2046     }
   2047 }
   2048 
   2049 fn update_messages(
   2050     new_messages: Vec<Message>,
   2051     mut messages: MutexGuard<Vec<Message>>,
   2052     datetime_fmt: &str,
   2053 ) {
   2054     let mut old_msg_ptr = 0;
   2055     for new_msg in new_messages.into_iter() {
   2056         loop {
   2057             if let Some(old_msg) = messages.get_mut(old_msg_ptr) {
   2058                 let new_parsed_dt = parse_date(&new_msg.date, datetime_fmt);
   2059                 let parsed_dt = parse_date(&old_msg.date, datetime_fmt);
   2060                 if new_parsed_dt < parsed_dt {
   2061                     old_msg.deleted = true;
   2062                     old_msg_ptr += 1;
   2063                     continue;
   2064                 }
   2065                 if new_parsed_dt == parsed_dt {
   2066                     if old_msg.text != new_msg.text {
   2067                         let mut found = false;
   2068                         let mut x = 0;
   2069                         loop {
   2070                             x += 1;
   2071                             if let Some(old_msg) = messages.get(old_msg_ptr + x) {
   2072                                 let parsed_dt = parse_date(&old_msg.date, datetime_fmt);
   2073                                 if new_parsed_dt == parsed_dt {
   2074                                     if old_msg.text == new_msg.text {
   2075                                         found = true;
   2076                                         break;
   2077                                     }
   2078                                     continue;
   2079                                 }
   2080                             }
   2081                             break;
   2082                         }
   2083                         if !found {
   2084                             messages.insert(old_msg_ptr, new_msg);
   2085                             old_msg_ptr += 1;
   2086                         }
   2087                     }
   2088                     old_msg_ptr += 1;
   2089                     break;
   2090                 }
   2091             }
   2092             messages.insert(old_msg_ptr, new_msg);
   2093             old_msg_ptr += 1;
   2094             break;
   2095         }
   2096     }
   2097     messages.truncate(1000);
   2098 }
   2099 
   2100 fn delete_message(
   2101     client: &Client,
   2102     full_url: &str,
   2103     params: &mut Vec<(&str, String)>,
   2104     date: String,
   2105     text: String,
   2106 ) -> anyhow::Result<()> {
   2107     params.extend(vec![
   2108         ("action", "admin".to_owned()),
   2109         ("do", "clean".to_owned()),
   2110         ("what", "choose".to_owned()),
   2111     ]);
   2112     let clean_resp_txt = client.post(full_url).form(&params).send()?.text()?;
   2113     let doc = Document::from(clean_resp_txt.as_str());
   2114     let nc = doc
   2115         .find(Attr("name", "nc"))
   2116         .next()
   2117         .context("nc not found")?;
   2118     let nc_value = nc.attr("value").context("nc value not found")?.to_owned();
   2119     let msgs = extract_messages(&doc)?;
   2120     if let Some(msg) = msgs
   2121         .iter()
   2122         .find(|m| m.date == date && m.text.text() == text)
   2123     {
   2124         let msg_id = msg.id.context("msg id not found")?;
   2125         params.extend(vec![
   2126             ("nc", nc_value.to_owned()),
   2127             ("what", "selected".to_owned()),
   2128             ("mid[]", format!("{}", msg_id)),
   2129         ]);
   2130         client.post(full_url).form(&params).send()?;
   2131     }
   2132     Ok(())
   2133 }
   2134 
   2135 impl ChatClient {
   2136     fn new(params: Params) -> Self {
   2137         // println!("session[2026] : {:?}",params.session);
   2138         let mut c = new_default_le_chat_php_client(params.clone());
   2139         c.config.url = params.url.unwrap_or(
   2140             "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion/index.php"
   2141                 .to_owned(),
   2142         );
   2143         c.config.page_php = params.page_php.unwrap_or("chat.php".to_owned());
   2144         c.config.datetime_fmt = params.datetime_fmt.unwrap_or("%m-%d %H:%M:%S".to_owned());
   2145         c.config.members_tag = params.members_tag.unwrap_or("[M] ".to_owned());
   2146         c.config.keepalive_send_to = params.keepalive_send_to.unwrap_or("0".to_owned());
   2147         // c.session = params.session;
   2148         Self {
   2149             le_chat_php_client: c,
   2150         }
   2151     }
   2152 
   2153     fn run_forever(&mut self) {
   2154         self.le_chat_php_client.run_forever();
   2155     }
   2156 }
   2157 
   2158 fn new_default_le_chat_php_client(params: Params) -> LeChatPHPClient {
   2159     let (color_tx, color_rx) = crossbeam_channel::unbounded();
   2160     let (tx, rx) = crossbeam_channel::unbounded();
   2161     let session = params.session.clone();
   2162     // println!("session[2050] : {:?}",params.session);
   2163     LeChatPHPClient {
   2164         base_client: BaseClient {
   2165             username: params.username,
   2166             password: params.password,
   2167         },
   2168         max_login_retry: params.max_login_retry,
   2169         guest_color: params.guest_color,
   2170         // session: params.session,
   2171         session,
   2172         last_key_event: None,
   2173         client: params.client,
   2174         manual_captcha: params.manual_captcha,
   2175         sxiv: params.sxiv,
   2176         refresh_rate: params.refresh_rate,
   2177         config: LeChatPHPConfig::new_black_hat_chat_config(),
   2178         is_muted: Arc::new(Mutex::new(false)),
   2179         show_sys: false,
   2180         display_guest_view: false,
   2181         display_member_view: false,
   2182         display_hidden_msgs: false,
   2183         tx,
   2184         rx: Arc::new(Mutex::new(rx)),
   2185         color_tx,
   2186         color_rx: Arc::new(Mutex::new(color_rx)),
   2187     }
   2188 }
   2189 
   2190 struct ChatClient {
   2191     le_chat_php_client: LeChatPHPClient,
   2192 }
   2193 
   2194 #[derive(Debug, Clone)]
   2195 struct Params {
   2196     url: Option<String>,
   2197     page_php: Option<String>,
   2198     datetime_fmt: Option<String>,
   2199     members_tag: Option<String>,
   2200     username: String,
   2201     password: String,
   2202     guest_color: String,
   2203     client: Client,
   2204     manual_captcha: bool,
   2205     sxiv: bool,
   2206     refresh_rate: u64,
   2207     max_login_retry: isize,
   2208     keepalive_send_to: Option<String>,
   2209     session: Option<String>,
   2210     log_level: String,
   2211 }
   2212 
   2213 #[derive(Clone)]
   2214 enum ExitSignal {
   2215     Terminate,
   2216     NeedLogin,
   2217 }
   2218 struct Sig {
   2219     tx: crossbeam_channel::Sender<ExitSignal>,
   2220     rx: crossbeam_channel::Receiver<ExitSignal>,
   2221     nb_rx: usize,
   2222 }
   2223 
   2224 impl Sig {
   2225     fn new() -> Self {
   2226         let (tx, rx) = crossbeam_channel::unbounded();
   2227         let nb_rx = 0;
   2228         Self { tx, rx, nb_rx }
   2229     }
   2230 
   2231     fn clone(&mut self) -> crossbeam_channel::Receiver<ExitSignal> {
   2232         self.nb_rx += 1;
   2233         self.rx.clone()
   2234     }
   2235 
   2236     fn signal(&self, signal: &ExitSignal) {
   2237         for _ in 0..self.nb_rx {
   2238             self.tx.send(signal.clone()).unwrap();
   2239         }
   2240     }
   2241 }
   2242 
   2243 fn trim_newline(s: &mut String) {
   2244     if s.ends_with('\n') {
   2245         s.pop();
   2246         if s.ends_with('\r') {
   2247             s.pop();
   2248         }
   2249     }
   2250 }
   2251 
   2252 fn get_guest_color(wanted: Option<String>) -> String {
   2253     match wanted.as_deref() {
   2254         Some("beige") => "F5F5DC",
   2255         Some("blue-violet") => "8A2BE2",
   2256         Some("brown") => "A52A2A",
   2257         Some("cyan") => "00FFFF",
   2258         Some("sky-blue") => "00BFFF",
   2259         Some("gold") => "FFD700",
   2260         Some("gray") => "808080",
   2261         Some("green") => "008000",
   2262         Some("hot-pink") => "FF69B4",
   2263         Some("light-blue") => "ADD8E6",
   2264         Some("light-green") => "90EE90",
   2265         Some("lime-green") => "32CD32",
   2266         Some("magenta") => "FF00FF",
   2267         Some("olive") => "808000",
   2268         Some("orange") => "FFA500",
   2269         Some("orange-red") => "FF4500",
   2270         Some("red") => "FF0000",
   2271         Some("royal-blue") => "4169E1",
   2272         Some("see-green") => "2E8B57",
   2273         Some("sienna") => "A0522D",
   2274         Some("silver") => "C0C0C0",
   2275         Some("tan") => "D2B48C",
   2276         Some("teal") => "008080",
   2277         Some("violet") => "EE82EE",
   2278         Some("white") => "FFFFFF",
   2279         Some("yellow") => "FFFF00",
   2280         Some("yellow-green") => "9ACD32",
   2281         Some(other) => COLOR1_RGX
   2282             .captures(other)
   2283             .map_or("", |captures| captures.get(1).map_or("", |m| m.as_str())),
   2284         None => "",
   2285     }
   2286     .to_owned()
   2287 }
   2288 
   2289 fn get_tor_client(socks_proxy_url: &str, no_proxy: bool) -> Client {
   2290     let ua = "Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0";
   2291     let mut builder = reqwest::blocking::ClientBuilder::new()
   2292         .redirect(Policy::none())
   2293         .cookie_store(true)
   2294         .user_agent(ua);
   2295     if !no_proxy {
   2296         let proxy = reqwest::Proxy::all(socks_proxy_url).unwrap();
   2297         builder = builder.proxy(proxy);
   2298     }
   2299     builder.build().unwrap()
   2300 }
   2301 
   2302 fn ask_username(username: Option<String>) -> String {
   2303     username.unwrap_or_else(|| {
   2304         print!("username: ");
   2305         let mut username_input = String::new();
   2306         io::stdout().flush().unwrap();
   2307         io::stdin().read_line(&mut username_input).unwrap();
   2308         trim_newline(&mut username_input);
   2309         username_input
   2310     })
   2311 }
   2312 
   2313 fn ask_password(password: Option<String>) -> String {
   2314     password.unwrap_or_else(|| rpassword::prompt_password("Password: ").unwrap())
   2315 }
   2316 
   2317 #[derive(Debug, Deserialize, Clone)]
   2318 struct Commands {
   2319     commands: HashMap<String, String>,
   2320 }
   2321 
   2322 impl Default for Commands {
   2323     fn default() -> Self {
   2324         Commands {
   2325             commands: HashMap::new(), // Initialize commands with empty HashMap
   2326         }
   2327     }
   2328 }
   2329 
   2330 fn parse_params() -> anyhow::Result<Params> {
   2331     let opts = Opts::parse();
   2332     let cfg = confy::load::<MyConfig>("bhcli", None).unwrap_or_default();
   2333 
   2334     let profile = cfg.profiles.get(&opts.profile);
   2335 
   2336     let username = opts
   2337         .username
   2338         .clone()
   2339         .or_else(|| profile.map(|p| p.username.clone()));
   2340     let password = opts
   2341         .password
   2342         .clone()
   2343         .or_else(|| profile.map(|p| p.password.clone()));
   2344 
   2345     let log_level = opts
   2346         .log_level
   2347         .clone()
   2348         .or(Some(cfg.log_level.level))
   2349         .unwrap_or_else(|| "error".to_string());
   2350 
   2351     let client = get_tor_client(&opts.socks_proxy_url, opts.no_proxy);
   2352     let guest_color = get_guest_color(opts.guest_color);
   2353     let final_username = ask_username(username);
   2354     let final_password = ask_password(password);
   2355 
   2356     Ok(Params {
   2357         url: opts.url,
   2358         page_php: opts.page_php,
   2359         datetime_fmt: opts.datetime_fmt,
   2360         members_tag: opts.members_tag,
   2361         username: final_username,
   2362         password: final_password,
   2363         guest_color,
   2364         client,
   2365         manual_captcha: opts.manual_captcha,
   2366         sxiv: opts.sxiv,
   2367         refresh_rate: opts.refresh_rate,
   2368         max_login_retry: opts.max_login_retry,
   2369         keepalive_send_to: opts.keepalive_send_to,
   2370         session: opts.session,
   2371         log_level, // now just a String
   2372     })
   2373 }
   2374 
   2375 fn setup_logging(level: &str) -> anyhow::Result<()> {
   2376     let level_filter = match level.to_lowercase().as_str() {
   2377         "off" => LevelFilter::Off,
   2378         "error" => LevelFilter::Error,
   2379         "warn" => LevelFilter::Warn,
   2380         "info" => LevelFilter::Info,
   2381         "debug" => LevelFilter::Debug,
   2382         "trace" => LevelFilter::Trace,
   2383         _ => LevelFilter::Error,
   2384     };
   2385 
   2386     let logfile = FileAppender::builder()
   2387         .encoder(Box::new(PatternEncoder::new("{d} {l} {t} - {m}{n}")))
   2388         .build("bhcli.log")?;
   2389 
   2390     let config = log4rs::config::Config::builder()
   2391         .appender(log4rs::config::Appender::builder().build("logfile", Box::new(logfile)))
   2392         .build(
   2393             log4rs::config::Root::builder()
   2394                 .appender("logfile")
   2395                 .build(level_filter),
   2396         )?;
   2397 
   2398     log4rs::init_config(config)?;
   2399     Ok(())
   2400 }
   2401 
   2402 #[tokio::main]
   2403 async fn main() -> anyhow::Result<()> {
   2404     let params = parse_params()?;
   2405     setup_logging(&params.log_level)?;
   2406     ChatClient::new(params).run_forever();
   2407     Ok(())
   2408 }
   2409 
   2410 #[derive(Debug, Clone)]
   2411 enum PostType {
   2412     Post(String, Option<String>),   // Message, SendTo
   2413     Kick(String, String),           // Message, Username
   2414     Upload(String, String, String), // FilePath, SendTo, Message
   2415     DeleteLast,                     // DeleteLast
   2416     DeleteAll,                      // DeleteAll
   2417     NewNickname(String),            // NewUsername
   2418     NewColor(String),               // NewColor
   2419     Profile(String, String),        // NewColor, NewUsername
   2420     Ignore(String),                 // Username
   2421     Unignore(String),               // Username
   2422     Clean(String, String),          // Clean message
   2423 }
   2424 
   2425 // Get username of other user (or ours if it's the only one)
   2426 fn get_username(own_username: &str, root: &StyledText, members_tag: &str) -> Option<String> {
   2427     match get_message(root, members_tag) {
   2428         Some((from, Some(to), _)) => {
   2429             if from == own_username {
   2430                 return Some(to);
   2431             }
   2432             return Some(from);
   2433         }
   2434         Some((from, None, _)) => {
   2435             return Some(from);
   2436         }
   2437         _ => return None,
   2438     }
   2439 }
   2440 
   2441 // Extract "from"/"to"/"message content" from a "StyledText"
   2442 fn get_message(root: &StyledText, members_tag: &str) -> Option<(String, Option<String>, String)> {
   2443     if let StyledText::Styled(_, children) = root {
   2444         let msg = children.get(0)?.text();
   2445         match children.get(children.len() - 1)? {
   2446             StyledText::Styled(_, children) => {
   2447                 let from = match children.get(children.len() - 1)? {
   2448                     StyledText::Text(t) => t.to_owned(),
   2449                     _ => return None,
   2450                 };
   2451                 return Some((from, None, msg));
   2452             }
   2453             StyledText::Text(t) => {
   2454                 if t == &members_tag {
   2455                     let from = match children.get(children.len() - 2)? {
   2456                         StyledText::Styled(_, children) => {
   2457                             match children.get(children.len() - 1)? {
   2458                                 StyledText::Text(t) => t.to_owned(),
   2459                                 _ => return None,
   2460                             }
   2461                         }
   2462                         _ => return None,
   2463                     };
   2464                     return Some((from, None, msg));
   2465                 } else if t == "[" {
   2466                     let from = match children.get(children.len() - 2)? {
   2467                         StyledText::Styled(_, children) => {
   2468                             match children.get(children.len() - 1)? {
   2469                                 StyledText::Text(t) => t.to_owned(),
   2470                                 _ => return None,
   2471                             }
   2472                         }
   2473                         _ => return None,
   2474                     };
   2475                     let to = match children.get(2)? {
   2476                         StyledText::Styled(_, children) => {
   2477                             match children.get(children.len() - 1)? {
   2478                                 StyledText::Text(t) => Some(t.to_owned()),
   2479                                 _ => return None,
   2480                             }
   2481                         }
   2482                         _ => return None,
   2483                     };
   2484                     return Some((from, to, msg));
   2485                 }
   2486             }
   2487             _ => return None,
   2488         }
   2489     }
   2490     return None;
   2491 }
   2492 
   2493 #[derive(Debug, PartialEq, Clone)]
   2494 enum MessageType {
   2495     UserMsg,
   2496     SysMsg,
   2497 }
   2498 
   2499 #[derive(Debug, PartialEq, Clone)]
   2500 struct Message {
   2501     id: Option<usize>,
   2502     typ: MessageType,
   2503     date: String,
   2504     upload_link: Option<String>,
   2505     text: StyledText,
   2506     deleted: bool, // Either or not a message was deleted on the chat
   2507     hide: bool,    // Either ot not to hide a specific message
   2508 }
   2509 
   2510 impl Message {
   2511     fn new(
   2512         id: Option<usize>,
   2513         typ: MessageType,
   2514         date: String,
   2515         upload_link: Option<String>,
   2516         text: StyledText,
   2517     ) -> Self {
   2518         Self {
   2519             id,
   2520             typ,
   2521             date,
   2522             upload_link,
   2523             text,
   2524             deleted: false,
   2525             hide: false,
   2526         }
   2527     }
   2528 }
   2529 
   2530 #[derive(Debug, PartialEq, Clone)]
   2531 enum StyledText {
   2532     Styled(tuiColor, Vec<StyledText>),
   2533     Text(String),
   2534     None,
   2535 }
   2536 
   2537 impl StyledText {
   2538     fn walk<F>(&self, mut clb: F)
   2539     where
   2540         F: FnMut(&StyledText),
   2541     {
   2542         let mut v: Vec<&StyledText> = vec![self];
   2543         loop {
   2544             if let Some(e) = v.pop() {
   2545                 clb(e);
   2546                 if let StyledText::Styled(_, children) = e {
   2547                     v.extend(children);
   2548                 }
   2549                 continue;
   2550             }
   2551             break;
   2552         }
   2553     }
   2554 
   2555     fn text(&self) -> String {
   2556         let mut s = String::new();
   2557         self.walk(|n| {
   2558             if let StyledText::Text(t) = n {
   2559                 s += t;
   2560             }
   2561         });
   2562         s
   2563     }
   2564 
   2565     // Return a vector of each text parts & what color it should be
   2566     fn colored_text(&self) -> Vec<(tuiColor, String)> {
   2567         let mut out: Vec<(tuiColor, String)> = vec![];
   2568         let mut v: Vec<(tuiColor, &StyledText)> = vec![(tuiColor::White, self)];
   2569         loop {
   2570             if let Some((el_color, e)) = v.pop() {
   2571                 match e {
   2572                     StyledText::Styled(tui_color, children) => {
   2573                         for child in children {
   2574                             v.push((*tui_color, child));
   2575                         }
   2576                     }
   2577                     StyledText::Text(t) => {
   2578                         out.push((el_color, t.to_owned()));
   2579                     }
   2580                     StyledText::None => {}
   2581                 }
   2582                 continue;
   2583             }
   2584             break;
   2585         }
   2586         out
   2587     }
   2588 }
   2589 
   2590 fn parse_color(color_str: &str) -> tuiColor {
   2591     let mut color = tuiColor::White;
   2592     if color_str == "red" {
   2593         return tuiColor::Red;
   2594     }
   2595     if let Ok(rgb) = Rgb::from_hex_str(color_str) {
   2596         color = tuiColor::Rgb(
   2597             rgb.get_red() as u8,
   2598             rgb.get_green() as u8,
   2599             rgb.get_blue() as u8,
   2600         );
   2601     }
   2602     color
   2603 }
   2604 
   2605 fn process_node(e: select::node::Node, mut color: tuiColor) -> (StyledText, Option<String>) {
   2606     match e.data() {
   2607         select::node::Data::Element(_, _) => {
   2608             let mut upload_link: Option<String> = None;
   2609             match e.name() {
   2610                 Some("span") => {
   2611                     if let Some(style) = e.attr("style") {
   2612                         if let Some(captures) = COLOR_RGX.captures(style) {
   2613                             let color_match = captures.get(1).unwrap().as_str();
   2614                             color = parse_color(color_match);
   2615                         }
   2616                     }
   2617                 }
   2618                 Some("font") => {
   2619                     if let Some(color_str) = e.attr("color") {
   2620                         color = parse_color(color_str);
   2621                     }
   2622                 }
   2623                 Some("a") => {
   2624                     color = tuiColor::White;
   2625                     if let (Some("attachement"), Some(href)) = (e.attr("class"), e.attr("href")) {
   2626                         upload_link = Some(href.to_owned());
   2627                     }
   2628                 }
   2629                 Some("style") => {
   2630                     return (StyledText::None, None);
   2631                 }
   2632                 _ => {}
   2633             }
   2634             let mut children_texts: Vec<StyledText> = vec![];
   2635             let children = e.children();
   2636             for child in children {
   2637                 let (st, ul) = process_node(child, color);
   2638                 if ul.is_some() {
   2639                     upload_link = ul;
   2640                 }
   2641                 children_texts.push(st);
   2642             }
   2643             children_texts.reverse();
   2644             (StyledText::Styled(color, children_texts), upload_link)
   2645         }
   2646         select::node::Data::Text(t) => (StyledText::Text(t.to_string()), None),
   2647         select::node::Data::Comment(_) => (StyledText::None, None),
   2648     }
   2649 }
   2650 
   2651 struct Users {
   2652     admin: Vec<(tuiColor, String)>,
   2653     staff: Vec<(tuiColor, String)>,
   2654     members: Vec<(tuiColor, String)>,
   2655     guests: Vec<(tuiColor, String)>,
   2656 }
   2657 
   2658 impl Default for Users {
   2659     fn default() -> Self {
   2660         Self {
   2661             admin: Default::default(),
   2662             staff: Default::default(),
   2663             members: Default::default(),
   2664             guests: Default::default(),
   2665         }
   2666     }
   2667 }
   2668 
   2669 impl Users {
   2670     fn all(&self) -> Vec<&(tuiColor, String)> {
   2671         let mut out = Vec::new();
   2672         out.extend(&self.admin);
   2673         out.extend(&self.staff);
   2674         out.extend(&self.members);
   2675         out.extend(&self.guests);
   2676         out
   2677     }
   2678 
   2679     // fn is_guest(&self, name: &str) -> bool {
   2680     //     self.guests.iter().find(|(_, username)| username == name).is_some()
   2681     // }
   2682 }
   2683 
   2684 fn extract_users(doc: &Document) -> Users {
   2685     let mut users = Users::default();
   2686 
   2687     if let Some(chatters) = doc.find(Attr("id", "chatters")).next() {
   2688         if let Some(tr) = chatters.find(Name("tr")).next() {
   2689             let mut th_count = 0;
   2690             for e in tr.children() {
   2691                 if let select::node::Data::Element(_, _) = e.data() {
   2692                     if e.name() == Some("th") {
   2693                         th_count += 1;
   2694                         continue;
   2695                     }
   2696                     for user_span in e.find(Name("span")) {
   2697                         if let Some(user_style) = user_span.attr("style") {
   2698                             if let Some(captures) = COLOR_RGX.captures(user_style) {
   2699                                 if let Some(color_match) = captures.get(1) {
   2700                                     let color = color_match.as_str().to_owned();
   2701                                     let tui_color = parse_color(&color);
   2702                                     let username = user_span.text();
   2703                                     match th_count {
   2704                                         1 => users.admin.push((tui_color, username)),
   2705                                         2 => users.staff.push((tui_color, username)),
   2706                                         3 => users.members.push((tui_color, username)),
   2707                                         4 => users.guests.push((tui_color, username)),
   2708                                         _ => {}
   2709                                     }
   2710                                 }
   2711                             }
   2712                         }
   2713                     }
   2714                 }
   2715             }
   2716         }
   2717     }
   2718     users
   2719 }
   2720 
   2721 fn remove_suffix<'a>(s: &'a str, suffix: &str) -> &'a str {
   2722     s.strip_suffix(suffix).unwrap_or(s)
   2723 }
   2724 
   2725 fn remove_prefix<'a>(s: &'a str, prefix: &str) -> &'a str {
   2726     s.strip_prefix(prefix).unwrap_or(s)
   2727 }
   2728 
   2729 fn extract_messages(doc: &Document) -> anyhow::Result<Vec<Message>> {
   2730     let msgs = doc
   2731         .find(Attr("id", "messages"))
   2732         .next()
   2733         .ok_or(anyhow!("failed to get messages div"))?
   2734         .find(Attr("class", "msg"))
   2735         .filter_map(|tag| {
   2736             let mut id: Option<usize> = None;
   2737             // if let Some(checkbox) = tag.find(Name("input")).next() {
   2738             //     let id_value: usize = checkbox.attr("value").unwrap().parse().unwrap();
   2739             //     id = Some(id_value);
   2740             // }
   2741             if let Some(checkbox) = tag.find(Name("input")).next() {
   2742                 if let Some(val) = checkbox.attr("value") {
   2743                     if let Ok(id_value) = val.parse::<usize>() {
   2744                         id = Some(id_value);
   2745                     }
   2746                 }
   2747             }
   2748             if let Some(date_node) = tag.find(Name("small")).next() {
   2749                 if let Some(msg_span) = tag.find(Name("span")).next() {
   2750                     let date = remove_suffix(&date_node.text(), " - ").to_owned();
   2751                     let typ = match msg_span.attr("class") {
   2752                         Some("usermsg") => MessageType::UserMsg,
   2753                         Some("sysmsg") => MessageType::SysMsg,
   2754                         _ => return None,
   2755                     };
   2756                     let (text, upload_link) = process_node(msg_span, tuiColor::White);
   2757                     return Some(Message::new(id, typ, date, upload_link, text));
   2758                 }
   2759             }
   2760             None
   2761         })
   2762         .collect::<Vec<_>>();
   2763     Ok(msgs)
   2764 }
   2765 
   2766 fn draw_terminal_frame(
   2767     f: &mut Frame<CrosstermBackend<io::Stdout>>,
   2768     app: &mut App,
   2769     messages: &Arc<Mutex<Vec<Message>>>,
   2770     users: &Arc<Mutex<Users>>,
   2771     username: &str,
   2772 ) {
   2773     if app.long_message.is_none() {
   2774         let hchunks = Layout::default()
   2775             .direction(Direction::Horizontal)
   2776             .constraints([Constraint::Min(1), Constraint::Length(25)].as_ref())
   2777             .split(f.size());
   2778 
   2779         {
   2780             let chunks = Layout::default()
   2781                 .direction(Direction::Vertical)
   2782                 .constraints(
   2783                     [
   2784                         Constraint::Length(1),
   2785                         Constraint::Length(3),
   2786                         Constraint::Min(1),
   2787                     ]
   2788                     .as_ref(),
   2789                 )
   2790                 .split(hchunks[0]);
   2791 
   2792             render_help_txt(f, app, chunks[0], username);
   2793             render_textbox(f, app, chunks[1]);
   2794             render_messages(f, app, chunks[2], messages);
   2795             render_users(f, hchunks[1], users);
   2796         }
   2797     } else {
   2798         let hchunks = Layout::default()
   2799             .direction(Direction::Horizontal)
   2800             .constraints([Constraint::Min(1)])
   2801             .split(f.size());
   2802         {
   2803             render_long_message(f, app, hchunks[0]);
   2804         }
   2805     }
   2806 }
   2807 
   2808 fn gen_lines(msg_txt: &StyledText, w: usize, line_prefix: &str) -> Vec<Vec<(tuiColor, String)>> {
   2809     let txt = msg_txt.text();
   2810     let wrapped = textwrap::fill(&txt, w);
   2811     let splits = wrapped.split("\n").collect::<Vec<&str>>();
   2812     let mut new_lines: Vec<Vec<(tuiColor, String)>> = Vec::new();
   2813     let mut ctxt = msg_txt.colored_text();
   2814     ctxt.reverse();
   2815     let mut ptr = 0;
   2816     let mut split_idx = 0;
   2817     let mut line: Vec<(tuiColor, String)> = Vec::new();
   2818     let mut first_in_line = true;
   2819     loop {
   2820         if let Some((color, mut txt)) = ctxt.pop() {
   2821             txt = txt.replace("\n", "");
   2822             if let Some(split) = splits.get(split_idx) {
   2823                 if let Some(chr) = txt.chars().next() {
   2824                     if chr == ' ' && first_in_line {
   2825                         let skipped: String = txt.chars().skip(1).collect();
   2826                         txt = skipped;
   2827                     }
   2828                 }
   2829 
   2830                 let remain = split.len() - ptr;
   2831                 if txt.len() <= remain {
   2832                     ptr += txt.len();
   2833                     line.push((color, txt));
   2834                     first_in_line = false;
   2835                 } else {
   2836                     //line.push((color, txt[0..remain].to_owned()));
   2837                     if let Some(valid_slice) = txt.get(0..remain) {
   2838                         line.push((color, valid_slice.to_owned()));
   2839                     } else {
   2840                         let valid_remain = txt
   2841                             .char_indices()
   2842                             .take_while(|&(i, _)| i < remain)
   2843                             .last()
   2844                             .map(|(i, _)| i)
   2845                             .unwrap_or(txt.len());
   2846 
   2847                         line.push((color, txt[..valid_remain].to_owned()));
   2848                     }
   2849 
   2850                     new_lines.push(line.clone());
   2851                     line.clear();
   2852                     line.push((tuiColor::White, line_prefix.to_owned()));
   2853                     //ctxt.push((color, txt[(remain)..].to_owned()));
   2854                     if let Some(valid_slice) = txt.get(remain..) {
   2855                         ctxt.push((color, valid_slice.to_owned()));
   2856                     } else {
   2857                         let valid_remain = txt
   2858                             .char_indices()
   2859                             .skip_while(|&(i, _)| i < remain) // Find first valid boundary after remain
   2860                             .map(|(i, _)| i)
   2861                             .next()
   2862                             .unwrap_or(txt.len());
   2863 
   2864                         ctxt.push((color, txt[valid_remain..].to_owned()));
   2865                     }
   2866 
   2867                     ptr = 0;
   2868                     split_idx += 1;
   2869                     first_in_line = true;
   2870                 }
   2871             }
   2872         } else {
   2873             new_lines.push(line);
   2874             break;
   2875         }
   2876     }
   2877     new_lines
   2878 }
   2879 
   2880 fn render_long_message(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
   2881     if let Some(m) = &app.long_message {
   2882         let new_lines = gen_lines(&m.text, (r.width - 2) as usize, "");
   2883 
   2884         let mut rows = vec![];
   2885         for line in new_lines.into_iter() {
   2886             let spans_vec: Vec<Span> = line
   2887                 .into_iter()
   2888                 .map(|(color, txt)| Span::styled(txt, Style::default().fg(color)))
   2889                 .collect();
   2890             rows.push(Spans::from(spans_vec));
   2891         }
   2892 
   2893         let messages_list_items = vec![ListItem::new(rows)];
   2894 
   2895         let messages_list = List::new(messages_list_items)
   2896             .block(Block::default().borders(Borders::ALL).title(""))
   2897             .highlight_style(
   2898                 Style::default()
   2899                     .bg(tuiColor::Rgb(50, 50, 50))
   2900                     .add_modifier(Modifier::BOLD),
   2901             );
   2902 
   2903         f.render_widget(messages_list, r);
   2904     }
   2905 }
   2906 
   2907 fn render_help_txt(
   2908     f: &mut Frame<CrosstermBackend<io::Stdout>>,
   2909     app: &mut App,
   2910     r: Rect,
   2911     curr_user: &str,
   2912 ) {
   2913     let (mut msg, style) = match app.input_mode {
   2914         InputMode::Normal => (
   2915             vec![
   2916                 Span::raw("Press "),
   2917                 Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
   2918                 Span::raw(" to exit, "),
   2919                 Span::styled("Q", Style::default().add_modifier(Modifier::BOLD)),
   2920                 Span::raw(" to logout, "),
   2921                 Span::styled("i", Style::default().add_modifier(Modifier::BOLD)),
   2922                 Span::raw(" to start editing."),
   2923             ],
   2924             Style::default(),
   2925         ),
   2926         InputMode::Editing | InputMode::EditingErr => (
   2927             vec![
   2928                 Span::raw("Press "),
   2929                 Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
   2930                 Span::raw(" to stop editing, "),
   2931                 Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
   2932                 Span::raw(" to record the message"),
   2933             ],
   2934             Style::default(),
   2935         ),
   2936         InputMode::LongMessage => (vec![], Style::default()),
   2937     };
   2938     msg.extend(vec![Span::raw(format!(" | {}", curr_user))]);
   2939     if app.is_muted {
   2940         let fg = tuiColor::Red;
   2941         let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   2942         msg.extend(vec![Span::raw(" | "), Span::styled("muted", style)]);
   2943     } else {
   2944         let fg = tuiColor::LightGreen;
   2945         let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   2946         msg.extend(vec![Span::raw(" | "), Span::styled("not muted", style)]);
   2947     }
   2948 
   2949     //Strange
   2950     if app.display_guest_view {
   2951         let fg = tuiColor::LightGreen;
   2952         let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   2953         msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]);
   2954     } else {
   2955         let fg = tuiColor::Gray;
   2956         let style = Style::default().fg(fg);
   2957         msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]);
   2958     }
   2959 
   2960     //Strange
   2961     if app.display_member_view {
   2962         let fg = tuiColor::LightGreen;
   2963         let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   2964         msg.extend(vec![Span::raw(" | "), Span::styled("M", style)]);
   2965     } else {
   2966         let fg = tuiColor::Gray;
   2967         let style = Style::default().fg(fg);
   2968         msg.extend(vec![Span::raw(" | "), Span::styled("M", style)]);
   2969     }
   2970 
   2971     if app.display_hidden_msgs {
   2972         let fg = tuiColor::LightGreen;
   2973         let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   2974         msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]);
   2975     } else {
   2976         let fg = tuiColor::Gray;
   2977         let style = Style::default().fg(fg);
   2978         msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]);
   2979     }
   2980     let mut text = Text::from(Spans::from(msg));
   2981     text.patch_style(style);
   2982     let help_message = Paragraph::new(text);
   2983     f.render_widget(help_message, r);
   2984 }
   2985 
   2986 fn render_textbox(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
   2987     let w = (r.width - 3) as usize;
   2988     let str = app.input.clone();
   2989     let mut input_str = str.as_str();
   2990     let mut overflow = 0;
   2991     if app.input_idx >= w {
   2992         overflow = std::cmp::max(app.input.width() - w, 0);
   2993         input_str = &str[overflow..];
   2994     }
   2995     let input = Paragraph::new(input_str)
   2996         .style(match app.input_mode {
   2997             InputMode::LongMessage => Style::default(),
   2998             InputMode::Normal => Style::default(),
   2999             InputMode::Editing => Style::default().fg(tuiColor::Yellow),
   3000             InputMode::EditingErr => Style::default().fg(tuiColor::Red),
   3001         })
   3002         .block(Block::default().borders(Borders::ALL).title("Input"));
   3003     f.render_widget(input, r);
   3004     match app.input_mode {
   3005         InputMode::LongMessage => {}
   3006         InputMode::Normal =>
   3007             // Hide the cursor. `Frame` does this by default, so we don't need to do anything here
   3008             {}
   3009 
   3010         InputMode::Editing | InputMode::EditingErr => {
   3011             // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
   3012             f.set_cursor(
   3013                 // Put cursor past the end of the input text
   3014                 r.x + app.input_idx as u16 - overflow as u16 + 1,
   3015                 // Move one line down, from the border to the input line
   3016                 r.y + 1,
   3017             )
   3018         }
   3019     }
   3020 }
   3021 
   3022 fn render_messages(
   3023     f: &mut Frame<CrosstermBackend<io::Stdout>>,
   3024     app: &mut App,
   3025     r: Rect,
   3026     messages: &Arc<Mutex<Vec<Message>>>,
   3027 ) {
   3028     // Messages
   3029     app.items.items.clear();
   3030     let messages = messages.lock().unwrap();
   3031     let messages_list_items: Vec<ListItem> = messages
   3032         .iter()
   3033         .filter_map(|m| {
   3034             if !app.display_hidden_msgs && m.hide {
   3035                 return None;
   3036             }
   3037             // Simulate a guest view (remove "PMs" and "Members chat" messages)
   3038             if app.display_guest_view {
   3039                 // TODO: this is not efficient at all
   3040                 let text = m.text.text();
   3041                 if text.starts_with(&app.members_tag) || text.starts_with(&app.staffs_tag) {
   3042                     return None;
   3043                 }
   3044                 if let Some((_, Some(_), _)) = get_message(&m.text, &app.members_tag) {
   3045                     return None;
   3046                 }
   3047             }
   3048 
   3049             // Strange
   3050             // Display only messages from members and staff
   3051             if app.display_member_view {
   3052                 // In members mode, include only messages from members and staff
   3053                 let text = m.text.text();
   3054                 if !text.starts_with(&app.members_tag) && !text.starts_with(&app.staffs_tag) {
   3055                     return None;
   3056                 }
   3057                 if let Some((_, Some(_), _)) = get_message(&m.text, &app.members_tag) {
   3058                     return None;
   3059                 }
   3060             }
   3061 
   3062             if app.filter != "" {
   3063                 if !m
   3064                     .text
   3065                     .text()
   3066                     .to_lowercase()
   3067                     .contains(&app.filter.to_lowercase())
   3068                 {
   3069                     return None;
   3070                 }
   3071             }
   3072 
   3073             app.items.items.push(m.clone());
   3074 
   3075             let new_lines = gen_lines(&m.text, (r.width - 20) as usize, " ".repeat(17).as_str());
   3076 
   3077             let mut rows = vec![];
   3078             let date_style = match (m.deleted, m.hide) {
   3079                 (false, true) => Style::default().fg(tuiColor::Gray),
   3080                 (false, _) => Style::default().fg(tuiColor::DarkGray),
   3081                 (true, _) => Style::default().fg(tuiColor::Red),
   3082             };
   3083             let mut spans_vec = vec![Span::styled(m.date.clone(), date_style)];
   3084             let show_sys_sep = app.show_sys && m.typ == MessageType::SysMsg;
   3085             let sep = if show_sys_sep { " * " } else { " - " };
   3086             spans_vec.push(Span::raw(sep));
   3087             for (idx, line) in new_lines.into_iter().enumerate() {
   3088                 // Spams can take your whole screen, so we limit to 5 lines.
   3089                 if idx >= 5 {
   3090                     spans_vec.push(Span::styled(
   3091                         "                 […]",
   3092                         Style::default().fg(tuiColor::White),
   3093                     ));
   3094                     rows.push(Spans::from(spans_vec));
   3095                     break;
   3096                 }
   3097                 for (color, txt) in line {
   3098                     spans_vec.push(Span::styled(txt, Style::default().fg(color)));
   3099                 }
   3100                 rows.push(Spans::from(spans_vec.clone()));
   3101                 spans_vec.clear();
   3102             }
   3103 
   3104             let style = match (m.deleted, m.hide) {
   3105                 (true, _) => Style::default().bg(tuiColor::Rgb(30, 0, 0)),
   3106                 (_, true) => Style::default().bg(tuiColor::Rgb(20, 20, 20)),
   3107                 _ => Style::default(),
   3108             };
   3109             Some(ListItem::new(rows).style(style))
   3110         })
   3111         .collect();
   3112 
   3113     let messages_list = List::new(messages_list_items)
   3114         .block(Block::default().borders(Borders::ALL).title("Messages"))
   3115         .highlight_style(
   3116             Style::default()
   3117                 .bg(tuiColor::Rgb(50, 50, 50))
   3118                 .add_modifier(Modifier::BOLD),
   3119         );
   3120     f.render_stateful_widget(messages_list, r, &mut app.items.state)
   3121 }
   3122 
   3123 fn render_users(f: &mut Frame<CrosstermBackend<io::Stdout>>, r: Rect, users: &Arc<Mutex<Users>>) {
   3124     // Users lists
   3125     let users = users.lock().unwrap();
   3126     let mut users_list: Vec<ListItem> = vec![];
   3127     let mut users_types: Vec<(&Vec<(tuiColor, String)>, &str)> = Vec::new();
   3128     users_types.push((&users.admin, "-- Admin --"));
   3129     users_types.push((&users.staff, "-- Staff --"));
   3130     users_types.push((&users.members, "-- Members --"));
   3131     users_types.push((&users.guests, "-- Guests --"));
   3132     for (users, label) in users_types.into_iter() {
   3133         users_list.push(ListItem::new(Span::raw(label)));
   3134         for (tui_color, username) in users.iter() {
   3135             let span = Span::styled(username, Style::default().fg(*tui_color));
   3136             users_list.push(ListItem::new(span));
   3137         }
   3138     }
   3139     let users = List::new(users_list).block(Block::default().borders(Borders::ALL).title("Users"));
   3140     f.render_widget(users, r);
   3141 }
   3142 
   3143 fn random_string(n: usize) -> String {
   3144     let s: Vec<u8> = thread_rng().sample_iter(&Alphanumeric).take(n).collect();
   3145     std::str::from_utf8(&s).unwrap().to_owned()
   3146 }
   3147 
   3148 #[derive(PartialEq, Clone)]
   3149 enum InputMode {
   3150     LongMessage,
   3151     Normal,
   3152     Editing,
   3153     EditingErr,
   3154 }
   3155 
   3156 /// App holds the state of the application
   3157 #[derive(Clone)]
   3158 struct App {
   3159     /// Current value of the input box
   3160     input: String,
   3161     input_idx: usize,
   3162     /// Current input mode
   3163     input_mode: InputMode,
   3164     is_muted: bool,
   3165     show_sys: bool,
   3166     display_guest_view: bool,
   3167     display_member_view: bool,
   3168     display_hidden_msgs: bool,
   3169     items: StatefulList<Message>,
   3170     filter: String,
   3171     members_tag: String,
   3172     staffs_tag: String,
   3173     long_message: Option<Message>,
   3174     commands: Commands,
   3175 }
   3176 
   3177 impl Default for App {
   3178     fn default() -> App {
   3179         App {
   3180             input: String::new(),
   3181             input_idx: 0,
   3182             input_mode: InputMode::Normal,
   3183             is_muted: false,
   3184             show_sys: false,
   3185             display_guest_view: false,
   3186             display_member_view: false,
   3187             display_hidden_msgs: false,
   3188             items: StatefulList::new(),
   3189             filter: "".to_owned(),
   3190             members_tag: "".to_owned(),
   3191             staffs_tag: "".to_owned(),
   3192             long_message: None,
   3193             commands: Commands::default(),
   3194         }
   3195     }
   3196 }
   3197 
   3198 impl App {
   3199     fn update_filter(&mut self) {
   3200         if let Some(captures) = FIND_RGX.captures(&self.input) {
   3201             // Find
   3202             self.filter = captures.get(1).map_or("", |m| m.as_str()).to_owned();
   3203         }
   3204     }
   3205 
   3206     fn clear_filter(&mut self) {
   3207         if FIND_RGX.is_match(&self.input) {
   3208             self.filter = "".to_owned();
   3209             self.input = "".to_owned();
   3210             self.input_idx = 0;
   3211         }
   3212     }
   3213 }
   3214 
   3215 pub enum Event<I> {
   3216     Input(I),
   3217     Tick,
   3218     Terminate,
   3219     NeedLogin,
   3220 }
   3221 
   3222 /// A small event handler that wrap termion input and tick events. Each event
   3223 /// type is handled in its own thread and returned to a common `Receiver`
   3224 struct Events {
   3225     messages_updated_rx: crossbeam_channel::Receiver<()>,
   3226     exit_rx: crossbeam_channel::Receiver<ExitSignal>,
   3227     rx: crossbeam_channel::Receiver<Event<CEvent>>,
   3228 }
   3229 
   3230 #[derive(Debug, Clone)]
   3231 struct Config {
   3232     pub exit_rx: crossbeam_channel::Receiver<ExitSignal>,
   3233     pub messages_updated_rx: crossbeam_channel::Receiver<()>,
   3234     pub tick_rate: Duration,
   3235 }
   3236 
   3237 impl Events {
   3238     fn with_config(config: Config) -> (Events, thread::JoinHandle<()>) {
   3239         let (tx, rx) = crossbeam_channel::unbounded();
   3240         let tick_rate = config.tick_rate;
   3241         let exit_rx = config.exit_rx;
   3242         let messages_updated_rx = config.messages_updated_rx;
   3243         let exit_rx1 = exit_rx.clone();
   3244         let thread_handle = thread::spawn(move || {
   3245             let mut last_tick = Instant::now();
   3246             loop {
   3247                 // poll for tick rate duration, if no events, sent tick event.
   3248                 let timeout = tick_rate
   3249                     .checked_sub(last_tick.elapsed())
   3250                     .unwrap_or_else(|| Duration::from_secs(0));
   3251                 if event::poll(timeout).unwrap() {
   3252                     let evt = event::read().unwrap();
   3253                     match evt {
   3254                         CEvent::FocusGained => {}
   3255                         CEvent::FocusLost => {}
   3256                         CEvent::Paste(_) => {}
   3257                         CEvent::Resize(_, _) => tx.send(Event::Input(evt)).unwrap(),
   3258                         CEvent::Key(_) => tx.send(Event::Input(evt)).unwrap(),
   3259                         CEvent::Mouse(mouse_event) => {
   3260                             match mouse_event.kind {
   3261                                 MouseEventKind::ScrollDown
   3262                                 | MouseEventKind::ScrollUp
   3263                                 | MouseEventKind::Down(_) => {
   3264                                     tx.send(Event::Input(evt)).unwrap();
   3265                                 }
   3266                                 _ => {}
   3267                             };
   3268                         }
   3269                     };
   3270                 }
   3271                 if last_tick.elapsed() >= tick_rate {
   3272                     select! {
   3273                         recv(&exit_rx1) -> _ => break,
   3274                         default => {},
   3275                     }
   3276                     last_tick = Instant::now();
   3277                 }
   3278             }
   3279         });
   3280         (
   3281             Events {
   3282                 rx,
   3283                 exit_rx,
   3284                 messages_updated_rx,
   3285             },
   3286             thread_handle,
   3287         )
   3288     }
   3289 
   3290     fn next(&self) -> Result<Event<CEvent>, crossbeam_channel::RecvError> {
   3291         select! {
   3292             recv(&self.rx) -> evt => evt,
   3293             recv(&self.messages_updated_rx) -> _ => Ok(Event::Tick),
   3294             recv(&self.exit_rx) -> v => match v {
   3295                 Ok(ExitSignal::Terminate) => Ok(Event::Terminate),
   3296                 Ok(ExitSignal::NeedLogin) => Ok(Event::NeedLogin),
   3297                 Err(_) => Ok(Event::Terminate),
   3298             },
   3299         }
   3300     }
   3301 }
   3302 
   3303 #[cfg(test)]
   3304 mod tests {
   3305     use super::*;
   3306 
   3307     #[test]
   3308     fn gen_lines_test() {
   3309         let txt = StyledText::Styled(
   3310             tuiColor::White,
   3311             vec![
   3312                 StyledText::Styled(
   3313                     tuiColor::Rgb(255, 255, 255),
   3314                     vec![
   3315                         StyledText::Text(" prmdbba pwuv💓".to_owned()),
   3316                         StyledText::Styled(
   3317                             tuiColor::Rgb(255, 255, 255),
   3318                             vec![StyledText::Styled(
   3319                                 tuiColor::Rgb(0, 255, 0),
   3320                                 vec![StyledText::Text("PMW".to_owned())],
   3321                             )],
   3322                         ),
   3323                         StyledText::Styled(
   3324                             tuiColor::Rgb(255, 255, 255),
   3325                             vec![StyledText::Styled(
   3326                                 tuiColor::Rgb(255, 255, 255),
   3327                                 vec![StyledText::Text("A".to_owned())],
   3328                             )],
   3329                         ),
   3330                         StyledText::Styled(
   3331                             tuiColor::Rgb(255, 255, 255),
   3332                             vec![StyledText::Styled(
   3333                                 tuiColor::Rgb(0, 255, 0),
   3334                                 vec![StyledText::Text("XOS".to_owned())],
   3335                             )],
   3336                         ),
   3337                         StyledText::Text(
   3338                             "pqb a mavx pkj fhsoeycg oruzb asd lk ruyaq re lheot mbnrw ".to_owned(),
   3339                         ),
   3340                     ],
   3341                 ),
   3342                 StyledText::Text(" - ".to_owned()),
   3343                 StyledText::Styled(
   3344                     tuiColor::Rgb(255, 255, 255),
   3345                     vec![StyledText::Text("rytxvgs".to_owned())],
   3346                 ),
   3347             ],
   3348         );
   3349         let lines = gen_lines(&txt, 71, "");
   3350         assert_eq!(lines.len(), 2);
   3351     }
   3352 }