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