├── .gitignore ├── Cargo.toml ├── .github └── workflows │ └── ci.yml ├── README.md ├── LICENSE └── src ├── pango.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rofi" 3 | version = "0.4.0" 4 | authors = ["Tibor Schneider "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "Library to crate rofi windows and parse the output" 8 | keywords = ["rofi", "gui"] 9 | categories = ["gui"] 10 | homepage = "https://github.com/tiborschneider/rofi-rs" 11 | repository = "https://github.com/tiborschneider/rofi-rs" 12 | readme = "README.md" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | thiserror = "1.0.19" 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Continuous Integration 3 | 4 | jobs: 5 | rustfmt: 6 | name: Rustfmt 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v2 11 | - name: Install Rust 12 | uses: actions-rs/toolchain@v1 13 | with: 14 | toolchain: stable 15 | profile: minimal 16 | override: true 17 | components: rustfmt 18 | - name: Check formatting 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: fmt 22 | args: --all -- --check 23 | 24 | clippy: 25 | name: Clippy 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v2 30 | - name: Install Rust 31 | uses: actions-rs/toolchain@v1 32 | with: 33 | toolchain: stable 34 | profile: minimal 35 | override: true 36 | components: clippy 37 | - name: Clippy Check 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: clippy 41 | args: -- -D warnings 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rofi Library for Rust 2 | Spawn [rofi](https://github.com/davatorium/rofi) windows, and parse the result appropriately. 3 | 4 | ## Simple example 5 | 6 | ```rust 7 | use rofi; 8 | use std::{fs, env}; 9 | 10 | let dir_entries = fs::read_dir(env::current_dir().unwrap()) 11 | .unwrap() 12 | .map(|d| format!("{:?}", d.unwrap().path())) 13 | .collect::>(); 14 | 15 | match rofi::Rofi::new(&dir_entries).run() { 16 | Ok(choice) => println!("Choice: {}", choice), 17 | Err(rofi::Error::Interrupted) => println!("Interrupted"), 18 | Err(e) => println!("Error: {}", e) 19 | } 20 | ``` 21 | 22 | ## Example of returning an index 23 | `rofi` can also be used to return an index of the selected item: 24 | 25 | ```rust 26 | use rofi; 27 | use std::{fs, env}; 28 | 29 | let dir_entries = fs::read_dir(env::current_dir().unwrap()) 30 | .unwrap() 31 | .map(|d| format!("{:?}", d.unwrap().path())) 32 | .collect::>(); 33 | 34 | match rofi::Rofi::new(&dir_entries).run_index() { 35 | Ok(element) => println!("Choice: {}", element), 36 | Err(rofi::Error::Interrupted) => println!("Interrupted"), 37 | Err(rofi::Error::NotFound) => println!("User input was not found"), 38 | Err(e) => println!("Error: {}", e) 39 | } 40 | ``` 41 | 42 | ## Example of using pango formatted strings 43 | `rofi` can display pango format. Here is a simple example (you have to call 44 | the `self..pango` function). 45 | 46 | ```rust 47 | use rofi; 48 | use rofi::pango::{Pango, FontSize}; 49 | use std::{fs, env}; 50 | 51 | let entries: Vec = vec![ 52 | Pango::new("Option 1").size(FontSize::Small).fg_color("#666000").build(), 53 | Pango::new("Option 2").size(FontSize::Large).fg_color("#deadbe").build(), 54 | ]; 55 | 56 | match rofi::Rofi::new(&entries).pango().run() { 57 | Ok(element) => println!("Choice: {}", element), 58 | Err(rofi::Error::Interrupted) => println!("Interrupted"), 59 | Err(e) => println!("Error: {}", e) 60 | } 61 | ``` 62 | 63 | ## Example of showing a message with no inputs 64 | `rofi` can display a message without any inputs. This is commonly used for error reporting. 65 | 66 | ```rust 67 | use rofi; 68 | 69 | match rofi::Rofi::new_message("Something went wrong").run() { 70 | Err(rofi::Error::Blank) => () // the expected case 71 | Ok(_) => () // should not happen 72 | Err(_) => () // Something went wrong 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/pango.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Tibor Schneider 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Pango markup language support 16 | //! https://developer.gnome.org/pygtk/stable/pango-markup-language.html 17 | 18 | use std::collections::HashMap; 19 | use std::fmt; 20 | 21 | /// Structure for writing Pango markup spans 22 | #[derive(Debug, Clone)] 23 | pub struct Pango<'a> { 24 | content: &'a str, 25 | options: HashMap<&'static str, &'a str>, 26 | } 27 | 28 | impl<'a> Pango<'a> { 29 | /// Generate a new pango class 30 | /// 31 | /// # Usage 32 | /// 33 | /// ``` 34 | /// use rofi::pango; 35 | /// let t = pango::Pango::new("test").build(); 36 | /// assert_eq!(t, "test"); 37 | /// ``` 38 | pub fn new(content: &'a str) -> Pango<'_> { 39 | Pango { 40 | content, 41 | options: HashMap::new(), 42 | } 43 | } 44 | 45 | /// Generate a new pango class with options capacity 46 | /// 47 | /// # Usage 48 | /// 49 | /// ``` 50 | /// use rofi::pango; 51 | /// let t = pango::Pango::with_capacity("test", 0).build(); 52 | /// assert_eq!(t, "test"); 53 | /// ``` 54 | pub fn with_capacity(content: &'a str, size: usize) -> Pango<'_> { 55 | Pango { 56 | content, 57 | options: HashMap::with_capacity(size), 58 | } 59 | } 60 | 61 | /// Generate the pango string 62 | /// 63 | /// # Usage 64 | /// 65 | /// ``` 66 | /// use rofi::pango; 67 | /// let t = pango::Pango::new("test") 68 | /// .slant_style(pango::SlantStyle::Italic) 69 | /// .size(pango::FontSize::Small) 70 | /// .build(); 71 | /// assert!(t == "test" || 72 | /// t == "test"); 73 | /// ``` 74 | pub fn build(&mut self) -> String { 75 | self.to_string() 76 | } 77 | 78 | /// Generates a pango string based on the options, but with a different 79 | /// content. 80 | /// 81 | /// ``` 82 | /// use rofi::pango; 83 | /// let mut p = pango::Pango::new(""); 84 | /// p.slant_style(pango::SlantStyle::Italic); 85 | /// p.size(pango::FontSize::Small); 86 | /// let t = p.build_content("test"); 87 | /// assert!(t == "test" || 88 | /// t == "test"); 89 | /// ``` 90 | pub fn build_content(&self, content: &str) -> String { 91 | self.to_string_with_content(content) 92 | } 93 | 94 | /// Set the font 95 | /// 96 | /// # Usage 97 | /// 98 | /// ``` 99 | /// use rofi::pango; 100 | /// let t = pango::Pango::new("test") 101 | /// .font_description("Sans Italic 12") 102 | /// .build(); 103 | /// assert_eq!(t, "test"); 104 | /// ``` 105 | pub fn font_description(&mut self, font: &'a str) -> &mut Self { 106 | self.options.insert("font_desc", font); 107 | self 108 | } 109 | 110 | /// set the font family 111 | /// 112 | /// # Usage 113 | /// 114 | /// ``` 115 | /// use rofi::pango; 116 | /// let t = pango::Pango::new("test") 117 | /// .font_family(pango::FontFamily::Monospace) 118 | /// .build(); 119 | /// assert_eq!(t, "test"); 120 | /// ``` 121 | pub fn font_family(&mut self, family: FontFamily) -> &mut Self { 122 | self.options.insert( 123 | "face", 124 | match family { 125 | FontFamily::Normal => "normal", 126 | FontFamily::Sans => "sans", 127 | FontFamily::Serif => "serif", 128 | FontFamily::Monospace => "monospace", 129 | }, 130 | ); 131 | self 132 | } 133 | 134 | /// Set the size of the font, relative to the configured font size 135 | /// 136 | /// # Usage 137 | /// 138 | /// ``` 139 | /// use rofi::pango; 140 | /// let t = pango::Pango::new("test") 141 | /// .size(pango::FontSize::Huge) 142 | /// .build(); 143 | /// assert_eq!(t, "test"); 144 | /// ``` 145 | pub fn size(&mut self, size: FontSize) -> &mut Self { 146 | self.options.insert( 147 | "size", 148 | match size { 149 | FontSize::VeryTiny => "xx-small", 150 | FontSize::Tiny => "x-small", 151 | FontSize::Small => "small", 152 | FontSize::Normal => "medium", 153 | FontSize::Large => "large", 154 | FontSize::Huge => "x-large", 155 | FontSize::VeryHuge => "xx-large", 156 | FontSize::Smaller => "smaller", 157 | FontSize::Larger => "larger", 158 | }, 159 | ); 160 | self 161 | } 162 | 163 | /// Set the slant style (italic / oblique / normal) 164 | /// 165 | /// # Usage 166 | /// 167 | /// ``` 168 | /// use rofi::pango; 169 | /// let t = pango::Pango::new("test") 170 | /// .slant_style(pango::SlantStyle::Oblique) 171 | /// .build(); 172 | /// assert_eq!(t, "test"); 173 | /// ``` 174 | pub fn slant_style(&mut self, style: SlantStyle) -> &mut Self { 175 | self.options.insert( 176 | "style", 177 | match style { 178 | SlantStyle::Normal => "normal", 179 | SlantStyle::Oblique => "oblique", 180 | SlantStyle::Italic => "italic", 181 | }, 182 | ); 183 | self 184 | } 185 | 186 | /// Set the font weight 187 | /// 188 | /// # Usage 189 | /// 190 | /// ``` 191 | /// use rofi::pango; 192 | /// let t = pango::Pango::new("test") 193 | /// .weight(pango::Weight::Bold) 194 | /// .build(); 195 | /// assert_eq!(t, "test"); 196 | /// ``` 197 | pub fn weight(&mut self, weight: Weight) -> &mut Self { 198 | self.options.insert( 199 | "weight", 200 | match weight { 201 | Weight::Thin => "100", 202 | Weight::UltraLight => "ultralight", 203 | Weight::Light => "light", 204 | Weight::Normal => "normal", 205 | Weight::Medium => "500", 206 | Weight::SemiBold => "600", 207 | Weight::Bold => "bold", 208 | Weight::UltraBold => "ultrabold", 209 | Weight::Heavy => "heavy", 210 | Weight::UltraHeavy => "1000", 211 | }, 212 | ); 213 | self 214 | } 215 | 216 | /// Set the alpha of the text 217 | /// Important: alpha must be fo the form: XX%, where XX is a number between 0 and 100. 218 | /// 219 | /// # Usage 220 | /// 221 | /// ``` 222 | /// use rofi::pango; 223 | /// let t = pango::Pango::new("test") 224 | /// .alpha("50%") 225 | /// .build(); 226 | /// assert_eq!(t, "test"); 227 | /// ``` 228 | pub fn alpha(&mut self, alpha: &'a str) -> &mut Self { 229 | self.options.insert("alpha", alpha); 230 | self 231 | } 232 | 233 | /// Use smallcaps 234 | /// 235 | /// # Usage 236 | /// 237 | /// ``` 238 | /// use rofi::pango; 239 | /// let t = pango::Pango::new("test") 240 | /// .small_caps() 241 | /// .build(); 242 | /// assert_eq!(t, "test"); 243 | /// ``` 244 | pub fn small_caps(&mut self) -> &mut Self { 245 | self.options.insert("variant", "smallcaps"); 246 | self 247 | } 248 | 249 | /// Set the stretch (expanded or condensed) 250 | /// 251 | /// # Usage 252 | /// 253 | /// ``` 254 | /// use rofi::pango; 255 | /// let t = pango::Pango::new("test") 256 | /// .stretch(pango::FontStretch::Condensed) 257 | /// .build(); 258 | /// assert_eq!(t, "test"); 259 | /// ``` 260 | pub fn stretch(&mut self, stretch: FontStretch) -> &mut Self { 261 | self.options.insert( 262 | "stretch", 263 | match stretch { 264 | FontStretch::UltraCondensed => "ultracondensed", 265 | FontStretch::ExtraCondensed => "extracondensed", 266 | FontStretch::Condensed => "condensed", 267 | FontStretch::SemiCondensed => "semicondensed", 268 | FontStretch::Normal => "normal", 269 | FontStretch::SemiExpanded => "semiexpanded", 270 | FontStretch::Expanded => "expanded", 271 | FontStretch::ExtraExpanded => "extraexpanded", 272 | FontStretch::UltraExpanded => "ultraexpanded", 273 | }, 274 | ); 275 | self 276 | } 277 | 278 | /// Set the foreground color 279 | /// 280 | /// # Usage 281 | /// 282 | /// ``` 283 | /// use rofi::pango; 284 | /// let t = pango::Pango::new("test") 285 | /// .fg_color("#00FF00") 286 | /// .build(); 287 | /// assert_eq!(t, "test"); 288 | /// ``` 289 | pub fn fg_color(&mut self, color: &'a str) -> &mut Self { 290 | self.options.insert("foreground", color); 291 | self 292 | } 293 | 294 | /// Set the background color 295 | /// 296 | /// # Usage 297 | /// 298 | /// ``` 299 | /// use rofi::pango; 300 | /// let t = pango::Pango::new("test") 301 | /// .bg_color("#00FF00") 302 | /// .build(); 303 | /// assert_eq!(t, "test"); 304 | /// ``` 305 | pub fn bg_color(&mut self, color: &'a str) -> &mut Self { 306 | self.options.insert("background", color); 307 | self 308 | } 309 | 310 | /// Set the underline style 311 | /// 312 | /// # Usage 313 | /// 314 | /// ``` 315 | /// use rofi::pango; 316 | /// let t = pango::Pango::new("test") 317 | /// .underline(pango::Underline::Double) 318 | /// .build(); 319 | /// assert_eq!(t, "test"); 320 | /// ``` 321 | pub fn underline(&mut self, underline: Underline) -> &mut Self { 322 | self.options.insert( 323 | "underline", 324 | match underline { 325 | Underline::None => "none", 326 | Underline::Single => "single", 327 | Underline::Double => "double", 328 | Underline::Low => "low", 329 | }, 330 | ); 331 | self 332 | } 333 | 334 | /// set the font to strike through 335 | /// 336 | /// # Usage 337 | /// 338 | /// ``` 339 | /// use rofi::pango; 340 | /// let t = pango::Pango::new("test") 341 | /// .strike_through() 342 | /// .build(); 343 | /// assert_eq!(t, "test"); 344 | /// ``` 345 | pub fn strike_through(&mut self) -> &mut Self { 346 | self.options.insert("strikethrough", "true"); 347 | self 348 | } 349 | 350 | fn to_string_with_content(&self, content: &str) -> String { 351 | if self.options.is_empty() { 352 | content.to_string() 353 | } else { 354 | format!( 355 | "{}", 356 | self.options 357 | .iter() 358 | .map(|(k, v)| format!("{}='{}'", k, v)) 359 | .collect::>() 360 | .join(" "), 361 | content 362 | ) 363 | } 364 | } 365 | } 366 | 367 | /// Enumeration over all available font families 368 | #[derive(Debug, Clone, Copy)] 369 | pub enum FontFamily { 370 | /// Normal font 371 | Normal, 372 | /// Sans Serif font 373 | Sans, 374 | /// Font including serif 375 | Serif, 376 | /// Monospaced font 377 | Monospace, 378 | } 379 | 380 | /// Enumeration over all avaliable font sizes 381 | #[derive(Debug, Clone, Copy)] 382 | pub enum FontSize { 383 | /// Very tiny font size, corresponsds to xx-small 384 | VeryTiny, 385 | /// Tiny font size, corresponds to x-small 386 | Tiny, 387 | /// Small font size, corresponds to small 388 | Small, 389 | /// Normal font size (default), corresponds to medium 390 | Normal, 391 | /// Large font size, corresponds to large 392 | Large, 393 | /// Huge font size, corresponds to x-large 394 | Huge, 395 | /// Very huge font size, corresponds to xx-large 396 | VeryHuge, 397 | /// Relative font size, makes content smaller than the parent 398 | Smaller, 399 | /// Relative font size, makes content larger than the parent 400 | Larger, 401 | } 402 | 403 | /// Enumeration over all possible slant styles 404 | #[derive(Debug, Clone, Copy)] 405 | pub enum SlantStyle { 406 | /// No slant 407 | Normal, 408 | /// Oblique, normal font skewed 409 | Oblique, 410 | /// Italic font, (different face) 411 | Italic, 412 | } 413 | 414 | /// Enumeration over all possible weights 415 | #[derive(Debug, Clone, Copy)] 416 | pub enum Weight { 417 | /// Thin weight (=100) 418 | Thin, 419 | /// Ultralight weight (=200) 420 | UltraLight, 421 | /// Light weight (=300) 422 | Light, 423 | /// Normal weight (=400) 424 | Normal, 425 | /// Medium weight (=500) 426 | Medium, 427 | /// SemiBold weight (=600) 428 | SemiBold, 429 | /// Bold weight (=700) 430 | Bold, 431 | /// Ultrabold weight (=800) 432 | UltraBold, 433 | /// Heavy (=900) 434 | Heavy, 435 | /// UltraHeavy weight (=1000) 436 | UltraHeavy, 437 | } 438 | 439 | /// enumeration over all possible font stretch modes 440 | #[derive(Debug, Clone, Copy)] 441 | pub enum FontStretch { 442 | /// UltraCondensed, letters are extremely close together 443 | UltraCondensed, 444 | /// ExtraCondensed, letters are very close together 445 | ExtraCondensed, 446 | /// Condensed, letters are close together 447 | Condensed, 448 | /// SemiCondensed, letters somewhat are close together 449 | SemiCondensed, 450 | /// Normal, normal spacing as defined by the font 451 | Normal, 452 | /// SemiExpanded, letters somewhat are far apart 453 | SemiExpanded, 454 | /// Expanded, letters somewhat far apart 455 | Expanded, 456 | /// ExtraExpanded, letters very far apart 457 | ExtraExpanded, 458 | /// UltraExpanded, letters extremely far apart 459 | UltraExpanded, 460 | } 461 | 462 | /// enumeration over all possible underline modes 463 | #[derive(Debug, Clone, Copy)] 464 | pub enum Underline { 465 | /// No underline mode 466 | None, 467 | /// Single, normal underline 468 | Single, 469 | /// Double 470 | Double, 471 | /// Low, only the lower line of double is drawn 472 | Low, 473 | } 474 | 475 | impl<'a> fmt::Display for Pango<'a> { 476 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 477 | if self.options.is_empty() { 478 | write!(f, "{}", self.content) 479 | } else { 480 | write!(f, "{}", self.content) 485 | } 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Tibor Schneider 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! # Rofi ui manager 16 | //! Spawn rofi windows, and parse the result appropriately. 17 | //! 18 | //! ## Simple example 19 | //! 20 | //! ``` 21 | //! use rofi; 22 | //! use std::{fs, env}; 23 | //! 24 | //! let dir_entries = fs::read_dir(env::current_dir().unwrap()) 25 | //! .unwrap() 26 | //! .map(|d| format!("{:?}", d.unwrap().path())) 27 | //! .collect::>(); 28 | //! 29 | //! match rofi::Rofi::new(&dir_entries).run() { 30 | //! Ok(choice) => println!("Choice: {}", choice), 31 | //! Err(rofi::Error::Interrupted) => println!("Interrupted"), 32 | //! Err(e) => println!("Error: {}", e) 33 | //! } 34 | //! ``` 35 | //! 36 | //! ## Example of returning an index 37 | //! `rofi` can also be used to return an index of the selected item: 38 | //! 39 | //! ``` 40 | //! use rofi; 41 | //! use std::{fs, env}; 42 | //! 43 | //! let dir_entries = fs::read_dir(env::current_dir().unwrap()) 44 | //! .unwrap() 45 | //! .map(|d| format!("{:?}", d.unwrap().path())) 46 | //! .collect::>(); 47 | //! 48 | //! match rofi::Rofi::new(&dir_entries).run_index() { 49 | //! Ok(element) => println!("Choice: {}", element), 50 | //! Err(rofi::Error::Interrupted) => println!("Interrupted"), 51 | //! Err(rofi::Error::NotFound) => println!("User input was not found"), 52 | //! Err(e) => println!("Error: {}", e) 53 | //! } 54 | //! ``` 55 | //! 56 | //! ## Example of using pango formatted strings 57 | //! `rofi` can display pango format. Here is a simple example (you have to call 58 | //! the `self..pango` function). 59 | //! 60 | //! ``` 61 | //! use rofi; 62 | //! use rofi::pango::{Pango, FontSize}; 63 | //! use std::{fs, env}; 64 | //! 65 | //! let entries: Vec = vec![ 66 | //! Pango::new("Option 1").size(FontSize::Small).fg_color("#666000").build(), 67 | //! Pango::new("Option 2").size(FontSize::Large).fg_color("#deadbe").build(), 68 | //! ]; 69 | //! 70 | //! match rofi::Rofi::new(&entries).pango().run() { 71 | //! Ok(element) => println!("Choice: {}", element), 72 | //! Err(rofi::Error::Interrupted) => println!("Interrupted"), 73 | //! Err(e) => println!("Error: {}", e) 74 | //! } 75 | //! ``` 76 | //! 77 | //! ## Example of using custom keyboard shortcuts with rofi 78 | //! 79 | //! ``` 80 | //! use rofi; 81 | //! use std::{fs, env}; 82 | //! 83 | //! let dir_entries = fs::read_dir(env::current_dir().unwrap()) 84 | //! .unwrap() 85 | //! .map(|d| format!("{:?}", d.unwrap().path())) 86 | //! .collect::>(); 87 | //! let mut r = rofi::Rofi::new(&dir_entries); 88 | //! match r.kb_custom(1, "Alt+n").unwrap().run() { 89 | //! Ok(choice) => println!("Choice: {}", choice), 90 | //! Err(rofi::Error::CustomKeyboardShortcut(exit_code)) => println!("exit code: {:?}", exit_code), 91 | //! Err(rofi::Error::Interrupted) => println!("Interrupted"), 92 | //! Err(e) => println!("Error: {}", e) 93 | //! } 94 | //! ``` 95 | 96 | #![deny(missing_docs, missing_debug_implementations, rust_2018_idioms)] 97 | 98 | pub mod pango; 99 | 100 | use std::io::{Read, Write}; 101 | use std::process::{Child, Command, Stdio}; 102 | use thiserror::Error; 103 | 104 | /// # Rofi Window Builder 105 | /// Rofi struct for displaying user interfaces. This struct is build after the 106 | /// non-consuming builder pattern. You can prepare a window, and draw it 107 | /// multiple times without reconstruction and reallocation. You can choose to 108 | /// return a handle to the child process `RofiChild`, which allows you to kill 109 | /// the process. 110 | #[derive(Debug, Clone)] 111 | pub struct Rofi<'a, T> 112 | where 113 | T: AsRef, 114 | { 115 | elements: &'a [T], 116 | case_sensitive: bool, 117 | lines: Option, 118 | message: Option, 119 | width: Width, 120 | format: Format, 121 | args: Vec, 122 | sort: bool, 123 | } 124 | 125 | /// Rofi child process. 126 | #[derive(Debug)] 127 | pub struct RofiChild { 128 | num_elements: T, 129 | p: Child, 130 | } 131 | 132 | impl RofiChild { 133 | fn new(p: Child, arg: T) -> Self { 134 | Self { 135 | num_elements: arg, 136 | p, 137 | } 138 | } 139 | /// Kill the Rofi process 140 | pub fn kill(&mut self) -> Result<(), Error> { 141 | Ok(self.p.kill()?) 142 | } 143 | } 144 | 145 | impl RofiChild { 146 | /// Wait for the result and return the output as a String. 147 | fn wait_with_output(&mut self) -> Result { 148 | let status = self.p.wait()?; 149 | let code = status.code().ok_or(Error::IoError(std::io::Error::new( 150 | std::io::ErrorKind::Interrupted, 151 | "Rofi process was interrupted", 152 | )))?; 153 | if status.success() { 154 | let mut buffer = String::new(); 155 | if let Some(mut reader) = self.p.stdout.take() { 156 | reader.read_to_string(&mut buffer)?; 157 | } 158 | if buffer.ends_with('\n') { 159 | buffer.pop(); 160 | } 161 | if buffer.is_empty() { 162 | Err(Error::Blank {}) 163 | } else { 164 | Ok(buffer) 165 | } 166 | } else if (10..=28).contains(&code) { 167 | Err(Error::CustomKeyboardShortcut(code - 9)) 168 | } else { 169 | Err(Error::Interrupted {}) 170 | } 171 | } 172 | } 173 | 174 | impl RofiChild { 175 | /// Wait for the result and return the output as an usize. 176 | fn wait_with_output(&mut self) -> Result { 177 | let status = self.p.wait()?; 178 | let code = status.code().ok_or(Error::IoError(std::io::Error::new( 179 | std::io::ErrorKind::Interrupted, 180 | "Rofi process was interrupted", 181 | )))?; 182 | if status.success() { 183 | let mut buffer = String::new(); 184 | if let Some(mut reader) = self.p.stdout.take() { 185 | reader.read_to_string(&mut buffer)?; 186 | } 187 | if buffer.ends_with('\n') { 188 | buffer.pop(); 189 | } 190 | if buffer.is_empty() { 191 | Err(Error::Blank {}) 192 | } else { 193 | let idx: isize = buffer.parse::()?; 194 | if idx < 0 || idx > self.num_elements as isize { 195 | Err(Error::NotFound {}) 196 | } else { 197 | Ok(idx as usize) 198 | } 199 | } 200 | } else if (10..=28).contains(&code) { 201 | Err(Error::CustomKeyboardShortcut(code - 9)) 202 | } else { 203 | Err(Error::Interrupted {}) 204 | } 205 | } 206 | } 207 | 208 | impl<'a, T> Rofi<'a, T> 209 | where 210 | T: AsRef, 211 | { 212 | /// Generate a new, unconfigured Rofi window based on the elements provided. 213 | pub fn new(elements: &'a [T]) -> Self { 214 | Self { 215 | elements, 216 | case_sensitive: false, 217 | lines: None, 218 | width: Width::None, 219 | format: Format::Text, 220 | args: Vec::new(), 221 | sort: false, 222 | message: None, 223 | } 224 | } 225 | 226 | /// Show the window, and return the selected string, including pango 227 | /// formatting if available 228 | pub fn run(&self) -> Result { 229 | self.spawn()?.wait_with_output() 230 | } 231 | 232 | /// show the window, and return the index of the selected string This 233 | /// function will overwrite any subsequent calls to `self.format`. 234 | pub fn run_index(&mut self) -> Result { 235 | self.spawn_index()?.wait_with_output() 236 | } 237 | 238 | /// Set sort flag 239 | pub fn set_sort(&mut self) -> &mut Self { 240 | self.sort = true; 241 | self 242 | } 243 | 244 | /// enable pango markup 245 | pub fn pango(&mut self) -> &mut Self { 246 | self.args.push("-markup-rows".to_string()); 247 | self 248 | } 249 | 250 | /// enable password mode 251 | pub fn password(&mut self) -> &mut Self { 252 | self.args.push("-password".to_string()); 253 | self 254 | } 255 | 256 | /// enable message dialog mode (-e) 257 | pub fn message_only(&mut self, message: impl Into) -> Result<&mut Self, Error> { 258 | if !self.elements.is_empty() { 259 | return Err(Error::ConfigErrorMessageAndOptions); 260 | } 261 | self.message = Some(message.into()); 262 | Ok(self) 263 | } 264 | 265 | /// Sets the number of lines. 266 | /// If this function is not called, use the number of lines provided in the 267 | /// elements vector. 268 | pub fn lines(&mut self, l: usize) -> &mut Self { 269 | self.lines = Some(l); 270 | self 271 | } 272 | 273 | /// Set the width of the window (overwrite the theme settings) 274 | pub fn width(&mut self, w: Width) -> Result<&mut Self, Error> { 275 | w.check()?; 276 | self.width = w; 277 | Ok(self) 278 | } 279 | 280 | /// Sets the case sensitivity (disabled by default) 281 | pub fn case_sensitive(&mut self, sensitivity: bool) -> &mut Self { 282 | self.case_sensitive = sensitivity; 283 | self 284 | } 285 | 286 | /// Set the prompt of the rofi window 287 | pub fn prompt(&mut self, prompt: impl Into) -> &mut Self { 288 | self.args.push("-p".to_string()); 289 | self.args.push(prompt.into()); 290 | self 291 | } 292 | 293 | /// Set the message of the rofi window (-mesg). Only available in dmenu mode. 294 | /// Docs: 295 | pub fn message(&mut self, message: impl Into) -> &mut Self { 296 | self.args.push("-mesg".to_string()); 297 | self.args.push(message.into()); 298 | self 299 | } 300 | 301 | /// Set the rofi theme 302 | /// This will make sure that rofi uses `~/.config/rofi/{theme}.rasi` 303 | pub fn theme(&mut self, theme: Option>) -> &mut Self { 304 | if let Some(t) = theme { 305 | self.args.push("-theme".to_string()); 306 | self.args.push(t.into()); 307 | } 308 | self 309 | } 310 | 311 | /// Set the return format of the rofi call. Default is `Format::Text`. If 312 | /// you call `self.spawn_index` later, the format will be overwritten with 313 | /// `Format::Index`. 314 | pub fn return_format(&mut self, format: Format) -> &mut Self { 315 | self.format = format; 316 | self 317 | } 318 | 319 | /// Set a custom keyboard shortcut. Rofi supports up to 19 custom keyboard shortcuts. 320 | /// 321 | /// `id` must be in the \[1,19\] range and identifies the keyboard shortcut 322 | /// 323 | /// `shortcut` can be any modifiers separated by `"+"`, with a letter or number at the end. 324 | /// Ex: "Control+Shift+n", "Alt+s", "Control+Alt+Shift+1 325 | /// 326 | /// [https://github.com/davatorium/rofi/blob/next/source/keyb.c#L211](https://github.com/davatorium/rofi/blob/next/source/keyb.c#L211) 327 | pub fn kb_custom(&mut self, id: u32, shortcut: &str) -> Result<&mut Self, String> { 328 | if !(1..=19).contains(&id) { 329 | return Err(format!("Attempting to set custom keyboard shortcut with invalid id: {}. Valid range is: [1,19]", id)); 330 | } 331 | self.args.push(format!("-kb-custom-{}", id)); 332 | self.args.push(shortcut.to_string()); 333 | Ok(self) 334 | } 335 | 336 | /// Returns a child process with the pre-prepared rofi window 337 | /// The child will produce the exact output as provided in the elements vector. 338 | pub fn spawn(&self) -> Result, std::io::Error> { 339 | Ok(RofiChild::new(self.spawn_child()?, String::new())) 340 | } 341 | 342 | /// Returns a child process with the pre-prepared rofi window. 343 | /// The child will produce the index of the chosen element in the vector. 344 | /// This function will overwrite any subsequent calls to `self.format`. 345 | pub fn spawn_index(&mut self) -> Result, std::io::Error> { 346 | self.format = Format::Index; 347 | Ok(RofiChild::new(self.spawn_child()?, self.elements.len())) 348 | } 349 | 350 | fn spawn_child(&self) -> Result { 351 | let mut child = Command::new("rofi") 352 | .args(match &self.message { 353 | Some(msg) => vec!["-e", msg], 354 | None => vec!["-dmenu"], 355 | }) 356 | .args(&self.args) 357 | .arg("-format") 358 | .arg(self.format.as_arg()) 359 | .arg("-l") 360 | .arg(match self.lines.as_ref() { 361 | Some(s) => format!("{}", s), 362 | None => format!("{}", self.elements.len()), 363 | }) 364 | .arg(match self.case_sensitive { 365 | true => "-case-sensitive", 366 | false => "-i", 367 | }) 368 | .args(match self.width { 369 | Width::None => vec![], 370 | Width::Percentage(x) => vec![ 371 | "-theme-str".to_string(), 372 | format!("window {{width: {}%;}}", x), 373 | ], 374 | Width::Pixels(x) => vec![ 375 | "-theme-str".to_string(), 376 | format!("window {{width: {}px;}}", x), 377 | ], 378 | }) 379 | .arg(match self.sort { 380 | true => "-sort", 381 | false => "", 382 | }) 383 | .stdin(Stdio::piped()) 384 | .stdout(Stdio::piped()) 385 | .stderr(Stdio::piped()) 386 | .spawn()?; 387 | 388 | if let Some(mut writer) = child.stdin.take() { 389 | for element in self.elements { 390 | writer.write_all(element.as_ref().as_bytes())?; 391 | writer.write_all(b"\n")?; 392 | } 393 | } 394 | 395 | Ok(child) 396 | } 397 | } 398 | 399 | static EMPTY_OPTIONS: Vec = vec![]; 400 | 401 | impl<'a> Rofi<'a, String> { 402 | /// Generate a new, Rofi window in "message only" mode with the given message. 403 | pub fn new_message(message: impl Into) -> Self { 404 | let mut rofi = Self::new(&EMPTY_OPTIONS); 405 | rofi.message_only(message) 406 | .expect("Invariant: provided empty options so it is safe to unwrap message_only"); 407 | rofi 408 | } 409 | } 410 | 411 | /// Width of the rofi window to overwrite the default width from the rogi theme. 412 | #[derive(Debug, Clone, Copy)] 413 | pub enum Width { 414 | /// No width specified, use the default one from the theme 415 | None, 416 | /// Width in percentage of the screen, must be between 0 and 100 417 | Percentage(usize), 418 | /// Width in pixels, must be greater than 100 419 | Pixels(usize), 420 | } 421 | 422 | impl Width { 423 | fn check(&self) -> Result<(), Error> { 424 | match self { 425 | Self::Percentage(x) => { 426 | if *x > 100 { 427 | Err(Error::InvalidWidth("Percentage must be between 0 and 100")) 428 | } else { 429 | Ok(()) 430 | } 431 | } 432 | Self::Pixels(x) => { 433 | if *x <= 100 { 434 | Err(Error::InvalidWidth("Pixels must be larger than 100")) 435 | } else { 436 | Ok(()) 437 | } 438 | } 439 | _ => Ok(()), 440 | } 441 | } 442 | } 443 | 444 | /// Different modes, how rofi should return the results 445 | #[derive(Debug, Clone, Copy)] 446 | pub enum Format { 447 | /// Regular text, including markup 448 | #[allow(dead_code)] 449 | Text, 450 | /// Text, where the markup is removed 451 | StrippedText, 452 | /// Text with the exact user input 453 | UserInput, 454 | /// Index of the chosen element 455 | Index, 456 | } 457 | 458 | impl Format { 459 | fn as_arg(&self) -> &'static str { 460 | match self { 461 | Format::Text => "s", 462 | Format::StrippedText => "p", 463 | Format::UserInput => "f", 464 | Format::Index => "i", 465 | } 466 | } 467 | } 468 | 469 | /// Rofi Error Type 470 | #[derive(Error, Debug)] 471 | pub enum Error { 472 | /// IO Error 473 | #[error("IO Error: {0}")] 474 | IoError(#[from] std::io::Error), 475 | /// Parse Int Error, only occurs when getting the index. 476 | #[error("Parse Int Error: {0}")] 477 | ParseIntError(#[from] std::num::ParseIntError), 478 | /// Error returned when the user has interrupted the action 479 | #[error("User interrupted the action")] 480 | Interrupted, 481 | /// Error returned when the user chose a blank option 482 | #[error("User chose a blank line")] 483 | Blank, 484 | /// Error returned the width is invalid, only returned in Rofi::width() 485 | #[error("Invalid width: {0}")] 486 | InvalidWidth(&'static str), 487 | /// Error, when the input of the user is not found. This only occurs when 488 | /// getting the index. 489 | #[error("User input was not found")] 490 | NotFound, 491 | /// Incompatible configuration: cannot specify non-empty options and message_only. 492 | #[error("Can't specify non-empty options and message_only")] 493 | ConfigErrorMessageAndOptions, 494 | /// A custom keyboard shortcut was used 495 | #[error("User used a custom keyboard shortcut")] 496 | CustomKeyboardShortcut(i32), 497 | } 498 | 499 | #[cfg(test)] 500 | mod rofitest { 501 | use super::*; 502 | #[test] 503 | fn simple_test() { 504 | let options = vec!["a", "b", "c", "d"]; 505 | let empty_options: Vec = Vec::new(); 506 | match Rofi::new(&options).prompt("choose c").run() { 507 | Ok(ret) => assert!(ret == "c"), 508 | _ => assert!(false), 509 | } 510 | match Rofi::new(&options).prompt("chose c").run_index() { 511 | Ok(ret) => assert!(ret == 2), 512 | _ => assert!(false), 513 | } 514 | match Rofi::new(&options) 515 | .prompt("press escape") 516 | .width(Width::Percentage(15)) 517 | .unwrap() 518 | .run_index() 519 | { 520 | Err(Error::Interrupted) => assert!(true), 521 | _ => assert!(false), 522 | } 523 | match Rofi::new(&options) 524 | .prompt("Enter something wrong") 525 | .run_index() 526 | { 527 | Err(Error::NotFound) => assert!(true), 528 | _ => assert!(false), 529 | } 530 | match Rofi::new(&empty_options) 531 | .prompt("Enter password") 532 | .password() 533 | .return_format(Format::UserInput) 534 | .run() 535 | { 536 | Ok(ret) => assert!(ret == "password"), 537 | _ => assert!(false), 538 | } 539 | match Rofi::new_message("A message with no input").run() { 540 | Err(Error::Blank) => (), // ok 541 | _ => assert!(false), 542 | } 543 | 544 | let mut r = Rofi::new(&options); 545 | match r 546 | .message("Press Alt+n") 547 | .kb_custom(1, "Alt+n") 548 | .unwrap() 549 | .run() 550 | { 551 | Err(Error::CustomKeyboardShortcut(exit_code)) => { 552 | assert_eq!(exit_code, 1) 553 | } 554 | _ => assert!(false), 555 | } 556 | } 557 | } 558 | --------------------------------------------------------------------------------