bhcli

A TUI for chatting on LE PHP Chats (onion)
git clone https://git.dasho.dev/n0tr1v/bhcli.git
Log | Files | Refs | README

main.rs (109868B)


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