Skip to main content

miracle_plugin/
plugin.rs

1use crate::input::{KeyboardEvent, PointerEvent};
2use crate::placement::Placement;
3use crate::window::{PluginWindow, WindowInfo};
4
5use super::animation::{AnimationFrameData, AnimationFrameResult};
6use super::host::*;
7use super::output::*;
8use super::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
41pub trait Plugin {
42    /// Handles the window opening animation.
43    ///
44    /// If None is returned, the animation is not handled by this plugin.
45    fn window_open_animation(
46        &mut self,
47        _data: &AnimationFrameData,
48    ) -> Option<AnimationFrameResult> {
49        None
50    }
51
52    /// Handles the window closing animation.
53    ///
54    /// If None is returned, the animation is not handled by this plugin.
55    fn window_close_animation(
56        &mut self,
57        _data: &AnimationFrameData,
58    ) -> Option<AnimationFrameResult> {
59        None
60    }
61
62    /// Handles the window movement animation.
63    ///
64    /// If None is returned, the animation is not handled by this plugin.
65    fn window_move_animation(
66        &mut self,
67        _data: &AnimationFrameData,
68    ) -> Option<AnimationFrameResult> {
69        None
70    }
71
72    /// Handles the workspace switching animation.
73    ///
74    /// If None is returned, the animation is not handled by this plugin.
75    fn workspace_switch_animation(
76        &mut self,
77        _data: &AnimationFrameData,
78    ) -> Option<AnimationFrameResult> {
79        None
80    }
81
82    /// Place a new window.
83    ///
84    // If None is returned, the placement is not handled by this plugin.
85    fn place_new_window(&mut self, _info: &WindowInfo) -> Option<Placement> {
86        None
87    }
88
89    /// Called when a window is about to be deleted.
90    ///
91    /// The window info is still valid at this point (the window has not yet
92    /// been removed from the compositor).
93    fn window_deleted(&mut self, _info: &WindowInfo) {}
94
95    /// Called when a window gains focus.
96    fn window_focused(&mut self, _info: &WindowInfo) {}
97
98    /// Called when a window loses focus.
99    fn window_unfocused(&mut self, _info: &WindowInfo) {}
100
101    /// Called when a workspace is created.
102    fn workspace_created(&mut self, _workspace: &Workspace) {}
103
104    /// Called when a workspace is removed.
105    fn workspace_removed(&mut self, _workspace: &Workspace) {}
106
107    /// Called when a workspace gains focus.
108    ///
109    /// `previous_id` is the internal ID of the previously focused workspace, if any.
110    fn workspace_focused(&mut self, _previous_id: Option<u64>, _current: &Workspace) {}
111
112    /// Called when a workspace's area (geometry) changes.
113    fn workspace_area_changed(&mut self, _workspace: &Workspace) {}
114
115    /// Called when a window's workspace has changed.
116    ///
117    /// This fires whenever a window is moved to a different workspace,
118    /// whether initiated by the user, a command, or a plugin.
119    fn window_workspace_changed(&mut self, _info: &WindowInfo, _workspace: &Workspace) {}
120
121    /// Handle a keyboard event.
122    ///
123    /// If the plugin returns `false`, the event is propagated to the next
124    /// handler in Miracle. If the plugin returns `true`, then the event is
125    /// consumed by the plugin.
126    fn handle_keyboard_input(&mut self, _event: KeyboardEvent) -> bool {
127        false
128    }
129
130    /// Handle a pointer event.
131    ///
132    /// If the plugin returns `false`, the event is propagated to the next
133    /// handler in Miracle. If the plugin returns `true`, then the event is
134    /// consumed by the plugin.
135    fn handle_pointer_event(&mut self, _event: PointerEvent) -> bool {
136        false
137    }
138}
139
140/// Lists the windows that are managed by this plugin.
141///
142/// A window that is managed by this plugin had to have been placed
143/// via a freestyle placement strategy, otherwise the tiling manager
144/// or the system is handling it independently.
145///
146/// Each returned [`PluginWindow`] wraps a [`WindowInfo`] (accessible via `Deref`) and
147/// additionally exposes setter methods for mutating the window's state, workspace,
148/// size, transform, and alpha.
149pub fn managed_windows() -> Vec<PluginWindow> {
150    let handle = unsafe { miracle_get_plugin_handle() };
151    let count = unsafe { miracle_num_managed_windows(handle) };
152
153    (0..count)
154        .filter_map(|i| {
155            const NAME_BUF_LEN: usize = 256;
156            let mut window_info =
157                std::mem::MaybeUninit::<crate::bindings::miracle_window_info_t>::uninit();
158            let mut name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
159
160            unsafe {
161                let result = miracle_get_managed_window_at(
162                    handle,
163                    i,
164                    window_info.as_mut_ptr() as i32,
165                    name_buf.as_mut_ptr() as i32,
166                    NAME_BUF_LEN as i32,
167                );
168
169                if result != 0 {
170                    return None;
171                }
172
173                let window_info = window_info.assume_init();
174                let name_len = name_buf
175                    .iter()
176                    .position(|&c| c == 0)
177                    .unwrap_or(NAME_BUF_LEN);
178                let name = String::from_utf8_lossy(&name_buf[..name_len]).into_owned();
179
180                Some(PluginWindow::from_window_info(
181                    WindowInfo::from_c_with_name(&window_info, name),
182                ))
183            }
184        })
185        .collect()
186}
187
188/// Get the number of outputs.
189pub fn num_outputs() -> u32 {
190    unsafe { miracle_num_outputs() }
191}
192
193/// Get an output by index.
194///
195/// Returns `None` if the index is out of bounds or if the call fails.
196pub fn get_output_at(index: u32) -> Option<Output> {
197    if index >= num_outputs() {
198        return None;
199    }
200
201    const NAME_BUF_LEN: usize = 256;
202    let mut output = std::mem::MaybeUninit::<crate::bindings::miracle_output_t>::uninit();
203    let mut name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
204
205    unsafe {
206        let result = miracle_get_output_at(
207            index,
208            output.as_mut_ptr() as i32,
209            name_buf.as_mut_ptr() as i32,
210            NAME_BUF_LEN as i32,
211        );
212
213        if result != 0 {
214            return None;
215        }
216
217        let output = output.assume_init();
218
219        // Find the null terminator to get the actual string length
220        let name_len = name_buf
221            .iter()
222            .position(|&c| c == 0)
223            .unwrap_or(NAME_BUF_LEN);
224        let name = String::from_utf8_lossy(&name_buf[..name_len]).into_owned();
225
226        Some(Output::from_c_with_name(&output, name))
227    }
228}
229
230/// Get all outputs.
231pub fn get_outputs() -> Vec<Output> {
232    let count = num_outputs();
233    (0..count).filter_map(get_output_at).collect()
234}
235
236/// Get the currently active workspace on the focused output.
237///
238/// Returns `None` if there is no focused output or no active workspace.
239pub fn get_active_workspace() -> Option<Workspace> {
240    const NAME_BUF_LEN: usize = 256;
241    let mut workspace = std::mem::MaybeUninit::<crate::bindings::miracle_workspace_t>::uninit();
242    let mut name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
243
244    unsafe {
245        let result = miracle_get_active_workspace(
246            workspace.as_mut_ptr() as i32,
247            name_buf.as_mut_ptr() as i32,
248            NAME_BUF_LEN as i32,
249        );
250
251        if result != 0 {
252            return None;
253        }
254
255        let workspace = workspace.assume_init();
256        if workspace.is_set == 0 {
257            return None;
258        }
259
260        let name_len = name_buf
261            .iter()
262            .position(|&c| c == 0)
263            .unwrap_or(NAME_BUF_LEN);
264        let name = String::from_utf8_lossy(&name_buf[..name_len]).into_owned();
265
266        Some(Workspace::from_c_with_name(&workspace, name))
267    }
268}
269
270/// Request a workspace by optional number and/or name.
271///
272/// If a workspace with the given number or name already exists, it is returned.
273/// Otherwise, a new workspace is created on the focused output.
274///
275/// If `focus` is true, the workspace will be focused after creation/lookup.
276///
277/// Returns `None` if the workspace could not be created.
278pub fn request_workspace(
279    number: Option<u32>,
280    name: Option<&str>,
281    focus: bool,
282) -> Option<Workspace> {
283    const NAME_BUF_LEN: usize = 256;
284    let mut workspace = std::mem::MaybeUninit::<crate::bindings::miracle_workspace_t>::uninit();
285    let mut out_name_buf: [u8; NAME_BUF_LEN] = [0; NAME_BUF_LEN];
286
287    let has_number: i32 = if number.is_some() { 1 } else { 0 };
288    let number_val: i32 = number.unwrap_or(0) as i32;
289
290    let name_ptr: i32 = match name {
291        Some(s) => s.as_ptr() as i32,
292        None => 0,
293    };
294    let name_len: i32 = match name {
295        Some(s) => s.len() as i32,
296        None => 0,
297    };
298
299    unsafe {
300        let result = miracle_request_workspace(
301            has_number,
302            number_val,
303            name_ptr,
304            name_len,
305            workspace.as_mut_ptr() as i32,
306            out_name_buf.as_mut_ptr() as i32,
307            NAME_BUF_LEN as i32,
308            if focus { 1 } else { 0 },
309        );
310
311        if result != 0 {
312            return None;
313        }
314
315        let workspace = workspace.assume_init();
316        if workspace.is_set == 0 {
317            return None;
318        }
319
320        let out_name_len = out_name_buf
321            .iter()
322            .position(|&c| c == 0)
323            .unwrap_or(NAME_BUF_LEN);
324        let ws_name = String::from_utf8_lossy(&out_name_buf[..out_name_len]).into_owned();
325
326        Some(Workspace::from_c_with_name(&workspace, ws_name))
327    }
328}
329
330/// Queue a custom per-frame animation with a callback.
331///
332/// `callback` receives `(animation_id, dt, elapsed_seconds)` every frame.
333/// The compositor automatically removes the animation after `duration_seconds`.
334///
335/// `dt` and `elapsed_seconds` are floats in seconds.
336///
337/// Returns the host-generated animation ID on success, or `None` on error.
338pub fn queue_custom_animation<F>(callback: F, duration_seconds: f32) -> Option<u32>
339where
340    F: FnMut(u32, f32, f32) + 'static,
341{
342    let handle = unsafe { miracle_get_plugin_handle() };
343    let mut animation_id: u32 = 0;
344    let mut dur = duration_seconds;
345    let result = unsafe {
346        crate::host::miracle_queue_custom_animation(
347            handle as i32,
348            &mut animation_id as *mut u32 as i32,
349            &mut dur as *mut f32 as i32,
350        )
351    };
352    if result == 0 {
353        custom_anim_callbacks().insert(animation_id, (Box::new(callback), duration_seconds));
354        Some(animation_id)
355    } else {
356        None
357    }
358}
359
360static mut _CUSTOM_ANIM_CALLBACKS: Option<
361    std::collections::HashMap<u32, (Box<dyn FnMut(u32, f32, f32)>, f32)>,
362> = None;
363
364/// Returns the global custom-animation callback registry.
365///
366/// # Safety
367/// Only safe in a single-threaded WASM context (which is always the case for miracle plugins).
368#[doc(hidden)]
369pub fn custom_anim_callbacks()
370-> &'static mut std::collections::HashMap<u32, (Box<dyn FnMut(u32, f32, f32)>, f32)> {
371    unsafe {
372        if (*std::ptr::addr_of!(_CUSTOM_ANIM_CALLBACKS)).is_none() {
373            _CUSTOM_ANIM_CALLBACKS = Some(std::collections::HashMap::new());
374        }
375        (*std::ptr::addr_of_mut!(_CUSTOM_ANIM_CALLBACKS))
376            .as_mut()
377            .unwrap()
378    }
379}
380
381#[macro_export]
382macro_rules! miracle_plugin {
383    ($plugin_type:ty) => {
384        static mut _MIRACLE_PLUGIN: Option<$plugin_type> = None;
385        static mut _MIRACLE_PLUGIN_HANDLE: u32 = 0;
386
387        #[unsafe(no_mangle)]
388        pub extern "C" fn miracle_get_plugin_handle() -> u32 {
389            unsafe { _MIRACLE_PLUGIN_HANDLE }
390        }
391
392        #[unsafe(no_mangle)]
393        pub extern "C" fn init(handle: i32) {
394            unsafe {
395                _MIRACLE_PLUGIN_HANDLE = handle as u32;
396                _MIRACLE_PLUGIN = Some(<$plugin_type>::default());
397            }
398        }
399
400        #[unsafe(no_mangle)]
401        pub extern "C" fn animate(data_ptr: i32, result_ptr: i32) -> i32 {
402            let plugin = unsafe {
403                match _MIRACLE_PLUGIN.as_mut() {
404                    Some(p) => p,
405                    None => return 0,
406                }
407            };
408
409            let c_data = unsafe {
410                &*(data_ptr as *const $crate::bindings::miracle_plugin_animation_frame_data_t)
411            };
412            let data: AnimationFrameData = (*c_data).into();
413
414            match c_data.type_ {
415                $crate::bindings::miracle_animation_type_miracle_animation_type_window_open => {
416                    match plugin.window_open_animation(&data) {
417                        Some(result) => {
418                            let c_result: $crate::bindings::miracle_plugin_animation_frame_result_t =
419                                result.into();
420                            unsafe {
421                                let out = &mut *(result_ptr
422                                    as *mut $crate::bindings::miracle_plugin_animation_frame_result_t);
423                                *out = c_result;
424                            }
425                            return 1;
426                        }
427                        None => 0,
428                    }
429                },
430                $crate::bindings::miracle_animation_type_miracle_animation_type_window_close => {
431                    match plugin.window_close_animation(&data) {
432                        Some(result) => {
433                            let c_result: $crate::bindings::miracle_plugin_animation_frame_result_t =
434                                result.into();
435                            unsafe {
436                                let out = &mut *(result_ptr
437                                    as *mut $crate::bindings::miracle_plugin_animation_frame_result_t);
438                                *out = c_result;
439                            }
440                            return 1;
441                        }
442                        None => 0,
443                    }
444                },
445                $crate::bindings::miracle_animation_type_miracle_animation_type_window_move => {
446                    match plugin.window_move_animation(&data) {
447                        Some(result) => {
448                            let c_result: $crate::bindings::miracle_plugin_animation_frame_result_t =
449                                result.into();
450                            unsafe {
451                                let out = &mut *(result_ptr
452                                    as *mut $crate::bindings::miracle_plugin_animation_frame_result_t);
453                                *out = c_result;
454                            }
455                            return 1;
456                        }
457                        None => 0,
458                    }
459                },
460                $crate::bindings::miracle_animation_type_miracle_animation_type_workspace_switch => {
461                    match plugin.workspace_switch_animation(&data) {
462                        Some(result) => {
463                            let c_result: $crate::bindings::miracle_plugin_animation_frame_result_t =
464                                result.into();
465                            unsafe {
466                                let out = &mut *(result_ptr
467                                    as *mut $crate::bindings::miracle_plugin_animation_frame_result_t);
468                                *out = c_result;
469                            }
470                            return 1;
471                        }
472                        None => 0,
473                    }
474                },
475                _ => 0
476            }
477
478        }
479
480        #[unsafe(no_mangle)]
481        pub extern "C" fn custom_animate(data_ptr: i32) -> i32 {
482            let raw = unsafe {
483                &*(data_ptr as *const $crate::animation::RawCustomAnimationData)
484            };
485
486            let callbacks = $crate::plugin::custom_anim_callbacks();
487            let done = if let Some((cb, dur)) = callbacks.get_mut(&raw.animation_id) {
488                cb(raw.animation_id, raw.dt, raw.elapsed_seconds);
489                raw.elapsed_seconds >= *dur
490            } else {
491                false
492            };
493            if done {
494                callbacks.remove(&raw.animation_id);
495            }
496
497            // Return value is ignored by the host; kept for WASM ABI compatibility.
498            0
499        }
500
501        #[unsafe(no_mangle)]
502        pub extern "C" fn place_new_window(
503            window_info_ptr: i32,
504            result_ptr: i32,
505            name_ptr: i32,
506            name_len: i32,
507        ) -> i32 {
508            let plugin = unsafe {
509                match _MIRACLE_PLUGIN.as_mut() {
510                    Some(p) => p,
511                    None => return 0,
512                }
513            };
514
515            let c_info = unsafe {
516                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
517            };
518
519            let name = if name_len > 0 {
520                let name_bytes = unsafe {
521                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
522                };
523                String::from_utf8_lossy(name_bytes).into_owned()
524            } else {
525                String::new()
526            };
527
528            let info = unsafe { $crate::window::WindowInfo::from_c_with_name(c_info, name) };
529
530            match plugin.place_new_window(&info) {
531                Some(placement) => {
532                    let c_placement: $crate::bindings::miracle_placement_t = placement.into();
533                    unsafe {
534                        let out = &mut *(result_ptr as *mut $crate::bindings::miracle_placement_t);
535                        *out = c_placement;
536                    }
537                    1
538                }
539                None => 0,
540            }
541        }
542
543        #[unsafe(no_mangle)]
544        pub extern "C" fn window_deleted(
545            window_info_ptr: i32,
546            name_ptr: i32,
547            name_len: i32,
548        ) {
549            let plugin = unsafe {
550                match _MIRACLE_PLUGIN.as_mut() {
551                    Some(p) => p,
552                    None => return,
553                }
554            };
555
556            let c_info = unsafe {
557                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
558            };
559
560            let name = if name_len > 0 {
561                let name_bytes = unsafe {
562                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
563                };
564                String::from_utf8_lossy(name_bytes).into_owned()
565            } else {
566                String::new()
567            };
568
569            let info = unsafe { $crate::window::WindowInfo::from_c_with_name(c_info, name) };
570
571            plugin.window_deleted(&info);
572        }
573
574        #[unsafe(no_mangle)]
575        pub extern "C" fn window_focused(
576            window_info_ptr: i32,
577            name_ptr: i32,
578            name_len: i32,
579        ) {
580            let plugin = unsafe {
581                match _MIRACLE_PLUGIN.as_mut() {
582                    Some(p) => p,
583                    None => return,
584                }
585            };
586
587            let c_info = unsafe {
588                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
589            };
590
591            let name = if name_len > 0 {
592                let name_bytes = unsafe {
593                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
594                };
595                String::from_utf8_lossy(name_bytes).into_owned()
596            } else {
597                String::new()
598            };
599
600            let info = unsafe { $crate::window::WindowInfo::from_c_with_name(c_info, name) };
601
602            plugin.window_focused(&info);
603        }
604
605        #[unsafe(no_mangle)]
606        pub extern "C" fn window_unfocused(
607            window_info_ptr: i32,
608            name_ptr: i32,
609            name_len: i32,
610        ) {
611            let plugin = unsafe {
612                match _MIRACLE_PLUGIN.as_mut() {
613                    Some(p) => p,
614                    None => return,
615                }
616            };
617
618            let c_info = unsafe {
619                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
620            };
621
622            let name = if name_len > 0 {
623                let name_bytes = unsafe {
624                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
625                };
626                String::from_utf8_lossy(name_bytes).into_owned()
627            } else {
628                String::new()
629            };
630
631            let info = unsafe { $crate::window::WindowInfo::from_c_with_name(c_info, name) };
632
633            plugin.window_unfocused(&info);
634        }
635
636        #[unsafe(no_mangle)]
637        pub extern "C" fn workspace_created(
638            workspace_info_ptr: i32,
639            name_ptr: i32,
640            name_len: i32,
641        ) {
642            let plugin = unsafe {
643                match _MIRACLE_PLUGIN.as_mut() {
644                    Some(p) => p,
645                    None => return,
646                }
647            };
648
649            let c_ws = unsafe {
650                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
651            };
652
653            let name = if name_len > 0 {
654                let name_bytes = unsafe {
655                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
656                };
657                String::from_utf8_lossy(name_bytes).into_owned()
658            } else {
659                String::new()
660            };
661
662            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
663            plugin.workspace_created(&ws);
664        }
665
666        #[unsafe(no_mangle)]
667        pub extern "C" fn workspace_removed(
668            workspace_info_ptr: i32,
669            name_ptr: i32,
670            name_len: i32,
671        ) {
672            let plugin = unsafe {
673                match _MIRACLE_PLUGIN.as_mut() {
674                    Some(p) => p,
675                    None => return,
676                }
677            };
678
679            let c_ws = unsafe {
680                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
681            };
682
683            let name = if name_len > 0 {
684                let name_bytes = unsafe {
685                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
686                };
687                String::from_utf8_lossy(name_bytes).into_owned()
688            } else {
689                String::new()
690            };
691
692            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
693            plugin.workspace_removed(&ws);
694        }
695
696        #[unsafe(no_mangle)]
697        pub extern "C" fn workspace_focused(
698            workspace_info_ptr: i32,
699            name_ptr: i32,
700            name_len: i32,
701            has_previous: i32,
702            previous_id: i64,
703        ) {
704            let plugin = unsafe {
705                match _MIRACLE_PLUGIN.as_mut() {
706                    Some(p) => p,
707                    None => return,
708                }
709            };
710
711            let c_ws = unsafe {
712                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
713            };
714
715            let name = if name_len > 0 {
716                let name_bytes = unsafe {
717                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
718                };
719                String::from_utf8_lossy(name_bytes).into_owned()
720            } else {
721                String::new()
722            };
723
724            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
725            let prev = if has_previous != 0 { Some(previous_id as u64) } else { None };
726            plugin.workspace_focused(prev, &ws);
727        }
728
729        #[unsafe(no_mangle)]
730        pub extern "C" fn workspace_area_changed(
731            workspace_info_ptr: i32,
732            name_ptr: i32,
733            name_len: i32,
734        ) {
735            let plugin = unsafe {
736                match _MIRACLE_PLUGIN.as_mut() {
737                    Some(p) => p,
738                    None => return,
739                }
740            };
741
742            let c_ws = unsafe {
743                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
744            };
745
746            let name = if name_len > 0 {
747                let name_bytes = unsafe {
748                    core::slice::from_raw_parts(name_ptr as *const u8, name_len as usize)
749                };
750                String::from_utf8_lossy(name_bytes).into_owned()
751            } else {
752                String::new()
753            };
754
755            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, name) };
756            plugin.workspace_area_changed(&ws);
757        }
758
759        #[unsafe(no_mangle)]
760        pub extern "C" fn window_workspace_changed(
761            window_info_ptr: i32,
762            window_name_ptr: i32,
763            window_name_len: i32,
764            workspace_info_ptr: i32,
765            workspace_name_ptr: i32,
766            workspace_name_len: i32,
767        ) {
768            let plugin = unsafe {
769                match _MIRACLE_PLUGIN.as_mut() {
770                    Some(p) => p,
771                    None => return,
772                }
773            };
774
775            let c_info = unsafe {
776                &*(window_info_ptr as *const $crate::bindings::miracle_window_info_t)
777            };
778
779            let window_name = if window_name_len > 0 {
780                let name_bytes = unsafe {
781                    core::slice::from_raw_parts(window_name_ptr as *const u8, window_name_len as usize)
782                };
783                String::from_utf8_lossy(name_bytes).into_owned()
784            } else {
785                String::new()
786            };
787
788            let info = unsafe { $crate::window::WindowInfo::from_c_with_name(c_info, window_name) };
789
790            let c_ws = unsafe {
791                &*(workspace_info_ptr as *const $crate::bindings::miracle_workspace_t)
792            };
793
794            let workspace_name = if workspace_name_len > 0 {
795                let name_bytes = unsafe {
796                    core::slice::from_raw_parts(workspace_name_ptr as *const u8, workspace_name_len as usize)
797                };
798                String::from_utf8_lossy(name_bytes).into_owned()
799            } else {
800                String::new()
801            };
802
803            let ws = unsafe { $crate::workspace::Workspace::from_c_with_name(c_ws, workspace_name) };
804            plugin.window_workspace_changed(&info, &ws);
805        }
806
807        #[unsafe(no_mangle)]
808        pub extern "C" fn handle_keyboard_input(event_ptr: i32) -> i32 {
809            let plugin = unsafe {
810                match _MIRACLE_PLUGIN.as_mut() {
811                    Some(p) => p,
812                    None => return 0,
813                }
814            };
815
816            let c_event = unsafe {
817                &*(event_ptr as *const $crate::bindings::miracle_keyboard_event_t)
818            };
819
820            let event = $crate::input::KeyboardEvent {
821                action: $crate::input::KeyboardAction::try_from(c_event.action)
822                    .unwrap_or_default(),
823                keysym: c_event.keysym,
824                scan_code: c_event.scan_code,
825                modifiers: $crate::input::InputEventModifiers::from(c_event.modifiers),
826            };
827
828            if plugin.handle_keyboard_input(event) { 1 } else { 0 }
829        }
830
831        #[unsafe(no_mangle)]
832        pub extern "C" fn handle_pointer_event(event_ptr: i32) -> i32 {
833            let plugin = unsafe {
834                match _MIRACLE_PLUGIN.as_mut() {
835                    Some(p) => p,
836                    None => return 0,
837                }
838            };
839
840            let c_event = unsafe {
841                &*(event_ptr as *const $crate::bindings::miracle_pointer_event_t)
842            };
843
844            let event = $crate::input::PointerEvent {
845                x: c_event.x,
846                y: c_event.y,
847                action: $crate::input::PointerAction::try_from(c_event.action)
848                    .unwrap_or_default(),
849                modifiers: $crate::input::InputEventModifiers::from(c_event.modifiers),
850                buttons: $crate::input::PointerButtons::from(c_event.buttons),
851            };
852
853            if plugin.handle_pointer_event(event) { 1 } else { 0 }
854        }
855    };
856}