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