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_auto_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;
114pub mod log;
115pub mod macros;
116pub mod sys;
117pub mod ui;
118
119/// ## Example
120/// ```ignore
121/// #[derive(Serialize, Deserialize, Debug, Default)]
122/// pub struct Config {
123///     s: String,
124/// }
125///
126/// pub struct App {
127///     config: Config,
128/// }
129///
130/// impl PluginApp for App {
131///     type Config = Config;
132///
133///     fn new(config: Option<Self::Config>) -> Self {
134///         Self {
135///             config: config.unwrap_or_default(),
136///         }
137///     }
138///
139///     fn config(&self) -> &Self::Config {
140///         &self.config
141///     }
142///
143///     fn into_config(self) -> Self::Config {
144///         self.config
145///     }
146/// }
147/// ```
148pub trait PluginApp: 'static {
149    type Config: Config;
150
151    fn new(config: Option<Self::Config>) -> Self;
152
153    /// Can be used to start services requiring to access the [`PluginApp`] through [`PluginHandler::with_app()`] or [`PluginHandler::app()`].
154    fn start(&self) {}
155
156    fn config(&self) -> &Self::Config;
157
158    fn into_config(self) -> Self::Config;
159}
160
161/// ## Example
162/// ```ignore
163/// PluginHandler::builder()
164///     .name("Test Plugin")
165///     .description("A test plugin for Everything")
166///     .author("Chaoses-Ib")
167///     .version("0.1.0")
168///     .link("https://github.com/Chaoses-Ib/IbEverythingLib")
169///     .options_pages(vec![
170///         OptionsPage::builder()
171///             .name("Test Plugin")
172///             .load(ui::winio::spawn::<options::MainModel>)
173///             .build(),
174///     ])
175///     .build()
176/// ```
177///
178/// ## Design
179/// - 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.
180/// - User defined static to work around generic static limit.
181///   - Interior mutability to make it easy to use with `static`. But `UnsafeCell` to avoid cost.
182///
183/// Config lifetime:
184/// - May be set with [`PluginHandler::builder()`] (as default value)
185/// - May be loaded when [`sys::EVERYTHING_PLUGIN_PM_START`]
186/// - Be read when start
187/// - Be read when loading (and rendering) options pages ([`sys::EVERYTHING_PLUGIN_PM_LOAD_OPTIONS_PAGE`])
188/// - Be written/applied when [`sys::EVERYTHING_PLUGIN_PM_SAVE_OPTIONS_PAGE`], zero, one or multiple times
189///   - TODO: Defer
190/// - Be saved when [`sys::EVERYTHING_PLUGIN_PM_SAVE_SETTINGS`] (can occur without prior [`sys::EVERYTHING_PLUGIN_PM_SAVE_OPTIONS_PAGE`])
191#[derive(Builder)]
192pub struct PluginHandler<A: PluginApp> {
193    #[builder(skip)]
194    host: OnceCell<PluginHost>,
195
196    #[builder(with = |x: impl Into<String>| CString::new(x.into()).unwrap())]
197    name: Option<CString>,
198    #[builder(with = |x: impl Into<String>| CString::new(x.into()).unwrap())]
199    description: Option<CString>,
200    #[builder(with = |x: impl Into<String>| CString::new(x.into()).unwrap())]
201    author: Option<CString>,
202    #[builder(with = |x: impl Into<String>| CString::new(x.into()).unwrap())]
203    version: Option<CString>,
204    #[builder(with = |x: impl Into<String>| CString::new(x.into()).unwrap())]
205    link: Option<CString>,
206
207    #[builder(skip)]
208    app: UnsafeCell<Option<A>>,
209
210    #[builder(default)]
211    options_pages: Vec<ui::OptionsPage<A>>,
212    #[builder(skip)]
213    options_message: Cell<ui::OptionsMessage>,
214
215    /// TODO: Feature cfg?
216    #[builder(skip)]
217    instance_name: UnsafeCell<Option<String>>,
218}
219
220unsafe impl<A: PluginApp> Send for PluginHandler<A> {}
221unsafe impl<A: PluginApp> Sync for PluginHandler<A> {}
222
223impl<A: PluginApp> PluginHandler<A> {
224    /// Panics if already initialized.
225    pub fn init_start(&self) {
226        self.handle(sys::EVERYTHING_PLUGIN_PM_INIT, 0 as _);
227        self.handle(sys::EVERYTHING_PLUGIN_PM_START, 0 as _);
228    }
229
230    /// Panics if already initialized.
231    pub fn init_start_with_config(&self, config: A::Config) {
232        self.handle(sys::EVERYTHING_PLUGIN_PM_INIT, 0 as _);
233        self.handle(
234            sys::EVERYTHING_PLUGIN_PM_START,
235            Box::into_raw(Box::new(config)) as _,
236        );
237    }
238
239    /// Panics if not initialized or already stopped.
240    pub fn stop_kill(&self) {
241        self.handle(sys::EVERYTHING_PLUGIN_PM_STOP, 0 as _);
242        self.handle(sys::EVERYTHING_PLUGIN_PM_KILL, 0 as _);
243    }
244
245    /// `None` before handling `EVERYTHING_PLUGIN_PM_INIT`
246    pub fn get_host(&self) -> Option<&PluginHost> {
247        self.host.get()
248    }
249
250    /// Not available before handling `EVERYTHING_PLUGIN_PM_INIT`
251    pub fn host(&self) -> &PluginHost {
252        debug_assert!(self.get_host().is_some(), "Plugin host not inited");
253        unsafe { self.get_host().unwrap_unchecked() }
254    }
255
256    pub fn handle_init_i18n(_msg: u32, _data: *mut c_void) {
257        #[cfg(feature = "rust-i18n")]
258        if _msg == sys::EVERYTHING_PLUGIN_PM_INIT {
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
270    /// You shouldn't and unlikely need to call this function from multiple threads.
271    pub fn handle(&self, msg: u32, data: *mut c_void) -> *mut c_void {
272        match msg {
273            sys::EVERYTHING_PLUGIN_PM_INIT => {
274                #[cfg(feature = "tracing")]
275                log::tracing_init();
276                debug!("Plugin init");
277
278                if !data.is_null() {
279                    _ = self.host.set(unsafe { PluginHost::from_data(data) });
280                }
281
282                *unsafe { &mut *self.instance_name.get() } =
283                    PluginHost::instance_name_from_main_thread();
284                debug!(instance_name = ?self.instance_name());
285
286                // #[cfg(feature = "rust-i18n")]
287                // rust_i18n::set_locale(&self.get_language_name());
288
289                1 as _
290            }
291            sys::EVERYTHING_PLUGIN_PM_GET_PLUGIN_VERSION => sys::EVERYTHING_PLUGIN_VERSION as _,
292            sys::EVERYTHING_PLUGIN_PM_GET_NAME => {
293                debug!("Plugin get name");
294                match &self.name {
295                    Some(name) => name.as_ptr() as _,
296                    None => 0 as _,
297                }
298            }
299            sys::EVERYTHING_PLUGIN_PM_GET_DESCRIPTION => {
300                debug!("Plugin get description");
301                match &self.description {
302                    Some(description) => description.as_ptr() as _,
303                    None => 0 as _,
304                }
305            }
306            sys::EVERYTHING_PLUGIN_PM_GET_AUTHOR => {
307                debug!("Plugin get author");
308                match &self.author {
309                    Some(author) => author.as_ptr() as _,
310                    None => 0 as _,
311                }
312            }
313            sys::EVERYTHING_PLUGIN_PM_GET_VERSION => {
314                debug!("Plugin get version");
315                match &self.version {
316                    Some(version) => version.as_ptr() as _,
317                    None => 0 as _,
318                }
319            }
320            sys::EVERYTHING_PLUGIN_PM_GET_LINK => {
321                debug!("Plugin get link");
322                match &self.link {
323                    Some(link) => link.as_ptr() as _,
324                    None => 0 as _,
325                }
326            }
327            sys::EVERYTHING_PLUGIN_PM_START => {
328                debug!("Plugin start");
329
330                self.app_new(self.load_settings(data));
331
332                1 as _
333            }
334            sys::EVERYTHING_PLUGIN_PM_STOP => {
335                debug!("Plugin stop");
336
337                // TODO
338
339                1 as _
340            }
341            sys::EVERYTHING_PLUGIN_PM_UNINSTALL => {
342                debug!("Plugin uninstall");
343
344                // TODO
345
346                1 as _
347            }
348            // Always the last message sent to the plugin
349            sys::EVERYTHING_PLUGIN_PM_KILL => {
350                debug!("Plugin kill");
351
352                self.app_into_config();
353
354                1 as _
355            }
356            sys::EVERYTHING_PLUGIN_PM_ADD_OPTIONS_PAGES => self.add_options_pages(data),
357            sys::EVERYTHING_PLUGIN_PM_LOAD_OPTIONS_PAGE => self.load_options_page(data),
358            sys::EVERYTHING_PLUGIN_PM_SAVE_OPTIONS_PAGE => self.save_options_page(data),
359            sys::EVERYTHING_PLUGIN_PM_GET_OPTIONS_PAGE_MINMAX => self.get_options_page_minmax(data),
360            sys::EVERYTHING_PLUGIN_PM_SIZE_OPTIONS_PAGE => self.size_options_page(data),
361            sys::EVERYTHING_PLUGIN_PM_OPTIONS_PAGE_PROC => self.options_page_proc(data),
362            sys::EVERYTHING_PLUGIN_PM_KILL_OPTIONS_PAGE => self.kill_options_page(data),
363            sys::EVERYTHING_PLUGIN_PM_SAVE_SETTINGS => self.save_settings(data),
364            _ => {
365                debug!(msg, ?data, "Plugin message");
366                0 as _
367            }
368        }
369    }
370
371    pub fn instance_name(&self) -> Option<&str> {
372        unsafe { &*self.instance_name.get() }.as_deref()
373    }
374
375    fn app_new(&self, config: Option<A::Config>) {
376        let app = unsafe { &mut *self.app.get() };
377        debug_assert!(app.is_none(), "App already inited");
378        *app = Some(A::new(config));
379        unsafe { app.as_ref().unwrap_unchecked() }.start();
380    }
381
382    fn app_into_config(&self) -> A::Config {
383        let app = unsafe { &mut *self.app.get() };
384        match app.take() {
385            Some(app) => app.into_config(),
386            None => unreachable!("App not inited"),
387        }
388    }
389
390    /// Not available during saving config and recreated afterwards. Use [`Self::with_app`] instead when possible.
391    pub unsafe fn app(&self) -> &A {
392        unsafe { &*self.app.get() }
393            .as_ref()
394            .expect("App not inited")
395    }
396
397    /// Not available during saving config.
398    pub fn with_app<T>(&self, f: impl FnOnce(&A) -> T) -> T {
399        f(unsafe { self.app() })
400    }
401}
402
403/// - [x] `instance_name` (non-official)
404/// - [x] `config_*`
405/// - [ ] `db_*`
406/// - [ ] `debug_*` (tracing)
407/// - [ ] `localization_get_*`
408/// - [x] `os_enable_or_disable_dlg_item`
409/// - [x] `os_get_(local_)?app_data_path_cat_filename`
410/// - [x] `plugin_?et_setting_string`
411/// - [ ] `property_*`
412/// - [x] `ui_options_add_plugin_page`
413/// - [x] `utf8_buf_(init|kill)`
414/// - [ ] `version_get_*`, `plugin_get_version`
415pub struct PluginHost {
416    get_proc_address: sys::everything_plugin_get_proc_address_t,
417}
418
419impl PluginHost {
420    pub fn new(get_proc_address: sys::everything_plugin_get_proc_address_t) -> Self {
421        Self { get_proc_address }
422    }
423
424    pub unsafe fn from_data(data: *mut c_void) -> Self {
425        Self::new(unsafe { mem::transmute(data) })
426    }
427
428    fn get_proc_address(
429        &self,
430    ) -> unsafe extern "system" fn(
431        name: *const sys::everything_plugin_utf8_t,
432    ) -> *mut ::std::os::raw::c_void {
433        unsafe { self.get_proc_address.unwrap_unchecked() }
434    }
435
436    /// You can `unwrap_unchecked()` if the API exists in all versions of Everything.
437    pub unsafe fn get<T: Copy>(&self, name: &str) -> Option<T> {
438        assert_eq!(mem::size_of::<T>(), mem::size_of::<fn()>());
439
440        trace!(name, "Plugin host get proc address");
441        let name = CString::new(name).unwrap();
442        let ptr = unsafe { (self.get_proc_address())(name.as_ptr() as _) };
443        if ptr.is_null() {
444            None
445        } else {
446            // let f: fn() = unsafe { mem::transmute(ptr) };
447            Some(unsafe { mem::transmute_copy(&ptr) })
448        }
449    }
450
451    /// Initialize a cbuf with an empty string.
452    ///
453    /// The cbuf must be killed with [`Self::utf8_buf_kill`]
454    ///
455    /// See also [`Self::utf8_buf_kill`]
456    ///
457    /// ## Note
458    /// Usage:
459    /// ```ignore
460    /// let mut cbuf = MaybeUninit::uninit();
461    /// host.utf8_buf_init(cbuf.as_mut_ptr());
462    ///
463    /// unsafe { os_get_app_data_path_cat_filename(filename.as_ptr() as _, cbuf.as_mut_ptr()) };
464    ///
465    /// // Or `utf8_buf_kill()`
466    /// self.utf8_buf_into_string(cbuf.as_mut_ptr())
467    /// ```
468    /// Do not move [`sys::everything_plugin_utf8_buf_t`].
469    pub fn utf8_buf_init(&self, cbuf: *mut sys::everything_plugin_utf8_buf_t) {
470        let utf8_buf_init: unsafe extern "system" fn(cbuf: *mut sys::everything_plugin_utf8_buf_t) =
471            unsafe { self.get("utf8_buf_init").unwrap_unchecked() };
472        unsafe { utf8_buf_init(cbuf) };
473    }
474
475    /// Kill a cbuf initialized with [`Self::utf8_buf_init`].
476    ///
477    /// Any allocated memory is returned to the system.
478    ///
479    /// See also [`Self::utf8_buf_init`]
480    pub fn utf8_buf_kill(&self, cbuf: *mut sys::everything_plugin_utf8_buf_t) {
481        let utf8_buf_kill: unsafe extern "system" fn(cbuf: *mut sys::everything_plugin_utf8_buf_t) =
482            unsafe { self.get("utf8_buf_kill").unwrap_unchecked() };
483        unsafe { utf8_buf_kill(cbuf) };
484    }
485
486    pub fn utf8_buf_into_string(&self, cbuf: *mut sys::everything_plugin_utf8_buf_t) -> String {
487        let s = unsafe { (*cbuf).to_string() };
488        self.utf8_buf_kill(cbuf);
489        s
490    }
491
492    pub fn ipc_window_from_main_thread() -> Option<IpcWindow> {
493        IpcWindow::from_current_thread()
494    }
495
496    pub fn instance_name_from_main_thread() -> Option<String> {
497        let ipc_window = Self::ipc_window_from_main_thread();
498        ipc_window.and_then(|w| w.instance_name().map(|s| s.to_string()))
499    }
500}
501
502impl Deref for sys::everything_plugin_utf8_buf_t {
503    type Target = str;
504
505    fn deref(&self) -> &Self::Target {
506        unsafe {
507            // str::from_raw_parts(self.buf, self.len)
508            str::from_utf8_unchecked(slice::from_raw_parts(self.buf, self.len))
509        }
510    }
511}