#![warn(
noop_method_call,
trivial_casts,
trivial_numeric_casts,
unused_import_braces,
unused_lifetimes,
unused_qualifications,
unsafe_op_in_unsafe_fn,
missing_docs,
missing_debug_implementations,
clippy::pedantic
)]
#![allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap
)]
use ::{
bitflags::bitflags,
cairo::ffi as cairo_sys,
pango::{
ffi as pango_sys,
glib::{ffi as glib_sys, translate::ToGlibPtrMut},
},
std::{
ffi::{c_void, CStr, CString},
mem::{self, ManuallyDrop},
os::raw::{c_char, c_int, c_uint},
panic, process, ptr,
},
};
pub use {cairo, pango, rofi_plugin_sys as ffi};
mod string;
pub use string::{format, String};
pub mod api;
pub use api::Api;
pub trait Mode<'rofi>: Sized + Sync {
const NAME: &'static str;
#[allow(clippy::result_unit_err)]
fn init(api: Api<'rofi>) -> Result<Self, ()>;
fn entries(&mut self) -> usize;
fn entry_content(&self, line: usize) -> String;
fn entry_style(&self, _line: usize) -> Style {
Style::NORMAL
}
fn entry_attributes(&self, _line: usize) -> Attributes {
Attributes::new()
}
fn entry_icon(&mut self, _line: usize, _height: u32) -> Option<cairo::Surface> {
None
}
fn react(&mut self, event: Event, input: &mut String) -> Action;
fn matches(&self, line: usize, matcher: Matcher<'_>) -> bool;
fn completed(&self, line: usize) -> String {
self.entry_content(line)
}
fn preprocess_input(&mut self, input: &str) -> String {
input.into()
}
fn message(&mut self) -> String {
String::new()
}
}
#[macro_export]
macro_rules! export_mode {
($t:ty $(,)?) => {
#[no_mangle]
pub static mut mode: $crate::ffi::Mode = $crate::raw_mode::<fn(&()) -> $t>();
};
}
#[must_use]
pub const fn raw_mode<T>() -> ffi::Mode
where
<[T; 0] as IntoIterator>::Item: GivesMode,
{
<RawModeHelper<T>>::VALUE
}
mod sealed {
use crate::Mode;
pub trait GivesMode: for<'rofi> GivesModeLifetime<'rofi> {}
impl<T: ?Sized + for<'rofi> GivesModeLifetime<'rofi>> GivesMode for T {}
pub trait GivesModeLifetime<'rofi> {
type Mode: Mode<'rofi>;
}
impl<'rofi, F: FnOnce(&'rofi ()) -> O, O: Mode<'rofi>> GivesModeLifetime<'rofi> for F {
type Mode = O;
}
}
use sealed::{GivesMode, GivesModeLifetime};
struct RawModeHelper<T>(T);
impl<T: GivesMode> RawModeHelper<T> {
const VALUE: ffi::Mode = ffi::Mode {
name: assert_c_str(<<T as GivesModeLifetime<'_>>::Mode as Mode>::NAME),
_init: Some(init::<T>),
_destroy: Some(destroy::<T>),
_get_num_entries: Some(get_num_entries::<T>),
_result: Some(result::<T>),
_get_display_value: Some(get_display_value::<T>),
_token_match: Some(token_match::<T>),
_get_icon: Some(get_icon::<T>),
_get_completion: Some(get_completion::<T>),
_preprocess_input: Some(preprocess_input::<T>),
_get_message: Some(get_message::<T>),
..ffi::Mode::default()
};
}
const fn assert_c_str(s: &'static str) -> *mut c_char {
let mut i = 0;
while i + 1 < s.len() {
assert!(s.as_bytes()[i] != 0, "string contains intermediary nul");
i += 1;
}
assert!(s.as_bytes()[i] == 0, "string is not nul-terminated");
s.as_ptr() as _
}
type ModeOf<'a, T> = <T as GivesModeLifetime<'a>>::Mode;
unsafe extern "C" fn init<T: GivesMode>(sw: *mut ffi::Mode) -> c_int {
if unsafe { ffi::mode_get_private_data(sw) }.is_null() {
let api = unsafe { Api::new(ptr::NonNull::from(&mut (*sw).display_name).cast()) };
let boxed: Box<ModeOf<'_, T>> =
match catch_panic(|| <ModeOf<'_, T>>::init(api).map(Box::new)) {
Ok(Ok(boxed)) => boxed,
Ok(Err(())) | Err(()) => return false.into(),
};
let ptr = Box::into_raw(boxed).cast::<c_void>();
unsafe { ffi::mode_set_private_data(sw, ptr) };
}
true.into()
}
unsafe extern "C" fn destroy<T: GivesMode>(sw: *mut ffi::Mode) {
let ptr = unsafe { ffi::mode_get_private_data(sw) };
if ptr.is_null() {
return;
}
let boxed = unsafe { <Box<ModeOf<'_, T>>>::from_raw(ptr.cast()) };
let _ = catch_panic(|| drop(boxed));
unsafe { ffi::mode_set_private_data(sw, ptr::null_mut()) };
}
unsafe extern "C" fn get_num_entries<T: GivesMode>(sw: *const ffi::Mode) -> c_uint {
let mode: &mut ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
catch_panic(|| mode.entries().try_into().unwrap_or(c_uint::MAX)).unwrap_or(0)
}
unsafe extern "C" fn result<T: GivesMode>(
sw: *mut ffi::Mode,
mretv: c_int,
input: *mut *mut c_char,
selected_line: c_uint,
) -> c_int {
let mode: &mut ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
let action = catch_panic(|| {
let selected = if selected_line == c_uint::MAX {
None
} else {
Some(selected_line as usize)
};
let event = match mretv {
ffi::menu::CANCEL => Event::Cancel { selected },
_ if mretv & ffi::menu::OK != 0 => Event::Ok {
alt: mretv & ffi::menu::CUSTOM_ACTION != 0,
selected: selected.expect("Ok event without selected line"),
},
_ if mretv & ffi::menu::CUSTOM_INPUT != 0 => Event::CustomInput {
alt: mretv & ffi::menu::CUSTOM_ACTION != 0,
selected,
},
ffi::menu::COMPLETE => Event::Complete { selected },
ffi::menu::ENTRY_DELETE => Event::DeleteEntry {
selected: selected.expect("DeleteEntry event without selected line"),
},
_ if mretv & ffi::menu::CUSTOM_COMMAND != 0 => Event::CustomCommand {
number: (mretv & ffi::menu::LOWER_MASK) as u8,
selected,
},
_ => panic!("unexpected mretv {mretv:X}"),
};
let input: &mut *mut c_char = unsafe { &mut *input };
let input_ptr: *mut c_char = mem::replace(&mut *input, ptr::null_mut());
let len = unsafe { libc::strlen(input_ptr) };
let mut input_string = unsafe { String::from_raw_parts(input_ptr.cast(), len, len + 1) };
let action = mode.react(event, &mut input_string);
if !input_string.is_empty() {
*input = input_string.into_raw().cast::<c_char>();
}
action
})
.unwrap_or(Action::Exit);
match action {
Action::SetMode(mode) => mode.into(),
Action::Next => ffi::NEXT_DIALOG,
Action::Previous => ffi::PREVIOUS_DIALOG,
Action::Reload => ffi::RELOAD_DIALOG,
Action::Reset => ffi::RESET_DIALOG,
Action::Exit => ffi::EXIT,
}
}
unsafe extern "C" fn get_display_value<T: GivesMode>(
sw: *const ffi::Mode,
selected_line: c_uint,
state: *mut c_int,
attr_list: *mut *mut glib_sys::GList,
get_entry: c_int,
) -> *mut c_char {
let mode: &ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
catch_panic(|| {
let line = selected_line as usize;
if !state.is_null() {
let style = mode.entry_style(line);
unsafe { *state = style.bits() as c_int };
}
if !attr_list.is_null() {
assert!(unsafe { *attr_list }.is_null());
let attributes = mode.entry_attributes(line);
unsafe { *attr_list = ManuallyDrop::new(attributes).list };
}
if get_entry == 0 {
ptr::null_mut()
} else {
mode.entry_content(line).into_raw().cast()
}
})
.unwrap_or(ptr::null_mut())
}
unsafe extern "C" fn token_match<T: GivesMode>(
sw: *const ffi::Mode,
tokens: *mut *mut ffi::RofiIntMatcher,
index: c_uint,
) -> c_int {
let mode: &ModeOf<'_, T> = unsafe { &*ffi::mode_get_private_data(sw).cast() };
catch_panic(|| {
let matcher = unsafe { Matcher::from_ffi(tokens) };
mode.matches(index as usize, matcher)
})
.unwrap_or(false)
.into()
}
unsafe extern "C" fn get_icon<T: GivesMode>(
sw: *const ffi::Mode,
selected_line: c_uint,
height: c_int,
) -> *mut cairo_sys::cairo_surface_t {
let mode: &mut ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
catch_panic(|| {
const NEGATIVE_HEIGHT: &str = "negative height passed into get_icon";
let height: u32 = height.try_into().expect(NEGATIVE_HEIGHT);
mode.entry_icon(selected_line as usize, height)
.map_or_else(ptr::null_mut, |surface| {
ManuallyDrop::new(surface).to_raw_none()
})
})
.unwrap_or(ptr::null_mut())
}
unsafe extern "C" fn get_completion<T: GivesMode>(
sw: *const ffi::Mode,
selected_line: c_uint,
) -> *mut c_char {
let mode: &ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
abort_on_panic(|| {
mode.completed(selected_line as usize)
.into_raw()
.cast::<c_char>()
})
}
unsafe extern "C" fn preprocess_input<T: GivesMode>(
sw: *mut ffi::Mode,
input: *const c_char,
) -> *mut c_char {
let mode: &mut ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
abort_on_panic(|| {
let input = unsafe { CStr::from_ptr(input) }
.to_str()
.expect("Input is not valid UTF-8");
let processed = mode.preprocess_input(input);
if processed.is_empty() {
ptr::null_mut()
} else {
processed.into_raw().cast::<c_char>()
}
})
}
unsafe extern "C" fn get_message<T: GivesMode>(sw: *const ffi::Mode) -> *mut c_char {
let mode: &mut ModeOf<'_, T> = unsafe { &mut *ffi::mode_get_private_data(sw).cast() };
catch_panic(|| {
let message = mode.message();
if message.is_empty() {
return ptr::null_mut();
}
message.into_raw().cast::<c_char>()
})
.unwrap_or(ptr::null_mut())
}
struct AbortOnDrop;
impl Drop for AbortOnDrop {
fn drop(&mut self) {
process::abort();
}
}
fn abort_on_panic<O, F: FnOnce() -> O>(f: F) -> O {
let guard = AbortOnDrop;
let res = f();
mem::forget(guard);
res
}
fn catch_panic<O, F: FnOnce() -> O>(f: F) -> Result<O, ()> {
panic::catch_unwind(panic::AssertUnwindSafe(f)).map_err(|e| {
let guard = AbortOnDrop;
drop(e);
mem::forget(guard);
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Event {
Cancel {
selected: Option<usize>,
},
Ok {
alt: bool,
selected: usize,
},
CustomInput {
alt: bool,
selected: Option<usize>,
},
Complete {
selected: Option<usize>,
},
DeleteEntry {
selected: usize,
},
CustomCommand {
number: u8,
selected: Option<usize>,
},
}
impl Event {
#[must_use]
pub const fn selected(&self) -> Option<usize> {
match *self {
Self::Cancel { selected }
| Self::CustomInput { selected, .. }
| Self::Complete { selected }
| Self::CustomCommand { selected, .. } => selected,
Self::Ok { selected, .. } | Self::DeleteEntry { selected } => Some(selected),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
SetMode(u16),
Next,
Previous,
Reload,
Reset,
Exit,
}
bitflags! {
#[derive(Default)]
pub struct Style: u32 {
const NORMAL = 0;
const URGENT = 1;
const ACTIVE = 2;
const SELECTED = 4;
const MARKUP = 8;
const ALT = 16;
const HIGHLIGHT = 32;
}
}
#[derive(Debug)]
pub struct Attributes {
list: *mut glib_sys::GList,
}
unsafe impl Send for Attributes {}
unsafe impl Sync for Attributes {}
impl Attributes {
#[must_use]
pub const fn new() -> Self {
Self {
list: ptr::null_mut(),
}
}
pub fn push<A: Into<pango::Attribute>>(&mut self, attribute: A) {
let attribute: pango::Attribute = attribute.into();
let raw: *mut pango_sys::PangoAttribute = ManuallyDrop::new(attribute).to_glib_none_mut().0;
self.list = unsafe { glib_sys::g_list_prepend(self.list, raw.cast()) };
}
}
impl Default for Attributes {
fn default() -> Self {
Self::new()
}
}
impl From<pango::Attribute> for Attributes {
fn from(attribute: pango::Attribute) -> Self {
let mut this = Self::new();
this.push(attribute);
this
}
}
impl Drop for Attributes {
fn drop(&mut self) {
unsafe extern "C" fn free_attribute(ptr: *mut c_void) {
unsafe { pango_sys::pango_attribute_destroy(ptr.cast()) }
}
unsafe { glib_sys::g_list_free_full(self.list, Some(free_attribute)) };
}
}
impl<A: Into<pango::Attribute>> Extend<A> for Attributes {
fn extend<T: IntoIterator<Item = A>>(&mut self, iter: T) {
iter.into_iter().for_each(|item| self.push(item));
}
}
impl<A: Into<pango::Attribute>> FromIterator<A> for Attributes {
fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
let mut this = Self::new();
this.extend(iter);
this
}
}
#[derive(Debug, Clone, Copy)]
pub struct Matcher<'a> {
ptr: Option<&'a *mut ffi::RofiIntMatcher>,
}
unsafe impl Send for Matcher<'_> {}
unsafe impl Sync for Matcher<'_> {}
impl Matcher<'_> {
pub(crate) unsafe fn from_ffi(ffi: *const *mut ffi::RofiIntMatcher) -> Self {
Self {
ptr: if ffi.is_null() {
None
} else {
Some(unsafe { &*ffi })
},
}
}
#[must_use]
pub fn matches(self, s: &str) -> bool {
let s = CString::new(s).expect("string contains null bytes");
self.matches_c_str(&*s)
}
#[must_use]
pub fn matches_c_str(self, s: &CStr) -> bool {
let ptr: *const *mut ffi::RofiIntMatcher = match self.ptr {
Some(ptr) => ptr,
None => return true,
};
0 != unsafe { ffi::helper::token_match(ptr, s.as_ptr()) }
}
}