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}