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}