use std::{path::Path, sync::Mutex};
use duat_core::{
Ns, cmd,
context::{self, Handle, Location, Record},
data::Pass,
hook::{self, FocusedOn, MsgLogged, OnMouseEvent, UnfocusedFrom},
mode::{self, MouseButton, TwoPointsPlace},
opts::PrintOpts,
text::{Point, Spawn, Text, TextMut, txt},
ui::{DynSpawnSpecs, Orientation, PushSpecs, PushTarget, Side, Widget},
};
use crate::widgets::Info;
pub fn add_logbook_hooks() {
use duat_core::mode::MouseEventKind::*;
hook::add::<MsgLogged>(|pa, rec| {
let Some(logbook) = context::handle_of::<LogBook>(pa) else {
return;
};
let (lb, area) = logbook.write_with_area(pa);
let mut fmt_rec = |fmt: &mut dyn FnMut(Record) -> Option<Text>| {
if let Some(rec_text) = fmt(rec.clone()) {
lb.text.insert_text(lb.text.len(), &rec_text);
lb.location_ranges
.push((lb.text.last_point(), rec.location()));
}
};
let mut global_fmt = GLOBAL_FMT.lock().unwrap();
if let Some(fmt) = lb.fmt.as_mut() {
fmt_rec(fmt);
} else if let Some(fmt) = global_fmt.as_mut() {
fmt_rec(fmt);
} else {
fmt_rec(&mut default_fmt);
}
area.scroll_ver(&lb.text, i32::MAX, lb.print_opts());
});
hook::add::<FocusedOn<LogBook>>(|pa, (_, logbook)| logbook.area().reveal(pa).unwrap());
hook::add::<UnfocusedFrom<LogBook>>(|pa, (logbook, _)| {
if logbook.read(pa).close_on_unfocus {
logbook.area().hide(pa).unwrap()
}
});
let location_ns = Ns::new();
hook::add::<OnMouseEvent<LogBook>>(move |pa, event| match event.kind {
ScrollDown | ScrollUp => {
let (lb, area) = event.handle.write_with_area(pa);
let scroll = if let ScrollDown = event.kind { 3 } else { -3 };
area.scroll_ver(&lb.text, scroll, lb.print_opts());
}
Moved => {
let Some(TwoPointsPlace::Within(points)) = event.points else {
return;
};
let lb = event.handle.write(pa);
let (Ok(i) | Err(i)) = lb
.location_ranges
.binary_search_by(|(end, _)| end.cmp(&points.real));
if let Some((_, location)) = lb.location_ranges.get(i) {
let spawn = Spawn::new(
Info::new(txt!("[log_book.location]{location}")),
DynSpawnSpecs {
orientation: Orientation::VerLeftBelow,
..DynSpawnSpecs::default()
},
);
lb.text.insert_tag(location_ns, points.real, spawn);
}
}
Down(MouseButton::Left) => {
let Some(TwoPointsPlace::Within(points)) = event.points else {
return;
};
let lb = event.handle.read(pa);
let (Ok(i) | Err(i)) = lb
.location_ranges
.binary_search_by(|(end, _)| end.cmp(&points.real));
if let Some((_, location)) = lb.location_ranges.get(i).cloned()
&& cmd::call(pa, format!("edit {}", location.file())).is_ok()
{
let buffer = context::get_buffer_by_path(pa, Path::new(location.file())).unwrap();
mode::reset_to(pa, &buffer);
buffer.selections_mut(pa).remove_extras();
buffer.edit_main(pa, |mut c| {
c.move_to_coords(location.line() - 1, location.column() - 1);
});
}
}
_ => {}
});
hook::add::<OnMouseEvent>(move |pa, _| {
for logbook in context::windows().handles_of::<LogBook>(pa) {
logbook.text_mut(pa).remove_tags(location_ns, ..);
}
});
}
#[allow(clippy::type_complexity)]
static GLOBAL_FMT: Mutex<Option<Box<dyn FnMut(Record) -> Option<Text> + Send>>> = Mutex::new(None);
pub struct LogBook {
text: Text,
location_ranges: Vec<(Point, Location)>,
fmt: Option<Box<dyn FnMut(Record) -> Option<Text> + Send>>,
pub close_on_unfocus: bool,
}
impl LogBook {
pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
self.fmt = Some(Box::new(fmt))
}
pub fn builder() -> LogBookOpts {
LogBookOpts::default()
}
}
impl Widget for LogBook {
fn text(&self) -> &Text {
&self.text
}
fn text_mut(&mut self) -> TextMut<'_> {
self.text.as_mut()
}
fn print_opts(&self) -> PrintOpts {
let mut opts = PrintOpts::new();
opts.wrap_lines = true;
opts.wrap_on_word = true;
opts
}
}
#[derive(Clone, Copy)]
pub struct LogBookOpts {
pub close_on_unfocus: bool,
pub hidden: bool,
pub side: Side,
pub height: f32,
pub width: f32,
pub show_source: bool,
}
impl LogBookOpts {
pub fn push_on(self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<LogBook> {
let logs = context::logs();
let records = logs.get(..).unwrap();
let mut global_fmt = GLOBAL_FMT.lock().unwrap();
let mut text = Text::new();
let mut path_ranges = Vec::new();
let fmt_recs = |fmt: &mut dyn FnMut(Record) -> Option<Text>| {
for rec in records.into_iter() {
if let Some(rec_text) = fmt(rec.clone()) {
text.insert_text(text.len(), &rec_text);
path_ranges.push((text.last_point(), rec.location()));
}
}
};
if let Some(fmt) = global_fmt.as_mut() {
fmt_recs(fmt);
} else {
fmt_recs(&mut default_fmt);
}
let log_book = LogBook {
text,
location_ranges: path_ranges,
fmt: None,
close_on_unfocus: self.close_on_unfocus,
};
let specs = match self.side {
Side::Right | Side::Left => PushSpecs {
side: self.side,
width: Some(self.width),
hidden: self.hidden,
cluster: false,
..Default::default()
},
Side::Above | Side::Below => PushSpecs {
side: self.side,
height: Some(self.height),
hidden: self.hidden,
cluster: false,
..Default::default()
},
};
push_target.push_outer(pa, log_book, specs)
}
pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
*GLOBAL_FMT.lock().unwrap() = Some(Box::new(fmt));
}
}
impl Default for LogBookOpts {
fn default() -> Self {
Self {
close_on_unfocus: true,
hidden: true,
side: Side::Below,
height: 8.0,
width: 50.0,
show_source: true,
}
}
}
fn default_fmt(rec: Record) -> Option<Text> {
use duat_core::context::Level::*;
let mut builder = Text::builder();
match rec.level() {
Error => builder.push(txt!("[log_book.error][[ERROR]][log_book.colon]: ")),
Warn => builder.push(txt!("[log_book.warn][[WARNING]][log_book.colon]:")),
Info => builder.push(txt!("[log_book.info][[INFO]][log_book.colon]: ")),
Debug => builder.push(txt!("[log_book.debug][[DEBUG]][log_book.colon]: ")),
Trace => unreachable!("Trace is not meant to be useable"),
};
builder.push(txt!(" {}", rec.text().clone(),));
builder.push('\n');
Some(builder.build())
}