├── .gitignore ├── Cargo.toml ├── README.md ├── examples └── reasonable_main_usage.rs └── src ├── config.rs ├── gui.rs ├── gui_result.rs ├── lib.rs └── reasonable_main ├── command.rs ├── main.rs └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rmenu" 3 | version = "0.1.4" 4 | authors = ["SuperCuber "] 5 | description = "A rofi and dmenu inspired menu" 6 | repository = "https://www.github.com/SuperCuber/rmenu" 7 | readme = "README.md" 8 | keywords = ["rofi", "dmenu", "menu"] 9 | categories = ["command-line-utilities", "gui"] 10 | license = "WTFPL" 11 | 12 | [dependencies] 13 | conrod = { version = "0.58.*", features = ["glium", "winit"] } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | I have used DMenu and Rofi in dmenu mode for a while as my main launchers (with (scripts)[TODO] wrapping them), 3 | but I've found that they don't give me enough control about how the output is filtered. 4 | This is my attempt at creating my own launcher menu - one that gives me full control about what is displayed. 5 | 6 | # Adding as a dependency 7 | This is a library so you will need to create a new binary project using Cargo. 8 | You can call it something like `my_rmenu`. 9 | Then you will need to add `rmenu` as a dependency. 10 | At the last line of Cargo.toml, add 11 | ``` 12 | rmenu = "*" 13 | ``` 14 | 15 | # Examples 16 | Examples can be found in the `examples` folder. 17 | You can probably understand how to use the library even without being a Rust guru if you read the documentation. 18 | 19 | # Documentation 20 | [Documentation](http://docs.rs/rmenu) 21 | -------------------------------------------------------------------------------- /examples/reasonable_main_usage.rs: -------------------------------------------------------------------------------- 1 | extern crate rmenu; 2 | use rmenu::color; 3 | 4 | fn main() { 5 | let cmd = rmenu::Command::new; 6 | rmenu::reasonable_main( 7 | &[ 8 | cmd("term", "Terminal", "termite"), 9 | cmd("ff", "Firefox", "firefox"), 10 | cmd("time", "Show time", "notify-send \"`date`\""), 11 | ], 12 | &rmenu::Config { 13 | canvas_color: color::GRAY.with_alpha(0.1), 14 | input_color: color::GRAY, 15 | unselected_color: color::WHITE, 16 | selected_color: color::BLACK, 17 | ..Default::default() 18 | }, 19 | ).unwrap(); 20 | } 21 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use conrod::color::{self, Color}; 2 | 3 | #[doc = "Configuration for the GUI. Check source for what the defaults are. 4 | 5 | **Note**: ALWAYS add `..Default::default()` when creating a Config 6 | since I may add more configuration options and I will consider it a non breaking change."] 7 | pub struct Config { 8 | #[doc = "Background color"] 9 | pub canvas_color: Color, 10 | #[doc = "Input color"] 11 | pub input_color: Color, 12 | #[doc = "Color of unselected options in the menu"] 13 | pub unselected_color: Color, 14 | #[doc = "Color of selected option in the menu"] 15 | pub selected_color: Color, 16 | 17 | #[doc = "Size of border around input"] 18 | pub input_border: f64, 19 | #[doc = "Color of border around input"] 20 | pub input_border_color: Color, 21 | 22 | #[doc = "Size of input box"] 23 | pub input_size: [f64; 2], 24 | #[doc = "Size of the output list"] 25 | pub output_size: [f64; 2], 26 | 27 | #[doc = "Padding above input"] 28 | pub input_top_padding: f64, 29 | #[doc = "Padding between input and output"] 30 | pub output_top_padding: f64, 31 | 32 | #[doc = "Path to a .ttf file with the font to use"] 33 | pub font: String, 34 | 35 | #[doc = "Disable escape to exit the menu - because it crashes on i3-gaps"] 36 | pub disable_esc: bool, 37 | } 38 | 39 | impl Default for Config { 40 | fn default() -> Config { 41 | Config { 42 | canvas_color: color::BLACK, 43 | input_color: color::BLUE, 44 | unselected_color: color::WHITE, 45 | selected_color: color::RED, 46 | 47 | input_border: 1.0, 48 | input_border_color: color::BLACK, 49 | 50 | input_size: [200.0, 25.0], 51 | output_size: [200.0, 1000.0], 52 | 53 | input_top_padding: 0.0, 54 | output_top_padding: 0.0, 55 | 56 | font: "/usr/share/fonts/TTF/Ubuntu-M.ttf".into(), 57 | 58 | disable_esc: false, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/gui.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | 3 | use conrod::{self, widget, Colorable, Positionable, Widget, Sizeable, Borderable}; 4 | use conrod::backend::glium::glium; 5 | use conrod::backend::glium::glium::Surface; 6 | 7 | use conrod::backend::glium::glium::glutin::VirtualKeyCode; 8 | 9 | use config::Config; 10 | use gui_result::GuiResult; 11 | 12 | struct State { 13 | input_text: String, 14 | selected: usize, 15 | } 16 | 17 | #[doc = "Runs the menu with the default config. 18 | The process function will filter the output based on the input."] 19 | pub fn run(process: F) -> GuiResult 20 | where 21 | F: Fn(&str) -> Vec, 22 | T: Into + From + Clone, 23 | { 24 | run_config(process, &Config::default()) 25 | } 26 | 27 | #[doc = "Runs with a configuration. See [run](fn.run.html)"] 28 | pub fn run_config(process: F, configuration: &Config) -> GuiResult 29 | where 30 | F: Fn(&str) -> Vec, 31 | T: Into + From + Clone, 32 | { 33 | let mut events_loop = glium::glutin::EventsLoop::new(); 34 | let window = glium::glutin::WindowBuilder::new() 35 | .with_fullscreen(Some(events_loop.get_primary_monitor())) 36 | .with_title("Rmenu"); 37 | let context = glium::glutin::ContextBuilder::new() 38 | .with_vsync(true) 39 | .with_multisampling(4); 40 | let display = glium::Display::new(window, context, &events_loop).unwrap(); 41 | 42 | // construct our `Ui`. 43 | let dimensions = events_loop.get_primary_monitor().get_dimensions(); 44 | let mut ui = conrod::UiBuilder::new([dimensions.0 as f64, dimensions.1 as f64]).build(); 45 | 46 | // Generate the widget identifiers. 47 | let ids = Ids::new(ui.widget_id_generator()); 48 | 49 | // Add a `Font` to the `Ui`'s `font::Map` from file. 50 | ui.fonts.insert_from_file(&configuration.font).unwrap(); 51 | 52 | // A type used for converting `conrod::render::Primitives` into `Command`s that can be used 53 | // for drawing to the glium `Surface`. 54 | let mut renderer = conrod::backend::glium::Renderer::new(&display).unwrap(); 55 | 56 | // The image map describing each of our widget->image mappings (in our case, none). 57 | let image_map = conrod::image::Map::::new(); 58 | 59 | let mut state = State { 60 | input_text: String::new(), 61 | selected: 0, 62 | }; 63 | 64 | let mut answer = GuiResult::Cancel; 65 | 66 | events_loop.run_forever(|event| { 67 | 68 | // Break from the loop upon `Escape` or closed window. 69 | match event.clone() { 70 | glium::glutin::Event::WindowEvent { event, .. } => { 71 | match event { 72 | glium::glutin::WindowEvent::Closed => return glium::glutin::ControlFlow::Break, 73 | glium::glutin::WindowEvent::KeyboardInput { 74 | input: glium::glutin::KeyboardInput { 75 | virtual_keycode: Some(VirtualKeyCode::Escape), .. 76 | }, 77 | .. 78 | } if !configuration.disable_esc => return glium::glutin::ControlFlow::Break, 79 | glium::glutin::WindowEvent::KeyboardInput { 80 | input: glium::glutin::KeyboardInput { 81 | virtual_keycode: Some(keycode), 82 | state: glium::glutin::ElementState::Pressed, 83 | .. 84 | }, 85 | .. 86 | } => { 87 | match keycode { 88 | VirtualKeyCode::Up => { 89 | state.selected = state.selected.saturating_sub(1); 90 | } 91 | VirtualKeyCode::Down => { 92 | state.selected = state.selected.saturating_add(1); 93 | } 94 | _ => {} 95 | } 96 | } 97 | _ => (), 98 | } 99 | } 100 | _ => (), 101 | } 102 | 103 | // Use the `winit` backend feature to convert the winit event to a conrod one. 104 | let input = match conrod::backend::winit::convert_event(event, &display) { 105 | None => return glium::glutin::ControlFlow::Continue, 106 | Some(input) => input, 107 | }; 108 | 109 | // Handle the input with the `Ui`. 110 | ui.handle_event(input); 111 | 112 | // Set the widgets. 113 | { 114 | let ui = &mut ui.set_widgets(); 115 | if let Some(ans) = set_widgets(ui, &ids, &mut state, &process, configuration) { 116 | answer = ans; 117 | return glium::glutin::ControlFlow::Break; 118 | } 119 | } 120 | 121 | // Draw the `Ui` if it has changed. 122 | if let Some(primitives) = ui.draw_if_changed() { 123 | renderer.fill(&display, primitives, &image_map); 124 | let mut target = display.draw(); 125 | target.clear_color(0.0, 0.0, 0.0, 1.0); 126 | renderer.draw(&display, &mut target, &image_map).unwrap(); 127 | target.finish().unwrap(); 128 | } 129 | 130 | glium::glutin::ControlFlow::Continue 131 | }); 132 | 133 | // TODO: Try to close the window 134 | 135 | answer 136 | } 137 | 138 | widget_ids!(struct Ids { canvas, scrollbar, input, output }); 139 | 140 | fn set_widgets( 141 | ui: &mut conrod::UiCell, 142 | ids: &Ids, 143 | state: &mut State, 144 | process: &F, 145 | config: &Config, 146 | ) -> Option> 147 | where 148 | F: Fn(&str) -> Vec, 149 | T: Into + From + Clone, 150 | { 151 | let canvas = widget::Canvas::new().scroll_kids_vertically().color( 152 | config.canvas_color, 153 | ); 154 | canvas.set(ids.canvas, ui); 155 | let height = canvas.get_h(ui).unwrap(); 156 | 157 | let list = process(&state.input_text); 158 | state.selected = std::cmp::min(state.selected, list.len().saturating_sub(1)); 159 | 160 | let mut is_confirmed = false; 161 | 162 | for event in widget::TextBox::new(&state.input_text) 163 | .color(config.input_color) 164 | .xy( 165 | [ 166 | 0.0, 167 | height / 2.0 - config.input_top_padding - config.input_size[1] / 2.0, 168 | ], 169 | ) 170 | .wh(config.input_size) 171 | .center_justify() 172 | .border(config.input_border) 173 | .border_color(config.input_border_color) 174 | .set(ids.input, ui) 175 | { 176 | match event { 177 | widget::text_box::Event::Update(edit) => { 178 | state.input_text = edit; 179 | } 180 | widget::text_box::Event::Enter => { 181 | if list.is_empty() { 182 | return Some(GuiResult::Custom(T::from(state.input_text.clone()))); 183 | } else { 184 | is_confirmed = true; 185 | } 186 | } 187 | } 188 | } 189 | 190 | let (mut items, _) = widget::List::flow_down(list.len()) 191 | .down_from(ids.input, config.input_size[1] + config.output_top_padding) 192 | .wh(config.output_size) 193 | .set(ids.output, ui); 194 | while let Some(item) = items.next(ui) { 195 | let i = item.i; 196 | let contents: String = list[i].clone().into(); 197 | let text = widget::Text::new(&contents) 198 | .color(if i == state.selected { 199 | if is_confirmed { 200 | return Some(GuiResult::Option(list[i].clone())); 201 | } 202 | config.selected_color 203 | } else { 204 | config.unselected_color 205 | }) 206 | .center_justify(); 207 | item.set(text, ui); 208 | } 209 | 210 | None 211 | } 212 | -------------------------------------------------------------------------------- /src/gui_result.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | #[doc = "Represents a result of the interaction between the user and the GUI."] 3 | pub enum GuiResult { 4 | #[doc = "The user cancelled the GUI without selecting an option"] 5 | Cancel, 6 | #[doc = "The user selected an option from the GUI"] 7 | Option(T), 8 | #[doc = "The user entered a custom option"] 9 | Custom(T), 10 | } 11 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate conrod; 3 | 4 | mod gui; 5 | mod config; 6 | mod gui_result; 7 | mod reasonable_main; 8 | 9 | pub use gui::{run, run_config}; 10 | pub use config::Config; 11 | pub use gui_result::GuiResult; 12 | pub use reasonable_main::{reasonable_main, Command}; 13 | pub use conrod::color; 14 | -------------------------------------------------------------------------------- /src/reasonable_main/command.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | #[doc = "Represents a command that is selectable in the menu"] 3 | pub struct Command { 4 | key: String, 5 | display: String, 6 | command: String, 7 | } 8 | 9 | impl Command { 10 | #[doc = "Creates a new instance of Command"] 11 | pub fn new(key: K, display: D, command: C) -> Command 12 | where 13 | K: Into, 14 | D: Into, 15 | C: Into, 16 | { 17 | Command { 18 | key: key.into(), 19 | display: display.into(), 20 | command: command.into(), 21 | } 22 | } 23 | 24 | #[doc = "Returns the key"] 25 | pub fn key(&self) -> &str { 26 | &self.key 27 | } 28 | #[doc = "Returns the display string"] 29 | pub fn display(&self) -> &str { 30 | &self.display 31 | } 32 | #[doc = "Returns the command"] 33 | pub fn command(&self) -> &str { 34 | &self.command 35 | } 36 | } 37 | 38 | impl Into for Command { 39 | #[doc = "Returns a string representation"] 40 | fn into(self) -> String { 41 | self.display.clone() 42 | } 43 | } 44 | 45 | impl From for Command { 46 | #[doc = "Creates a Command where key, display, and command are equal to arg"] 47 | fn from(arg: String) -> Command { 48 | Command::new(arg.clone(), arg.clone(), arg) 49 | } 50 | } 51 | 52 | impl Clone for Command { 53 | fn clone(&self) -> Self { 54 | Command { 55 | key: self.key.clone(), 56 | display: self.display.clone(), 57 | command: self.command.clone(), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/reasonable_main/main.rs: -------------------------------------------------------------------------------- 1 | use reasonable_main::command; 2 | use gui; 3 | use config; 4 | use gui_result; 5 | 6 | use std::process; 7 | use std::io; 8 | 9 | #[doc = "A reasonable main function. 10 | Commands are filtered by command.key().starts_with(input), 11 | then selected_command.command() is executed."] 12 | pub fn reasonable_main(options: &[command::Command], config: &config::Config) -> io::Result<()> { 13 | let ans = gui::run_config( 14 | |input| filter(options, |option| option.key().starts_with(input)), 15 | config, 16 | ); 17 | 18 | // Execution 19 | match ans { 20 | gui_result::GuiResult::Option(ref cmd) | 21 | gui_result::GuiResult::Custom(ref cmd) => { 22 | let (shell, flag) = if cfg!(target_os = "windows") { 23 | ("cmd", "/C") 24 | } else { 25 | ("sh", "-c") 26 | }; 27 | process::Command::new(shell) 28 | .arg(flag) 29 | .arg(&cmd.command()) 30 | .spawn()?; 31 | } 32 | gui_result::GuiResult::Cancel => {} 33 | } 34 | 35 | Ok(()) 36 | } 37 | 38 | fn filter(vector: &[T], function: F) -> Vec 39 | where 40 | F: FnMut(&&T) -> bool, 41 | T: Clone, 42 | { 43 | vector.iter().filter(function).cloned().collect() 44 | } 45 | -------------------------------------------------------------------------------- /src/reasonable_main/mod.rs: -------------------------------------------------------------------------------- 1 | mod main; 2 | mod command; 3 | 4 | pub use self::main::reasonable_main; 5 | pub use self::command::Command; 6 | --------------------------------------------------------------------------------