use std::{
collections::VecDeque,
error::Error,
io::{self, BufWriter, IsTerminal, Write},
sync::{
mpsc::{self, Sender},
LazyLock,
},
thread,
time::SystemTime,
};
use terminal_size::{terminal_size, Height};
struct Console {
history_len: usize,
event_log: VecDeque<(SystemTime, String)>,
}
impl Console {
fn push_event(&mut self, description: String) {
self.event_log.truncate(self.history_len - 1);
self.event_log.push_front((SystemTime::now(), description));
}
fn new(history_len: usize) -> Self {
Self {
history_len,
event_log: VecDeque::new(),
}
}
fn render(&self, to: &mut impl Write, start_time: SystemTime, state: &str) -> io::Result<()> {
let mut lines: Option<usize> = terminal_size().map(|(_, Height(h))| h.into());
to.write_all(b"\x1B[2J\x1B[H")?;
if let Some(lines) = &mut lines {
let remaining = lines.saturating_sub(state.lines().count());
if remaining > 0 {
to.write_all(state.as_bytes())?;
*lines = remaining;
} else {
writeln!(to, "Terminal height is too low! Can't show server state.").unwrap();
*lines = lines.saturating_sub(2); }
}
let lines = lines.unwrap_or(usize::MAX);
let mut iter = self.event_log.iter().take(lines).rev().peekable();
while let Some((time, desc)) = iter.next() {
if let Ok(duration) = time.duration_since(start_time) {
write!(to, "\x1B[90m{duration:<9.3?}:\x1B[0m ").unwrap();
}
to.write_all(desc.as_bytes())?;
if iter.peek().is_some() {
to.write_all(b"\n")?;
}
}
Ok(())
}
}
enum ConsoleUpdate {
State(String),
Log(String),
}
static CONSOLE: LazyLock<Sender<ConsoleUpdate>> = LazyLock::new(|| {
let start_time = SystemTime::now();
let (tx, rx) = mpsc::channel();
if let Err(e) = thread::Builder::new()
.name(format!("{}::console_ui_thread", module_path!()))
.spawn(move || {
let mut stdout = BufWriter::new(io::stdout().lock());
let mut console = Console::new(1024);
let mut state = String::new();
while let Ok(update) = rx.recv() {
match update {
ConsoleUpdate::State(s) => state = s,
ConsoleUpdate::Log(description) => console.push_event(description),
}
console
.render(&mut stdout, start_time, &state)
.and_then(|()| stdout.flush())
.unwrap_or_else(|e| panic!("Can't render UI to console!\n{e}"));
}
})
{
panic!("Can't spawn UI thread!\n{e}");
}
tx
});
#[must_use]
pub fn is_terminal() -> bool {
io::stdout().is_terminal()
}
pub fn log_string(event: &str, style: &str) {
if is_terminal() {
for line in event.split('\n') {
CONSOLE
.send(ConsoleUpdate::Log(format!("\x1B{style}{line}\x1B[0m")))
.expect("Console UI thread disconnected");
}
} else {
println!("{event}");
}
}
pub fn update_state(state: String) {
if is_terminal() {
CONSOLE
.send(ConsoleUpdate::State(state))
.expect("Console UI thread disconnected");
}
}
pub fn log_error(mut error: &dyn Error) {
log_string(&format!("{error}"), "[31m");
while let Some(source) = error.source() {
log_string(&format!("Caused by: {source}"), "[35m");
error = source;
}
}
macro_rules! debug {
($($arg:tt)*) => {{
if std::env::var("SERVER_VERBOSE").is_ok() {
crate::console::log_string(&format!($($arg)*), "[90m");
}
}};
}
macro_rules! error {
($err:expr, $($arg:tt)*) => {{
crate::console::log_string(&format!($($arg)*), "[33m");
crate::console::log_error($err);
}};
($err:expr) => {{
crate::console::log_error($err);
}}
}
macro_rules! log {
($($arg:tt)*) => {{
crate::console::log_string(&format!($($arg)*), "[0m");
}};
}
macro_rules! warning {
($($arg:tt)*) => {{
crate::console::log_string(&format!($($arg)*), "[33m");
}};
}
pub(crate) use {debug, error, log, warning};