Skip to main content

miracle_plugin/
plugin.rs

1use crate::animation::{AnimationFrameData, AnimationFrameResult};
2use crate::config::Configuration;
3use crate::host::*;
4use crate::input::{KeyboardEvent, PointerEvent};
5use crate::output::*;
6use crate::placement::Placement;
7use crate::window::Window;
8use crate::workspace::*;
9
10unsafe extern "C" {
11    fn miracle_get_plugin_handle() -> u32;
12}
13
14/// Retrieve the raw JSON string of userdata configured for this plugin in the YAML config.
15///
16/// Returns `None` if no userdata was provided. The JSON object contains all keys from
17/// the plugin's config entry except `path`.
18pub fn get_userdata_json() -> Option<String> {
19    let handle = unsafe { miracle_get_plugin_handle() };
20    let mut buf = vec![0u8; 4096];
21    loop {
22        let result = unsafe {
23            crate::host::miracle_get_plugin_userdata(
24                handle,
25                buf.as_mut_ptr() as i32,
26                buf.len() as i32,
27            )
28        };
29        if result == 0 {
30            return None;
31        } else if result == -1 {
32            buf.resize(buf.len() * 2, 0);
33        } else {
34            return std::str::from_utf8(&buf[..result as usize])
35                .ok()
36                .map(|s| s.to_owned());
37        }
38    }
39}
40
41/// The core plugin trait.
42///
43/// Implement this trait to respond to compositor events. All methods have default no-op
44/// implementations, so you only need to override the hooks you care about.
45///
46/// Register your implementation with the [`miracle_plugin!`] macro:
47///
48/// ```rust,ignore
49/// #[derive(Default)]
50/// struct MyPlugin;
51/// impl Plugin for MyPlugin {}
52/// miracle_plugin::miracle_plugin!(MyPlugin);
53/// ```
54pub trait Plugin {
55    /// Handles the window opening animation.
56    ///
57    /// If None is returned, the animation is not handled by this plugin.
58    fn window_open_animation(
59        &mut self,
60        _data: &AnimationFrameData,
61        _window: &Window,
62    ) -> Option<AnimationFrameResult> {
63        None
64    }
65
66    /// Handles the window closing animation.
67    ///
68    /// If None is returned, the animation is not handled by this plugin.
69    fn window_close_animation(
70        &mut self,
71        _data: &AnimationFrameData,
72        _window: &Window,
73    ) -> Option<AnimationFrameResult> {
74        None
75    }
76
77    /// Handles the window movement animation.
78    ///
79    /// If None is returned, the animation is not handled by this plugin.
80    fn window_move_animation(
81        &mut self,
82        _data: &AnimationFrameData,
83        _window: &Window,
84    ) -> Option<AnimationFrameResult> {
85        None
86    }
87
88    /// Handles the workspace switching animation.
89    ///
90    /// If None is returned, the animation is not handled by this plugin.
91    fn workspace_switch_animation(
92        &mut self,
93        _data: &AnimationFrameData,
94        _workspace: &Workspace,
95    ) -> Option<AnimationFrameResult> {
96        None
97    }
98
99    /// Dictate the placement of a newly opened window.
100    ///
101    /// Return a [`Placement`] to override where and how the window is placed.
102    /// Return `None` to let the compositor handle placement normally.
103    fn place_new_window(&mut self, _info: &Window) -> Option<Placement> {
104        None
105    }
106
107    /// Called when a window is about to be deleted.
108    ///
109    /// The window info is still valid at this point (the window has not yet
110    /// been removed from the compositor).
111    fn window_deleted(&mut self, _info: &Window) {}
112
113    /// Called when a window gains focus.
114    fn window_focused(&mut self, _info: &Window) {}
115
116    /// Called when a window loses focus.
117    fn window_unfocused(&mut self, _info: &Window) {}
118
119    /// Called when a workspace is created.
120    fn workspace_created(&mut self, _workspace: &Workspace) {}
121
122    /// Called when a workspace is removed.
123    fn workspace_removed(&mut self, _workspace: &Workspace) {}
124
125    /// Called when a workspace gains focus.
126    ///
127    /// `previous_id` is the internal ID of the previously focused workspace, if any.
128    fn workspace_focused(&mut self, _previous_id: Option<u64>, _current: &Workspace) {}
129
130    /// Called when a workspace's area (geometry) changes.
131    fn workspace_area_changed(&mut self, _workspace: &Workspace) {}
132
133    /// Called when a window's workspace has changed.
134    ///
135    /// This fires whenever a window is moved to a different workspace,
136    /// whether initiated by the user, a command, or a plugin.
137    fn window_workspace_changed(&mut self, _info: &Window, _workspace: &Workspace) {}
138
139    /// Called on every config reload. Return a [`Configuration`] with the fields
140    /// this plugin wants to override, or `None` to leave the compositor's config unchanged.
141    ///
142    /// Only fields that are `Some(...)` are applied; `None` fields are ignored.
143    /// The `plugins` key cannot be set by plugins and is intentionally absent from
144    /// [`Configuration`].
145    ///
146    /// Results from all loaded plugins are merged before being applied to the
147    /// file-based configuration (last-loaded plugin wins on field conflicts).
148    fn configure(&mut self) -> Option<Configuration> {
149        None
150    }
151
152    /// Handle a keyboard event.
153    ///
154    /// If the plugin returns `false`, the event is propagated to the next
155    /// handler in Miracle. If the plugin returns `true`, then the event is
156    /// consumed by the plugin.
157    fn handle_keyboard_input(&mut self, _event: KeyboardEvent) -> bool {
158        false
159    }
160
161    /// Handle a pointer event.
162    ///
163    /// If the plugin returns `false`, the event is propagated to the next
164    /// handler in Miracle. If the plugin returns `true`, then the event is
165    /// consumed by the plugin.
166    fn handle_pointer_event(&mut self, _event: PointerEvent) -> bool {
167        false
168    }
169
170    /// Handle an IPC command addressed to this plugin's namespace.
171    ///
172    /// `payload` is the raw JSON string sent by the IPC client under the command's
173    /// `"payload"` field. Return `Some(response_json)` to reply with an arbitrary JSON
174    /// string, or `None` to decline the command (the client receives `success: false`).
175    ///
176    /// Only invoked when this plugin has claimed a namespace via [`register_namespace`]
177    /// and the incoming command targets that namespace.
178    fn handle_command(&mut self, _payload: &str) -> Option<String> {
179        None
180    }
181}
182
183/// Lists the windows that are managed by this plugin.
184///
185/// A window that is managed by this plugin had to have been placed
186/// via a freestyle placement strategy, otherwise the tiling manager
187/// or the system is handling it independently.
188///
189/// Each returned [`Window`] exposes both read-only fields and setter methods
190/// for mutating the window's state, workspace, size, transform, and alpha.
191pub fn managed_windows() -> Vec<Window> {
192    let handle = unsafe { miracle_get_plugin_handle() };
193    let count = unsafe { miracle_num_managed_windows(handle) };
194
195    (0..count)
196        .filter_map(|i| {
197            const NAME_BUF_LEN: usize = 256;
198            let mut window_info =
199                std::mem::MaybeUninit::<crate::bindings::miracle_window_info_t>::uninit();
200            let mut name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
201
202            unsafe {
203                let result = miracle_get_managed_window_at(
204                    handle,
205                    i,
206                    window_info.as_mut_ptr() as i32,
207                    name_buf.as_mut_ptr() as i32,
208                    NAME_BUF_LEN as i32,
209                );
210
211                if result != 0 {
212                    return None;
213                }
214
215                let window_info = window_info.assume_init();
216                let name_len = name_buf
217                    .iter()
218                    .position(|&c| c == 0)
219                    .unwrap_or(NAME_BUF_LEN);
220                let name = String::from_utf8_lossy(&name_buf[..name_len]).into_owned();
221
222                Some(Window::from_c_with_name(&window_info, name))
223            }
224        })
225        .collect()
226}
227
228/// Register a namespace that this plugin owns for IPC commands and events.
229///
230/// Once registered, IPC clients can send commands to `{ "plugin": <ns>, "payload": ... }`
231/// (routed to [`Plugin::handle_command`]) and subscribe to this plugin's events by `<ns>`.
232///
233/// A plugin may own at most one namespace, and each namespace may be owned by at most one
234/// plugin. Returns `Ok(())` on success, or `Err(())` if this plugin already owns a
235/// namespace or `ns` is already taken by another plugin.
236pub fn register_namespace(ns: &str) -> Result<(), ()> {
237    let handle = unsafe { miracle_get_plugin_handle() };
238    let result = unsafe {
239        miracle_plugin_register_namespace(handle as i32, ns.as_ptr() as i32, ns.len() as i32)
240    };
241    if result == 0 {
242        Ok(())
243    } else {
244        Err(())
245    }
246}
247
248/// Publish an event on this plugin's registered namespace.
249///
250/// `payload` should be a JSON string; it is delivered to every IPC client subscribed to
251/// this plugin's namespace, wrapped as `{ "plugin": <ns>, "payload": <payload> }`.
252///
253/// Returns `Ok(())` on success, or `Err(())` if this plugin has not registered a namespace.
254pub fn publish_event(payload: &str) -> Result<(), ()> {
255    let handle = unsafe { miracle_get_plugin_handle() };
256    let result = unsafe {
257        miracle_plugin_publish_event(handle as i32, payload.as_ptr() as i32, payload.len() as i32)
258    };
259    if result == 0 {
260        Ok(())
261    } else {
262        Err(())
263    }
264}
265
266/// Get the number of outputs.
267pub fn num_outputs() -> u32 {
268    unsafe { miracle_num_outputs() }
269}
270
271/// Get an output by index.
272///
273/// Returns `None` if the index is out of bounds or if the call fails.
274pub fn get_output_at(index: u32) -> Option<Output> {
275    if index >= num_outputs() {
276        return None;
277    }
278
279    const NAME_BUF_LEN: usize = 256;
280    let mut output = std::mem::MaybeUninit::<crate::bindings::miracle_output_t>::uninit();
281    let mut name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
282
283    unsafe {
284        let result = miracle_get_output_at(
285            index,
286            output.as_mut_ptr() as i32,
287            name_buf.as_mut_ptr() as i32,
288            NAME_BUF_LEN as i32,
289        );
290
291        if result != 0 {
292            return None;
293        }
294
295        let output = output.assume_init();
296
297        // Find the null terminator to get the actual string length
298        let name_len = name_buf
299            .iter()
300            .position(|&c| c == 0)
301            .unwrap_or(NAME_BUF_LEN);
302        let name = String::from_utf8_lossy(&name_buf[..name_len]).into_owned();
303
304        Some(Output::from_c_with_name(&output, name))
305    }
306}
307
308/// Get all outputs.
309pub fn get_outputs() -> Vec<Output> {
310    let count = num_outputs();
311    (0..count).filter_map(get_output_at).collect()
312}
313
314/// Get the currently active workspace on the focused output.
315///
316/// Returns `None` if there is no focused output or no active workspace.
317pub fn get_active_workspace() -> Option<Workspace> {
318    const NAME_BUF_LEN: usize = 256;
319    let mut workspace = std::mem::MaybeUninit::<crate::bindings::miracle_workspace_t>::uninit();
320    let mut name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
321
322    unsafe {
323        let result = miracle_get_active_workspace(
324            workspace.as_mut_ptr() as i32,
325            name_buf.as_mut_ptr() as i32,
326            NAME_BUF_LEN as i32,
327        );
328
329        if result != 0 {
330            return None;
331        }
332
333        let workspace = workspace.assume_init();
334        if workspace.is_set == 0 {
335            return None;
336        }
337
338        let name_len = name_buf
339            .iter()
340            .position(|&c| c == 0)
341            .unwrap_or(NAME_BUF_LEN);
342        let name = String::from_utf8_lossy(&name_buf[..name_len]).into_owned();
343
344        Some(Workspace::from_c_with_name(&workspace, name))
345    }
346}
347
348/// Request a workspace by optional number and/or name.
349///
350/// If a workspace with the given number or name already exists, it is returned.
351/// Otherwise, a new workspace is created on the focused output.
352///
353/// If `focus` is true, the workspace will be focused after creation/lookup.
354///
355/// Returns `None` if the workspace could not be created.
356pub fn request_workspace(
357    number: Option<u32>,
358    name: Option<&str>,
359    focus: bool,
360) -> Option<Workspace> {
361    const NAME_BUF_LEN: usize = 256;
362    let mut workspace = std::mem::MaybeUninit::<crate::bindings::miracle_workspace_t>::uninit();
363    let mut out_name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
364
365    let has_number: i32 = if number.is_some() { 1 } else { 0 };
366    let number_val: i32 = number.unwrap_or(0) as i32;
367
368    let name_ptr: i32 = match name {
369        Some(s) => s.as_ptr() as i32,
370        None => 0,
371    };
372    let name_len: i32 = match name {
373        Some(s) => s.len() as i32,
374        None => 0,
375    };
376
377    unsafe {
378        let result = miracle_request_workspace(
379            has_number,
380            number_val,
381            name_ptr,
382            name_len,
383            workspace.as_mut_ptr() as i32,
384            out_name_buf.as_mut_ptr() as i32,
385            NAME_BUF_LEN as i32,
386            if focus { 1 } else { 0 },
387        );
388
389        if result != 0 {
390            return None;
391        }
392
393        let workspace = workspace.assume_init();
394        if workspace.is_set == 0 {
395            return None;
396        }
397
398        let out_name_len = out_name_buf
399            .iter()
400            .position(|&c| c == 0)
401            .unwrap_or(NAME_BUF_LEN);
402        let ws_name = String::from_utf8_lossy(&out_name_buf[..out_name_len]).into_owned();
403
404        Some(Workspace::from_c_with_name(&workspace, ws_name))
405    }
406}
407
408/// Queue a custom per-frame animation with a callback.
409///
410/// `callback` receives `(animation_id, dt, elapsed_seconds)` every frame.
411/// The compositor automatically removes the animation after `duration_seconds`.
412///
413/// `dt` and `elapsed_seconds` are floats in seconds.
414///
415/// Returns the host-generated animation ID on success, or `None` on error.
416pub fn queue_custom_animation<F>(callback: F, duration_seconds: f32) -> Option<u32>
417where
418    F: FnMut(u32, f32, f32) + 'static,
419{
420    let handle = unsafe { miracle_get_plugin_handle() };
421    let mut animation_id: u32 = 0;
422    let mut dur = duration_seconds;
423    let result = unsafe {
424        crate::host::miracle_queue_custom_animation(
425            handle as i32,
426            &mut animation_id as *mut u32 as i32,
427            &mut dur as *mut f32 as i32,
428        )
429    };
430    if result == 0 {
431        custom_anim_callbacks().insert(animation_id, (Box::new(callback), duration_seconds));
432        Some(animation_id)
433    } else {
434        None
435    }
436}
437
438/// Stop a custom animation queued with [`queue_custom_animation`] before its
439/// duration elapses. Its callback is not called again.
440///
441/// Returns `Err` if the animation is not running.
442pub fn cancel_custom_animation(animation_id: u32) -> Result<(), ()> {
443    if custom_anim_callbacks().remove(&animation_id).is_none() {
444        // The callback may be the one running right now, in which case it is out of
445        // the registry until it returns. Make sure it is not put back.
446        let running = unsafe { &mut *std::ptr::addr_of_mut!(_RUNNING_CUSTOM_ANIM) };
447        if let Some((id, cancelled)) = running {
448            if *id == animation_id {
449                *cancelled = true;
450            }
451        }
452    }
453    let handle = unsafe { miracle_get_plugin_handle() };
454    let r = unsafe { crate::host::miracle_cancel_custom_animation(handle as i32, animation_id as i32) };
455    if r == 0 { Ok(()) } else { Err(()) }
456}
457
458/// Register a multi-pass GLSL shader with the compositor.
459///
460/// Each element of `passes` is a complete `sample_to_rgba(vec2 texcoord)` function.
461/// The compositor chains them so that pass *i* samples the output of pass *i-1*:
462///
463/// - **Pass 0** — `tex` is the window's content texture; `tex_source` is the same.
464/// - **Pass i (i > 0)** — `tex` is the output of pass *i-1*; `tex_source` is
465///   always the original window content, useful for combining a processed result
466///   with the untouched original (e.g. bloom = blur + original).
467/// - **Final pass** — the alpha mask, rounded-corner SDF, and window transform are
468///   applied after `sample_to_rgba` returns, exactly as with a single-pass shader.
469/// - `surfaceSize` (the window size in pixels) is available in every pass.
470///
471/// Returns the shader ID on success (pass to [`crate::window::Window::set_shader`]),
472/// or `None` if registration failed.
473pub fn register_window_shader(passes: &[&str]) -> Option<u8> {
474    // Build an array of [ptr, len] descriptors pointing into WASM linear memory.
475    // In wasm32 Rust pointers are linear-memory offsets, so casting to i32 is correct.
476    let descriptors: Vec<[i32; 2]> = passes
477        .iter()
478        .map(|p| [p.as_ptr() as i32, p.len() as i32])
479        .collect();
480    let handle = unsafe { miracle_get_plugin_handle() };
481    let result = unsafe {
482        crate::host::miracle_register_window_sample_to_rgba(
483            handle as i32,
484            descriptors.as_ptr() as i32,
485            passes.len() as i32,
486        )
487    };
488    if result >= 0 {
489        Some(result as u8)
490    } else {
491        None
492    }
493}
494
495/// Register a geometry shader with the compositor.
496///
497/// `source` is a complete GLSL ES 3.20 geometry shader. It runs between the
498/// compositor's vertex and fragment stages, for both the window content and its
499/// border, and must follow this interface:
500///
501/// ```glsl
502/// #version 320 es
503/// layout(triangles) in;
504/// layout(triangle_strip, max_vertices = N) out;
505///
506/// in vec2 v_texcoord[];          // pass through, interpolated, to g_texcoord
507/// in vec2 v_local[];             // [0, 1] across the window, (0, 0) at the top left
508/// in vec4 v_world[];             // position in screen pixels
509/// out vec2 g_texcoord;
510///
511/// uniform mat4 u_world_to_clip;  // gl_Position = u_world_to_clip * world
512/// uniform vec2 u_window_size;    // window size in pixels
513/// ```
514///
515/// It may also declare any uniforms set with
516/// [`crate::window::Window::set_shader_uniform`].
517///
518/// The content and the border only line up if the displacement is a function of
519/// `v_local`, `v_world` and the uniforms alone. Displaced vertices may leave the
520/// window's bounds.
521///
522/// Geometry shaders require an OpenGL ES 3.2 context. Without one, or if the shader
523/// fails to compile, the compositor logs a warning and draws the window without it.
524///
525/// Returns the shader ID on success (pass to
526/// [`crate::window::Window::set_geometry_shader`]), or `None` if registration failed.
527pub fn register_window_geometry_shader(source: &str) -> Option<u8> {
528    let handle = unsafe { miracle_get_plugin_handle() };
529    // In wasm32 Rust pointers are linear-memory offsets, so casting to i32 is correct.
530    let result = unsafe {
531        crate::host::miracle_register_window_geometry_shader(
532            handle as i32,
533            source.as_ptr() as i32,
534            source.len() as i32,
535        )
536    };
537    if result >= 0 {
538        Some(result as u8)
539    } else {
540        None
541    }
542}
543
544/// Set or clear the full-screen (output) shader.
545///
546/// Each element of `passes` is a complete `vec4 sample_to_rgba(in vec2 texcoord)`
547/// GLSL function applied as a post-process over the whole composited output.
548/// Like [`register_window_shader`] the passes are chained: pass *i* reads pass
549/// *i-1*'s output as `tex`, `tex_source` is always the original screen content,
550/// and `surfaceSize` is the output size in pixels. Two optional time uniforms
551/// are also available: `time` (seconds since the compositor started) and
552/// `timeOfDay` (seconds since local midnight). Pass an empty slice to clear the
553/// shader and revert to the configured `output_filter.shader_path`.
554///
555/// The plugin's screen shader overrides the config path until cleared or the
556/// plugin unloads. Returns `Ok` on success, `Err` on failure.
557pub fn set_screen_shader(passes: &[&str]) -> Result<(), ()> {
558    let handle = unsafe { miracle_get_plugin_handle() };
559    // In wasm32 Rust pointers are linear-memory offsets, so casting to i32 is correct.
560    let descriptors: Vec<[i32; 2]> = passes
561        .iter()
562        .map(|p| [p.as_ptr() as i32, p.len() as i32])
563        .collect();
564    let r = unsafe {
565        crate::host::miracle_set_screen_shader(
566            handle as i32,
567            descriptors.as_ptr() as i32,
568            passes.len() as i32,
569        )
570    };
571    if r == 0 {
572        Ok(())
573    } else {
574        Err(())
575    }
576}
577
578static mut _CUSTOM_ANIM_CALLBACKS: Option<
579    std::collections::HashMap<u32, (Box<dyn FnMut(u32, f32, f32)>, f32)>,
580> = None;
581
582/// The custom animation whose callback is running, and whether it has been cancelled
583/// from inside that callback.
584static mut _RUNNING_CUSTOM_ANIM: Option<(u32, bool)> = None;
585
586/// Runs one frame of the custom animation \p animation_id.
587///
588/// The callback is taken out of the registry while it runs, so that it may queue or
589/// cancel custom animations (including itself) without aliasing the registry.
590#[doc(hidden)]
591pub fn run_custom_animation(animation_id: u32, dt: f32, elapsed_seconds: f32) {
592    let Some((mut cb, duration)) = custom_anim_callbacks().remove(&animation_id) else {
593        return;
594    };
595
596    unsafe { _RUNNING_CUSTOM_ANIM = Some((animation_id, false)) };
597    cb(animation_id, dt, elapsed_seconds);
598    let cancelled = unsafe { (*std::ptr::addr_of_mut!(_RUNNING_CUSTOM_ANIM)).take() }
599        .is_some_and(|(_, cancelled)| cancelled);
600
601    if !cancelled && elapsed_seconds < duration {
602        custom_anim_callbacks().insert(animation_id, (cb, duration));
603    }
604}
605
606/// Returns the global custom-animation callback registry.
607///
608/// # Safety
609/// Only safe in a single-threaded WASM context (which is always the case for miracle plugins).
610#[doc(hidden)]
611pub fn custom_anim_callbacks()
612-> &'static mut std::collections::HashMap<u32, (Box<dyn FnMut(u32, f32, f32)>, f32)> {
613    unsafe {
614        if (*std::ptr::addr_of!(_CUSTOM_ANIM_CALLBACKS)).is_none() {
615            _CUSTOM_ANIM_CALLBACKS = Some(std::collections::HashMap::new());
616        }
617        (*std::ptr::addr_of_mut!(_CUSTOM_ANIM_CALLBACKS))
618            .as_mut()
619            .unwrap()
620    }
621}
622
623/// Registers a type as a miracle-wm plugin.
624///
625/// The type must implement [`Default`] and [`Plugin`]. This macro generates all of the
626/// required WASM export functions (`init`, `animate`, `place_new_window`, etc.) that the
627/// compositor calls at runtime.
628///
629/// # Example
630///
631/// ```rust,ignore
632/// #[derive(Default)]
633/// struct MyPlugin;
634/// impl Plugin for MyPlugin {}
635/// miracle_plugin!(MyPlugin);
636/// ```
637#[macro_export]
638macro_rules! miracle_plugin {
639    ($plugin_type:ty) => {
640        static mut _MIRACLE_PLUGIN: Option<$plugin_type> = None;
641        static mut _MIRACLE_PLUGIN_HANDLE: u32 = 0;
642
643        #[unsafe(no_mangle)]
644        pub extern "C" fn miracle_get_plugin_handle() -> u32 {
645            unsafe { _MIRACLE_PLUGIN_HANDLE }
646        }
647
648        #[unsafe(no_mangle)]
649        pub extern "C" fn init(handle: i32) {
650            unsafe {
651                _MIRACLE_PLUGIN_HANDLE = handle as u32;
652                _MIRACLE_PLUGIN = Some(<$plugin_type>::default());
653            }
654        }
655
656        #[unsafe(no_mangle)]
657        pub extern "C" fn animate(data_ptr: i32, result_ptr: i32) -> i32 {
658            let plugin = unsafe {
659                match _MIRACLE_PLUGIN.as_mut() {
660                    Some(p) => p,
661                    None => return 0,
662                }
663            };
664
665            let c_data = unsafe {
666                &*(data_ptr as *const $crate::bindings::miracle_plugin_animation_frame_data_t)
667            };
668            let data: $crate::animation::AnimationFrameData = (*c_data).into();
669
670            let extract_window = || {
671                let bytes = unsafe {
672                    core::slice::from_raw_parts(c_data.window_name.as_ptr() as *const u8, 256)
673                };
674                let len = bytes.iter().position(|&b| b == 0).unwrap_or(256);
675                let name = String::from_utf8_lossy(&bytes[..len]).into_owned();
676                unsafe { $crate::window::Window::from_c_with_name(&c_data.window_info, name) }
677            };
678
679            let extract_workspace = || {
680                let bytes = unsafe {
681                    core::slice::from_raw_parts(c_data.workspace_name.as_ptr() as *const u8, 256)
682                };
683                let len = bytes.iter().position(|&b| b == 0).unwrap_or(256);
684                let name = String::from_utf8_lossy(&bytes[..len]).into_owned();
685                unsafe { $crate::workspace::Workspace::from_c_with_name(&c_data.workspace, name) }
686            };
687
688            let write_result = |result: $crate::animation::AnimationFrameResult| {
689                let c_result: $crate::bindings::miracle_plugin_animation_frame_result_t =
690                    result.into();
691                unsafe {
692                    let out = &mut *(result_ptr
693                        as *mut $crate::bindings::miracle_plugin_animation_frame_result_t);
694                    *out = c_result;
695                }
696            };
697
698            match c_data.type_ {
699                $crate::bindings::miracle_animation_type_miracle_animation_type_window_open => {
700                    let window = extract_window();
701                    match plugin.window_open_animation(&data, &window) {
702                        Some(result) => { write_result(result); 1 }
703                        None => 0,
704                    }
705                },
706                $crate::bindings::miracle_animation_type_miracle_animation_type_window_close => {
707                        let window = extract_window();
708                    match plugin.window_close_animation(&data, &window) {
709                        Some(result) => { write_result(result); 1 }
710                        None => 0,
711                    }
712                },
713                $crate::bindings::miracle_animation_type_miracle_animation_type_window_move => {
714                    let window = extract_window();
715                    match plugin.window_move_animation(&data, &window) {
716                        Some(result) => { write_result(result); 1 }
717                        None => 0,
718                    }
719                },
720                $crate::bindings::miracle_animation_type_miracle_animation_type_workspace_switch => {
721                    let workspace = extract_workspace();
722                    match plugin.workspace_switch_animation(&data, &workspace) {
723                        Some(result) => { write_result(result); 1 }
724                        None => 0,
725                    }
726                },
727                _ => 0
728            }
729
730        }
731
732        #[unsafe(no_mangle)]
733        pub extern "C" fn custom_animate(data_ptr: i32) -> i32 {
734            let raw = unsafe {
735                &*(data_ptr as *const $crate::animation::RawCustomAnimationData)
736            };
737
738            $crate::plugin::run_custom_animation(raw.animation_id, raw.dt, raw.elapsed_seconds);
739
740            // Return value is ignored by the host; kept for WASM ABI compatibility.
741            0
742        }
743
744        #[unsafe(no_mangle)]
745        pub extern "C" fn place_new_window(
746            window_info_ptr: i32,
747            result_ptr: i32,
748            name_ptr: i32,
749            name_len: i32,
750        ) -> i32 {
751            let plugin = unsafe {
752                match _MIRACLE_PLUGIN.as_mut() {
753                    Some(p) => p,
754                    None => return 0,
755                }
756            };
757
758            let c_info = unsafe {
759                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
760            };
761
762            let name = if name_len > 0 {
763                let name_bytes = unsafe {
764                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
765                };
766                String::from_utf8_lossy(name_bytes).into_owned()
767            } else {
768                String::new()
769            };
770
771            let info = unsafe { $crate::window::Window::from_c_with_name(c_info, name) };
772
773            match plugin.place_new_window(&info) {
774                Some(placement) => {
775                    let c_placement: $crate::bindings::miracle_placement_t = placement.into();
776                    unsafe {
777                        let out = &mut *(result_ptr as *mut $crate::bindings::miracle_placement_t);
778                        *out = c_placement;
779                    }
780                    1
781                }
782                None => 0,
783            }
784        }
785
786        #[unsafe(no_mangle)]
787        pub extern "C" fn window_deleted(
788            window_info_ptr: i32,
789            name_ptr: i32,
790            name_len: i32,
791        ) {
792            let plugin = unsafe {
793                match _MIRACLE_PLUGIN.as_mut() {
794                    Some(p) => p,
795                    None => return,
796                }
797            };
798
799            let c_info = unsafe {
800                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
801            };
802
803            let name = if name_len > 0 {
804                let name_bytes = unsafe {
805                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
806                };
807                String::from_utf8_lossy(name_bytes).into_owned()
808            } else {
809                String::new()
810            };
811
812            let info = unsafe { $crate::window::Window::from_c_with_name(c_info, name) };
813
814            plugin.window_deleted(&info);
815        }
816
817        #[unsafe(no_mangle)]
818        pub extern "C" fn window_focused(
819            window_info_ptr: i32,
820            name_ptr: i32,
821            name_len: i32,
822        ) {
823            let plugin = unsafe {
824                match _MIRACLE_PLUGIN.as_mut() {
825                    Some(p) => p,
826                    None => return,
827                }
828            };
829
830            let c_info = unsafe {
831                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
832            };
833
834            let name = if name_len > 0 {
835                let name_bytes = unsafe {
836                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
837                };
838                String::from_utf8_lossy(name_bytes).into_owned()
839            } else {
840                String::new()
841            };
842
843            let info = unsafe { $crate::window::Window::from_c_with_name(c_info, name) };
844
845            plugin.window_focused(&info);
846        }
847
848        #[unsafe(no_mangle)]
849        pub extern "C" fn window_unfocused(
850            window_info_ptr: i32,
851            name_ptr: i32,
852            name_len: i32,
853        ) {
854            let plugin = unsafe {
855                match _MIRACLE_PLUGIN.as_mut() {
856                    Some(p) => p,
857                    None => return,
858                }
859            };
860
861            let c_info = unsafe {
862                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
863            };
864
865            let name = if name_len > 0 {
866                let name_bytes = unsafe {
867                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
868                };
869                String::from_utf8_lossy(name_bytes).into_owned()
870            } else {
871                String::new()
872            };
873
874            let info = unsafe { $crate::window::Window::from_c_with_name(c_info, name) };
875
876            plugin.window_unfocused(&info);
877        }
878
879        #[unsafe(no_mangle)]
880        pub extern "C" fn workspace_created(
881            workspace_info_ptr: i32,
882            name_ptr: i32,
883            name_len: i32,
884        ) {
885            let plugin = unsafe {
886                match _MIRACLE_PLUGIN.as_mut() {
887                    Some(p) => p,
888                    None => return,
889                }
890            };
891
892            let c_ws = unsafe {
893                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
894            };
895
896            let name = if name_len > 0 {
897                let name_bytes = unsafe {
898                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
899                };
900                String::from_utf8_lossy(name_bytes).into_owned()
901            } else {
902                String::new()
903            };
904
905            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
906            plugin.workspace_created(&ws);
907        }
908
909        #[unsafe(no_mangle)]
910        pub extern "C" fn workspace_removed(
911            workspace_info_ptr: i32,
912            name_ptr: i32,
913            name_len: i32,
914        ) {
915            let plugin = unsafe {
916                match _MIRACLE_PLUGIN.as_mut() {
917                    Some(p) => p,
918                    None => return,
919                }
920            };
921
922            let c_ws = unsafe {
923                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
924            };
925
926            let name = if name_len > 0 {
927                let name_bytes = unsafe {
928                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
929                };
930                String::from_utf8_lossy(name_bytes).into_owned()
931            } else {
932                String::new()
933            };
934
935            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
936            plugin.workspace_removed(&ws);
937        }
938
939        #[unsafe(no_mangle)]
940        pub extern "C" fn workspace_focused(
941            workspace_info_ptr: i32,
942            name_ptr: i32,
943            name_len: i32,
944            has_previous: i32,
945            previous_id: i64,
946        ) {
947            let plugin = unsafe {
948                match _MIRACLE_PLUGIN.as_mut() {
949                    Some(p) => p,
950                    None => return,
951                }
952            };
953
954            let c_ws = unsafe {
955                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
956            };
957
958            let name = if name_len > 0 {
959                let name_bytes = unsafe {
960                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
961                };
962                String::from_utf8_lossy(name_bytes).into_owned()
963            } else {
964                String::new()
965            };
966
967            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
968            let prev = if has_previous != 0 { Some(previous_id as u64) } else { None };
969            plugin.workspace_focused(prev, &ws);
970        }
971
972        #[unsafe(no_mangle)]
973        pub extern "C" fn workspace_area_changed(
974            workspace_info_ptr: i32,
975            name_ptr: i32,
976            name_len: i32,
977        ) {
978            let plugin = unsafe {
979                match _MIRACLE_PLUGIN.as_mut() {
980                    Some(p) => p,
981                    None => return,
982                }
983            };
984
985            let c_ws = unsafe {
986                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
987            };
988
989            let name = if name_len > 0 {
990                let name_bytes = unsafe {
991                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
992                };
993                String::from_utf8_lossy(name_bytes).into_owned()
994            } else {
995                String::new()
996            };
997
998            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
999            plugin.workspace_area_changed(&ws);
1000        }
1001
1002        #[unsafe(no_mangle)]
1003        pub extern "C" fn window_workspace_changed(
1004            window_info_ptr: i32,
1005            window_name_ptr: i32,
1006            window_name_len: i32,
1007            workspace_info_ptr: i32,
1008            workspace_name_ptr: i32,
1009            workspace_name_len: i32,
1010        ) {
1011            let plugin = unsafe {
1012                match _MIRACLE_PLUGIN.as_mut() {
1013                    Some(p) => p,
1014                    None => return,
1015                }
1016            };
1017
1018            let c_info = unsafe {
1019                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
1020            };
1021
1022            let window_name = if window_name_len > 0 {
1023                let name_bytes = unsafe {
1024                    core::slice::from_raw_parts(window_name_ptr as *const u8, window_name_len as usize)
1025                };
1026                String::from_utf8_lossy(name_bytes).into_owned()
1027            } else {
1028                String::new()
1029            };
1030
1031            let info = unsafe { $crate::window::Window::from_c_with_name(c_info, window_name) };
1032
1033            let c_ws = unsafe {
1034                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
1035            };
1036
1037            let workspace_name = if workspace_name_len > 0 {
1038                let name_bytes = unsafe {
1039                    core::slice::from_raw_parts(workspace_name_ptr as *const u8, workspace_name_len as usize)
1040                };
1041                String::from_utf8_lossy(name_bytes).into_owned()
1042            } else {
1043                String::new()
1044            };
1045
1046            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, workspace_name) };
1047            plugin.window_workspace_changed(&info, &ws);
1048        }
1049
1050        #[unsafe(no_mangle)]
1051        pub extern "C" fn configure(buf_ptr: i32, buf_len: i32) -> i32 {
1052            let plugin = unsafe {
1053                match _MIRACLE_PLUGIN.as_mut() {
1054                    Some(p) => p,
1055                    None => return 0,
1056                }
1057            };
1058            $crate::__private::run_configure(plugin, buf_ptr, buf_len)
1059        }
1060
1061        #[unsafe(no_mangle)]
1062        pub extern "C" fn handle_keyboard_input(event_ptr: i32) -> i32 {
1063            let plugin = unsafe {
1064                match _MIRACLE_PLUGIN.as_mut() {
1065                    Some(p) => p,
1066                    None => return 0,
1067                }
1068            };
1069
1070            let c_event = unsafe {
1071                &*(event_ptr as *const $crate::bindings::miracle_keyboard_event_t)
1072            };
1073
1074            let event = $crate::input::KeyboardEvent {
1075                action: $crate::input::KeyboardAction::try_from(c_event.action)
1076                    .unwrap_or_default(),
1077                keysym: c_event.keysym,
1078                scan_code: c_event.scan_code,
1079                modifiers: $crate::input::InputEventModifiers::from(c_event.modifiers),
1080            };
1081
1082            if plugin.handle_keyboard_input(event) { 1 } else { 0 }
1083        }
1084
1085        #[unsafe(no_mangle)]
1086        pub extern "C" fn handle_pointer_event(event_ptr: i32) -> i32 {
1087            let plugin = unsafe {
1088                match _MIRACLE_PLUGIN.as_mut() {
1089                    Some(p) => p,
1090                    None => return 0,
1091                }
1092            };
1093
1094            let c_event = unsafe {
1095                &*(event_ptr as *const $crate::bindings::miracle_pointer_event_t)
1096            };
1097
1098            let event = $crate::input::PointerEvent {
1099                x: c_event.x,
1100                y: c_event.y,
1101                action: $crate::input::PointerAction::try_from(c_event.action)
1102                    .unwrap_or_default(),
1103                modifiers: $crate::input::InputEventModifiers::from(c_event.modifiers),
1104                buttons: $crate::input::PointerButtons::from(c_event.buttons),
1105            };
1106
1107            if plugin.handle_pointer_event(event) { 1 } else { 0 }
1108        }
1109
1110        #[unsafe(no_mangle)]
1111        pub extern "C" fn handle_plugin_command(buf_ptr: i32, in_len: i32, buf_cap: i32) -> i32 {
1112            let plugin = unsafe {
1113                match _MIRACLE_PLUGIN.as_mut() {
1114                    Some(p) => p,
1115                    None => return -1,
1116                }
1117            };
1118
1119            // Read the request payload the host wrote into the shared buffer.
1120            let payload = if in_len > 0 {
1121                let bytes = unsafe {
1122                    core::slice::from_raw_parts(buf_ptr as *const u8, in_len as usize)
1123                };
1124                String::from_utf8_lossy(bytes).into_owned()
1125            } else {
1126                String::new()
1127            };
1128
1129            match plugin.handle_command(&payload) {
1130                Some(response) => {
1131                    let bytes = response.as_bytes();
1132                    if bytes.len() > buf_cap as usize {
1133                        return -1;
1134                    }
1135                    unsafe {
1136                        core::ptr::copy_nonoverlapping(
1137                            bytes.as_ptr(),
1138                            buf_ptr as *mut u8,
1139                            bytes.len(),
1140                        );
1141                    }
1142                    bytes.len() as i32
1143                }
1144                None => -1,
1145            }
1146        }
1147    };
1148}