Skip to main content

miracle_plugin/
plugin.rs

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