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
14pub 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 fn window_open_animation(
46 &mut self,
47 _data: &AnimationFrameData,
48 ) -> Option<AnimationFrameResult> {
49 None
50 }
51
52 fn window_close_animation(
56 &mut self,
57 _data: &AnimationFrameData,
58 ) -> Option<AnimationFrameResult> {
59 None
60 }
61
62 fn window_move_animation(
66 &mut self,
67 _data: &AnimationFrameData,
68 ) -> Option<AnimationFrameResult> {
69 None
70 }
71
72 fn workspace_switch_animation(
76 &mut self,
77 _data: &AnimationFrameData,
78 ) -> Option<AnimationFrameResult> {
79 None
80 }
81
82 fn place_new_window(&mut self, _info: &WindowInfo) -> Option<Placement> {
86 None
87 }
88
89 fn window_deleted(&mut self, _info: &WindowInfo) {}
94
95 fn window_focused(&mut self, _info: &WindowInfo) {}
97
98 fn window_unfocused(&mut self, _info: &WindowInfo) {}
100
101 fn workspace_created(&mut self, _workspace: &Workspace) {}
103
104 fn workspace_removed(&mut self, _workspace: &Workspace) {}
106
107 fn workspace_focused(&mut self, _previous_id: Option<u64>, _current: &Workspace) {}
111
112 fn workspace_area_changed(&mut self, _workspace: &Workspace) {}
114
115 fn window_workspace_changed(&mut self, _info: &WindowInfo, _workspace: &Workspace) {}
120
121 fn handle_keyboard_input(&mut self, _event: KeyboardEvent) -> bool {
127 false
128 }
129
130 fn handle_pointer_event(&mut self, _event: PointerEvent) -> bool {
136 false
137 }
138}
139
140pub 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
188pub fn num_outputs() -> u32 {
190 unsafe { miracle_num_outputs() }
191}
192
193pub 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 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
230pub fn get_outputs() -> Vec<Output> {
232 let count = num_outputs();
233 (0..count).filter_map(get_output_at).collect()
234}
235
236pub 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
270pub 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
330pub 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#[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 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}