Skip to main content

everything_plugin/
lib.rs

1//! Rust binding for [Everything](https://www.voidtools.com/)'s [plugin SDK](https://www.voidtools.com/forum/viewtopic.php?t=16535).
2//!
3//! Features:
4//! - Load and save config with [Serde](https://github.com/serde-rs/serde)
5//! - Make options pages GUI using [Winio](https://github.com/compio-rs/winio) in MVU (Elm) architecture
6//! - Internationalization with [rust-i18n](https://github.com/longbridge/rust-i18n)
7//! - Log with [tracing](https://github.com/tokio-rs/tracing)
8//!
9//! ## Example
10//! ```rust
11//! mod options;
12//!
13//! #[derive(Serialize, Deserialize, Debug, Default)]
14//! pub struct Config {
15//!     s: String,
16//! }
17//!
18//! pub struct App {
19//!     config: Config,
20//! }
21//!
22//! impl PluginApp for App {
23//!     type Config = Config;
24//!
25//!     fn new(config: Option<Self::Config>) -> Self {
26//!         Self {
27//!             config: config.unwrap_or_default(),
28//!         }
29//!     }
30//!
31//!     fn config(&self) -> &Self::Config {
32//!         &self.config
33//!     }
34//!
35//!     fn into_config(self) -> Self::Config {
36//!         self.config
37//!     }
38//! }
39//!
40//! plugin_main!(App, {
41//!     PluginHandler::builder()
42//!         .name("Test Plugin")
43//!         .description("A test plugin for Everything")
44//!         .author("Chaoses-Ib")
45//!         .version("0.1.0")
46//!         .link("https://github.com/Chaoses-Ib/IbEverythingLib")
47//!         .options_pages(vec![
48//!             OptionsPage::builder()
49//!                 .name("Test Plugin")
50//!                 .load(ui::winio::spawn::<options::MainModel>)
51//!                 .build(),
52//!         ])
53//!         .build()
54//! });
55//! ```
56//!
57//! ## Detachable design
58//! The API is designed to allow the app to be easily detached from Everything and run independently. Either for standalone distribution or testing.
59//!
60//! This also means a standalone Winio app can be relatively easily integrated into Everything as a plugin.
61//!
62//! Components:
63//! - tracing
64//! - Winio
65//! - Serde
66//! - [`PluginApp`]
67//! - [`PluginHandler`]
68//!   - [`PluginHandler::init_start()`], [`PluginHandler::init_start_with_config()`]
69//!   - [`PluginHandler::stop_kill()`]
70//!   - [`PluginHandler::get_host()`]
71//!
72//! TODO:
73//! - Tray icon and menu itmes / tabs
74//! - Load & save config with file
75//! - Unified host/IPC API
76//!
77//! ## Build
78//! ### Static CRT
79//! `.cargo/config.toml`:
80//! ```toml
81//! [target.'cfg(all(target_os = "windows", target_env = "msvc"))']
82//! rustflags = ["-C", "target-feature=+crt-static"]
83//! ```
84//! Increase build size by ~100 KiB.
85//!
86//! ## Debugging
87//! - `.\Everything64.exe -debug`
88//!   
89//!   Unlike `-debug`, `-debug-log` doesn't work with stdout/stderr outputs.
90//!
91//! ## Features
92#![cfg_attr(docsrs, feature(doc_cfg))]
93#![cfg_attr(feature = "doc", doc = document_features::document_features!())]
94
95use core::str;
96use std::{
97    cell::{Cell, OnceCell, UnsafeCell},
98    ffi::{CString, c_void},
99    mem,
100    ops::Deref,
101    slice,
102};
103
104use bon::Builder;
105use everything_ipc::IpcWindow;
106use tracing::{debug, trace};
107
108use crate::data::Config;
109
110pub use everything_ipc as ipc;
111pub use serde;
112
113pub mod data;
114#[cfg(feature = "tracing")]
115pub mod log;
116pub mod macros;
117pub mod sys;
118pub mod ui;
119
120/// ## Example
121/// ```ignore
122/// #[derive(Serialize, Deserialize, Debug, Default)]
123/// pub struct Config {
124///     s: String,
125/// }
126///
127/// pub struct App {
128///     config: Config,
129/// }
130///
131/// impl PluginApp for App {
132///     type Config = Config;
133///
134///     fn new(config: Option<Self::Config>) -> Self {
135///         Self {
136///             config: config.unwrap_or_default(),
137///         }
138///     }
139///
140///     fn config(&self) -> &Self::Config {
141///         &self.config
142///     }
143///
144///     fn into_config(self) -> Self::Config {
145///         self.config
146///     }
147/// }
148/// ```
149pub trait PluginApp: 'static {
150    type Config: Config;
151
152    fn new(config: Option<Self::Config>) -> Self;
153
154    /// Can be used to start services requiring to access the [`PluginApp`] through [`PluginHandler::with_app()`] or [`PluginHandler::app()`].
155    fn start(&self) {}
156
157    fn config(&self) -> &Self::Config;
158
159    fn into_config(self) -> Self::Config;
160}
161
162/// ## Example
163/// ```ignore
164/// PluginHandler::builder()
165///     .name("Test Plugin")
166///     .description("A test plugin for Everything")
167///     .author("Chaoses-Ib")
168///     .version("0.1.0")
169///     .link("https://github.com/Chaoses-Ib/IbEverythingLib")
170///     .options_pages(vec![
171///         OptionsPage::builder()
172///             .name("Test Plugin")
173///             .load(ui::winio::spawn::<options::MainModel>)
174///             .build(),
175///     ])
176///     .build()
177/// ```
178///
179/// ## Design
180/// - Config may be accessed from multiple threads, and options pages need to modify it. To avoid race conditions, either config is cloned when modifying, and then [`PluginApp`] is reloaded with it, i.e. [`arc_swap::ArcSwap`]; or [`PluginApp`] is shutdown before modifying and then restarted.
181/// - User defined static to work around generic static limit.
182///   - Interior mutability to make it easy to use with `static`. But `UnsafeCell` to avoid cost.
183///
184/// Config lifetime:
185/// - May be set with [`PluginHandler::builder()`] (as default value)
186/// - May be loaded when [`sys::EVERYTHING_PLUGIN_PM_START`]
187/// - Be read when start
188/// - Be read when loading (and rendering) options pages ([`sys::EVERYTHING_PLUGIN_PM_LOAD_OPTIONS_PAGE`])
189/// - Be written/applied when [`sys::EVERYTHING_PLUGIN_PM_SAVE_OPTIONS_PAGE`], zero, one or multiple times
190///   - TODO: Defer
191/// - Be saved when [`sys::EVERYTHING_PLUGIN_PM_SAVE_SETTINGS`] (can occur without prior [`sys::EVERYTHING_PLUGIN_PM_SAVE_OPTIONS_PAGE`])
192#[derive(Builder)]
193pub struct PluginHandler<A: PluginApp> {
194    #[builder(skip)]
195    host: OnceCell<PluginHost>,
196
197    #[builder(with = |x: impl Into<String>| CString::new(x.into()).unwrap())]
198    name: Option<CString>,
199    #[builder(with = |x: impl Into<String>| CString::new(x.into()).unwrap())]
200    description: Option<CString>,
201    #[builder(with = |x: impl Into<String>| CString::new(x.into()).unwrap())]
202    author: Option<CString>,
203    #[builder(with = |x: impl Into<String>| CString::new(x.into()).unwrap())]
204    version: Option<CString>,
205    #[builder(with = |x: impl Into<String>| CString::new(x.into()).unwrap())]
206    link: Option<CString>,
207
208    #[builder(skip)]
209    app: UnsafeCell<Option<A>>,
210
211    #[builder(default)]
212    options_pages: Vec<ui::OptionsPage<A>>,
213    #[builder(skip)]
214    options_message: Cell<ui::OptionsMessage>,
215
216    /// TODO: Feature cfg?
217    #[builder(skip)]
218    instance_name: UnsafeCell<Option<String>>,
219}
220
221unsafe impl<A: PluginApp> Send for PluginHandler<A> {}
222unsafe impl<A: PluginApp> Sync for PluginHandler<A> {}
223
224impl<A: PluginApp> PluginHandler<A> {
225    /// Panics if already initialized.
226    pub fn init_start(&self) {
227        self.handle(sys::EVERYTHING_PLUGIN_PM_INIT, 0 as _);
228        self.handle(sys::EVERYTHING_PLUGIN_PM_START, 0 as _);
229    }
230
231    /// Panics if already initialized.
232    pub fn init_start_with_config(&self, config: A::Config) {
233        self.handle(sys::EVERYTHING_PLUGIN_PM_INIT, 0 as _);
234        self.handle(
235            sys::EVERYTHING_PLUGIN_PM_START,
236            Box::into_raw(Box::new(config)) as _,
237        );
238    }
239
240    /// Panics if not initialized or already stopped.
241    pub fn stop_kill(&self) {
242        self.handle(sys::EVERYTHING_PLUGIN_PM_STOP, 0 as _);
243        self.handle(sys::EVERYTHING_PLUGIN_PM_KILL, 0 as _);
244    }
245
246    /// `None` before handling `EVERYTHING_PLUGIN_PM_INIT`
247    pub fn get_host(&self) -> Option<&PluginHost> {
248        self.host.get()
249    }
250
251    /// Not available before handling `EVERYTHING_PLUGIN_PM_INIT`
252    pub fn host(&self) -> &PluginHost {
253        debug_assert!(self.get_host().is_some(), "Plugin host not inited");
254        unsafe { self.get_host().unwrap_unchecked() }
255    }
256
257    #[cfg(feature = "rust-i18n")]
258    fn init_i18n(data: *mut c_void) {
259        let language = if !data.is_null() {
260            let host = unsafe { PluginHost::from_data(data) };
261            host.config_get_language_name()
262        } else {
263            PluginHost::get_thread_language_name()
264        };
265
266        rust_i18n::set_locale(&language);
267    }
268
269    /// Should be called before [`PluginHandler`] is created, as some fields may depend on the locale.
270    ///
271    /// Already called in the [`plugin_main!`] macro. (Requiring manually calling is a footgun: [IbEverythingExt #100](https://github.com/Chaoses-Ib/IbEverythingExt/issues/100))
272    pub fn handle_init_i18n(_msg: u32, _data: *mut c_void) {
273        #[cfg(feature = "rust-i18n")]
274        if _msg == sys::EVERYTHING_PLUGIN_PM_INIT {
275            use std::sync::Once;
276            static INIT: Once = Once::new();
277
278            if INIT.is_completed() && !_data.is_null() {
279                // If i18n is inited, tracing should also be inited.
280                debug!("i18n reinit");
281                // Allow reinit (DLL hijacking and plugin)
282                Self::init_i18n(_data);
283            }
284            INIT.call_once(|| {
285                Self::init_i18n(_data);
286            });
287        }
288    }
289
290    /// You shouldn't and unlikely need to call this function from multiple threads.
291    pub fn handle(&self, msg: u32, data: *mut c_void) -> *mut c_void {
292        match msg {
293            sys::EVERYTHING_PLUGIN_PM_INIT => {
294                if !self.app_is_some() {
295                    #[cfg(feature = "tracing")]
296                    let _ = log::tracing_try_init();
297                    debug!("Plugin init");
298                } else {
299                    // Allow reinit (DLL hijacking and plugin)
300                    debug!("Plugin reinit");
301                    self.app_into_config();
302                }
303
304                if !data.is_null() {
305                    _ = self.host.set(unsafe { PluginHost::from_data(data) });
306                }
307
308                *unsafe { &mut *self.instance_name.get() } =
309                    PluginHost::instance_name_from_main_thread();
310                debug!(instance_name = ?self.instance_name());
311
312                // #[cfg(feature = "rust-i18n")]
313                // rust_i18n::set_locale(&self.get_language_name());
314
315                1 as _
316            }
317            sys::EVERYTHING_PLUGIN_PM_GET_PLUGIN_VERSION => sys::EVERYTHING_PLUGIN_VERSION as _,
318            sys::EVERYTHING_PLUGIN_PM_GET_NAME => {
319                debug!("Plugin get name");
320                match &self.name {
321                    Some(name) => name.as_ptr() as _,
322                    None => 0 as _,
323                }
324            }
325            sys::EVERYTHING_PLUGIN_PM_GET_DESCRIPTION => {
326                debug!("Plugin get description");
327                match &self.description {
328                    Some(description) => description.as_ptr() as _,
329                    None => 0 as _,
330                }
331            }
332            sys::EVERYTHING_PLUGIN_PM_GET_AUTHOR => {
333                debug!("Plugin get author");
334                match &self.author {
335                    Some(author) => author.as_ptr() as _,
336                    None => 0 as _,
337                }
338            }
339            sys::EVERYTHING_PLUGIN_PM_GET_VERSION => {
340                debug!("Plugin get version");
341                match &self.version {
342                    Some(version) => version.as_ptr() as _,
343                    None => 0 as _,
344                }
345            }
346            sys::EVERYTHING_PLUGIN_PM_GET_LINK => {
347                debug!("Plugin get link");
348                match &self.link {
349                    Some(link) => link.as_ptr() as _,
350                    None => 0 as _,
351                }
352            }
353            sys::EVERYTHING_PLUGIN_PM_START => {
354                debug!("Plugin start");
355
356                self.app_new(self.load_settings(data));
357
358                1 as _
359            }
360            sys::EVERYTHING_PLUGIN_PM_STOP => {
361                debug!("Plugin stop");
362
363                // TODO
364
365                1 as _
366            }
367            sys::EVERYTHING_PLUGIN_PM_UNINSTALL => {
368                debug!("Plugin uninstall");
369
370                // TODO
371
372                1 as _
373            }
374            // Always the last message sent to the plugin
375            sys::EVERYTHING_PLUGIN_PM_KILL => {
376                debug!("Plugin kill");
377
378                self.app_into_config();
379
380                1 as _
381            }
382            sys::EVERYTHING_PLUGIN_PM_ADD_OPTIONS_PAGES => self.add_options_pages(data),
383            sys::EVERYTHING_PLUGIN_PM_LOAD_OPTIONS_PAGE => self.load_options_page(data),
384            sys::EVERYTHING_PLUGIN_PM_SAVE_OPTIONS_PAGE => self.save_options_page(data),
385            sys::EVERYTHING_PLUGIN_PM_GET_OPTIONS_PAGE_MINMAX => self.get_options_page_minmax(data),
386            sys::EVERYTHING_PLUGIN_PM_SIZE_OPTIONS_PAGE => self.size_options_page(data),
387            sys::EVERYTHING_PLUGIN_PM_OPTIONS_PAGE_PROC => self.options_page_proc(data),
388            sys::EVERYTHING_PLUGIN_PM_KILL_OPTIONS_PAGE => self.kill_options_page(data),
389            sys::EVERYTHING_PLUGIN_PM_SAVE_SETTINGS => self.save_settings(data),
390            _ => {
391                debug!(msg, ?data, "Plugin message");
392                0 as _
393            }
394        }
395    }
396
397    pub fn instance_name(&self) -> Option<&str> {
398        unsafe { &*self.instance_name.get() }.as_deref()
399    }
400
401    fn app_is_some(&self) -> bool {
402        let app = unsafe { &*self.app.get() };
403        app.is_some()
404    }
405
406    fn app_new(&self, config: Option<A::Config>) {
407        let app = unsafe { &mut *self.app.get() };
408        debug_assert!(app.is_none(), "App already inited");
409        *app = Some(A::new(config));
410        unsafe { app.as_ref().unwrap_unchecked() }.start();
411    }
412
413    fn app_into_config(&self) -> A::Config {
414        let app = unsafe { &mut *self.app.get() };
415        match app.take() {
416            Some(app) => app.into_config(),
417            None => unreachable!("App not inited"),
418        }
419    }
420
421    /// Not available during saving config and recreated afterwards. Use [`Self::with_app`] instead when possible.
422    pub unsafe fn app(&self) -> &A {
423        unsafe { &*self.app.get() }
424            .as_ref()
425            .expect("App not inited")
426    }
427
428    /// Not available during saving config.
429    pub fn with_app<T>(&self, f: impl FnOnce(&A) -> T) -> T {
430        f(unsafe { self.app() })
431    }
432}
433
434/// - [x] `instance_name` (non-official)
435/// - [x] `config_*`
436/// - [ ] `db_*`
437/// - [ ] `debug_*` (tracing)
438/// - [ ] `localization_get_*`
439/// - [x] `os_enable_or_disable_dlg_item`
440/// - [x] `os_get_(local_)?app_data_path_cat_filename`
441/// - [x] `plugin_?et_setting_string`
442/// - [ ] `property_*`
443/// - [x] `ui_options_add_plugin_page`
444/// - [x] `utf8_buf_(init|kill)`
445/// - [ ] `version_get_*`, `plugin_get_version`
446pub struct PluginHost {
447    get_proc_address: sys::everything_plugin_get_proc_address_t,
448}
449
450impl PluginHost {
451    pub fn new(get_proc_address: sys::everything_plugin_get_proc_address_t) -> Self {
452        Self { get_proc_address }
453    }
454
455    pub unsafe fn from_data(data: *mut c_void) -> Self {
456        Self::new(unsafe { mem::transmute(data) })
457    }
458
459    fn get_proc_address(
460        &self,
461    ) -> unsafe extern "system" fn(
462        name: *const sys::everything_plugin_utf8_t,
463    ) -> *mut ::std::os::raw::c_void {
464        unsafe { self.get_proc_address.unwrap_unchecked() }
465    }
466
467    /// You can `unwrap_unchecked()` if the API exists in all versions of Everything.
468    pub unsafe fn get<T: Copy>(&self, name: &str) -> Option<T> {
469        assert_eq!(mem::size_of::<T>(), mem::size_of::<fn()>());
470
471        trace!(name, "Plugin host get proc address");
472        let name = CString::new(name).unwrap();
473        let ptr = unsafe { (self.get_proc_address())(name.as_ptr() as _) };
474        if ptr.is_null() {
475            None
476        } else {
477            // let f: fn() = unsafe { mem::transmute(ptr) };
478            Some(unsafe { mem::transmute_copy(&ptr) })
479        }
480    }
481
482    /// Initialize a cbuf with an empty string.
483    ///
484    /// The cbuf must be killed with [`Self::utf8_buf_kill`]
485    ///
486    /// See also [`Self::utf8_buf_kill`]
487    ///
488    /// ## Note
489    /// Usage:
490    /// ```ignore
491    /// let mut cbuf = MaybeUninit::uninit();
492    /// host.utf8_buf_init(cbuf.as_mut_ptr());
493    ///
494    /// unsafe { os_get_app_data_path_cat_filename(filename.as_ptr() as _, cbuf.as_mut_ptr()) };
495    ///
496    /// // Or `utf8_buf_kill()`
497    /// self.utf8_buf_into_string(cbuf.as_mut_ptr())
498    /// ```
499    /// Do not move [`sys::everything_plugin_utf8_buf_t`].
500    pub fn utf8_buf_init(&self, cbuf: *mut sys::everything_plugin_utf8_buf_t) {
501        let utf8_buf_init: unsafe extern "system" fn(cbuf: *mut sys::everything_plugin_utf8_buf_t) =
502            unsafe { self.get("utf8_buf_init").unwrap_unchecked() };
503        unsafe { utf8_buf_init(cbuf) };
504    }
505
506    /// Kill a cbuf initialized with [`Self::utf8_buf_init`].
507    ///
508    /// Any allocated memory is returned to the system.
509    ///
510    /// See also [`Self::utf8_buf_init`]
511    pub fn utf8_buf_kill(&self, cbuf: *mut sys::everything_plugin_utf8_buf_t) {
512        let utf8_buf_kill: unsafe extern "system" fn(cbuf: *mut sys::everything_plugin_utf8_buf_t) =
513            unsafe { self.get("utf8_buf_kill").unwrap_unchecked() };
514        unsafe { utf8_buf_kill(cbuf) };
515    }
516
517    pub fn utf8_buf_into_string(&self, cbuf: *mut sys::everything_plugin_utf8_buf_t) -> String {
518        let s = unsafe { (*cbuf).to_string() };
519        self.utf8_buf_kill(cbuf);
520        s
521    }
522
523    pub fn ipc_window_from_main_thread() -> Option<IpcWindow> {
524        IpcWindow::from_current_thread()
525    }
526
527    pub fn instance_name_from_main_thread() -> Option<String> {
528        let ipc_window = Self::ipc_window_from_main_thread();
529        ipc_window.and_then(|w| w.instance_name().map(|s| s.to_string()))
530    }
531}
532
533impl Deref for sys::everything_plugin_utf8_buf_t {
534    type Target = str;
535
536    fn deref(&self) -> &Self::Target {
537        unsafe {
538            // str::from_raw_parts(self.buf, self.len)
539            str::from_utf8_unchecked(slice::from_raw_parts(self.buf, self.len))
540        }
541    }
542}