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}