duat_core/lib.rs
1//! # duat-core
2//!
3//! The core of Duat, this crate is meant to be used only for the
4//! creation of plugins for Duat.
5//!
6//! # Quick Start
7//!
8//! The capabilities of `duat-core` are largely the same as the those
9//! of Duat, however, the main difference is the multi [`Ui`] APIs of
10//! this crate. In it, the public functions and types are defined in
11//! terms of `U: Ui`, which means that they can work on various
12//! different interfaces:
13//!
14//! ```rust
15//! # use duat_core::{
16//! # mode::{self, Cursors, EditHelper, KeyCode, KeyEvent, Mode, key}, ui::Ui, widgets::File,
17//! # };
18//! #[derive(Default, Clone)]
19//! struct FindSeq(Option<char>);
20//!
21//! impl<U: Ui> Mode<U> for FindSeq {
22//! type Widget = File;
23//!
24//! fn send_key(&mut self, key: KeyEvent, file: &mut File, area: &U::Area) {
25//! use KeyCode::*;
26//! let mut helper = EditHelper::new(file, area);
27//!
28//! // Make sure that the typed key is a character.
29//! let key!(Char(c)) = key else {
30//! mode::reset();
31//! return;
32//! };
33//! // Checking if a character was already sent.
34//! let Some(first) = self.0 else {
35//! self.0 = Some(c);
36//! return;
37//! };
38//!
39//! helper.move_many(.., |mut m| {
40//! let pat: String = [first, c].iter().collect();
41//! let matched = m.search_fwd(pat, None).next();
42//! if let Some((p0, p1)) = matched {
43//! m.move_to(p0);
44//! m.set_anchor();
45//! m.move_to(p1);
46//! if m.is_incl() {
47//! m.move_hor(-1)
48//! }
49//! }
50//! });
51//!
52//! mode::reset();
53//! }
54//! }
55//! ```
56//!
57//! In this example, I have created a [`Mode`] for [`File`]s. This
58//! mode is (I think) popular within Vim circles. It's like the `f`
59//! key in Vim, but it lets you look for a sequence of 2 characters,
60//! instead of just one.
61//!
62//! What's great about it is that it will work no matter what editing
63//! model the user is using. It could be Vim inspired, Kakoune
64//! inspired, Emacs inspired, doesn't matter. All the user has to do
65//! to use this mode is this:
66//!
67//! ```rust
68//! # struct Normal;
69//! # #[derive(Default, Clone)]
70//! # struct FindSeq;
71//! # fn map<M>(take: &str, give: &impl std::any::Any) {}
72//! # // I fake it here because this function is from duat, not duat-core
73//! map::<Normal>("<C-s>", &FindSeq::default());
74//! ```
75//!
76//! And now, whenever the usert types `Control S` in `Normal` mode,
77//! the mode will switch to `FindSeq`. You could replace `Normal` with
78//! any other mode, from any other editing model, and this would still
79//! work.
80//!
81//! Of course, this is most useful for plugins, for your own
82//! configuration, you should probably just rely on [`map`] to
83//! accomplish the same thing.
84//!
85//! Okay, but that was a relatively simple example, here's a more
86//! advanced example, which makes use of more of Duat's features.
87//!
88//! This is a copy of [EasyMotion], a plugin for
89//! Vim/Neovim/Kakoune/Emacs that lets you skip around the screen with
90//! at most 2 keypresses.
91//!
92//! In order to emulate it, we use [ghost text] and [concealment]:
93//!
94//! ```rust
95//! # use duat_core::{
96//! # mode::{self, Cursors, EditHelper, KeyCode, KeyEvent, Mode, key},
97//! # text::{Key, Point, Tag, text}, ui::{Area, Ui}, widgets::File,
98//! # };
99//! #[derive(Clone)]
100//! pub struct EasyMotion {
101//! is_line: bool,
102//! key: Key,
103//! points: Vec<(Point, Point)>,
104//! seq: String,
105//! }
106//!
107//! impl EasyMotion {
108//! pub fn word() -> Self {
109//! Self {
110//! is_line: false,
111//! key: Key::new(),
112//! points: Vec::new(),
113//! seq: String::new(),
114//! }
115//! }
116//!
117//! pub fn line() -> Self {
118//! Self {
119//! is_line: true,
120//! key: Key::new(),
121//! points: Vec::new(),
122//! seq: String::new(),
123//! }
124//! }
125//! }
126//!
127//! impl<U: Ui> Mode<U> for EasyMotion {
128//! type Widget = File;
129//!
130//! fn on_switch(&mut self, file: &mut File, area: &<U as Ui>::Area) {
131//! let cfg = file.print_cfg();
132//! let text = file.text_mut();
133//!
134//! let regex = match self.is_line {
135//! true => "[^\n\\s][^\n]+",
136//! false => "[^\n\\s]+",
137//! };
138//! let (start, _) = area.first_points(text, cfg);
139//! let (end, _) = area.last_points(text, cfg);
140//! self.points = text.search_fwd(regex, (start, end)).unwrap().collect();
141//!
142//! let seqs = key_seqs(self.points.len());
143//!
144//! for (seq, (p1, _)) in seqs.iter().zip(&self.points) {
145//! let ghost = text!([EasyMotionWord] seq);
146//!
147//! text.insert_tag(p1.byte(), Tag::GhostText(ghost), self.key);
148//! text.insert_tag(p1.byte(), Tag::StartConceal, self.key);
149//! let seq_end = p1.byte() + seq.chars().count() ;
150//! text.insert_tag(seq_end, Tag::EndConceal, self.key);
151//! }
152//! }
153//!
154//! fn send_key(&mut self, key: KeyEvent, file: &mut File, area: &U::Area) {
155//! let char = match key {
156//! key!(KeyCode::Char(c)) => c,
157//! // Return a char that will never match.
158//! _ => '❌'
159//! };
160//! self.seq.push(char);
161//!
162//! let mut helper = EditHelper::new(file, area);
163//! helper.cursors_mut().remove_extras();
164//!
165//! let seqs = key_seqs(self.points.len());
166//! for (seq, &(p1, p2)) in seqs.iter().zip(&self.points) {
167//! if *seq == self.seq {
168//! helper.move_main(|mut m| {
169//! m.move_to(p1);
170//! m.set_anchor();
171//! m.move_to(p2);
172//! });
173//! mode::reset();
174//! } else if seq.starts_with(&self.seq) {
175//! continue;
176//! }
177//!
178//! helper.text_mut().remove_tags(p1.byte(), self.key);
179//! helper.text_mut().remove_tags(p1.byte() + seq.len(), self.key);
180//! }
181//!
182//! if self.seq.chars().count() == 2 || !LETTERS.contains(char) {
183//! mode::reset();
184//! }
185//! }
186//! }
187//!
188//! fn key_seqs(len: usize) -> Vec<String> {
189//! let double = len / LETTERS.len();
190//!
191//! let mut seqs = Vec::new();
192//! seqs.extend(LETTERS.chars().skip(double).map(char::into));
193//!
194//! let chars = LETTERS.chars().take(double);
195//! seqs.extend(chars.flat_map(|c1| LETTERS.chars().map(move |c2| format!("{c1}{c2}"))));
196//!
197//! seqs
198//! }
199//!
200//! static LETTERS: &str = "abcdefghijklmnopqrstuvwxyz";
201//! ```
202//! All that this plugin is doing is:
203//!
204//! - Search on the screen for words/lines;
205//! - In the beginning of said words/lines, add a [`Tag::GhostText`];
206//! - Also add a [`Tag::StartConceal`] and a [`Tag::EndConceal`];
207//! - Then, just match the typed keys and [remove] tags accordingly;
208//! - [Move] to the matched sequence, if it exists;
209//!
210//! Now, in order to use this mode, it's the exact same thing as
211//! `FindSeq`:
212//!
213//! ```rust
214//! # struct Normal;
215//! #[derive(Clone)]
216//! # pub struct EasyMotion;
217//! # impl EasyMotion {
218//! # pub fn word() -> Self {
219//! # Self
220//! # }
221//! # pub fn line() -> Self {
222//! # Self
223//! # }
224//! # }
225//! # fn map<M>(take: &str, give: &impl std::any::Any) {}
226//! # // I fake it here because this function is from duat, not duat-core
227//! map::<Normal>("<CA-w>", &EasyMotion::word());
228//! map::<Normal>("<CA-l>", &EasyMotion::line());
229//! ```
230//!
231//! [`Mode`]: crate::mode::Mode
232//! [`File`]: crate::widgets::File
233//! [`map`]: https://docs.rs/duat/0.2.0/duat/prelude/fn.map.html
234//! [EasyMotion]: https://github.com/easymotion/vim-easymotion
235//! [ghost text]: crate::text::Tag::GhostText
236//! [concealment]: crate::text::Tag::StartConceal
237//! [`Tag::GhostText`]: crate::text::Tag::GhostText
238//! [`Tag::StartConceal`]: crate::text::Tag::StartConceal
239//! [`Tag::EndConceal`]: crate::text::Tag::EndConceal
240//! [remove]: crate::text::Text::remove_tags
241//! [Move]: crate::mode::Mover::move_to
242#![feature(
243 let_chains,
244 decl_macro,
245 step_trait,
246 type_alias_impl_trait,
247 trait_alias,
248 debug_closure_helpers
249)]
250#![allow(clippy::single_range_in_vec_init)]
251
252use std::{
253 any::{TypeId, type_name},
254 collections::HashMap,
255 marker::PhantomData,
256 sync::{
257 Arc, LazyLock, Once,
258 atomic::{AtomicBool, Ordering},
259 },
260 time::Duration,
261};
262
263#[allow(unused_imports)]
264use dirs_next::cache_dir;
265pub use parking_lot::{Mutex, RwLock};
266use ui::Window;
267use widgets::{File, Node, Widget};
268
269pub use self::data::context;
270use self::{
271 text::{Text, err, hint},
272 ui::Ui,
273};
274
275pub mod cache;
276pub mod cfg;
277pub mod cmd;
278pub mod data;
279pub mod form;
280pub mod hooks;
281pub mod mode;
282pub mod session;
283pub mod text;
284pub mod ui;
285pub mod widgets;
286
287pub mod prelude {
288 //! The prelude of Duat
289 pub use crate::{
290 Error, cmd,
291 data::{self, RwData},
292 form,
293 text::{Builder, Text, err, hint, ok, text},
294 ui, widgets,
295 };
296}
297
298/// A plugin for Duat
299///
300/// Plugins must follow the builder pattern, and can be specific to
301/// certain [`Ui`]s. Generally, plugins should do all the setup
302/// necessary for their function when [`Plugin::plug`] is called.
303///
304/// [`Plugin`] will usually be [plugged] by a `macro` in the Duat
305/// config crate. This macro requires that the [`Plugin`] be
306/// compatible with the [`Ui`]. And this can cause some inconvenience
307/// for the end user. For example, say we have a plugin like this:
308///
309/// ```rust
310/// # use duat_core::{Plugin, ui::Ui};
311/// struct MyPlugin;
312///
313/// impl<U: Ui> Plugin<U> for MyPlugin {
314/// fn new() -> Self {
315/// MyPlugin
316/// }
317///
318/// fn plug(self) {
319/// //..
320/// }
321/// }
322///
323/// impl MyPlugin {
324/// pub fn modify(self) -> Self {
325/// //..
326/// # self
327/// }
328/// }
329/// ```
330///
331/// In the config crate, the user would have to add the plugin in a
332/// really awkward way:
333///
334/// ```rust
335/// # use duat_core::Plugin;
336/// # macro_rules! plug {
337/// # ($($plug:expr),+) => {};
338/// # }
339/// # struct MyPlugin;
340/// # impl<U: duat_core::ui::Ui> duat_core::Plugin<U> for MyPlugin {
341/// # fn new() -> Self {
342/// # MyPlugin
343/// # }
344/// # fn plug(self) {}
345/// # }
346/// # fn test<Ui: duat_core::ui::Ui>() {
347/// plug!(<MyPlugin as Plugin<Ui>>::new().modify());
348/// # }
349/// ```
350///
351/// To prevent that, just add a [`Ui`] [`PhantomData`] parameter:
352///
353/// ```rust
354/// # use std::marker::PhantomData;
355/// # use duat_core::{Plugin, ui::Ui};
356/// struct MyPlugin<U>(PhantomData<U>);
357///
358/// impl<U: Ui> Plugin<U> for MyPlugin<U> {
359/// fn new() -> Self {
360/// MyPlugin(PhantomData)
361/// }
362///
363/// fn plug(self) {
364/// //..
365/// }
366/// }
367///
368/// impl<U> MyPlugin<U> {
369/// pub fn modify(self) -> Self {
370/// //..
371/// # self
372/// }
373/// }
374/// ```
375/// And now the plugin can be plugged much more normally:
376///
377///
378/// ```rust
379/// # use std::marker::PhantomData;
380/// # use duat_core::Plugin;
381/// # macro_rules! plug {
382/// # ($($plug:expr),+) => {};
383/// # }
384/// # struct MyPlugin<U>(PhantomData<U>);
385/// # impl<U: duat_core::ui::Ui> duat_core::Plugin<U> for MyPlugin<U> {
386/// # fn new() -> Self {
387/// # MyPlugin(PhantomData)
388/// # }
389/// # fn plug(self) {}
390/// # }
391/// # impl<U> MyPlugin<U> {
392/// # pub fn modify(self) -> Self {
393/// # self
394/// # }
395/// # }
396/// # fn test<Ui: duat_core::ui::Ui>() {
397/// plug!(MyPlugin::new().modify());
398/// # }
399/// ```
400/// [plugged]: Plugin::plug
401pub trait Plugin<U: Ui>: Sized {
402 /// Returns a builder pattern instance of this [`Plugin`]
403 fn new() -> Self;
404
405 /// Sets up the [`Plugin`]
406 fn plug(self);
407}
408
409pub mod thread {
410 //! Multithreading for Duat
411 //!
412 //! The main rationale behind multithreading in Duat is not so
413 //! much the performance gains, but more to allow for multi
414 //! tasking, as some plugins (like an LSP) may block for a while,
415 //! which would be frustrating for end users.
416 //!
417 //! The functions in this module differ from [`std::thread`] in
418 //! that they synchronize with Duat, telling the application when
419 //! there are no more threads running, so Duat can safely quit or
420 //! reload.
421 use std::{
422 sync::{
423 LazyLock,
424 atomic::{AtomicUsize, Ordering},
425 mpsc,
426 },
427 thread::JoinHandle,
428 };
429
430 use parking_lot::{Mutex, Once};
431
432 /// Duat's [`JoinHandle`]s
433 pub static HANDLES: AtomicUsize = AtomicUsize::new(0);
434 static ACTIONS: LazyLock<(mpsc::Sender<SentHook>, Mutex<mpsc::Receiver<SentHook>>)> =
435 LazyLock::new(|| {
436 let (sender, receiver) = mpsc::channel();
437 (sender, Mutex::new(receiver))
438 });
439
440 /// Spawns a new thread, returning a [`JoinHandle`] for it.
441 ///
442 /// Use this function instead of [`std::thread::spawn`].
443 ///
444 /// The threads from this function work in the same way that
445 /// threads from [`std::thread::spawn`] work, but it has
446 /// synchronicity with Duat, and makes sure that the
447 /// application won't exit or reload the configuration before
448 /// all spawned threads have stopped.
449 pub fn spawn<R: Send + 'static>(f: impl FnOnce() -> R + Send + 'static) -> JoinHandle<R> {
450 HANDLES.fetch_add(1, Ordering::Relaxed);
451 std::thread::spawn(|| {
452 let ret = f();
453 HANDLES.fetch_sub(1, Ordering::Relaxed);
454 ret
455 })
456 }
457
458 /// Queues an action
459 ///
460 /// All queued actions will be done in the sequence that they came
461 /// in, sequentially in a single thread.
462 pub(crate) fn queue<R>(f: impl FnOnce() -> R + Send + 'static) {
463 fn queue_inner(f: Box<dyn FnOnce() + Send + 'static>) {
464 static LOOP: Once = Once::new();
465 let (sender, receiver) = &*ACTIONS;
466
467 LOOP.call_once(|| {
468 spawn(|| {
469 let receiver = receiver.lock();
470 while let Ok(SentHook::Fn(f)) = receiver.recv() {
471 f();
472 }
473 });
474 });
475
476 if !crate::context::will_reload_or_quit() {
477 sender.send(SentHook::Fn(f)).unwrap();
478 }
479 }
480
481 queue_inner(Box::new(|| {
482 f();
483 }));
484 }
485
486 /// Returns true if there are any threads still running
487 pub(crate) fn still_running() -> bool {
488 HANDLES.load(Ordering::Relaxed) > 0
489 }
490
491 /// Stops the thread running the queue
492 pub(crate) fn quit_queue() {
493 let (sender, _) = &*ACTIONS;
494 sender.send(SentHook::Quit).unwrap()
495 }
496
497 enum SentHook {
498 Fn(Box<dyn FnOnce() + Send>),
499 Quit,
500 }
501}
502
503pub mod clipboard {
504 //! Clipboard interaction for Duat
505 //!
506 //! Just a regular clipboard, no image functionality.
507 use std::sync::OnceLock;
508
509 pub use arboard::Clipboard;
510 use parking_lot::Mutex;
511
512 static CLIPB: OnceLock<&'static Mutex<Clipboard>> = OnceLock::new();
513
514 /// Gets a [`String`] from the clipboard
515 ///
516 /// This can fail if the clipboard does not contain UTF-8 encoded
517 /// text.
518 ///
519 /// Or if there is no clipboard i guess
520 pub fn get_text() -> Option<String> {
521 CLIPB.get().unwrap().lock().get_text().ok()
522 }
523
524 /// Sets a [`String`] to the clipboard
525 pub fn set_text(text: impl std::fmt::Display) {
526 let clipb = CLIPB.get().unwrap();
527 clipb.lock().set_text(text.to_string()).unwrap();
528 }
529
530 pub(crate) fn set_clipboard(clipb: &'static Mutex<Clipboard>) {
531 CLIPB.set(clipb).map_err(|_| {}).expect("Setup ran twice");
532 }
533}
534
535/// A checker that returns `true` every `duration`
536///
537/// This is primarily used within [`WidgetCfg::build`], where a
538/// `checker` must be returned in order to update the widget.
539///
540/// [`WidgetCfg::build`]: crate::widgets::WidgetCfg::build
541pub fn periodic_checker(duration: Duration) -> impl Fn() -> bool {
542 let check = Arc::new(AtomicBool::new(false));
543 crate::thread::spawn({
544 let check = check.clone();
545 move || {
546 while !crate::context::will_reload_or_quit() {
547 std::thread::sleep(duration);
548 check.store(true, Ordering::Release);
549 }
550 }
551 });
552
553 move || check.fetch_and(false, Ordering::Acquire)
554}
555
556/// An error that can be displayed as [`Text`] in Duat
557pub trait DuatError {
558 fn into_text(self) -> Box<Text>;
559}
560
561/// Error for failures in Duat
562#[derive(Clone)]
563pub enum Error<E> {
564 /// The caller for a command already pertains to another
565 CallerAlreadyExists(String),
566 /// No commands have the given caller as one of their own
567 CallerNotFound(String),
568 /// The command failed internally
569 CommandFailed(Box<Text>),
570 /// There was no caller and no arguments
571 Empty,
572 /// Arguments could not be parsed correctly
573 FailedParsing(Box<Text>),
574 /// The [`Layout`] does not allow for another file to open
575 ///
576 /// [`Layout`]: ui::Layout
577 LayoutDisallowsFile(PhantomData<E>),
578 /// The [`Ui`] still hasn't created the first file
579 ///
580 /// [`Ui`]: ui::Ui
581 NoFileYet,
582 /// Since the [`Ui`] has no file, widgets can't relate to it
583 ///
584 /// [`Ui`]: ui::Ui
585 NoFileForRelated,
586 /// The [`Ui`] still hasn't created the first widget (a file)
587 ///
588 /// [`Ui`]: ui::Ui
589 NoWidgetYet,
590 /// The checked widget is not of the type given
591 WidgetIsNot,
592}
593
594impl<E1> Error<E1> {
595 /// Converts [`Error<E1>`] to [`Error<E2>`]
596 #[doc(hidden)]
597 pub fn into_other_type<E2>(self) -> Error<E2> {
598 match self {
599 Self::CallerAlreadyExists(caller) => Error::CallerAlreadyExists(caller),
600 Self::CallerNotFound(caller) => Error::CallerNotFound(caller),
601 Self::CommandFailed(failure) => Error::CommandFailed(failure),
602 Self::Empty => Error::Empty,
603 Self::FailedParsing(failure) => Error::FailedParsing(failure),
604 Self::NoFileYet => Error::NoFileYet,
605 Self::NoFileForRelated => Error::NoFileForRelated,
606 Self::NoWidgetYet => Error::NoWidgetYet,
607 Self::WidgetIsNot => Error::WidgetIsNot,
608 Self::LayoutDisallowsFile(_) => Error::LayoutDisallowsFile(PhantomData),
609 }
610 }
611}
612
613impl<E> DuatError for Error<E> {
614 /// Turns the [`Error`] into formatted [`Text`]
615 fn into_text(self) -> Box<Text> {
616 let early = hint!(
617 "Try this after " [*a] "OnUiStart" []
618 ", maybe by using hooks::add::<OnUiStart>"
619 );
620
621 match self {
622 Self::CallerAlreadyExists(caller) => Box::new(err!(
623 "The caller " [*a] caller [] " already exists."
624 )),
625 Self::CallerNotFound(caller) => {
626 Box::new(err!("The caller " [*a] caller [] " was not found."))
627 }
628 Self::CommandFailed(failure) => failure,
629 Self::Empty => Box::new(err!("The command is empty.")),
630 Self::FailedParsing(failure) => failure,
631 Self::NoFileYet => Box::new(err!("There is no file yet. " early)),
632 Self::NoFileForRelated => Box::new(err!(
633 "There is no file for a related " [*a] { type_name::<E>() } [] " to exist. " early
634 )),
635 Self::NoWidgetYet => Box::new(err!("There can be no widget yet. " early)),
636 Self::WidgetIsNot => Box::new(err!(
637 "The widget is not " [*a] { type_name::<E>() } [] ". " early
638 )),
639 Self::LayoutDisallowsFile(_) => Box::new(err!(
640 "The " [*a] "Layout" [] " disallows the addition of more files."
641 )),
642 }
643 }
644}
645
646impl<E> std::fmt::Debug for Error<E> {
647 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
648 let mut debug = f.debug_tuple(match self {
649 Self::CallerAlreadyExists(_) => "CallerAlreadyExists",
650 Self::CallerNotFound(_) => "CallerNotFound",
651 Self::CommandFailed(_) => "CommandFailed",
652 Self::Empty => "Empty ",
653 Self::FailedParsing(_) => "FailedParsing",
654 Self::NoFileYet => "NoFileYet ",
655 Self::NoFileForRelated => "NoFileForRelated ",
656 Self::NoWidgetYet => "NoWidgetYet ",
657 Self::WidgetIsNot => "WidgetIsNot ",
658 Self::LayoutDisallowsFile(_) => "LayoutDisallowsFile",
659 });
660
661 match self {
662 Self::CallerAlreadyExists(str) | Self::CallerNotFound(str) => debug.field(&str),
663 Self::CommandFailed(text) | Self::FailedParsing(text) => debug.field(&text),
664 Self::Empty
665 | Self::NoFileYet
666 | Self::NoFileForRelated
667 | Self::NoWidgetYet
668 | Self::WidgetIsNot
669 | Self::LayoutDisallowsFile(_) => &mut debug,
670 }
671 .finish()
672 }
673}
674
675pub type Result<T, E> = std::result::Result<T, Error<E>>;
676
677/// Takes a type and generates an appropriate name for it
678///
679/// Use this function if you need a name of a type to be
680/// referrable by string, such as by commands or by the
681/// user.
682pub fn duat_name<T: ?Sized + 'static>() -> &'static str {
683 fn duat_name_inner(type_id: TypeId, type_name: &str) -> &'static str {
684 static NAMES: LazyLock<RwLock<HashMap<TypeId, &'static str>>> =
685 LazyLock::new(RwLock::default);
686 let mut names = NAMES.write();
687
688 if let Some(name) = names.get(&type_id) {
689 name
690 } else {
691 let mut name = String::new();
692
693 for path in type_name.split_inclusive(['<', '>', ',', ' ']) {
694 for segment in path.split("::") {
695 let is_type = segment.chars().any(|c| c.is_uppercase());
696 let is_punct = segment.chars().all(|c| !c.is_alphanumeric());
697 let is_dyn = segment.starts_with("dyn");
698 if is_type || is_punct || is_dyn {
699 name.push_str(segment);
700 }
701 }
702 }
703
704 names.insert(type_id, name.leak());
705 names.get(&type_id).unwrap()
706 }
707 }
708
709 duat_name_inner(TypeId::of::<T>(), std::any::type_name::<T>())
710}
711
712/// Returns the source crate of a given type
713///
714/// This is primarily used on the [`cache`] module.
715pub fn src_crate<T: ?Sized + 'static>() -> &'static str {
716 fn src_crate_inner(type_id: TypeId, type_name: &'static str) -> &'static str {
717 static CRATES: LazyLock<RwLock<HashMap<TypeId, &'static str>>> =
718 LazyLock::new(|| RwLock::new(HashMap::new()));
719 let mut crates = CRATES.write();
720
721 if let Some(src_crate) = crates.get(&type_id) {
722 src_crate
723 } else {
724 let src_crate = type_name.split([' ', ':']).find(|w| *w != "dyn").unwrap();
725
726 crates.insert(type_id, src_crate);
727 crates.get(&type_id).unwrap()
728 }
729 }
730
731 src_crate_inner(TypeId::of::<T>(), std::any::type_name::<T>())
732}
733
734/// Convenience function for the bounds of a range
735fn get_ends(range: impl std::ops::RangeBounds<usize>, max: usize) -> (usize, usize) {
736 let start = match range.start_bound() {
737 std::ops::Bound::Included(start) => *start,
738 std::ops::Bound::Excluded(start) => *start + 1,
739 std::ops::Bound::Unbounded => 0,
740 };
741 let end = match range.end_bound() {
742 std::ops::Bound::Included(end) => (*end + 1).min(max),
743 std::ops::Bound::Excluded(end) => (*end).min(max),
744 std::ops::Bound::Unbounded => max,
745 };
746
747 (start, end)
748}
749
750/// An entry for a file with the given name
751#[allow(clippy::result_large_err)]
752fn file_entry<'a, U: Ui>(
753 windows: &'a [Window<U>],
754 name: &str,
755) -> std::result::Result<(usize, usize, &'a Node<U>), Text> {
756 windows
757 .iter()
758 .enumerate()
759 .flat_map(window_index_widget)
760 .find(|(.., node)| matches!(node.inspect_as(|f: &File| f.name() == name), Some(true)))
761 .ok_or_else(|| err!("File with name " [*a] name [] " not found."))
762}
763
764/// An entry for a widget of a specific type
765#[allow(clippy::result_large_err)]
766fn widget_entry<W: Widget<U>, U: Ui>(
767 windows: &[Window<U>],
768 w: usize,
769) -> std::result::Result<(usize, usize, &Node<U>), Text> {
770 let cur_file = context::cur_file::<U>().unwrap();
771
772 if let Some(node) = cur_file.get_related_widget::<W>() {
773 windows
774 .iter()
775 .enumerate()
776 .flat_map(window_index_widget)
777 .find(|(.., n)| n.ptr_eq(node.widget()))
778 } else {
779 iter_around(windows, w, 0).find(|(.., node)| node.data_is::<W>())
780 }
781 .ok_or(err!("No widget of type " [*a] { type_name::<W>() } [] " found."))
782}
783
784/// Iterator over a group of windows, that returns the window's index
785fn window_index_widget<U: Ui>(
786 (index, window): (usize, &Window<U>),
787) -> impl ExactSizeIterator<Item = (usize, usize, &Node<U>)> + DoubleEndedIterator {
788 window
789 .nodes()
790 .enumerate()
791 .map(move |(i, entry)| (index, i, entry))
792}
793
794/// Iterates around a specific widget, going forwards
795fn iter_around<U: Ui>(
796 windows: &[Window<U>],
797 window: usize,
798 widget: usize,
799) -> impl Iterator<Item = (usize, usize, &Node<U>)> + '_ {
800 let prev_len: usize = windows.iter().take(window).map(Window::len_widgets).sum();
801
802 windows
803 .iter()
804 .enumerate()
805 .skip(window)
806 .flat_map(window_index_widget)
807 .skip(widget + 1)
808 .chain(
809 windows
810 .iter()
811 .enumerate()
812 .take(window + 1)
813 .flat_map(window_index_widget)
814 .take(prev_len + widget),
815 )
816}
817
818/// Iterates around a specific widget, going backwards
819fn iter_around_rev<U: Ui>(
820 windows: &[Window<U>],
821 window: usize,
822 widget: usize,
823) -> impl Iterator<Item = (usize, usize, &Node<U>)> {
824 let next_len: usize = windows.iter().skip(window).map(Window::len_widgets).sum();
825
826 windows
827 .iter()
828 .enumerate()
829 .rev()
830 .skip(windows.len() - window)
831 .flat_map(move |(i, win)| {
832 window_index_widget((i, win))
833 .rev()
834 .skip(win.len_widgets() - widget)
835 })
836 .chain(
837 windows
838 .iter()
839 .enumerate()
840 .rev()
841 .take(windows.len() - window)
842 .flat_map(move |(i, win)| window_index_widget((i, win)).rev())
843 .take(next_len - (widget + 1)),
844 )
845}
846
847// Debugging objects.
848#[doc(hidden)]
849pub static DEBUG_TIME_START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
850#[doc(hidden)]
851pub static HOOK: Once = Once::new();
852#[doc(hidden)]
853pub static LOG: LazyLock<Mutex<String>> = LazyLock::new(|| Mutex::new(String::new()));
854
855/// Log information to be shown when panicking
856#[doc(hidden)]
857pub macro log_panic($($text:tt)*) {{
858 #[cfg(not(debug_assertions))] {
859 compile_error!("You are not supposed to use log_panic on release profiles!");
860 }
861
862 use std::{fmt::Write, time::Instant};
863
864 use crate::{HOOK, LOG};
865
866 let mut text = format!($($text)*);
867
868 HOOK.call_once(|| {
869 let old_hook = std::panic::take_hook();
870 std::panic::set_hook(Box::new(move |info| {
871 old_hook(info);
872 println!("Logs:");
873 println!("{}\n", LOG.lock().unwrap());
874 }));
875 });
876
877 if let Some(start) = $crate::DEBUG_TIME_START.get()
878 && text != "" {
879 if text.lines().count() > 1 {
880 let chars = text.char_indices().filter_map(|(pos, char)| (char == '\n').then_some(pos));
881 let nl_indices: Vec<usize> = chars.collect();
882 for index in nl_indices.iter().rev() {
883 text.insert_str(index + 1, " ");
884 }
885
886 let duration = Instant::now().duration_since(*start);
887 write!(LOG.lock().unwrap(), "\nat {:.4?}:\n {text}", duration).unwrap();
888 } else {
889 let duration = Instant::now().duration_since(*start);
890 write!(LOG.lock().unwrap(), "\nat {:.4?}: {text}", duration).unwrap();
891 }
892 } else {
893 write!(LOG.lock().unwrap(), "\n{text}").unwrap();
894 }
895}}
896
897/// Log information to a log file
898#[doc(hidden)]
899pub macro log_file($($text:tt)*) {{
900 #[cfg(not(debug_assertions))] {
901 compile_error!("You are not supposed to use log_file on release profiles!");
902 }
903
904 if let Some(cache) = cache_dir() {
905 let mut file = std::io::BufWriter::new(
906 std::fs::OpenOptions::new()
907 .create(true)
908 .append(true)
909 .open(cache.join("duat/log"))
910 .unwrap(),
911 );
912
913 use std::{io::Write, time::Instant};
914
915 let mut text = format!($($text)*);
916
917 if let Some(start) = $crate::DEBUG_TIME_START.get()
918 && text != "" {
919 if text.lines().count() > 1 {
920 let chars = text
921 .char_indices()
922 .filter_map(|(pos, char)| (char == '\n').then_some(pos));
923 let nl_indices: Vec<usize> = chars.collect();
924 for index in nl_indices.iter().rev() {
925 text.insert_str(index + 1, " ");
926 }
927
928 let duration = Instant::now().duration_since(*start);
929 write!(file, "\nat {:.4?}:\n {text}", duration).unwrap();
930 } else {
931 let duration = Instant::now().duration_since(*start);
932 write!(file, "\nat {:.4?}: {text}", duration).unwrap();
933 }
934 } else {
935 write!(file, "\n{text}").unwrap();
936 }
937 }
938}}