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}