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/// Starts the logging and error handling. Can be used by unittests to get more insights.
137#[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}