3 changed files with 418 additions and 375 deletions
@ -0,0 +1,376 @@ |
|||||
|
use super::{Close, LoggingFrontend, Stdin, Stdout}; |
||||
|
use std::sync::mpsc; |
||||
|
|
||||
|
use std::io::Write; |
||||
|
|
||||
|
use crossterm::event::MouseEventKind; |
||||
|
use crossterm::{ |
||||
|
cursor::{MoveTo, MoveToNextLine}, |
||||
|
event::{Event, KeyCode, KeyModifiers}, |
||||
|
queue, |
||||
|
style::Print, |
||||
|
terminal::{Clear, ClearType}, |
||||
|
}; |
||||
|
|
||||
|
use fern::Dispatch; |
||||
|
use fern::colors::{Color, ColoredLevelConfig}; |
||||
|
|
||||
|
mod color_message; |
||||
|
use color_message::print_line; |
||||
|
|
||||
|
use crate::allocator::{self, Allocator}; |
||||
|
use crate::logger::simple_formatter; |
||||
|
|
||||
|
pub(super) struct ANSIFrontend; |
||||
|
|
||||
|
impl LoggingFrontend for ANSIFrontend { |
||||
|
fn setup_with_handler<F: Fn(String) -> String + Send + 'static>( |
||||
|
properties: &crate::server_properties::ServerProperties, |
||||
|
handler: F, |
||||
|
logger_filter: Dispatch |
||||
|
) -> anyhow::Result<(Close, Stdin<String>)> { |
||||
|
let (stdout, stdin, close, join_handle) = |
||||
|
TerminalHandler::new(handler, 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); |
||||
|
|
||||
|
|
||||
|
logger_filter |
||||
|
.chain(stdout_logger) |
||||
|
.apply()?; |
||||
|
log::info!("Saving output to output.log"); |
||||
|
// std::process::exit(1);
|
||||
|
Ok(( |
||||
|
Close { |
||||
|
close, |
||||
|
handle: join_handle, |
||||
|
close_handler: Box::new(|| queue!(std::io::stdout(), crossterm::event::DisableMouseCapture).unwrap()) |
||||
|
}, |
||||
|
stdin, |
||||
|
)) |
||||
|
} |
||||
|
|
||||
|
fn cleanup(&self) { |
||||
|
cleanup() |
||||
|
} |
||||
|
|
||||
|
fn name() -> &'static str { |
||||
|
"ANSI" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fn cleanup() { |
||||
|
log::info!(target: "cleanup", "Exiting server"); |
||||
|
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"); |
||||
|
} |
||||
|
|
||||
|
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.info(Color::Green).debug(Color::BrightBlack); |
||||
|
d.format( |
||||
|
move |out: fern::FormatCallback, message: _, record: &log::Record| { |
||||
|
out.finish(format_args!( |
||||
|
"{colors_line}{}[{}{colors_line}][{:>9}][{}] {}\x1B[0m", |
||||
|
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), |
||||
|
colors_level.clone().color(record.level()), |
||||
|
allocator::as_string(Allocator::allocated()), |
||||
|
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(),
|
||||
|
)) |
||||
|
}, |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
struct TerminalHandler<T, F> |
||||
|
where |
||||
|
T: Send + 'static, |
||||
|
F: Fn(String) -> T + Send + 'static, |
||||
|
{ |
||||
|
stdout: mpsc::Receiver<String>, |
||||
|
stdin: mpsc::Sender<T>, |
||||
|
close: mpsc::Receiver<()>, |
||||
|
handler: F, |
||||
|
lines: LimitedVec<String>, |
||||
|
command: String, |
||||
|
use_colors: bool, |
||||
|
} |
||||
|
|
||||
|
impl<T, F> TerminalHandler<T, F> |
||||
|
where |
||||
|
T: Send + 'static, |
||||
|
F: Fn(String) -> T + Send + 'static, |
||||
|
{ |
||||
|
#[allow(clippy::new_ret_no_self)] |
||||
|
fn new( |
||||
|
handler: F, |
||||
|
use_colors: bool, |
||||
|
) -> ( |
||||
|
Stdout, |
||||
|
Stdin<T>, |
||||
|
mpsc::Sender<()>, |
||||
|
tokio::task::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 = tokio::task::spawn_blocking(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(); |
||||
|
}); |
||||
|
std::panic::set_hook(Box::new(|info| { |
||||
|
let thread = std::thread::current(); |
||||
|
let name = thread.name().unwrap_or("<unnamed>"); |
||||
|
log::error!("thread '{}' {}", name, info); |
||||
|
log::info!("Press any key to exit"); |
||||
|
loop { |
||||
|
match crossterm::event::read() { |
||||
|
// Event::Resize(_cols, rows) => self.lines.size = rows as usize - 1,
|
||||
|
Ok(Event::Key(_)) => break, |
||||
|
Ok(_) => (), |
||||
|
Err(e) => { |
||||
|
log::error!( |
||||
|
"Errored while panicking, this is unrecoverable, exiting: {}", |
||||
|
e |
||||
|
); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
cleanup(); |
||||
|
std::process::exit(0) |
||||
|
})); |
||||
|
(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() { |
||||
|
updated = true; |
||||
|
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.kind { |
||||
|
MouseEventKind::ScrollUp => self.lines.scroll_up(), |
||||
|
MouseEventKind::ScrollDown => self.lines.scroll_down(), |
||||
|
_ => { |
||||
|
updated = false; |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
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 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
impl<T, F> Drop for TerminalHandler<T, F> |
||||
|
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") |
||||
|
} |
||||
|
|
||||
|
struct LimitedVec<T> { |
||||
|
inner: Vec<T>, |
||||
|
size: usize, |
||||
|
pos: usize, |
||||
|
} |
||||
|
|
||||
|
impl<T> LimitedVec<T> { |
||||
|
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<dyn Iterator<Item = String> + '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().saturating_sub(self.pos); |
||||
|
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; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue