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