1use std::{path::Path, sync::Mutex};
11
12use duat_core::{
13 Ns, cmd,
14 context::{self, Handle, Location, Record},
15 data::Pass,
16 hook::{self, FocusedOn, MsgLogged, OnMouseEvent, UnfocusedFrom},
17 mode::{self, MouseButton, TwoPointsPlace},
18 opts::PrintOpts,
19 text::{Point, Spawn, Text, TextMut, txt},
20 ui::{DynSpawnSpecs, Orientation, PushSpecs, PushTarget, Side, Widget},
21};
22
23use crate::widgets::Info;
24
25pub fn add_logbook_hooks() {
26 use duat_core::mode::MouseEventKind::*;
27 hook::add::<MsgLogged>(|pa, rec| {
28 let Some(logbook) = context::handle_of::<LogBook>(pa) else {
29 return;
30 };
31
32 let (lb, area) = logbook.write_with_area(pa);
33
34 let mut fmt_rec = |fmt: &mut dyn FnMut(Record) -> Option<Text>| {
35 if let Some(rec_text) = fmt(rec.clone()) {
36 lb.text.insert_text(lb.text.len(), &rec_text);
37 lb.location_ranges
38 .push((lb.text.last_point(), rec.location()));
39 }
40 };
41
42 let mut global_fmt = GLOBAL_FMT.lock().unwrap();
43
44 if let Some(fmt) = lb.fmt.as_mut() {
45 fmt_rec(fmt);
46 } else if let Some(fmt) = global_fmt.as_mut() {
47 fmt_rec(fmt);
48 } else {
49 fmt_rec(&mut default_fmt);
50 }
51
52 area.scroll_ver(&lb.text, i32::MAX, lb.print_opts());
53 });
54
55 hook::add::<FocusedOn<LogBook>>(|pa, (_, logbook)| logbook.area().reveal(pa).unwrap());
56
57 hook::add::<UnfocusedFrom<LogBook>>(|pa, (logbook, _)| {
58 if logbook.read(pa).close_on_unfocus {
59 logbook.area().hide(pa).unwrap()
60 }
61 });
62
63 let location_ns = Ns::new();
64
65 hook::add::<OnMouseEvent<LogBook>>(move |pa, event| match event.kind {
66 ScrollDown | ScrollUp => {
67 let (lb, area) = event.handle.write_with_area(pa);
68 let scroll = if let ScrollDown = event.kind { 3 } else { -3 };
69 area.scroll_ver(&lb.text, scroll, lb.print_opts());
70 }
71 Moved => {
72 let Some(TwoPointsPlace::Within(points)) = event.points else {
73 return;
74 };
75
76 let lb = event.handle.write(pa);
77 let (Ok(i) | Err(i)) = lb
78 .location_ranges
79 .binary_search_by(|(end, _)| end.cmp(&points.real));
80
81 if let Some((_, location)) = lb.location_ranges.get(i) {
82 let spawn = Spawn::new(
83 Info::new(txt!("[log_book.location]{location}")),
84 DynSpawnSpecs {
85 orientation: Orientation::VerLeftBelow,
86 ..DynSpawnSpecs::default()
87 },
88 );
89 lb.text.insert_tag(location_ns, points.real, spawn);
90 }
91 }
92 Down(MouseButton::Left) => {
93 let Some(TwoPointsPlace::Within(points)) = event.points else {
94 return;
95 };
96
97 let lb = event.handle.read(pa);
98 let (Ok(i) | Err(i)) = lb
99 .location_ranges
100 .binary_search_by(|(end, _)| end.cmp(&points.real));
101
102 if let Some((_, location)) = lb.location_ranges.get(i).cloned()
103 && cmd::call(pa, format!("edit {}", location.file())).is_ok()
104 {
105 let buffer = context::get_buffer_by_path(pa, Path::new(location.file())).unwrap();
106 mode::reset_to(pa, &buffer);
107 buffer.selections_mut(pa).remove_extras();
108 buffer.edit_main(pa, |mut c| {
109 c.move_to_coords(location.line() - 1, location.column() - 1);
110 });
111 }
112 }
113 _ => {}
114 });
115
116 hook::add::<OnMouseEvent>(move |pa, _| {
117 for logbook in context::windows().handles_of::<LogBook>(pa) {
118 logbook.text_mut(pa).remove_tags(location_ns, ..);
119 }
120 });
121}
122
123#[allow(clippy::type_complexity)]
124static GLOBAL_FMT: Mutex<Option<Box<dyn FnMut(Record) -> Option<Text> + Send>>> = Mutex::new(None);
125
126pub struct LogBook {
130 text: Text,
131 location_ranges: Vec<(Point, Location)>,
132 fmt: Option<Box<dyn FnMut(Record) -> Option<Text> + Send>>,
133 pub close_on_unfocus: bool,
136}
137
138impl LogBook {
139 pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
141 self.fmt = Some(Box::new(fmt))
142 }
143
144 pub fn builder() -> LogBookOpts {
146 LogBookOpts::default()
147 }
148}
149
150impl Widget for LogBook {
151 fn text(&self) -> &Text {
152 &self.text
153 }
154
155 fn text_mut(&mut self) -> TextMut<'_> {
156 self.text.as_mut()
157 }
158
159 fn print_opts(&self) -> PrintOpts {
160 let mut opts = PrintOpts::new();
161 opts.wrap_lines = true;
162 opts.wrap_on_word = true;
163 opts
164 }
165}
166
167#[derive(Clone, Copy)]
169pub struct LogBookOpts {
170 pub close_on_unfocus: bool,
172 pub hidden: bool,
174 pub side: Side,
176 pub height: f32,
179 pub width: f32,
182 pub show_source: bool,
189}
190
191impl LogBookOpts {
192 pub fn push_on(self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<LogBook> {
194 let logs = context::logs();
195 let records = logs.get(..).unwrap();
196
197 let mut global_fmt = GLOBAL_FMT.lock().unwrap();
198 let mut text = Text::new();
199 let mut path_ranges = Vec::new();
200
201 let fmt_recs = |fmt: &mut dyn FnMut(Record) -> Option<Text>| {
202 for rec in records.into_iter() {
203 if let Some(rec_text) = fmt(rec.clone()) {
204 text.insert_text(text.len(), &rec_text);
205 path_ranges.push((text.last_point(), rec.location()));
206 }
207 }
208 };
209
210 if let Some(fmt) = global_fmt.as_mut() {
211 fmt_recs(fmt);
212 } else {
213 fmt_recs(&mut default_fmt);
214 }
215
216 let log_book = LogBook {
217 text,
218 location_ranges: path_ranges,
219 fmt: None,
220 close_on_unfocus: self.close_on_unfocus,
221 };
222
223 let specs = match self.side {
224 Side::Right | Side::Left => PushSpecs {
225 side: self.side,
226 width: Some(self.width),
227 hidden: self.hidden,
228 cluster: false,
229 ..Default::default()
230 },
231 Side::Above | Side::Below => PushSpecs {
232 side: self.side,
233 height: Some(self.height),
234 hidden: self.hidden,
235 cluster: false,
236 ..Default::default()
237 },
238 };
239
240 push_target.push_outer(pa, log_book, specs)
241 }
242
243 pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
251 *GLOBAL_FMT.lock().unwrap() = Some(Box::new(fmt));
252 }
253}
254
255impl Default for LogBookOpts {
256 fn default() -> Self {
257 Self {
258 close_on_unfocus: true,
259 hidden: true,
260 side: Side::Below,
261 height: 8.0,
262 width: 50.0,
263 show_source: true,
264 }
265 }
266}
267
268fn default_fmt(rec: Record) -> Option<Text> {
269 use duat_core::context::Level::*;
270 let mut builder = Text::builder();
271
272 match rec.level() {
273 Error => builder.push(txt!("[log_book.error][[ERROR]][log_book.colon]: ")),
274 Warn => builder.push(txt!("[log_book.warn][[WARNING]][log_book.colon]:")),
275 Info => builder.push(txt!("[log_book.info][[INFO]][log_book.colon]: ")),
276 Debug => builder.push(txt!("[log_book.debug][[DEBUG]][log_book.colon]: ")),
277 Trace => unreachable!("Trace is not meant to be useable"),
278 };
279
280 builder.push(txt!(" {}", rec.text().clone(),));
281
282 builder.push('\n');
283
284 Some(builder.build())
285}