rofi_mode/
lib.rs

1//! [![crates.io](https://img.shields.io/crates/v/rofi-mode.svg)](https://crates.io/crates/rofi-mode)
2//! ![License](https://img.shields.io/badge/License-MIT-green.svg)
3//!
4//! A high-level Rust library for creating Rofi plugins and custom modes
5//!
6//! # Getting started
7//!
8//! First of all,
9//! create a new library with `cargo new --lib my_awesome_plugin`
10//! and add these lines to the `Cargo.toml`:
11//!
12//! ```toml
13//! [lib]
14//! crate-type = ["cdylib"]
15//! ```
16//!
17//! That will force Cargo to generate your library as a `.so` file,
18//! which is what Rofi loads its plugins from.
19//!
20//! Then, add this crate as a dependency using the following command:
21//!
22//! ```bash
23//! cargo add rofi-mode
24//! ```
25//!
26//! Now in your `lib.rs`,
27//! create a struct and implement the [`Mode`] trait for it.
28//! For example, here is a no-op mode with no entries:
29//!
30//! ```no_run
31//! struct Mode;
32//!
33//! impl rofi_mode::Mode<'_> for Mode {
34//!     const NAME: &'static str = "an-example-mode\0";
35//!     fn init(_api: rofi_mode::Api<'_>) -> Result<Self, ()> {
36//!         Ok(Self)
37//!     }
38//!     fn entries(&mut self) -> usize { 0 }
39//!     fn entry_content(&self, _line: usize) -> rofi_mode::String { unreachable!() }
40//!     fn react(
41//!         &mut self,
42//!         _event: rofi_mode::Event,
43//!         _input: &mut rofi_mode::String,
44//!     ) -> rofi_mode::Action {
45//!         rofi_mode::Action::Exit
46//!     }
47//!     fn matches(&self, _line: usize, _matcher: rofi_mode::Matcher<'_>) -> bool {
48//!         unreachable!()
49//!     }
50//! }
51//! ```
52//!
53//! You then need to export your mode to Rofi via the [`export_mode!`] macro:
54//!
55//! ```ignore
56//! rofi_mode::export_mode!(Mode);
57//! ```
58//!
59//! Build your library using `cargo build`
60//! then copy the resulting dylib file
61//! (e.g. `/target/debug/libmy_awesome_plugin.so`)
62//! into `/lib/rofi`
63//! so that Rofi will pick up on it
64//! when it starts up
65//! (alternatively,
66//! you can set the `ROFI_PLUGIN_PATH` environment variable
67//! to the directory your `.so` file is in).
68//! You can then run your mode from Rofi's command line:
69//!
70//! ```sh
71//! rofi -modi an-example-mode -show an-example-mode
72//! ```
73//!
74//! Build with `RUSTFLAGS="--cfg rofi_next"` to support unreleased Rofi versions,
75//! and drop support for released Rofi versions.
76//! This will turn on semver-exempt features, so use with caution.
77//!
78//! # Examples
79//!
80//! - See [examples/basic] for a basic example of a non-trivial Rofi mode,
81//!     which allows the user to add to the list of entries by writing in the Rofi box.
82//! - See [examples/file-browser] for a Rofi mode implementing a simple file browser.
83//!
84//! [`Mode`]: https://docs.rs/rofi-mode/latest/rofi_mode/trait.Mode.html
85//! [`export_mode!`]: https://docs.rs/rofi-mode/latest/rofi_mode/macro.export_mode.html
86//! [examples/basic]: https://github.com/SabrinaJewson/rofi-mode.rs/tree/main/examples/basic
87//! [examples/file-browser]: https://github.com/SabrinaJewson/rofi-mode.rs/tree/main/examples/file-browser
88#![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
117/// A mode supported by Rofi.
118///
119/// You can implement this trait on your own type to define a mode,
120/// then export it in the shared library using [`export_mode!`].
121pub trait Mode<'rofi>: Sized + Sync {
122    /// The name of the mode.
123    ///
124    /// This string must be nul-terminated
125    /// and contain no intermediate nul characters.
126    /// This means it will look something like:
127    ///
128    /// ```
129    /// const NAME: &'static str = "my-mode\0";
130    /// ```
131    const NAME: &'static str;
132
133    /// Initialize the mode.
134    ///
135    /// # Errors
136    ///
137    /// This function is allowed to error,
138    /// in which case Rofi will display a message:
139    ///
140    /// ```text
141    /// Failed to initialize the mode: {your mode name}
142    /// ```
143    #[allow(clippy::result_unit_err)]
144    fn init(api: Api<'rofi>) -> Result<Self, ()>;
145
146    /// Get the number of entries offered by the mode.
147    fn entries(&mut self) -> usize;
148
149    /// Get the text content of a particular entry in the list.
150    ///
151    /// The `line` parameter is the index of the relevant entry. It is always `< self.entries()`.
152    fn entry_content(&self, line: usize) -> String;
153
154    /// Get the text style of an entry in the list.
155    ///
156    /// The `line` parameter is the index of the relevant entry. It is always `< self.entries()`.
157    ///
158    /// The default implementation returns [`Style::NORMAL`].
159    fn entry_style(&self, _line: usize) -> Style {
160        Style::NORMAL
161    }
162
163    /// Get the text attributes associated with a particular entry in the list.
164    ///
165    /// The `line` parameter is the index of the relevant entry. It is always `< self.entries()`.
166    ///
167    /// The default implementation returns an empty attribute list.
168    fn entry_attributes(&self, _line: usize) -> Attributes {
169        Attributes::new()
170    }
171
172    /// Get the icon of a particular entry in the list, if it has one.
173    ///
174    /// The `line` parameter is the index of the relevant entry. It is always `< self.entries()`.
175    ///
176    /// The default implementation always returns [`None`].
177    ///
178    /// The given height is guaranteed to be `<= i32::MAX`;
179    /// that is, you can always safely cast it to an `i32`.
180    ///
181    /// You can load icons using [`Api::query_icon`],
182    /// or perform it manually with Cairo’s APIs.
183    fn entry_icon(&mut self, _line: usize, _height: u32) -> Option<cairo::Surface> {
184        None
185    }
186
187    /// Process the result of a user's selection
188    /// in response to them pressing enter, escape etc,
189    /// returning the next action to be taken.
190    ///
191    /// `input` contains the current state of the input text box
192    /// and can be mutated to change its contents.
193    fn react(&mut self, event: Event, input: &mut String) -> Action;
194
195    /// Find whether a specific line matches the given matcher.
196    ///
197    /// The `line` parameter is the index of the relevant entry. It is always `< self.entries()`.
198    fn matches(&self, line: usize, matcher: Matcher<'_>) -> bool;
199
200    /// Get the completed value of an entry.
201    ///
202    /// This is called when the user triggers the `kb-row-select` keybind
203    /// (control+space by default)
204    /// which sets the content of the input box to the selected item.
205    /// It is also used by the sorting algorithm.
206    ///
207    /// Note that it is _not_ called on an [`Event::Complete`],
208    /// [`Self::react`] is called then instead.
209    ///
210    /// The `line` parameter is the index of the relevant entry. It is always `< self.entries()`.
211    ///
212    /// The default implementation forwards to [`Self::entry_content`].
213    fn completed(&self, line: usize) -> String {
214        self.entry_content(line)
215    }
216
217    /// Preprocess the user's input before using it to filter and/or sort.
218    /// This is typically used to strip markup.
219    ///
220    /// The default implementation returns the input unchanged.
221    fn preprocess_input(&mut self, input: &str) -> String {
222        input.into()
223    }
224
225    /// Get the message to show in the message bar.
226    ///
227    /// The returned string must be valid [Pango markup].
228    ///
229    /// The default implementation returns an empty string.
230    ///
231    /// [Pango markup]: https://docs.gtk.org/Pango/pango_markup.html
232    fn message(&mut self) -> String {
233        String::new()
234    }
235}
236
237/// Declare a mode to be exported by this crate.
238///
239/// This declares a public `#[no_mangle]` static item named `mode`
240/// which Rofi reads in from your plugin cdylib.
241#[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/// Convert an implementation of [`Mode`] to its raw FFI `Mode` struct.
250///
251/// You generally do not want to call this function unless you're doing low-level stuff —
252/// most of the time the [`export_mode!`] macro is what you want.
253///
254/// # Panics
255///
256/// This function panics if the implementation of [`Mode`] is invalid.
257#[must_use]
258pub const fn raw_mode<T>() -> ffi::Mode
259where
260    // Workaround to get trait bounds in `const fn` on stable
261    <[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/// An event triggered by the user.
536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
537pub enum Event {
538    /// The user cancelled the operation, for example by pressing escape.
539    Cancel {
540        /// The index of the line that was selected at the time of cancellation,
541        /// if one was selected.
542        ///
543        /// If present, this will be < [`Mode::entries`].
544        selected: Option<usize>,
545    },
546    /// The user accepted an option from the list (ctrl+j, ctrl+m or enter by default).
547    Ok {
548        /// Whether the alt binding was used (shift+enter by default).
549        alt: bool,
550        /// The line that was selected.
551        selected: usize,
552    },
553    /// The user entered an input not on the list (ctrl+return by default).
554    CustomInput {
555        /// Whether the alt binding was used (ctrl+shift+return by default).
556        alt: bool,
557        /// The index of the line that was selected at the time of the event,
558        /// if one was selected.
559        ///
560        /// If present, this will be < [`Mode::entries`].
561        selected: Option<usize>,
562    },
563    /// The user used the `kb-mode-complete` binding (control+l by default).
564    ///
565    /// If this happens,
566    /// you should set the `input` value
567    /// to the currently selected entry
568    /// if there is one.
569    Complete {
570        /// The index of the line that was selected at the time of the event,
571        /// if one was selected.
572        ///
573        /// If present, this will be < [`Mode::entries`].
574        selected: Option<usize>,
575    },
576    /// The user used the `kb-delete-entry` binding (shift+delete by default).
577    DeleteEntry {
578        /// The index of the entry that was selected to be deleted.
579        ///
580        /// If present, this will be < [`Mode::entries`].
581        selected: usize,
582    },
583    /// The user ran a custom command.
584    CustomCommand {
585        /// The number of the custom command, in the range [0, 18].
586        number: u8,
587        /// The index of the line that was selected at the time of the event,
588        /// if one was selected.
589        ///
590        /// If present, this will be < [`Mode::entries`].
591        selected: Option<usize>,
592    },
593}
594
595impl Event {
596    /// Get the index of the line that was selected at the time of the event,
597    /// if one was selected.
598    #[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/// An action caused by reacting to an [`Event`].
611#[derive(Debug, Clone, Copy, PartialEq, Eq)]
612pub enum Action {
613    /// Change the active mode to one with the given index.
614    ///
615    /// The index must be < 1000.
616    SetMode(u16),
617    /// Switch to the next mode.
618    Next,
619    /// Switch to the previous mode.
620    Previous,
621    /// Reload the current mode.
622    Reload,
623    /// Reset the current mode: this reloads the mode and unsets user input.
624    Reset,
625    /// Exit Rofi.
626    Exit,
627}
628
629bitflags! {
630    /// The style of a text entry in the list.
631    #[derive(Default)]
632    pub struct Style: u32 {
633        /// The normal style.
634        const NORMAL = 0;
635        /// The text in the box is urgent.
636        const URGENT = 1;
637        /// The text in the box is active.
638        const ACTIVE = 2;
639        /// The text in the box is selected.
640        const SELECTED = 4;
641        /// The text in the box has [Pango markup].
642        ///
643        /// [Pango markup]: https://docs.gtk.org/Pango/pango_markup.html
644        const MARKUP = 8;
645
646        /// The text is on an alternate row.
647        const ALT = 16;
648        /// The text has inverted colors.
649        const HIGHLIGHT = 32;
650    }
651}
652
653/// A collection of attributes that can be applied to text.
654#[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    /// Create a new empty collection of attributes.
664    #[must_use]
665    pub const fn new() -> Self {
666        Self {
667            list: ptr::null_mut(),
668        }
669    }
670
671    /// An an attribute to the list.
672    pub fn push<A: Into<pango::Attribute>>(&mut self, attribute: A) {
673        let attribute: pango::Attribute = attribute.into();
674        // Convert the attribute into its raw form without copying.
675        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/// A pattern matcher.
719#[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    /// Check whether this matcher matches the given string.
739    ///
740    /// # Panics
741    ///
742    /// Panics if the inner string contains null bytes.
743    #[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    /// Check whether this matches matches the given C string.
750    #[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;