use std::sync::mpsc; use std::io::Write; use crossterm::{ cursor::{MoveTo, MoveToNextLine}, event::{Event, KeyCode, KeyModifiers, MouseEvent}, queue, style::Print, terminal::{Clear, ClearType}, }; use fern::colors::{Color, ColoredLevelConfig}; mod color_message; use color_message::print_line; // TODO: Cleanup pub fn setup( properties: &crate::server_properties::ServerProperties, ) -> Result<(Close, Stdin), fern::InitError> { let (stdout, stdin, close, join_handle) = TerminalHandler::new( |x| { if x == "sv_cheats" { log::info!("CHEATS ENABLED") } x }, properties.use_colors, ); // let simple_formatter = |out: fern::FormatCallback, message: _, record: &log::Record| { // out.finish(format_args!( // "{}[{}][{}] {}", // chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), // record.level(), // record.target(), // message, // )) // }; let stdout_logger = if properties.use_colors { colored_formatter(fern::Dispatch::new()) } else { simple_formatter(fern::Dispatch::new()) } .chain(stdout); let file = simple_formatter(fern::Dispatch::new()).chain( std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open("output.log")?, ); fern::Dispatch::new() .level(log::LevelFilter::Debug) .level_for("h2", log::LevelFilter::Info) .level_for("tokio::codec", log::LevelFilter::Info) .level_for("sqlx::query", log::LevelFilter::Info) .chain(stdout_logger) .chain(file) .apply()?; log::info!("Saving output to output.log"); // std::process::exit(1); Ok(( Close { close, handle: join_handle, }, stdin, )) } fn simple_formatter(d: fern::Dispatch) -> fern::Dispatch { d.format( |out: fern::FormatCallback, message: _, record: &log::Record| { out.finish(format_args!( "{}[{}][{}] {}", chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), record.level(), record.target(), message, )) }, ) } fn colored_formatter(d: fern::Dispatch) -> fern::Dispatch { let colors_line = ColoredLevelConfig::new() .error(Color::Red) .warn(Color::Yellow) // we actually don't need to specify the color for debug and info, they are white by default .info(Color::White) .debug(Color::BrightBlack) // depending on the terminals color scheme, this is the same as the background color .trace(Color::BrightBlack); let colors_level = colors_line .clone() .info(Color::Green) .debug(Color::BrightBlack); d.format( move |out: fern::FormatCallback, message: _, record: &log::Record| { out.finish(format_args!( "{colors_line}{}[{}{colors_line}][{}] {}\x1B[0m", chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), colors_level.clone().color(record.level()), record.target(), message, colors_line = format!( "\x1B[{}m", colors_line.clone().get_color(&record.level()).to_fg_str() ), // colors_level = colors_level.clone().get_color(&record.level()).to_fg_str(), )) }, ) } pub type Stdout = mpsc::Sender; pub type Stdin = mpsc::Receiver; struct TerminalHandler where T: Send + 'static, F: Fn(String) -> T + Send + 'static, { stdout: mpsc::Receiver, stdin: mpsc::Sender, close: mpsc::Receiver<()>, handler: F, lines: LimitedVec, command: String, use_colors: bool, } impl TerminalHandler where T: Send + 'static, F: Fn(String) -> T + Send + 'static, { fn new( handler: F, use_colors: bool, ) -> ( Stdout, Stdin, mpsc::Sender<()>, std::thread::JoinHandle<()>, ) { crossterm::terminal::enable_raw_mode().unwrap(); let mut stdout = std::io::stdout(); queue!(stdout, crossterm::event::EnableMouseCapture).unwrap(); let (stdout_send, stdout_recv) = mpsc::channel(); let (stdin_send, stdin_recv) = mpsc::channel(); let (close_send, close_recv) = mpsc::channel(); let join_handle = std::thread::spawn(move || { let mut s = Self { stdout: stdout_recv, stdin: stdin_send, close: close_recv, handler, lines: LimitedVec::new(crossterm::terminal::size().unwrap().1 as usize - 1), command: String::new(), use_colors, }; loop { if s.iterate() { break; } } cleanup(); }); (stdout_send, stdin_recv, close_send, join_handle) } fn iterate(&mut self) -> bool { let mut updated = false; match self.close.try_recv() { Err(mpsc::TryRecvError::Empty) => (), _ => return true, } while is_event_available() { match crossterm::event::read().unwrap() { Event::Resize(_cols, rows) => self.lines.size = rows as usize - 1, Event::Key(k) => match (k.code, k.modifiers) { (KeyCode::Char('c'), KeyModifiers::CONTROL) => { cleanup(); std::process::exit(0) } (k, _m) => match k { KeyCode::Char(c) => self.command.push(c), KeyCode::Backspace => { self.command.pop(); } KeyCode::Enter => { log::info!(target: "command", " >> {}", self.command); match self.stdin.send((self.handler)(self.command.clone())) { Ok(_) => (), Err(e) => log::error!("{}", e), }; self.command = String::new(); } KeyCode::Esc => { self.lines.scroll_to_bottom(); } _ => (), }, }, Event::Mouse(m) => match m { MouseEvent::ScrollUp(_, _, _) => self.lines.scroll_up(), MouseEvent::ScrollDown(_, _, _) => self.lines.scroll_down(), _ => (), }, } updated = true; } while let Some(x) = match self.stdout.try_recv() { Err(mpsc::TryRecvError::Empty) => None, Err(mpsc::TryRecvError::Disconnected) => return true, Ok(v) => Some(v), } { for line in x.split('\n') { if !line.is_empty() { self.lines.push(line.trim_end().to_string()); } } updated = true; } let mut stdout = std::io::stdout(); // println!("Got stdout"); if updated { queue!(stdout, Clear(ClearType::All), MoveTo(0, 0)).unwrap(); let mut i = 0; let size = crossterm::terminal::size().unwrap(); for v in self.lines.iter(self.use_colors) { print_line(&v, self.use_colors, &mut stdout); queue!(stdout, MoveToNextLine(1)).unwrap(); i += 1; if i >= size.1 as usize - 1 { break; } // println!("{}", v); } queue!( stdout, MoveTo(0, size.1), // Clear(ClearType::CurrentLine), Print(&format!(" >> {}", self.command)) ) .unwrap(); stdout.flush().unwrap(); } false } } fn cleanup() { let mut stdout = std::io::stdout(); queue!( stdout, Clear(ClearType::All), MoveTo(0, 0), crossterm::event::DisableMouseCapture ) .unwrap(); crossterm::terminal::disable_raw_mode().unwrap(); stdout.flush().unwrap(); println!("Exiting server, output has been saved to output.log"); } impl Drop for TerminalHandler where T: Send + 'static, F: Fn(String) -> T + Send + 'static, { fn drop(&mut self) { queue!(std::io::stdout(), crossterm::event::DisableMouseCapture).unwrap(); } } fn is_event_available() -> bool { crossterm::event::poll(std::time::Duration::from_millis(10)) .expect("Error polling for terminal events") } pub struct Close { close: mpsc::Sender<()>, handle: std::thread::JoinHandle<()>, } impl Close { pub fn close(self) { self.close.send(()).unwrap(); self.handle.join().unwrap(); println!("CLOSING"); queue!(std::io::stdout(), crossterm::event::DisableMouseCapture).unwrap(); } } struct LimitedVec { inner: Vec, size: usize, pos: usize, } impl LimitedVec { fn new(size: usize) -> Self { Self { inner: Vec::with_capacity(size), size, pos: 0, } } fn push(&mut self, v: T) { // while self.inner.len() >= self.size { // self.inner.remove(0); // } if self.pos != 0 { self.pos += 1; } self.inner.push(v); // assert!(self.inner.len() <= self.size) } fn iter<'a>(&'a self, use_colors: bool) -> Box + 'a> where T: std::fmt::Display, { let start = if self.inner.len() < self.size { 0 } else { self.inner .len() .checked_sub(self.size) .and_then(|x| x.checked_sub(self.pos)) .unwrap_or(0) }; let mut end = self.inner.len().checked_sub(self.pos).unwrap_or(0); if end < self.inner.len() { end = self.inner.len() } let iter = self.inner[start..end].iter(); let map = iter.enumerate().map(move |(i, x)| { format!( "{}{}", if use_colors { format!( "\x1B[{}m{:>5}\x1B[0m ", fern::colors::Color::BrightBlack.to_fg_str(), start + i ) } else { String::new() }, x ) }); Box::new(map) } fn scroll_up(&mut self) { if self.pos + self.size < self.inner.len() { // log::info!("SCROLLING UP"); self.pos += 1; } } fn scroll_down(&mut self) { if self.pos > 0 { // log::info!("SCROLLING DOWN"); self.pos -= 1; } } fn scroll_to_bottom(&mut self) { self.pos = 0; } }