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}}