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
171/// Lists the windows that are managed by this plugin.
172///
173/// A window that is managed by this plugin had to have been placed
174/// via a freestyle placement strategy, otherwise the tiling manager
175/// or the system is handling it independently.
176///
177/// Each returned [`Window`] exposes both read-only fields and setter methods
178/// for mutating the window's state, workspace, size, transform, and alpha.
179pub fn managed_windows() -> Vec<Window> {
180    let handle = unsafe { miracle_get_plugin_handle() };
181    let count = unsafe { miracle_num_managed_windows(handle) };
182
183    (0..count)
184        .filter_map(|i| {
185            const NAME_BUF_LEN: usize = 256;
186            let mut window_info =
187                std::mem::MaybeUninit::<crate::bindings::miracle_window_info_t>::uninit();
188            let mut name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
189
190            unsafe {
191                let result = miracle_get_managed_window_at(
192                    handle,
193                    i,
194                    window_info.as_mut_ptr() as i32,
195                    name_buf.as_mut_ptr() as i32,
196                    NAME_BUF_LEN as i32,
197                );
198
199                if result != 0 {
200                    return None;
201                }
202
203                let window_info = window_info.assume_init();
204                let name_len = name_buf
205                    .iter()
206                    .position(|&c| c == 0)
207                    .unwrap_or(NAME_BUF_LEN);
208                let name = String::from_utf8_lossy(&name_buf[..name_len]).into_owned();
209
210                Some(Window::from_c_with_name(&window_info, name))
211            }
212        })
213        .collect()
214}
215
216/// Get the number of outputs.
217pub fn num_outputs() -> u32 {
218    unsafe { miracle_num_outputs() }
219}
220
221/// Get an output by index.
222///
223/// Returns `None` if the index is out of bounds or if the call fails.
224pub fn get_output_at(index: u32) -> Option<Output> {
225    if index >= num_outputs() {
226        return None;
227    }
228
229    const NAME_BUF_LEN: usize = 256;
230    let mut output = std::mem::MaybeUninit::<crate::bindings::miracle_output_t>::uninit();
231    let mut name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
232
233    unsafe {
234        let result = miracle_get_output_at(
235            index,
236            output.as_mut_ptr() as i32,
237            name_buf.as_mut_ptr() as i32,
238            NAME_BUF_LEN as i32,
239        );
240
241        if result != 0 {
242            return None;
243        }
244
245        let output = output.assume_init();
246
247        // Find the null terminator to get the actual string length
248        let name_len = name_buf
249            .iter()
250            .position(|&c| c == 0)
251            .unwrap_or(NAME_BUF_LEN);
252        let name = String::from_utf8_lossy(&name_buf[..name_len]).into_owned();
253
254        Some(Output::from_c_with_name(&output, name))
255    }
256}
257
258/// Get all outputs.
259pub fn get_outputs() -> Vec<Output> {
260    let count = num_outputs();
261    (0..count).filter_map(get_output_at).collect()
262}
263
264/// Get the currently active workspace on the focused output.
265///
266/// Returns `None` if there is no focused output or no active workspace.
267pub fn get_active_workspace() -> Option<Workspace> {
268    const NAME_BUF_LEN: usize = 256;
269    let mut workspace = std::mem::MaybeUninit::<crate::bindings::miracle_workspace_t>::uninit();
270    let mut name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
271
272    unsafe {
273        let result = miracle_get_active_workspace(
274            workspace.as_mut_ptr() as i32,
275            name_buf.as_mut_ptr() as i32,
276            NAME_BUF_LEN as i32,
277        );
278
279        if result != 0 {
280            return None;
281        }
282
283        let workspace = workspace.assume_init();
284        if workspace.is_set == 0 {
285            return None;
286        }
287
288        let name_len = name_buf
289            .iter()
290            .position(|&c| c == 0)
291            .unwrap_or(NAME_BUF_LEN);
292        let name = String::from_utf8_lossy(&name_buf[..name_len]).into_owned();
293
294        Some(Workspace::from_c_with_name(&workspace, name))
295    }
296}
297
298/// Request a workspace by optional number and/or name.
299///
300/// If a workspace with the given number or name already exists, it is returned.
301/// Otherwise, a new workspace is created on the focused output.
302///
303/// If `focus` is true, the workspace will be focused after creation/lookup.
304///
305/// Returns `None` if the workspace could not be created.
306pub fn request_workspace(
307    number: Option<u32>,
308    name: Option<&str>,
309    focus: bool,
310) -> Option<Workspace> {
311    const NAME_BUF_LEN: usize = 256;
312    let mut workspace = std::mem::MaybeUninit::<crate::bindings::miracle_workspace_t>::uninit();
313    let mut out_name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
314
315    let has_number: i32 = if number.is_some() { 1 } else { 0 };
316    let number_val: i32 = number.unwrap_or(0) as i32;
317
318    let name_ptr: i32 = match name {
319        Some(s) => s.as_ptr() as i32,
320        None => 0,
321    };
322    let name_len: i32 = match name {
323        Some(s) => s.len() as i32,
324        None => 0,
325    };
326
327    unsafe {
328        let result = miracle_request_workspace(
329            has_number,
330            number_val,
331            name_ptr,
332            name_len,
333            workspace.as_mut_ptr() as i32,
334            out_name_buf.as_mut_ptr() as i32,
335            NAME_BUF_LEN as i32,
336            if focus { 1 } else { 0 },
337        );
338
339        if result != 0 {
340            return None;
341        }
342
343        let workspace = workspace.assume_init();
344        if workspace.is_set == 0 {
345            return None;
346        }
347
348        let out_name_len = out_name_buf
349            .iter()
350            .position(|&c| c == 0)
351            .unwrap_or(NAME_BUF_LEN);
352        let ws_name = String::from_utf8_lossy(&out_name_buf[..out_name_len]).into_owned();
353
354        Some(Workspace::from_c_with_name(&workspace, ws_name))
355    }
356}
357
358/// Queue a custom per-frame animation with a callback.
359///
360/// `callback` receives `(animation_id, dt, elapsed_seconds)` every frame.
361/// The compositor automatically removes the animation after `duration_seconds`.
362///
363/// `dt` and `elapsed_seconds` are floats in seconds.
364///
365/// Returns the host-generated animation ID on success, or `None` on error.
366pub fn queue_custom_animation<F>(callback: F, duration_seconds: f32) -> Option<u32>
367where
368    F: FnMut(u32, f32, f32) + 'static,
369{
370    let handle = unsafe { miracle_get_plugin_handle() };
371    let mut animation_id: u32 = 0;
372    let mut dur = duration_seconds;
373    let result = unsafe {
374        crate::host::miracle_queue_custom_animation(
375            handle as i32,
376            &mut animation_id as *mut u32 as i32,
377            &mut dur as *mut f32 as i32,
378        )
379    };
380    if result == 0 {
381        custom_anim_callbacks().insert(animation_id, (Box::new(callback), duration_seconds));
382        Some(animation_id)
383    } else {
384        None
385    }
386}
387
388/// Register a multi-pass GLSL shader with the compositor.
389///
390/// Each element of `passes` is a complete `sample_to_rgba(vec2 texcoord)` function.
391/// The compositor chains them so that pass *i* samples the output of pass *i-1*:
392///
393/// - **Pass 0** — `tex` is the window's content texture; `tex_source` is the same.
394/// - **Pass i (i > 0)** — `tex` is the output of pass *i-1*; `tex_source` is
395///   always the original window content, useful for combining a processed result
396///   with the untouched original (e.g. bloom = blur + original).
397/// - **Final pass** — the alpha mask, rounded-corner SDF, and window transform are
398///   applied after `sample_to_rgba` returns, exactly as with a single-pass shader.
399/// - `surfaceSize` (the window size in pixels) is available in every pass.
400///
401/// Returns the shader ID on success (pass to [`crate::window::Window::set_shader`]),
402/// or `None` if registration failed.
403pub fn register_window_shader(passes: &[&str]) -> Option<u8> {
404    // Build an array of [ptr, len] descriptors pointing into WASM linear memory.
405    // In wasm32 Rust pointers are linear-memory offsets, so casting to i32 is correct.
406    let descriptors: Vec<[i32; 2]> = passes
407        .iter()
408        .map(|p| [p.as_ptr() as i32, p.len() as i32])
409        .collect();
410    let handle = unsafe { miracle_get_plugin_handle() };
411    let result = unsafe {
412        crate::host::miracle_register_window_sample_to_rgba(
413            handle as i32,
414            descriptors.as_ptr() as i32,
415            passes.len() as i32,
416        )
417    };
418    if result >= 0 {
419        Some(result as u8)
420    } else {
421        None
422    }
423}
424
425/// Set or clear the full-screen (output) shader.
426///
427/// Each element of `passes` is a complete `vec4 sample_to_rgba(in vec2 texcoord)`
428/// GLSL function applied as a post-process over the whole composited output.
429/// Like [`register_window_shader`] the passes are chained: pass *i* reads pass
430/// *i-1*'s output as `tex`, `tex_source` is always the original screen content,
431/// and `surfaceSize` is the output size in pixels. Two optional time uniforms
432/// are also available: `time` (seconds since the compositor started) and
433/// `timeOfDay` (seconds since local midnight). Pass an empty slice to clear the
434/// shader and revert to the configured `output_filter.shader_path`.
435///
436/// The plugin's screen shader overrides the config path until cleared or the
437/// plugin unloads. Returns `Ok` on success, `Err` on failure.
438pub fn set_screen_shader(passes: &[&str]) -> Result<(), ()> {
439    let handle = unsafe { miracle_get_plugin_handle() };
440    // In wasm32 Rust pointers are linear-memory offsets, so casting to i32 is correct.
441    let descriptors: Vec<[i32; 2]> = passes
442        .iter()
443        .map(|p| [p.as_ptr() as i32, p.len() as i32])
444        .collect();
445    let r = unsafe {
446        crate::host::miracle_set_screen_shader(
447            handle as i32,
448            descriptors.as_ptr() as i32,
449            passes.len() as i32,
450        )
451    };
452    if r == 0 {
453        Ok(())
454    } else {
455        Err(())
456    }
457}
458
459static mut _CUSTOM_ANIM_CALLBACKS: Option<
460    std::collections::HashMap<u32, (Box<dyn FnMut(u32, f32, f32)>, f32)>,
461> = None;
462
463/// Returns the global custom-animation callback registry.
464///
465/// # Safety
466/// Only safe in a single-threaded WASM context (which is always the case for miracle plugins).
467#[doc(hidden)]
468pub fn custom_anim_callbacks()
469-> &'static mut std::collections::HashMap<u32, (Box<dyn FnMut(u32, f32, f32)>, f32)> {
470    unsafe {
471        if (*std::ptr::addr_of!(_CUSTOM_ANIM_CALLBACKS)).is_none() {
472            _CUSTOM_ANIM_CALLBACKS = Some(std::collections::HashMap::new());
473        }
474        (*std::ptr::addr_of_mut!(_CUSTOM_ANIM_CALLBACKS))
475            .as_mut()
476            .unwrap()
477    }
478}
479
480/// Registers a type as a miracle-wm plugin.
481///
482/// The type must implement [`Default`] and [`Plugin`]. This macro generates all of the
483/// required WASM export functions (`init`, `animate`, `place_new_window`, etc.) that the
484/// compositor calls at runtime.
485///
486/// # Example
487///
488/// ```rust,ignore
489/// #[derive(Default)]
490/// struct MyPlugin;
491/// impl Plugin for MyPlugin {}
492/// miracle_plugin!(MyPlugin);
493/// ```
494#[macro_export]
495macro_rules! miracle_plugin {
496    ($plugin_type:ty) => {
497        static mut _MIRACLE_PLUGIN: Option<$plugin_type> = None;
498        static mut _MIRACLE_PLUGIN_HANDLE: u32 = 0;
499
500        #[unsafe(no_mangle)]
501        pub extern "C" fn miracle_get_plugin_handle() -> u32 {
502            unsafe { _MIRACLE_PLUGIN_HANDLE }
503        }
504
505        #[unsafe(no_mangle)]
506        pub extern "C" fn init(handle: i32) {
507            unsafe {
508                _MIRACLE_PLUGIN_HANDLE = handle as u32;
509                _MIRACLE_PLUGIN = Some(<$plugin_type>::default());
510            }
511        }
512
513        #[unsafe(no_mangle)]
514        pub extern "C" fn animate(data_ptr: i32, result_ptr: i32) -> i32 {
515            let plugin = unsafe {
516                match _MIRACLE_PLUGIN.as_mut() {
517                    Some(p) => p,
518                    None => return 0,
519                }
520            };
521
522            let c_data = unsafe {
523                &*(data_ptr as *const $crate::bindings::miracle_plugin_animation_frame_data_t)
524            };
525            let data: $crate::animation::AnimationFrameData = (*c_data).into();
526
527            let extract_window = || {
528                let bytes = unsafe {
529                    core::slice::from_raw_parts(c_data.window_name.as_ptr() as *const u8, 256)
530                };
531                let len = bytes.iter().position(|&b| b == 0).unwrap_or(256);
532                let name = String::from_utf8_lossy(&bytes[..len]).into_owned();
533                unsafe { $crate::window::Window::from_c_with_name(&c_data.window_info, name) }
534            };
535
536            let extract_workspace = || {
537                let bytes = unsafe {
538                    core::slice::from_raw_parts(c_data.workspace_name.as_ptr() as *const u8, 256)
539                };
540                let len = bytes.iter().position(|&b| b == 0).unwrap_or(256);
541                let name = String::from_utf8_lossy(&bytes[..len]).into_owned();
542                unsafe { $crate::workspace::Workspace::from_c_with_name(&c_data.workspace, name) }
543            };
544
545            let write_result = |result: $crate::animation::AnimationFrameResult| {
546                let c_result: $crate::bindings::miracle_plugin_animation_frame_result_t =
547                    result.into();
548                unsafe {
549                    let out = &mut *(result_ptr
550                        as *mut $crate::bindings::miracle_plugin_animation_frame_result_t);
551                    *out = c_result;
552                }
553            };
554
555            match c_data.type_ {
556                $crate::bindings::miracle_animation_type_miracle_animation_type_window_open => {
557                    let window = extract_window();
558                    match plugin.window_open_animation(&data, &window) {
559                        Some(result) => { write_result(result); 1 }
560                        None => 0,
561                    }
562                },
563                $crate::bindings::miracle_animation_type_miracle_animation_type_window_close => {
564                        let window = extract_window();
565                    match plugin.window_close_animation(&data, &window) {
566                        Some(result) => { write_result(result); 1 }
567                        None => 0,
568                    }
569                },
570                $crate::bindings::miracle_animation_type_miracle_animation_type_window_move => {
571                    let window = extract_window();
572                    match plugin.window_move_animation(&data, &window) {
573                        Some(result) => { write_result(result); 1 }
574                        None => 0,
575                    }
576                },
577                $crate::bindings::miracle_animation_type_miracle_animation_type_workspace_switch => {
578                    let workspace = extract_workspace();
579                    match plugin.workspace_switch_animation(&data, &workspace) {
580                        Some(result) => { write_result(result); 1 }
581                        None => 0,
582                    }
583                },
584                _ => 0
585            }
586
587        }
588
589        #[unsafe(no_mangle)]
590        pub extern "C" fn custom_animate(data_ptr: i32) -> i32 {
591            let raw = unsafe {
592                &*(data_ptr as *const $crate::animation::RawCustomAnimationData)
593            };
594
595            let callbacks = $crate::plugin::custom_anim_callbacks();
596            let done = if let Some((cb, dur)) = callbacks.get_mut(&raw.animation_id) {
597                cb(raw.animation_id, raw.dt, raw.elapsed_seconds);
598                raw.elapsed_seconds >= *dur
599            } else {
600                false
601            };
602            if done {
603                callbacks.remove(&raw.animation_id);
604            }
605
606            // Return value is ignored by the host; kept for WASM ABI compatibility.
607            0
608        }
609
610        #[unsafe(no_mangle)]
611        pub extern "C" fn place_new_window(
612            window_info_ptr: i32,
613            result_ptr: i32,
614            name_ptr: i32,
615            name_len: i32,
616        ) -> i32 {
617            let plugin = unsafe {
618                match _MIRACLE_PLUGIN.as_mut() {
619                    Some(p) => p,
620                    None => return 0,
621                }
622            };
623
624            let c_info = unsafe {
625                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
626            };
627
628            let name = if name_len > 0 {
629                let name_bytes = unsafe {
630                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
631                };
632                String::from_utf8_lossy(name_bytes).into_owned()
633            } else {
634                String::new()
635            };
636
637            let info = unsafe { $crate::window::Window::from_c_with_name(c_info, name) };
638
639            match plugin.place_new_window(&info) {
640                Some(placement) => {
641                    let c_placement: $crate::bindings::miracle_placement_t = placement.into();
642                    unsafe {
643                        let out = &mut *(result_ptr as *mut $crate::bindings::miracle_placement_t);
644                        *out = c_placement;
645                    }
646                    1
647                }
648                None => 0,
649            }
650        }
651
652        #[unsafe(no_mangle)]
653        pub extern "C" fn window_deleted(
654            window_info_ptr: i32,
655            name_ptr: i32,
656            name_len: i32,
657        ) {
658            let plugin = unsafe {
659                match _MIRACLE_PLUGIN.as_mut() {
660                    Some(p) => p,
661                    None => return,
662                }
663            };
664
665            let c_info = unsafe {
666                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
667            };
668
669            let name = if name_len > 0 {
670                let name_bytes = unsafe {
671                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
672                };
673                String::from_utf8_lossy(name_bytes).into_owned()
674            } else {
675                String::new()
676            };
677
678            let info = unsafe { $crate::window::Window::from_c_with_name(c_info, name) };
679
680            plugin.window_deleted(&info);
681        }
682
683        #[unsafe(no_mangle)]
684        pub extern "C" fn window_focused(
685            window_info_ptr: i32,
686            name_ptr: i32,
687            name_len: i32,
688        ) {
689            let plugin = unsafe {
690                match _MIRACLE_PLUGIN.as_mut() {
691                    Some(p) => p,
692                    None => return,
693                }
694            };
695
696            let c_info = unsafe {
697                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
698            };
699
700            let name = if name_len > 0 {
701                let name_bytes = unsafe {
702                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
703                };
704                String::from_utf8_lossy(name_bytes).into_owned()
705            } else {
706                String::new()
707            };
708
709            let info = unsafe { $crate::window::Window::from_c_with_name(c_info, name) };
710
711            plugin.window_focused(&info);
712        }
713
714        #[unsafe(no_mangle)]
715        pub extern "C" fn window_unfocused(
716            window_info_ptr: i32,
717            name_ptr: i32,
718            name_len: i32,
719        ) {
720            let plugin = unsafe {
721                match _MIRACLE_PLUGIN.as_mut() {
722                    Some(p) => p,
723                    None => return,
724                }
725            };
726
727            let c_info = unsafe {
728                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
729            };
730
731            let name = if name_len > 0 {
732                let name_bytes = unsafe {
733                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
734                };
735                String::from_utf8_lossy(name_bytes).into_owned()
736            } else {
737                String::new()
738            };
739
740            let info = unsafe { $crate::window::Window::from_c_with_name(c_info, name) };
741
742            plugin.window_unfocused(&info);
743        }
744
745        #[unsafe(no_mangle)]
746        pub extern "C" fn workspace_created(
747            workspace_info_ptr: i32,
748            name_ptr: i32,
749            name_len: i32,
750        ) {
751            let plugin = unsafe {
752                match _MIRACLE_PLUGIN.as_mut() {
753                    Some(p) => p,
754                    None => return,
755                }
756            };
757
758            let c_ws = unsafe {
759                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_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 ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
772            plugin.workspace_created(&ws);
773        }
774
775        #[unsafe(no_mangle)]
776        pub extern "C" fn workspace_removed(
777            workspace_info_ptr: i32,
778            name_ptr: i32,
779            name_len: i32,
780        ) {
781            let plugin = unsafe {
782                match _MIRACLE_PLUGIN.as_mut() {
783                    Some(p) => p,
784                    None => return,
785                }
786            };
787
788            let c_ws = unsafe {
789                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
790            };
791
792            let name = if name_len > 0 {
793                let name_bytes = unsafe {
794                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
795                };
796                String::from_utf8_lossy(name_bytes).into_owned()
797            } else {
798                String::new()
799            };
800
801            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
802            plugin.workspace_removed(&ws);
803        }
804
805        #[unsafe(no_mangle)]
806        pub extern "C" fn workspace_focused(
807            workspace_info_ptr: i32,
808            name_ptr: i32,
809            name_len: i32,
810            has_previous: i32,
811            previous_id: i64,
812        ) {
813            let plugin = unsafe {
814                match _MIRACLE_PLUGIN.as_mut() {
815                    Some(p) => p,
816                    None => return,
817                }
818            };
819
820            let c_ws = unsafe {
821                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
822            };
823
824            let name = if name_len > 0 {
825                let name_bytes = unsafe {
826                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
827                };
828                String::from_utf8_lossy(name_bytes).into_owned()
829            } else {
830                String::new()
831            };
832
833            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
834            let prev = if has_previous != 0 { Some(previous_id as u64) } else { None };
835            plugin.workspace_focused(prev, &ws);
836        }
837
838        #[unsafe(no_mangle)]
839        pub extern "C" fn workspace_area_changed(
840            workspace_info_ptr: i32,
841            name_ptr: i32,
842            name_len: i32,
843        ) {
844            let plugin = unsafe {
845                match _MIRACLE_PLUGIN.as_mut() {
846                    Some(p) => p,
847                    None => return,
848                }
849            };
850
851            let c_ws = unsafe {
852                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
853            };
854
855            let name = if name_len > 0 {
856                let name_bytes = unsafe {
857                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
858                };
859                String::from_utf8_lossy(name_bytes).into_owned()
860            } else {
861                String::new()
862            };
863
864            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
865            plugin.workspace_area_changed(&ws);
866        }
867
868        #[unsafe(no_mangle)]
869        pub extern "C" fn window_workspace_changed(
870            window_info_ptr: i32,
871            window_name_ptr: i32,
872            window_name_len: i32,
873            workspace_info_ptr: i32,
874            workspace_name_ptr: i32,
875            workspace_name_len: i32,
876        ) {
877            let plugin = unsafe {
878                match _MIRACLE_PLUGIN.as_mut() {
879                    Some(p) => p,
880                    None => return,
881                }
882            };
883
884            let c_info = unsafe {
885                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
886            };
887
888            let window_name = if window_name_len > 0 {
889                let name_bytes = unsafe {
890                    core::slice::from_raw_parts(window_name_ptr as *const u8, window_name_len as usize)
891                };
892                String::from_utf8_lossy(name_bytes).into_owned()
893            } else {
894                String::new()
895            };
896
897            let info = unsafe { $crate::window::Window::from_c_with_name(c_info, window_name) };
898
899            let c_ws = unsafe {
900                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
901            };
902
903            let workspace_name = if workspace_name_len > 0 {
904                let name_bytes = unsafe {
905                    core::slice::from_raw_parts(workspace_name_ptr as *const u8, workspace_name_len as usize)
906                };
907                String::from_utf8_lossy(name_bytes).into_owned()
908            } else {
909                String::new()
910            };
911
912            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, workspace_name) };
913            plugin.window_workspace_changed(&info, &ws);
914        }
915
916        #[unsafe(no_mangle)]
917        pub extern "C" fn configure(buf_ptr: i32, buf_len: i32) -> i32 {
918            let plugin = unsafe {
919                match _MIRACLE_PLUGIN.as_mut() {
920                    Some(p) => p,
921                    None => return 0,
922                }
923            };
924            $crate::__private::run_configure(plugin, buf_ptr, buf_len)
925        }
926
927        #[unsafe(no_mangle)]
928        pub extern "C" fn handle_keyboard_input(event_ptr: i32) -> i32 {
929            let plugin = unsafe {
930                match _MIRACLE_PLUGIN.as_mut() {
931                    Some(p) => p,
932                    None => return 0,
933                }
934            };
935
936            let c_event = unsafe {
937                &*(event_ptr as *const $crate::bindings::miracle_keyboard_event_t)
938            };
939
940            let event = $crate::input::KeyboardEvent {
941                action: $crate::input::KeyboardAction::try_from(c_event.action)
942                    .unwrap_or_default(),
943                keysym: c_event.keysym,
944                scan_code: c_event.scan_code,
945                modifiers: $crate::input::InputEventModifiers::from(c_event.modifiers),
946            };
947
948            if plugin.handle_keyboard_input(event) { 1 } else { 0 }
949        }
950
951        #[unsafe(no_mangle)]
952        pub extern "C" fn handle_pointer_event(event_ptr: i32) -> i32 {
953            let plugin = unsafe {
954                match _MIRACLE_PLUGIN.as_mut() {
955                    Some(p) => p,
956                    None => return 0,
957                }
958            };
959
960            let c_event = unsafe {
961                &*(event_ptr as *const $crate::bindings::miracle_pointer_event_t)
962            };
963
964            let event = $crate::input::PointerEvent {
965                x: c_event.x,
966                y: c_event.y,
967                action: $crate::input::PointerAction::try_from(c_event.action)
968                    .unwrap_or_default(),
969                modifiers: $crate::input::InputEventModifiers::from(c_event.modifiers),
970                buttons: $crate::input::PointerButtons::from(c_event.buttons),
971            };
972
973            if plugin.handle_pointer_event(event) { 1 } else { 0 }
974        }
975    };
976}