1#![warn(
89 noop_method_call,
90 trivial_casts,
91 trivial_numeric_casts,
92 unused_import_braces,
93 unused_lifetimes,
94 unused_qualifications,
95 unsafe_op_in_unsafe_fn,
96 missing_docs,
97 missing_debug_implementations,
98 clippy::pedantic
99)]
100#![allow(
101 clippy::cast_sign_loss,
102 clippy::cast_possible_truncation,
103 clippy::cast_possible_wrap
104)]
105
106pub use cairo;
107pub use pango;
108pub use rofi_plugin_sys as ffi;
109
110mod string;
111pub use string::format;
112pub use string::String;
113
114pub mod api;
115pub use api::Api;
116
117pub trait Mode<'rofi>: Sized + Sync {
122 const NAME: &'static str;
132
133 #[allow(clippy::result_unit_err)]
144 fn init(api: Api<'rofi>) -> Result<Self, ()>;
145
146 fn entries(&mut self) -> usize;
148
149 fn entry_content(&self, line: usize) -> String;
153
154 fn entry_style(&self, _line: usize) -> Style {
160 Style::NORMAL
161 }
162
163 fn entry_attributes(&self, _line: usize) -> Attributes {
169 Attributes::new()
170 }
171
172 fn entry_icon(&mut self, _line: usize, _height: u32) -> Option<cairo::Surface> {
184 None
185 }
186
187 fn react(&mut self, event: Event, input: &mut String) -> Action;
194
195 fn matches(&self, line: usize, matcher: Matcher<'_>) -> bool;
199
200 fn completed(&self, line: usize) -> String {
214 self.entry_content(line)
215 }
216
217 fn preprocess_input(&mut self, input: &str) -> String {
222 input.into()
223 }
224
225 fn message(&mut self) -> String {
233 String::new()
234 }
235}
236
237#[macro_export]
242macro_rules! export_mode {
243 ($t:ty $(,)?) => {
244 #[no_mangle]
245 pub static mut mode: $crate::ffi::Mode = $crate::raw_mode::<fn(&()) -> $t>();
246 };
247}
248
249#[must_use]
258pub const fn raw_mode<T>() -> ffi::Mode
259where
260 <[T; 0] as IntoIterator>::Item: GivesMode,
262{
263 <RawModeHelper<T>>::VALUE
264}
265
266mod sealed {
267 use crate::Mode;
268
269 pub trait GivesMode: for<'rofi> GivesModeLifetime<'rofi> {}
270 impl<T: ?Sized + for<'rofi> GivesModeLifetime<'rofi>> GivesMode for T {}
271
272 pub trait GivesModeLifetime<'rofi> {
273 type Mode: Mode<'rofi>;
274 }
275 impl<'rofi, F: FnOnce(&'rofi ()) -> O, O: Mode<'rofi>> GivesModeLifetime<'rofi> for F {
276 type Mode = O;
277 }
278}
279use sealed::GivesMode;
280use sealed::GivesModeLifetime;
281
282struct RawModeHelper<T>(T);
283impl<T: GivesMode> RawModeHelper<T> {
284 const VALUE: ffi::Mode = ffi::Mode {
285 name: assert_c_str(<<T as GivesModeLifetime<'_>>::Mode as Mode>::NAME),
286 _init: Some(init::<T>),
287 _destroy: Some(destroy::<T>),
288 _get_num_entries: Some(get_num_entries::<T>),
289 _result: Some(result::<T>),
290 _get_display_value: Some(get_display_value::<T>),
291 _token_match: Some(token_match::<T>),
292 _get_icon: Some(get_icon::<T>),
293 _get_completion: Some(get_completion::<T>),
294 _preprocess_input: Some(preprocess_input::<T>),
295 _get_message: Some(get_message::<T>),
296 ..ffi::Mode::default()
297 };
298}
299
300const fn assert_c_str(s: &'static str) -> *mut c_char {
301 let mut i = 0;
302 while i + 1 < s.len() {
303 assert!(s.as_bytes()[i] != 0, "string contains intermediary nul");
304 i += 1;
305 }
306 assert!(s.as_bytes()[i] == 0, "string is not nul-terminated");
307 s.as_ptr() as _
308}
309
310type ModeOf<'a, T> = <T as GivesModeLifetime<'a>>::Mode;
311
312unsafe extern "C" fn init<T: GivesMode>(sw: *mut ffi::Mode) -> c_int {
313 if unsafe { ffi::mode_get_private_data(sw) }.is_null() {
314 let api = unsafe { Api::new(ptr::NonNull::from(&mut (*sw).display_name).cast()) };
315
316 let boxed: Box<ModeOf<'_, T>> =
317 match catch_panic(|| <ModeOf<'_, T>>::init(api).map(Box::new)) {
318 Ok(Ok(boxed)) => boxed,
319 Ok(Err(())) | Err(()) => return false.into(),
320 };
321 let ptr = Box::into_raw(boxed).cast::<c_void>();
322 unsafe { ffi::mode_set_private_data(sw, ptr) };
323 }
324 true.into()
325}
326
327unsafe extern "C" fn destroy<T: GivesMode>(sw: *mut ffi::Mode) {
328 let ptr = unsafe { ffi::mode_get_private_data(sw) };
329 if ptr.is_null() {
330 return;
331 }
332 let boxed = unsafe { <Box<ModeOf<'_, T>>>::from_raw(ptr.cast()) };
333 _ = catch_panic(|| drop(boxed));
334 unsafe { ffi::mode_set_private_data(sw, ptr::null_mut()) };
335}
336
337unsafe extern "C" fn get_num_entries<T: GivesMode>(sw: *const ffi::Mode) -> c_uint {
338 let mode: &mut ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
339 catch_panic(|| mode.entries().try_into().unwrap_or(c_uint::MAX)).unwrap_or(0)
340}
341
342unsafe extern "C" fn result<T: GivesMode>(
343 sw: *mut ffi::Mode,
344 mretv: c_int,
345 input: *mut *mut c_char,
346 selected_line: c_uint,
347) -> c_int {
348 let mode: &mut ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
349 let action = catch_panic(|| {
350 let selected = if selected_line == c_uint::MAX {
351 None
352 } else {
353 Some(selected_line as usize)
354 };
355
356 let event = match mretv {
357 ffi::menu::CANCEL => Event::Cancel { selected },
358 _ if mretv & ffi::menu::OK != 0 => Event::Ok {
359 alt: mretv & ffi::menu::CUSTOM_ACTION != 0,
360 selected: selected.expect("Ok event without selected line"),
361 },
362 _ if mretv & ffi::menu::CUSTOM_INPUT != 0 => Event::CustomInput {
363 alt: mretv & ffi::menu::CUSTOM_ACTION != 0,
364 selected,
365 },
366 ffi::menu::COMPLETE => Event::Complete { selected },
367 ffi::menu::ENTRY_DELETE => Event::DeleteEntry {
368 selected: selected.expect("DeleteEntry event without selected line"),
369 },
370 _ if mretv & ffi::menu::CUSTOM_COMMAND != 0 => Event::CustomCommand {
371 number: (mretv & ffi::menu::LOWER_MASK) as u8,
372 selected,
373 },
374 _ => panic!("unexpected mretv {mretv:X}"),
375 };
376
377 let input: &mut *mut c_char = unsafe { &mut *input };
378 let input_ptr: *mut c_char = mem::replace(&mut *input, ptr::null_mut());
379
380 let mut input_string = if input_ptr.is_null() {
381 String::new()
382 } else {
383 let len = unsafe { libc::strlen(input_ptr) };
384 unsafe { String::from_raw_parts(input_ptr.cast(), len, len + 1) }
385 };
386
387 let action = mode.react(event, &mut input_string);
388
389 if !input_string.is_empty() {
390 *input = input_string.into_raw().cast::<c_char>();
391 }
392
393 action
394 })
395 .unwrap_or(Action::Exit);
396
397 match action {
398 Action::SetMode(mode) => mode.into(),
399 Action::Next => ffi::NEXT_DIALOG,
400 Action::Previous => ffi::PREVIOUS_DIALOG,
401 Action::Reload => ffi::RELOAD_DIALOG,
402 Action::Reset => ffi::RESET_DIALOG,
403 Action::Exit => ffi::EXIT,
404 }
405}
406
407unsafe extern "C" fn get_display_value<T: GivesMode>(
408 sw: *const ffi::Mode,
409 selected_line: c_uint,
410 state: *mut c_int,
411 attr_list: *mut *mut glib_sys::GList,
412 get_entry: c_int,
413) -> *mut c_char {
414 let mode: &ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
415 catch_panic(|| {
416 let line = selected_line as usize;
417
418 if !state.is_null() {
419 let style = mode.entry_style(line);
420 unsafe { *state = style.bits() as c_int };
421 }
422
423 if !attr_list.is_null() {
424 assert!(unsafe { *attr_list }.is_null());
425 let attributes = mode.entry_attributes(line);
426 unsafe { *attr_list = ManuallyDrop::new(attributes).list };
427 }
428
429 if get_entry == 0 {
430 ptr::null_mut()
431 } else {
432 mode.entry_content(line).into_raw().cast()
433 }
434 })
435 .unwrap_or(ptr::null_mut())
436}
437
438unsafe extern "C" fn token_match<T: GivesMode>(
439 sw: *const ffi::Mode,
440 tokens: *mut *mut ffi::RofiIntMatcher,
441 index: c_uint,
442) -> c_int {
443 let mode: &ModeOf<'_, T> = unsafe { &*ffi::mode_get_private_data(sw).cast() };
444 catch_panic(|| {
445 let matcher = unsafe { Matcher::from_ffi(tokens) };
446 mode.matches(index as usize, matcher)
447 })
448 .unwrap_or(false)
449 .into()
450}
451
452unsafe extern "C" fn get_icon<T: GivesMode>(
453 sw: *const ffi::Mode,
454 selected_line: c_uint,
455 height: c_int,
456) -> *mut cairo_sys::cairo_surface_t {
457 let mode: &mut ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
458 catch_panic(|| {
459 const NEGATIVE_HEIGHT: &str = "negative height passed into get_icon";
460
461 let height: u32 = height.try_into().expect(NEGATIVE_HEIGHT);
462
463 mode.entry_icon(selected_line as usize, height)
464 .map_or_else(ptr::null_mut, |surface| {
465 ManuallyDrop::new(surface).to_raw_none()
466 })
467 })
468 .unwrap_or(ptr::null_mut())
469}
470
471unsafe extern "C" fn get_completion<T: GivesMode>(
472 sw: *const ffi::Mode,
473 selected_line: c_uint,
474) -> *mut c_char {
475 let mode: &ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
476 abort_on_panic(|| {
477 mode.completed(selected_line as usize)
478 .into_raw()
479 .cast::<c_char>()
480 })
481}
482
483unsafe extern "C" fn preprocess_input<T: GivesMode>(
484 sw: *mut ffi::Mode,
485 input: *const c_char,
486) -> *mut c_char {
487 let mode: &mut ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
488 abort_on_panic(|| {
489 let input = unsafe { CStr::from_ptr(input) }
490 .to_str()
491 .expect("Input is not valid UTF-8");
492 let processed = mode.preprocess_input(input);
493 if processed.is_empty() {
494 ptr::null_mut()
495 } else {
496 processed.into_raw().cast::<c_char>()
497 }
498 })
499}
500
501unsafe extern "C" fn get_message<T: GivesMode>(sw: *const ffi::Mode) -> *mut c_char {
502 let mode: &mut ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
503 catch_panic(|| {
504 let message = mode.message();
505 if message.is_empty() {
506 return ptr::null_mut();
507 }
508 message.into_raw().cast::<c_char>()
509 })
510 .unwrap_or(ptr::null_mut())
511}
512
513struct AbortOnDrop;
514impl Drop for AbortOnDrop {
515 fn drop(&mut self) {
516 process::abort();
517 }
518}
519
520fn abort_on_panic<O, F: FnOnce() -> O>(f: F) -> O {
521 let guard = AbortOnDrop;
522 let res = f();
523 mem::forget(guard);
524 res
525}
526
527fn catch_panic<O, F: FnOnce() -> O>(f: F) -> Result<O, ()> {
528 panic::catch_unwind(panic::AssertUnwindSafe(f)).map_err(|e| {
529 let guard = AbortOnDrop;
530 drop(e);
531 mem::forget(guard);
532 })
533}
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq)]
537pub enum Event {
538 Cancel {
540 selected: Option<usize>,
545 },
546 Ok {
548 alt: bool,
550 selected: usize,
552 },
553 CustomInput {
555 alt: bool,
557 selected: Option<usize>,
562 },
563 Complete {
570 selected: Option<usize>,
575 },
576 DeleteEntry {
578 selected: usize,
582 },
583 CustomCommand {
585 number: u8,
587 selected: Option<usize>,
592 },
593}
594
595impl Event {
596 #[must_use]
599 pub const fn selected(&self) -> Option<usize> {
600 match *self {
601 Self::Cancel { selected }
602 | Self::CustomInput { selected, .. }
603 | Self::Complete { selected }
604 | Self::CustomCommand { selected, .. } => selected,
605 Self::Ok { selected, .. } | Self::DeleteEntry { selected } => Some(selected),
606 }
607 }
608}
609
610#[derive(Debug, Clone, Copy, PartialEq, Eq)]
612pub enum Action {
613 SetMode(u16),
617 Next,
619 Previous,
621 Reload,
623 Reset,
625 Exit,
627}
628
629bitflags! {
630 #[derive(Default)]
632 pub struct Style: u32 {
633 const NORMAL = 0;
635 const URGENT = 1;
637 const ACTIVE = 2;
639 const SELECTED = 4;
641 const MARKUP = 8;
645
646 const ALT = 16;
648 const HIGHLIGHT = 32;
650 }
651}
652
653#[derive(Debug)]
655pub struct Attributes {
656 list: *mut glib_sys::GList,
657}
658
659unsafe impl Send for Attributes {}
660unsafe impl Sync for Attributes {}
661
662impl Attributes {
663 #[must_use]
665 pub const fn new() -> Self {
666 Self {
667 list: ptr::null_mut(),
668 }
669 }
670
671 pub fn push<A: Into<pango::Attribute>>(&mut self, attribute: A) {
673 let attribute: pango::Attribute = attribute.into();
674 let raw: *mut pango_sys::PangoAttribute = ManuallyDrop::new(attribute).to_glib_none_mut().0;
676 self.list = unsafe { glib_sys::g_list_prepend(self.list, raw.cast()) };
677 }
678}
679
680impl Default for Attributes {
681 fn default() -> Self {
682 Self::new()
683 }
684}
685
686impl From<pango::Attribute> for Attributes {
687 fn from(attribute: pango::Attribute) -> Self {
688 let mut this = Self::new();
689 this.push(attribute);
690 this
691 }
692}
693
694impl Drop for Attributes {
695 fn drop(&mut self) {
696 unsafe extern "C" fn free_attribute(ptr: *mut c_void) {
697 unsafe { pango_sys::pango_attribute_destroy(ptr.cast()) }
698 }
699
700 unsafe { glib_sys::g_list_free_full(self.list, Some(free_attribute)) };
701 }
702}
703
704impl<A: Into<pango::Attribute>> Extend<A> for Attributes {
705 fn extend<T: IntoIterator<Item = A>>(&mut self, iter: T) {
706 iter.into_iter().for_each(|item| self.push(item));
707 }
708}
709
710impl<A: Into<pango::Attribute>> FromIterator<A> for Attributes {
711 fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
712 let mut this = Self::new();
713 this.extend(iter);
714 this
715 }
716}
717
718#[derive(Debug, Clone, Copy)]
720pub struct Matcher<'a> {
721 ptr: Option<&'a *mut ffi::RofiIntMatcher>,
722}
723
724unsafe impl Send for Matcher<'_> {}
725unsafe impl Sync for Matcher<'_> {}
726
727impl Matcher<'_> {
728 pub(crate) unsafe fn from_ffi(ffi: *const *mut ffi::RofiIntMatcher) -> Self {
729 Self {
730 ptr: if ffi.is_null() {
731 None
732 } else {
733 Some(unsafe { &*ffi })
734 },
735 }
736 }
737
738 #[must_use]
744 pub fn matches(self, s: &str) -> bool {
745 let s = CString::new(s).expect("string contains null bytes");
746 self.matches_c_str(&s)
747 }
748
749 #[must_use]
751 pub fn matches_c_str(self, s: &CStr) -> bool {
752 let ptr: *const *mut ffi::RofiIntMatcher = match self.ptr {
753 Some(ptr) => ptr,
754 None => return true,
755 };
756 0 != unsafe { ffi::helper::token_match(ptr, s.as_ptr()) }
757 }
758}
759
760use bitflags::bitflags;
761use cairo::ffi as cairo_sys;
762use pango::ffi as pango_sys;
763use pango::glib::ffi as glib_sys;
764use pango::glib::translate::ToGlibPtrMut;
765use std::ffi::c_void;
766use std::ffi::CStr;
767use std::ffi::CString;
768use std::mem;
769use std::mem::ManuallyDrop;
770use std::os::raw::c_char;
771use std::os::raw::c_int;
772use std::os::raw::c_uint;
773use std::panic;
774use std::process;
775use std::ptr;