Skip to main content

rofi_mode/
api.rs

1//! Interface to Rofi's API.
2#![allow(clippy::unused_self)] // It's needed for the lifetime
3
4/// The Rofi API,
5/// controlled by a lifetime
6/// to be only accessible while Rofi is running.
7#[derive(Debug)]
8pub struct Api<'rofi> {
9    display_name: ptr::NonNull<*mut u8>,
10    // Values are irrelevant when `*display_name == NULL`
11    display_name_len: usize,
12    display_name_capacity: usize,
13    lifetime: PhantomData<&'rofi ()>,
14}
15
16// SAFETY: All the methods take `&self` or `&mut self` appropriately to enforce thread-safety.
17// Additionally, this type's lifetime ensures that it can't be used on a separate thread outside of
18// when `Mode`'s methods run (since scoped threads only work inside a scope).
19unsafe impl Send for Api<'_> {}
20unsafe impl Sync for Api<'_> {}
21
22impl Api<'_> {
23    pub(crate) unsafe fn new(display_name: ptr::NonNull<*mut u8>) -> Self {
24        Self {
25            display_name,
26            display_name_len: 0,
27            display_name_capacity: 0,
28            lifetime: PhantomData,
29        }
30    }
31
32    /// Get the display name of the current mode (the text displayed before the colon).
33    ///
34    /// Returns [`None`] if there isn't one,
35    /// in which case Rofi shows the [mode name] instead.
36    ///
37    /// [mode name]: crate::Mode::NAME
38    #[must_use]
39    pub fn display_name(&self) -> Option<&str> {
40        // SAFETY: Rofi never mutates the display name, and we only mutate it with an `&mut Api`.
41        let ptr = *unsafe { self.display_name.as_ref() };
42
43        if ptr.is_null() {
44            return None;
45        }
46
47        let slice = unsafe { slice::from_raw_parts(ptr, self.display_name_len) };
48
49        Some(unsafe { str::from_utf8_unchecked(slice) })
50    }
51
52    fn change_display_name(&mut self, display_name: Option<String>) -> Option<String> {
53        // SAFETY: In order for functions on this type to be called, we must be inside one of
54        // `Mode`'s methods. This means that Rofi guarantees us it won't be reading the display
55        // name at this point in time.
56        let ptr = unsafe { self.display_name.as_mut() };
57
58        let old_len = self.display_name_len;
59        let old_capacity = self.display_name_capacity;
60        let old_ptr = *ptr;
61
62        if let Some(display_name) = &display_name {
63            self.display_name_len = display_name.len();
64            self.display_name_capacity = display_name.capacity();
65        }
66        *ptr = display_name.map_or_else(ptr::null_mut, String::into_raw);
67
68        if old_ptr.is_null() {
69            None
70        } else {
71            Some(unsafe { String::from_raw_parts(old_ptr, old_len, old_capacity) })
72        }
73    }
74
75    /// Take the current display name,
76    /// leaving [`None`] in its place
77    /// and returning the previous display name.
78    /// This will cause Rofi to display the [mode name](crate::Mode::NAME) instead.
79    ///
80    /// Returns [`None`] if there was no previous display name.
81    pub fn take_display_name(&mut self) -> Option<String> {
82        self.change_display_name(None)
83    }
84
85    /// Replace the current display name,
86    /// returning the previous one.
87    ///
88    /// Returns [`None`] if there was no previous display name.
89    pub fn replace_display_name(&mut self, display_name: String) -> Option<String> {
90        self.change_display_name(Some(display_name))
91    }
92
93    /// Set the display name of the current mode.
94    ///
95    /// # Panics
96    ///
97    /// Panics if the given string contains any interior nul bytes.
98    pub fn set_display_name<T: Display>(&mut self, display_name: T) {
99        let mut buf = self.take_display_name().unwrap_or_default();
100        buf.clear();
101        write!(buf, "{display_name}").unwrap();
102        self.replace_display_name(buf);
103    }
104
105    /// Check whether the given file path is an image in one of Rofi's supported formats,
106    /// by looking at its file extension.
107    #[must_use]
108    pub fn supports_image<P: AsRef<Path>>(&self, path: P) -> bool {
109        let mut path = path.as_ref().as_os_str().as_bytes().to_owned();
110        path.push(b'\0');
111
112        let res = unsafe { ffi::icon_fetcher::file_is_image(path.as_ptr().cast()) };
113
114        res != 0
115    }
116
117    /// Query the icon theme for an icon with a specific name and size.
118    ///
119    /// `name` can also be a full path.
120    ///
121    /// # Panics
122    ///
123    /// Panics if `name` contains interior nul bytes.
124    #[must_use]
125    pub fn query_icon(&mut self, name: &str, size: u32) -> IconRequest {
126        let name = CString::new(name).expect("name contained nul bytes");
127        self.query_icon_cstr(&name, size)
128    }
129
130    /// Query the icon theme for an icon with a specific name and size.
131    ///
132    /// `name` can also be a full path.
133    #[must_use]
134    pub fn query_icon_cstr(&mut self, name: &CStr, size: u32) -> IconRequest {
135        let uid = unsafe {
136            ffi::icon_fetcher::query(name.as_ptr(), size.try_into().unwrap_or(c_int::MAX))
137        };
138        IconRequest { uid }
139    }
140
141    /// Query the icon theme for an icon with a specific name and size.
142    ///
143    /// `name` can also be a full path.
144    ///
145    /// # Panics
146    ///
147    /// Panics if `name` contains interior nul bytes.
148    #[must_use]
149    pub fn query_icon_wh(&mut self, name: &str, width: u32, height: u32) -> IconRequest {
150        let name = CString::new(name).expect("name contained nul bytes");
151        self.query_icon_wh_cstr(&name, width, height)
152    }
153
154    /// Query the icon theme for an icon with a specific name and size.
155    ///
156    /// `name` can also be a full path.
157    #[must_use]
158    pub fn query_icon_wh_cstr(&mut self, name: &CStr, width: u32, height: u32) -> IconRequest {
159        let uid = unsafe {
160            ffi::icon_fetcher::query_advanced(
161                name.as_ptr(),
162                width.try_into().unwrap_or(c_int::MAX),
163                height.try_into().unwrap_or(c_int::MAX),
164            )
165        };
166        IconRequest { uid }
167    }
168
169    /// Finalize an icon request and retrieve the inner icon.
170    ///
171    /// The returned icon will be the best match for the requested size,
172    /// but you may need to resize it to desired size.
173    ///
174    /// It may be ergonomically preferable to use [`IconRequest::wait`] instead of this function.
175    ///
176    /// # Errors
177    ///
178    /// Errors if the icon was not found, or an error occurred inside the returned Cairo surface.
179    #[allow(clippy::needless_pass_by_value)]
180    pub fn retrieve_icon(&mut self, request: IconRequest) -> Result<cairo::Surface, IconError> {
181        let ptr = unsafe { ffi::icon_fetcher::get(request.uid) };
182        if ptr.is_null() {
183            return Err(IconError::NotFound);
184        }
185        unsafe { cairo::Surface::from_raw_full(ptr) }.map_err(IconError::Surface)
186    }
187
188    /// Hides the view.
189    ///
190    /// Be warned that this is a private API, so while we expose it for some use-cases, using it is
191    /// technically unreliable.
192    pub fn hide(&mut self) {
193        unsafe { ffi::view::hide() };
194    }
195}
196
197/// A request sent to the icon fetcher.
198///
199/// This can be finalized using [`Api::retrieve_icon`].
200#[derive(Debug)]
201pub struct IconRequest {
202    uid: u32,
203}
204
205impl IconRequest {
206    /// Wait for the request to be fulfilled.
207    ///
208    /// This is a wrapper around [`Api::retrieve_icon`] — see that method for more.
209    #[allow(clippy::missing_errors_doc)]
210    pub fn wait(self, api: &mut Api<'_>) -> Result<cairo::Surface, IconError> {
211        api.retrieve_icon(self)
212    }
213}
214
215/// An error retrieving an icon.
216#[derive(Debug)]
217pub enum IconError {
218    /// The icon was not found.
219    #[non_exhaustive]
220    NotFound,
221    /// An error occurred inside the Cairo surface.
222    #[non_exhaustive]
223    Surface(cairo::Error),
224}
225
226impl Display for IconError {
227    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
228        f.write_str("failed to retrieve icon")
229    }
230}
231
232impl Error for IconError {
233    fn source(&self) -> Option<&(dyn Error + 'static)> {
234        match self {
235            Self::NotFound => Some(&IconNotFound),
236            Self::Surface(e) => Some(e),
237        }
238    }
239}
240
241#[derive(Debug)]
242struct IconNotFound;
243
244impl Display for IconNotFound {
245    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
246        f.write_str("icon not found")
247    }
248}
249
250impl Error for IconNotFound {}
251
252use crate::ffi;
253use crate::String;
254use std::error::Error;
255use std::ffi::CStr;
256use std::ffi::CString;
257use std::fmt;
258use std::fmt::Display;
259use std::fmt::Formatter;
260use std::fmt::Write as _;
261use std::marker::PhantomData;
262use std::os::raw::c_int;
263use std::os::unix::ffi::OsStrExt;
264use std::path::Path;
265use std::ptr;
266use std::slice;
267use std::str;