1use {
2 anyhow::{Context, Result},
3 flexi_logger::{
4 Criterion, DeferredNow, Duplicate, FileSpec, Logger, LoggerHandle,
5 Naming, Record, WriteMode,
6 },
7 regex::Regex,
8 std::{
9 fmt::Write as FmtWrite,
10 path::{Path, PathBuf},
11 sync::LazyLock,
12 },
13 textwrap::{termwidth, Options},
14};
15
16#[derive(Debug)]
17pub struct Location {
18 pub file: &'static str,
19 pub line: u32,
20 pub col: u32,
21}
22
23impl std::fmt::Display for Location {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 f.write_fmt(format_args!("{}:{}:[{}]", self.file, self.line, self.col))
26 }
27}
28
29pub trait ErrorLocationMessage {
30 fn location(self, location: Location, msg: impl AsRef<str>) -> Self;
31}
32
33impl<T> ErrorLocationMessage for Result<T> {
34 fn location(self, location: Location, msg: impl AsRef<str>) -> Self {
35 self.with_context(|| format!("{} - {}", location, msg.as_ref()))
36 }
37}
38
39static RELATIVE_ROOT_DIR: LazyLock<PathBuf> =
42 LazyLock::new(|| std::env::current_dir().unwrap());
43
44static LAST_NEWLINE_DELIM_MACHER: LazyLock<Regex> =
45 LazyLock::new(|| Regex::new(r"(│)(.*)$").unwrap());
46
47static LOGGER_HANDLE: LazyLock<LoggerHandle> = LazyLock::new(|| {
51 Logger::try_with_env_or_str(
52 "debug, tracing::span=off, winit=warn, sctk=warn",
53 )
54 .unwrap()
55 .log_to_stdout()
56 .duplicate_to_stdout(Duplicate::All)
57 .log_to_file(FileSpec::default().directory("logs"))
58 .rotate(
59 Criterion::AgeOrSize(flexi_logger::Age::Hour, 1024 * 1024 * 8),
60 Naming::Timestamps,
61 flexi_logger::Cleanup::KeepLogFiles(2),
62 )
63 .format(multiline_format)
64 .write_mode(WriteMode::Async)
65 .start()
66 .expect("Unable to start the logger!")
67});
68
69pub fn setup() {
71 LOGGER_HANDLE.flush();
72}
73
74fn multiline_format(
79 w: &mut dyn std::io::Write,
80 now: &mut DeferredNow,
81 record: &Record,
82) -> Result<(), std::io::Error> {
83 let size = termwidth().min(80);
84 let wrap_options = Options::new(size)
85 .initial_indent("╭ ")
86 .subsequent_indent("│ ");
87
88 let file = Path::new(record.file().unwrap());
89 let file_message = if let Ok(relative_path) =
90 file.strip_prefix(RELATIVE_ROOT_DIR.as_path())
91 {
92 relative_path.to_str().unwrap().to_owned()
93 } else {
94 file.to_str().unwrap().to_owned()
95 };
96
97 let mut full_line = String::new();
98 writeln!(
99 full_line,
100 "{} [{}] {}:{}",
101 record.level(),
102 now.now().format("%H:%M:%S%.3f"),
103 file_message,
104 record.line().unwrap_or(0),
105 )
106 .expect("unable to format first log line");
107
108 write!(&mut full_line, "{}", &record.args())
109 .expect("unable to format log!");
110
111 let wrapped = textwrap::fill(&full_line, wrap_options);
112 let formatted = LAST_NEWLINE_DELIM_MACHER.replace(&wrapped, "╰$2");
113
114 writeln!(w, "{formatted}")
115}