duat_core/
lib.rs

1//! The core of Duat, for use by Duat's built-in plugins.
2//!
3//! This crate isn't really meant for public use, since it is used
4//! only by a select few plugins. Configuration crates and plugins
5//! should make use of the [duat] crate.
6//!
7//! [duat]: https://crates.io/duat
8#![warn(rustdoc::unescaped_backticks)]
9#![allow(clippy::single_range_in_vec_init)]
10
11use std::{any::TypeId, sync::Mutex};
12
13#[allow(unused_imports)]
14use dirs_next::cache_dir;
15pub use lender;
16
17pub use self::ranges::Ranges;
18
19pub mod buffer;
20pub mod cmd;
21pub mod context;
22pub mod data;
23pub mod form;
24pub mod hook;
25pub mod mode;
26pub mod opts;
27mod ranges;
28#[doc(hidden)]
29pub mod session;
30pub mod text;
31pub mod ui;
32pub mod utils;
33
34/// A plugin for Duat
35///
36/// Plugins should mostly follow the builder pattern, but you can use
37/// fields if you wish to. When calling [`Plugin::plug`], the plugin's
38/// settings should be taken into account, and all of its setup should
39/// be done:
40///
41/// ```rust
42/// # use duat_core::{Plugin, Plugins};
43/// // It's not a supertrait of Plugin, but you must implement
44/// // Default in order to use the plugin.
45/// #[derive(Default)]
46/// struct MyPlugin(bool);
47///
48/// impl Plugin for MyPlugin {
49///     // With the Plugins struct, you can require other plugins
50///     // within your plugin
51///     fn plug(self, plugins: &Plugins) {
52///         //..
53///     }
54/// }
55///
56/// impl MyPlugin {
57///     /// Returns a new instance of the [`MyPlugin`] plugin
58///     pub fn new() -> Self {
59///         Self(false)
60///     }
61///
62///     /// Modifies [`MyPlugin`]
63///     pub fn modify(self) -> Self {
64///         //..
65/// #       self
66///     }
67/// }
68/// ```
69///
70/// [plugged]: Plugin::plug
71/// [`PhantomData`]: std::marker::PhantomData
72pub trait Plugin: 'static {
73    /// Sets up the [`Plugin`]
74    fn plug(self, plugins: &Plugins);
75}
76
77static PLUGINS: Plugins = Plugins(Mutex::new(Vec::new()));
78
79/// A struct for [`Plugin`]s to declare dependencies on other
80/// [`Plugin`]s
81pub struct Plugins(Mutex<Vec<(PluginFn, TypeId)>>);
82
83impl Plugins {
84    /// Returnss a new instance of [`Plugins`]
85    ///
86    /// **FOR USE BY THE DUAT EXECUTABLE ONLY**
87    #[doc(hidden)]
88    pub fn _new() -> &'static Self {
89        &PLUGINS
90    }
91
92    /// Require that a [`Plugin`] be added
93    ///
94    /// This plugin may have already been added, or it might be added
95    /// by this call.
96    ///
97    /// For built-in [`Plugin`]s, if they are required by some
98    /// `Plugin`, then they will be added before that `Plugin` is
99    /// added. Otherwise, they will be added at the end of the `setup`
100    /// function.
101    pub fn require<P: Plugin + Default>(&self) {
102        // SAFETY: This function can only push new elements to the list, not
103        // accessing the !Send functions within.
104        let mut plugins = self.0.lock().unwrap();
105        if !plugins.iter().any(|(_, ty)| *ty == TypeId::of::<P>()) {
106            plugins.push((
107                Some(Box::new(|plugins| P::default().plug(plugins))),
108                TypeId::of::<P>(),
109            ));
110        };
111    }
112}
113
114// SAFETY: The !Send functions are only accessed from the main thread
115unsafe impl Send for Plugins {}
116unsafe impl Sync for Plugins {}
117
118pub mod clipboard {
119    //! Clipboard interaction for Duat
120    //!
121    //! Just a regular clipboard, no image functionality.
122    use std::sync::{Mutex, OnceLock};
123
124    /// A clipboard for Duat, can be platform based, or local
125    #[doc(hidden)]
126    #[allow(private_interfaces)]
127    pub enum Clipboard {
128        #[cfg(target_os = "android")]
129        Platform,
130        #[cfg(not(target_os = "android"))]
131        Platform(arboard::Clipboard, &'static ClipboardFunctions),
132        Local(String),
133    }
134
135    impl Default for Clipboard {
136        fn default() -> Self {
137            #[cfg(not(target_os = "android"))]
138            match arboard::Clipboard::new() {
139                Ok(clipb) => Self::Platform(clipb, ClipboardFunctions::new()),
140                Err(_) => Self::Local(String::new()),
141            }
142
143            #[cfg(target_os = "android")]
144            Self::Platform
145        }
146    }
147
148    static CLIPB: OnceLock<&'static Mutex<Clipboard>> = OnceLock::new();
149
150    /// Gets a [`String`] from the clipboard
151    ///
152    /// This can fail if the clipboard does not contain UTF-8 encoded
153    /// text.
154    ///
155    /// Or if there is no clipboard I guess
156    pub fn get_text() -> Option<String> {
157        let mut clipb = CLIPB.get().unwrap().lock().unwrap();
158        match &mut *clipb {
159            #[cfg(target_os = "android")]
160            Clipboard::Platform => clipboard::get_text()
161                .map_err(|err| crate::context::error!("{err}"))
162                .ok(),
163            #[cfg(not(target_os = "android"))]
164            Clipboard::Platform(clipb, fns) => (fns.get_text)(clipb),
165            Clipboard::Local(clipb) => Some(clipb.clone()).filter(String::is_empty),
166        }
167    }
168
169    /// Sets a [`String`] to the clipboard
170    pub fn set_text(text: impl std::fmt::Display) {
171        let mut clipb = CLIPB.get().unwrap().lock().unwrap();
172        match &mut *clipb {
173            #[cfg(target_os = "android")]
174            Clipboard::Platform => {
175                if let Err(err) = clipboard::set_text(text.to_string()) {
176                    crate::context::error!("{err}");
177                }
178            }
179            #[cfg(not(target_os = "android"))]
180            Clipboard::Platform(clipb, fns) => (fns.set_text)(clipb, text.to_string()),
181            Clipboard::Local(clipb) => *clipb = text.to_string(),
182        }
183    }
184
185    #[cfg(not(target_os = "android"))]
186    struct ClipboardFunctions {
187        get_text: fn(&mut arboard::Clipboard) -> Option<String>,
188        set_text: fn(&mut arboard::Clipboard, String),
189    }
190
191    impl ClipboardFunctions {
192        fn new() -> &'static Self {
193            &Self {
194                get_text: |clipb| clipb.get_text().ok(),
195                set_text: |clipb, text| clipb.set_text(text).unwrap(),
196            }
197        }
198    }
199
200    pub(crate) fn set_clipboard(clipb: &'static Mutex<Clipboard>) {
201        CLIPB.set(clipb).map_err(|_| {}).expect("Setup ran twice");
202    }
203}
204
205////////// Text Builder macros (for pub/private bending)
206#[doc(hidden)]
207pub mod private_exports {
208    pub use format_like::format_like;
209}
210
211/// Converts a string to a valid priority
212#[doc(hidden)]
213pub const fn priority(priority: &str) -> u8 {
214    let mut bytes = priority.as_bytes();
215    let mut val = 0;
216
217    while let [byte, rest @ ..] = bytes {
218        assert!(b'0' <= *byte && *byte <= b'9', "invalid digit");
219        val = val * 10 + (*byte - b'0') as usize;
220        bytes = rest;
221    }
222
223    assert!(val <= 250, "priority cannot exceed 250");
224
225    val as u8
226}
227
228type PluginFn = Option<Box<dyn FnOnce(&Plugins)>>;
229
230/// Tries to evaluate a block that returns [`Result<T, Text>`]
231///
232/// If the block returns [`Ok`], this macro will resolve to `T`. If it
233/// returns [`Err`], it will log the error with [`context::error!`],
234/// then it will return from the function. As an example, this:
235///
236/// ```rust
237/// # duat_core::doc_duat!(duat);
238/// # fn test() {
239/// use duat::prelude::*;
240///
241/// let ret = try_or_log_err! {
242///     let value = result_fn()?;
243///     value
244/// };
245///
246/// fn result_fn() -> Result<usize, Text> {
247///     Err(txt!(":("))
248/// }
249/// # }
250/// ```
251///
252/// Will expand into:
253///
254/// ```rust
255/// # duat_core::doc_duat!(duat);
256/// # fn test() {
257/// use duat::prelude::*;
258///
259/// let ret = match (|| -> Result<_, Text> { Ok(result_fn()?) })() {
260///     Ok(ret) => ret,
261///     Err(err) => {
262///         context::error!("{err}");
263///         return;
264///     }
265/// };
266///
267/// fn result_fn() -> Result<usize, Text> {
268///     Err(txt!(":("))
269/// }
270/// # }
271/// ```
272///
273/// Note the [`Ok`] wrapping the tokens, so it works like the `try`
274/// keyword in that regard.
275#[macro_export]
276macro_rules! try_or_log_err {
277    { $($tokens:tt)* } => {
278        match (|| -> Result<_, $crate::text::Text> { Ok({ $($tokens)* }) })() {
279             Ok(ret) => ret,
280             Err(err) => {
281                 $crate::context::error!("{err}");
282                 return;
283             }
284        }
285    }
286}