rustofi/
lib.rs

1//! This Rust library enables the construction of complex multipage applications that use Rofi to
2//! display their UI. The basic idea is to create a `AppPage` or `SearchPage` as an application
3//! main menu and feed it in possible selections and actions. These selections and actions can
4//! then navigate you to an `ItemList`, an `EntryBox` an `ActionList` or another main menu.
5//!
6//! Typically you will want to create an AppPage with some options and actions,
7//! then display it in a loop checking the return for the `RustofiOptionType` to exit on.
8//! `AppPage` and `SearchPage` will automatically add an exit option to simplify the loop exit cases, while
9//! `ItemList` and `ActionList` will add a cancel option.
10//!
11//! # Simplest Possible Example
12//!
13//! The below example gets even simpler than creating an AppRoot, just displaying a list of strings
14//! and utilizing a callback to print the selected item. Notice the loop in main checking the
15//! return variant of the rofi window
16//!
17//! ```no_run
18//! use rustofi::components::ItemList;
19//! use rustofi::RustofiResult;
20//!
21//! fn simple_app() -> RustofiResult {
22//!     let rustofi_entries = vec![
23//!         "Entry 1".to_string(),
24//!         "Entry 2".to_string(),
25//!         "Entry 3".to_string(),
26//!     ];
27//!     ItemList::new(rustofi_entries, Box::new(simple_callback)).display("Select an entry".to_string())
28//! }
29//!
30//! pub fn simple_callback(s: &String) -> RustofiResult {
31//!     println!("Clicked on item: {}", s);
32//!     RustofiResult::Success
33//! }
34//!
35//! fn main() {
36//!     loop {
37//!         match simple_app() {
38//!             RustofiResult::Error => break,
39//!             RustofiResult::Exit => break,
40//!             RustofiResult::Cancel => break,
41//!             RustofiResult::Blank => break,
42//!             _ => {}
43//!         }
44//!     }
45//! }
46//! ```
47
48/// extra rofi window types usable to create an application, essentially navigation result pages
49pub mod components;
50/// the error(s) returned by this crate
51pub mod errors;
52/// raw representation of a rofi command, use this to create new components, or your own from-scratch
53/// apps
54pub mod window;
55
56use std::clone::Clone;
57use std::fmt::Display;
58
59use crate::window::{Dimensions, Location, Window};
60
61/// enum declaring all possible return values from a rofi window constructed
62/// using this library. Callbacks should also generally return this type, specifying
63/// `Success`, `Error`, `Exit` or `Cancel` in most cases
64pub enum RustofiResult {
65    /// A standard item
66    Selection(String),
67    /// An action item
68    Action(String),
69    /// The operation completed successfully
70    Success,
71    /// The blank entry was selected. Note this entry isn't actually blank but a single space
72    Blank,
73    /// Something went wrong creating the rofi window or in the callback
74    Error,
75    /// `ItemList` or `ActionList` was cancelled, used to return to a main menu
76    Cancel,
77    /// Used internally when the automatically added `[exit]` entry is selected
78    Exit
79}
80
81/// Wrapper around a callback that returns a RustofiResult
82pub trait RustofiCallback<T>: FnMut(&T) -> RustofiResult {
83    fn clone_boxed(&self) -> Box<dyn RustofiCallback<T>>;
84}
85impl<T, C> RustofiCallback<T> for C
86where
87    C: 'static + Clone + FnMut(&T) -> RustofiResult
88{
89    fn clone_boxed(&self) -> Box<dyn RustofiCallback<T>> {
90        Box::new(self.clone())
91    }
92}
93impl<T: 'static> Clone for Box<dyn RustofiCallback<T>> {
94    fn clone(&self) -> Self {
95        self.clone_boxed()
96    }
97}
98
99/// Trait implemented by `SearchPage` and `AppPage`.
100pub trait RustofiComponent<'a> {
101    /// returns a rofi window with special initial options for the implementation
102    fn create_window() -> Window<'a>;
103    /// set the callback associated with actions
104    fn action(self, acb: Box<dyn FnMut(&String) -> RustofiResult>) -> Self;
105    /// set the callback associated with the blank entry item
106    fn blank(self, bcb: Box<dyn FnMut() -> RustofiResult>) -> Self;
107    /// set the optional actions to display
108    fn actions(self, actions: Vec<String>) -> Self;
109    /// customize the implementation's rofi window
110    fn window(self, window: Window<'a>) -> Self;
111    /// run the rofi command
112    fn display(&mut self, prompt: String) -> RustofiResult;
113}
114
115/// `AppPage` displays a single column rofi window and is meant to be used as a main menu
116/// of sorts for your application. `items` should be associated with a data model, while `actions`
117/// should be either operations you can perform on those items, or actions you can take within the
118/// app (switch pages for example)
119pub struct AppPage<'a, T> {
120    /// standard list items, will be displayed in the rofi window using to_string()
121    pub items: Vec<T>,
122    /// callback called whenever an item in the `items` vector is selected
123    pub item_callback: Box<dyn RustofiCallback<T>>,
124    /// callback called whenever a blank entry is selected
125    pub blank_callback: Box<dyn FnMut() -> RustofiResult>,
126    /// additional action entries, meant to be operations on standard items
127    pub actions: Vec<String>,
128    /// callback called whenever a custom action is selected (NOT on Exit or Cancel)
129    pub action_callback: Box<dyn FnMut(&String) -> RustofiResult>,
130    /// rofi window instance
131    pub window: Window<'a>
132}
133
134impl<'a, T: Display + Clone> AppPage<'a, T> {
135    /// create the initial bare minumum AppPage, without showing the window yet
136    pub fn new(items: Vec<T>, item_callback: Box<dyn RustofiCallback<T>>) -> Self {
137        AppPage {
138            items,
139            item_callback,
140            actions: vec![" ".to_string(), "[exit]".to_string()],
141            blank_callback: Box::new(|| RustofiResult::Blank),
142            action_callback: Box::new(|_| RustofiResult::Action("".to_string())),
143            window: SearchPage::<T>::create_window()
144        }
145    }
146
147    /// A message usually displayed right beneath the prompt in a rofi window. You can
148    /// use this to display instructions
149    pub fn message(mut self, message: &'static str) -> Self {
150        self.window = self.window.message(message);
151        self
152    }
153}
154
155impl<'a, T: Display + Clone> RustofiComponent<'a> for AppPage<'a, T> {
156    /// create a centred single column rofi window with Pango markup enabled
157    fn create_window() -> Window<'a> {
158        Window::new("AppList")
159            .format('s')
160            .location(Location::MiddleCentre)
161            .add_args(vec!["-markup-rows".to_string()])
162    }
163
164    /// set the callback to be run when an action is selected
165    fn action(mut self, acb: Box<dyn FnMut(&String) -> RustofiResult>) -> Self {
166        self.action_callback = acb;
167        self
168    }
169
170    /// set the callback to be run when the blank entry is selected
171    fn blank(mut self, bcb: Box<dyn FnMut() -> RustofiResult>) -> Self {
172        self.blank_callback = bcb;
173        self
174    }
175
176    /// set the actions in the AppPage. This should only be called once as it overwrites
177    /// the previous settings
178    fn actions(mut self, mut actions: Vec<String>) -> Self {
179        actions.insert(0, " ".to_string());
180        actions.insert(0, "[exit]".to_string());
181        self.actions = actions;
182        self
183    }
184
185    /// set a completely custom window
186    fn window(mut self, window: Window<'a>) -> Self {
187        self.window = window.format('s'); // ensure we're in string mode
188        self
189    }
190
191    /// run the rofi and match the selection to a `RustofiResult`
192    fn display(&mut self, prompt: String) -> RustofiResult {
193        let mut display_options: Vec<String> = self.items.iter().map(|s| s.to_string()).collect();
194        display_options.append(self.actions.as_mut());
195        let response = self
196            .window
197            .clone()
198            .prompt(prompt)
199            .lines(display_options.len() as i32)
200            .show(display_options.clone());
201        match response {
202            Ok(input) => {
203                if input == "[exit]" || input == "" {
204                    RustofiResult::Exit
205                } else if input == " " {
206                    (self.blank_callback)()
207                } else {
208                    // check if the entry matches one of the list items
209                    for item in self.items.clone() {
210                        if input == item.to_string() {
211                            return (self.item_callback)(&item);
212                        }
213                    }
214
215                    // check if the entry matches one of the action items
216                    for item in self.actions.clone() {
217                        if input == item.to_string() {
218                            return (self.action_callback)(&input);
219                        }
220                    }
221                    // if the entry isn't an action or an existing entry item, return exit
222                    RustofiResult::Exit
223                }
224            }
225            Err(_) => RustofiResult::Error
226        }
227    }
228}
229
230/// `SearchPage` displays a multi column rofi window and is meant to be used as a search page
231/// of sorts for your application. `items` should be associated with a data model, while `actions`
232/// should be either operations you can perform on those items, or actions you can take within the
233/// app (switch pages for example). The `search_callback` allows you to refresh the data models
234/// displayed or perform an operation on custom entry
235pub struct SearchPage<'a, T> {
236    /// standard list items, will be displayed in the rofi window using to_string()
237    pub items: Vec<T>,
238    /// callback called whenever an item in the `items` vector is selected
239    pub item_callback: Box<dyn RustofiCallback<T>>,
240    /// callback called whenever a blank entry is selected
241    pub blank_callback: Box<dyn FnMut() -> RustofiResult>,
242    /// additional action entries, meant to be operations on standard items
243    pub actions: Vec<String>,
244    /// callback called whenever a custom action is selected (NOT on Exit or Cancel)
245    pub action_callback: Box<dyn FnMut(&String) -> RustofiResult>,
246    /// callback to be run when no other entry matches
247    pub search_callback: Box<dyn FnMut(&String) -> RustofiResult>,
248    /// rofi window instance
249    pub window: Window<'a>
250}
251
252impl<'a, T: Display + Clone> SearchPage<'a, T> {
253    /// create the initial bare minumum AppPage, without showing the window yet
254    pub fn new(
255        items: Vec<T>, item_callback: Box<dyn RustofiCallback<T>>,
256        search_callback: Box<dyn FnMut(&String) -> RustofiResult>
257    ) -> Self {
258        SearchPage {
259            items,
260            item_callback,
261            actions: vec![" ".to_string(), "[cancel]".to_string()],
262            blank_callback: Box::new(|| RustofiResult::Blank),
263            action_callback: Box::new(|_| RustofiResult::Action("".to_string())),
264            search_callback,
265            window: SearchPage::<T>::create_window()
266        }
267    }
268}
269
270impl<'a, T: Display + Clone> RustofiComponent<'a> for SearchPage<'a, T> {
271    /// create a rofi window with 4 columns
272    fn create_window() -> Window<'a> {
273        Window::new("Search")
274            .format('s')
275            .location(Location::MiddleCentre)
276            .dimensions(Dimensions {
277                width: 640,
278                height: 480,
279                lines: 5,
280                columns: 4
281            })
282            .add_args(vec!["-markup-rows".to_string()])
283    }
284
285    /// set the callback to be run when an action is selected
286    fn action(mut self, acb: Box<dyn FnMut(&String) -> RustofiResult>) -> Self {
287        self.action_callback = acb;
288        self
289    }
290
291    /// set the callback to be run when the blank entry is selected
292    fn blank(mut self, bcb: Box<dyn FnMut() -> RustofiResult>) -> Self {
293        self.blank_callback = bcb;
294        self
295    }
296
297    /// set the actions in the AppPage. This should only be called once as it overwrites
298    /// the previous settings
299    fn actions(mut self, mut actions: Vec<String>) -> Self {
300        actions.insert(0, " ".to_string());
301        actions.insert(0, "[exit]".to_string());
302        self.actions = actions;
303        self
304    }
305
306    /// set a completely custom window
307    fn window(mut self, window: Window<'a>) -> Self {
308        self.window = window.format('s'); // ensure we're in string mode
309        self
310    }
311
312    /// display the search window and match the entry against the actions, standard items
313    /// and finally if nothing matches, run the search callback
314    fn display(&mut self, prompt: String) -> RustofiResult {
315        let mut display_options: Vec<String> = self.items.iter().map(|s| s.to_string()).collect();
316        display_options.append(self.actions.as_mut());
317        let response = self
318            .window
319            .clone()
320            .prompt(prompt)
321            .show(display_options.clone());
322        match response {
323            Ok(input) => {
324                if input == "[exit]" {
325                    RustofiResult::Exit
326                } else if input == " " {
327                    (self.blank_callback)()
328                } else if input == "" {
329                    RustofiResult::Cancel
330                } else {
331                    // check if the entry matches one of the list items
332                    for item in self.items.clone() {
333                        if input == item.to_string() {
334                            return (self.item_callback)(&item);
335                        }
336                    }
337
338                    // check if the entry matches one of the action items
339                    for item in self.actions.clone() {
340                        if input == item.to_string() {
341                            return (self.action_callback)(&input);
342                        }
343                    }
344                    // if the entry isn't an action or an existing entry item,
345                    // run the search callback
346                    (self.search_callback)(&input)
347                }
348            }
349            Err(_) => RustofiResult::Error
350        }
351    }
352}