libsurfer/
logs.rs
1use std::{borrow::Cow, sync::Mutex};
2
3use color_eyre::Result;
4use ecolor::Color32;
5use egui::{self, RichText, TextWrapMode};
6use egui_extras::{Column, TableBuilder, TableRow};
7use log::{Level, Log, Record};
8
9use crate::{message::Message, SystemState};
10
11pub static EGUI_LOGGER: EguiLogger = EguiLogger {
12 records: Mutex::new(vec![]),
13};
14
15#[macro_export]
16macro_rules! try_log_error {
17 ($expr:expr, $what:expr $(,)?) => {
18 if let Err(e) = $expr {
19 error!("{}: {}", $what, e)
20 }
21 };
22}
23
24#[derive(Clone)]
25pub struct LogMessage<'a> {
26 pub msg: Cow<'a, str>,
27 pub level: Level,
28}
29
30pub struct EguiLogger<'a> {
31 records: Mutex<Vec<LogMessage<'a>>>,
32}
33
34impl EguiLogger<'_> {
35 pub fn records(&self) -> Vec<LogMessage<'_>> {
36 self.records
37 .lock()
38 .expect("Failed to lock logger. Thread poisoned?")
39 .to_vec()
40 }
41}
42
43impl Log for EguiLogger<'_> {
44 fn enabled(&self, _metadata: &log::Metadata) -> bool {
45 true
46 }
47
48 fn log(&self, record: &Record) {
49 self.records
50 .lock()
51 .expect("Failed to lock logger. Thread poisoned?")
52 .push(LogMessage {
53 msg: format!("{}", record.args()).into(),
54 level: record.level(),
55 });
56 }
57
58 fn flush(&self) {}
59}
60
61impl SystemState {
62 pub fn draw_log_window(&self, ctx: &egui::Context, msgs: &mut Vec<Message>) {
63 let mut open = true;
64 egui::Window::new("Logs")
65 .open(&mut open)
66 .collapsible(true)
67 .resizable(true)
68 .show(ctx, |ui| {
69 ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
70
71 egui::ScrollArea::new([true, false]).show(ui, |ui| {
72 TableBuilder::new(ui)
73 .column(Column::auto().resizable(true))
74 .column(Column::remainder())
75 .vscroll(true)
76 .stick_to_bottom(true)
77 .header(20.0, |mut header| {
78 header.col(|ui| {
79 ui.heading("Level");
80 });
81 header.col(|ui| {
82 ui.heading("Message");
83 });
84 })
85 .body(|body| {
86 let records = EGUI_LOGGER.records();
87 let heights = records
88 .iter()
89 .map(|record| {
90 let height = record.msg.lines().count() as f32;
91
92 height * 15.
93 })
94 .collect::<Vec<_>>();
95
96 body.heterogeneous_rows(heights.into_iter(), |mut row: TableRow| {
97 let record = &records[row.index()];
98 row.col(|ui| {
99 let (color, text) = match record.level {
100 log::Level::Error => (Color32::RED, "Error"),
101 log::Level::Warn => (Color32::YELLOW, "Warn"),
102 log::Level::Info => (Color32::GREEN, "Info"),
103 log::Level::Debug => (Color32::BLUE, "Debug"),
104 log::Level::Trace => (Color32::GRAY, "Trace"),
105 };
106
107 ui.colored_label(color, text);
108 });
109 row.col(|ui| {
110 ui.label(RichText::new(record.msg.clone()).monospace());
111 });
112 });
113 });
114 })
115 });
116 if !open {
117 msgs.push(Message::SetLogsVisible(false));
118 }
119 }
120}
121
122pub fn setup_logging(platform_logger: fern::Dispatch) -> Result<()> {
123 let egui_log_config = fern::Dispatch::new()
124 .level(log::LevelFilter::Info)
125 .level_for("surfer", log::LevelFilter::Trace)
126 .format(move |out, message, _record| out.finish(format_args!(" {message}")))
127 .chain(&EGUI_LOGGER as &(dyn log::Log + 'static));
128
129 fern::Dispatch::new()
130 .chain(platform_logger)
131 .chain(egui_log_config)
132 .apply()?;
133 Ok(())
134}
135
136#[cfg(not(target_arch = "wasm32"))]
138pub fn start_logging() -> Result<()> {
139 let colors = fern::colors::ColoredLevelConfig::new()
140 .error(fern::colors::Color::Red)
141 .warn(fern::colors::Color::Yellow)
142 .info(fern::colors::Color::Green)
143 .debug(fern::colors::Color::Blue)
144 .trace(fern::colors::Color::White);
145
146 let stdout_config = fern::Dispatch::new()
147 .level(log::LevelFilter::Info)
148 .level_for("surfer", log::LevelFilter::Trace)
149 .format(move |out, message, record| {
150 out.finish(format_args!(
151 "[{}] {}",
152 colors.color(record.level()),
153 message
154 ));
155 })
156 .chain(std::io::stdout());
157 setup_logging(stdout_config)?;
158
159 color_eyre::install()?;
160 Ok(())
161}