├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── applications.rs ├── arguments.rs ├── main.rs └── x11.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Tests 13 | run: cargo test --verbose 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /aur -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.11.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "atty" 16 | version = "0.2.14" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 19 | dependencies = [ 20 | "hermit-abi", 21 | "libc", 22 | "winapi", 23 | ] 24 | 25 | [[package]] 26 | name = "bitflags" 27 | version = "1.2.1" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 30 | 31 | [[package]] 32 | name = "clap" 33 | version = "2.33.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 36 | dependencies = [ 37 | "ansi_term", 38 | "atty", 39 | "bitflags", 40 | "strsim", 41 | "textwrap", 42 | "unicode-width", 43 | "vec_map", 44 | ] 45 | 46 | [[package]] 47 | name = "fuzzy-matcher" 48 | version = "0.3.7" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 51 | dependencies = [ 52 | "thread_local", 53 | ] 54 | 55 | [[package]] 56 | name = "heck" 57 | version = "0.3.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 60 | dependencies = [ 61 | "unicode-segmentation", 62 | ] 63 | 64 | [[package]] 65 | name = "hermit-abi" 66 | version = "0.1.6" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" 69 | dependencies = [ 70 | "libc", 71 | ] 72 | 73 | [[package]] 74 | name = "lazy_static" 75 | version = "1.4.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 78 | 79 | [[package]] 80 | name = "libc" 81 | version = "0.2.66" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" 84 | 85 | [[package]] 86 | name = "maybe-uninit" 87 | version = "2.0.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 90 | 91 | [[package]] 92 | name = "once_cell" 93 | version = "1.7.2" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" 96 | 97 | [[package]] 98 | name = "pkg-config" 99 | version = "0.3.17" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" 102 | 103 | [[package]] 104 | name = "proc-macro-error" 105 | version = "0.4.8" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "875077759af22fa20b610ad4471d8155b321c89c3f2785526c9839b099be4e0a" 108 | dependencies = [ 109 | "proc-macro-error-attr", 110 | "proc-macro2", 111 | "quote", 112 | "rustversion", 113 | "syn", 114 | ] 115 | 116 | [[package]] 117 | name = "proc-macro-error-attr" 118 | version = "0.4.8" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "c5717d9fa2664351a01ed73ba5ef6df09c01a521cb42cb65a061432a826f3c7a" 121 | dependencies = [ 122 | "proc-macro2", 123 | "quote", 124 | "rustversion", 125 | "syn", 126 | "syn-mid", 127 | ] 128 | 129 | [[package]] 130 | name = "proc-macro2" 131 | version = "1.0.8" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" 134 | dependencies = [ 135 | "unicode-xid", 136 | ] 137 | 138 | [[package]] 139 | name = "quote" 140 | version = "1.0.2" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 143 | dependencies = [ 144 | "proc-macro2", 145 | ] 146 | 147 | [[package]] 148 | name = "rlaunch" 149 | version = "1.3.13" 150 | dependencies = [ 151 | "fuzzy-matcher", 152 | "structopt", 153 | "x11-dl", 154 | ] 155 | 156 | [[package]] 157 | name = "rustversion" 158 | version = "1.0.2" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "b3bba175698996010c4f6dce5e7f173b6eb781fce25d2cfc45e27091ce0b79f6" 161 | dependencies = [ 162 | "proc-macro2", 163 | "quote", 164 | "syn", 165 | ] 166 | 167 | [[package]] 168 | name = "strsim" 169 | version = "0.8.0" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 172 | 173 | [[package]] 174 | name = "structopt" 175 | version = "0.3.9" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "a1bcbed7d48956fcbb5d80c6b95aedb553513de0a1b451ea92679d999c010e98" 178 | dependencies = [ 179 | "clap", 180 | "lazy_static", 181 | "structopt-derive", 182 | ] 183 | 184 | [[package]] 185 | name = "structopt-derive" 186 | version = "0.4.2" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "095064aa1f5b94d14e635d0a5684cf140c43ae40a0fd990708d38f5d669e5f64" 189 | dependencies = [ 190 | "heck", 191 | "proc-macro-error", 192 | "proc-macro2", 193 | "quote", 194 | "syn", 195 | ] 196 | 197 | [[package]] 198 | name = "syn" 199 | version = "1.0.14" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5" 202 | dependencies = [ 203 | "proc-macro2", 204 | "quote", 205 | "unicode-xid", 206 | ] 207 | 208 | [[package]] 209 | name = "syn-mid" 210 | version = "0.5.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" 213 | dependencies = [ 214 | "proc-macro2", 215 | "quote", 216 | "syn", 217 | ] 218 | 219 | [[package]] 220 | name = "textwrap" 221 | version = "0.11.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 224 | dependencies = [ 225 | "unicode-width", 226 | ] 227 | 228 | [[package]] 229 | name = "thread_local" 230 | version = "1.1.3" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" 233 | dependencies = [ 234 | "once_cell", 235 | ] 236 | 237 | [[package]] 238 | name = "unicode-segmentation" 239 | version = "1.6.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 242 | 243 | [[package]] 244 | name = "unicode-width" 245 | version = "0.1.7" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 248 | 249 | [[package]] 250 | name = "unicode-xid" 251 | version = "0.2.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 254 | 255 | [[package]] 256 | name = "vec_map" 257 | version = "0.8.1" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 260 | 261 | [[package]] 262 | name = "winapi" 263 | version = "0.3.8" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 266 | dependencies = [ 267 | "winapi-i686-pc-windows-gnu", 268 | "winapi-x86_64-pc-windows-gnu", 269 | ] 270 | 271 | [[package]] 272 | name = "winapi-i686-pc-windows-gnu" 273 | version = "0.4.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 276 | 277 | [[package]] 278 | name = "winapi-x86_64-pc-windows-gnu" 279 | version = "0.4.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 282 | 283 | [[package]] 284 | name = "x11-dl" 285 | version = "2.18.5" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "2bf981e3a5b3301209754218f962052d4d9ee97e478f4d26d4a6eced34c1fef8" 288 | dependencies = [ 289 | "lazy_static", 290 | "libc", 291 | "maybe-uninit", 292 | "pkg-config", 293 | ] 294 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rlaunch" 3 | version = "1.3.13" 4 | authors = ["Ponas "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | x11-dl = "2.18.5" 9 | structopt = "0.3.9" 10 | fuzzy-matcher = "0.3.7" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Mykolas Peteraitis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # rlaunch ![Rust](https://github.com/PonasKovas/rlaunch/workflows/Rust/badge.svg?branch=actions) ![GitHub](https://img.shields.io/github/license/PonasKovas/rlaunch) ![AUR version](https://img.shields.io/aur/version/rlaunch) 4 | 5 | rlaunch is a fast, light-weight and modern application launcher for X11 written in Rust that I made because `dmenu` was too slow for me. I'm glad to say that indeed rlaunch works a lot faster than `dmenu` (at least for me, I haven't tested it on other computers). 6 | 7 | ### ⚠️ This project is in passive maintenance mode — no active development, but PRs and issues are still reviewed. 8 | 9 | ![demo](https://i.imgur.com/vOMr0Ci.gif) 10 | 11 | ## Getting Started 12 | 13 | This should work on all linux distributions and DEs that use X11, but if it doesn't - feel free to file an issue. 14 | 15 | ### Usage 16 | 17 | ``` 18 | rlaunch 1.3.13 19 | A simple and light-weight tool for launching applications and running commands on X11. 20 | 21 | USAGE: 22 | rlaunch [FLAGS] [OPTIONS] 23 | 24 | FLAGS: 25 | -b, --bottom Show the bar on the bottom of the screen 26 | --help Prints help information 27 | -p, --path Scan the PATH variable 28 | -V, --version Prints version information 29 | 30 | OPTIONS: 31 | --color0 The color of the bar background [default: #2e2c2c] 32 | --color1 The color of the selected suggestion background [default: #1286a1] 33 | --color2 The color of the text [default: #ffffff] 34 | --color3 The color of the suggestions text [default: #ffffff] 35 | --color4 The color of the file scanning progress bar [default: #242222] 36 | -f, --font The font used on the bar [default: DejaVu Sans Mono] 37 | -h, --height The height of the bar (in pixels) [default: 22] 38 | -t, --terminal The terminal to use when launching applications that require a terminal [default: i3- 39 | sensible-terminal] 40 | ``` 41 | 42 | ### Installing 43 | 44 | [This application is available on the AUR](https://aur.archlinux.org/packages/rlaunch/) 45 | ``` 46 | $ git clone https://aur.archlinux.org/rlaunch.git 47 | $ cd rlaunch 48 | $ makepkg -si 49 | ``` 50 | 51 | ### Compiling from source 52 | You will need `cargo` for this. 53 | ``` 54 | $ git clone https://github.com/PonasKovas/rlaunch.git 55 | $ cd rlaunch 56 | $ cargo build --release 57 | ``` 58 | After running these commands, the compiled binary will be `./target/release/rlaunch` 59 | 60 | ## Contributing 61 | 62 | Feel free to make pull requests and issues, I will try to respond asap. 63 | 64 | ## Authors 65 | 66 | * [PonasKovas](https://github.com/PonasKovas) 67 | 68 | ## License 69 | 70 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 71 | -------------------------------------------------------------------------------- /src/applications.rs: -------------------------------------------------------------------------------- 1 | use std::env::var; 2 | use std::fs::{read_dir, read_to_string}; 3 | use std::sync::Mutex; 4 | use std::time::Instant; 5 | use std::collections::BTreeMap; 6 | 7 | pub type Apps = BTreeMap; 8 | type DirID = String; 9 | 10 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] 11 | pub struct App { 12 | pub exec: String, 13 | pub show_terminal: bool, 14 | } 15 | 16 | pub fn read_applications(apps: &Mutex, scan_path: bool, progress: &Mutex<(u32, u32)>) { 17 | let xdg_data_home = match var("XDG_DATA_HOME") { 18 | Ok(h) => (h + "/applications", "".to_owned()), 19 | Err(_) => match var("HOME") { 20 | Ok(s) => (s + "/.local/share/applications", "".to_owned()), 21 | Err(_) => { 22 | eprintln!("$HOME not set!"); 23 | ("".to_owned(), "".to_owned()) 24 | } 25 | }, 26 | }; 27 | let mut xdg_data_dirs = match var("XDG_DATA_DIRS") { 28 | Ok(d) => d 29 | .split(':') 30 | .map(|s| (s.to_owned() + "/applications", "".to_owned())) 31 | .collect(), 32 | Err(_) => "/usr/local/share/:/usr/share/" 33 | .split(':') 34 | .map(|s| (s.to_owned() + "/applications", "".to_owned())) 35 | .collect(), 36 | }; 37 | 38 | // all dirs that might have `applications` dir inside that we need to scan 39 | let mut share_dirs = vec![xdg_data_home]; 40 | share_dirs.append(&mut xdg_data_dirs); 41 | 42 | // count the files for the progress bar 43 | let mut files_to_scan = 0; 44 | let mut i = 0; 45 | let mut len = share_dirs.len(); 46 | while i < len { 47 | let files = match read_dir(&share_dirs[i].0) { 48 | Ok(f) => f, 49 | Err(_) => { 50 | share_dirs.remove(i); 51 | len -= 1; 52 | continue; 53 | } 54 | }; 55 | for file in files { 56 | match file { 57 | Ok(f) => { 58 | if f.path().is_dir() { 59 | share_dirs.insert( 60 | i + 1, 61 | match f.path().to_str() { 62 | Some(s) => ( 63 | s.to_owned(), 64 | share_dirs[i].1.to_owned() 65 | + "/" 66 | + f.file_name().to_str().unwrap(), 67 | ), 68 | None => continue, 69 | }, 70 | ); 71 | } else { 72 | files_to_scan += 1; 73 | } 74 | } 75 | Err(_) => continue, 76 | } 77 | } 78 | i += 1; 79 | len = share_dirs.len(); 80 | } 81 | 82 | // and files in $PATH too, if -p flag set 83 | if scan_path { 84 | if let Ok(path) = var("PATH") { 85 | for dir in path.split(':') { 86 | let files = match read_dir(dir) { 87 | Ok(f) => f, 88 | Err(_) => { 89 | continue; 90 | } 91 | }; 92 | for file in files { 93 | match file { 94 | Ok(f) => { 95 | if f.path().is_dir() { 96 | continue; 97 | } else { 98 | files_to_scan += 1; 99 | } 100 | } 101 | Err(_) => continue, 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | // get the progress bar ready 109 | progress.lock().unwrap().1 = files_to_scan; 110 | 111 | let now = Instant::now(); 112 | 113 | // start actually scanning files 114 | scan_desktop_entries(apps, share_dirs, progress); 115 | 116 | if scan_path { 117 | scan_path_dirs(apps, progress); 118 | } 119 | 120 | println!( 121 | "Finished reading all {} applications ({}s)", 122 | files_to_scan, 123 | now.elapsed().as_secs_f64() 124 | ); 125 | } 126 | 127 | fn scan_desktop_entries( 128 | apps: &Mutex, 129 | dirs: Vec<(String, DirID)>, 130 | progress: &Mutex<(u32, u32)>, 131 | ) { 132 | let mut scanned_ids = Vec::new(); 133 | for dir in dirs { 134 | println!("scanning {:?}", dir); 135 | 'files: for file in read_dir(dir.0).unwrap() { 136 | let file = file.unwrap(); 137 | 138 | if file.path().is_dir() { 139 | continue; 140 | } 141 | 142 | // update progress 143 | progress.lock().unwrap().0 += 1; 144 | 145 | let path = file.path(); 146 | 147 | // if file doesn't end in .desktop, move on 148 | if !path 149 | .extension() 150 | .map(|ext| ext == "desktop") 151 | .unwrap_or(false) 152 | { 153 | continue; 154 | } 155 | 156 | // get the freedesktop.org file ID 157 | let mut file_id = String::new(); 158 | if !dir.1.is_empty() { 159 | file_id += &dir.1[1..].replace('/', "-"); 160 | file_id += "-"; 161 | } 162 | if let Some(stem) = path.file_stem() { 163 | file_id += &stem.to_string_lossy(); 164 | }; 165 | 166 | // if there were any other files with the same ID before, ignore this file 167 | if scanned_ids.contains(&file_id) { 168 | continue; 169 | } 170 | scanned_ids.push(file_id); 171 | 172 | // cool. now we can start parsing the file 173 | let contents = match read_to_string(path) { 174 | Ok(contents) => contents, 175 | Err(_) => continue, 176 | }; 177 | 178 | let mut name = String::new(); 179 | let mut exec = String::new(); 180 | let mut app_type = String::new(); 181 | let mut terminal = String::new(); 182 | for line in contents.lines() { 183 | if line.starts_with("Hidden=") { 184 | let mut value = line[7..].to_string(); 185 | remove_quotes(&mut value); 186 | match value.trim().to_lowercase().parse() { 187 | Err(_) | Ok(true) => { 188 | // hidden or couldnt parse 189 | continue 'files; 190 | } 191 | _ => {} 192 | } 193 | } else if line.starts_with("NoDisplay=") { 194 | let mut value = line[10..].to_string(); 195 | remove_quotes(&mut value); 196 | match value.trim().to_lowercase().parse() { 197 | Err(_) | Ok(true) => { 198 | // nodisplay or couldnt parse 199 | continue 'files; 200 | } 201 | _ => {} 202 | } 203 | } else if exec == "" && line.starts_with("Exec=") { 204 | exec = line[5..].to_string(); 205 | // remove any arguments 206 | while let Some(i) = exec.find('%') { 207 | exec.replace_range(i..(i + 2), ""); 208 | } 209 | remove_quotes(&mut exec); 210 | exec = exec.trim().to_owned(); 211 | } else if name == "" && line.starts_with("Name=") { 212 | name = line[5..].to_string(); 213 | remove_quotes(&mut name); 214 | } else if app_type == "" && line.starts_with("Type=") { 215 | app_type = line[5..].to_string(); 216 | } else if terminal == "" && line.starts_with("Terminal=") { 217 | terminal = line[9..].to_string(); 218 | } 219 | } 220 | 221 | if name == "" { 222 | continue; 223 | } 224 | if app_type != "Application" { 225 | continue; 226 | } 227 | if exec == "" { 228 | continue; 229 | } 230 | terminal.make_ascii_lowercase(); 231 | let terminal = !(terminal == "" || terminal == "false"); 232 | 233 | apps.lock().unwrap().insert(name, App { 234 | exec, 235 | show_terminal: terminal, 236 | }); 237 | } 238 | } 239 | } 240 | 241 | fn scan_path_dirs(apps: &Mutex, progress: &Mutex<(u32, u32)>) { 242 | if let Ok(path) = var("PATH") { 243 | for dir in path.split(':') { 244 | let files = match read_dir(dir) { 245 | Ok(f) => f, 246 | Err(_) => { 247 | continue; 248 | } 249 | }; 250 | for file in files { 251 | let file = match file { 252 | Ok(f) => { 253 | if f.path().is_dir() { 254 | continue; 255 | } 256 | f 257 | } 258 | Err(_) => continue, 259 | }; 260 | 261 | // update progress 262 | progress.lock().unwrap().0 += 1; 263 | 264 | let path = file.path(); 265 | let name = match path 266 | .file_name() 267 | .map(|name| name.to_string_lossy().into_owned()) 268 | { 269 | Some(name) => name, 270 | None => continue, 271 | }; 272 | let exec = path.to_string_lossy().into_owned(); 273 | 274 | apps.lock().unwrap().insert(name, App { 275 | exec, 276 | show_terminal: false, 277 | }); 278 | } 279 | } 280 | } 281 | } 282 | 283 | fn remove_quotes(string: &mut String) { 284 | // remove quotes if present 285 | if string.len() > 1 && string.starts_with('"') && string.ends_with('"') { 286 | *string = string[1..string.len() - 1].to_string(); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/arguments.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[derive(Debug, StructOpt, Clone)] 4 | #[structopt( 5 | name = "rlaunch", 6 | about = "A simple, light-weight and modern tool for launching applications and running commands on X11." 7 | )] 8 | pub struct Args { 9 | /// The color of the bar background 10 | #[structopt(long, default_value = "#2e2c2c", parse(try_from_str = parse_color))] 11 | pub color0: u64, 12 | 13 | /// The color of the selected suggestion background 14 | #[structopt(long, default_value = "#1286a1", parse(try_from_str = parse_color))] 15 | pub color1: u64, 16 | 17 | /// The color of the text 18 | #[structopt(long, default_value = "#ffffff", parse(try_from_str = parse_color))] 19 | pub color2: u64, 20 | 21 | /// The color of the suggestions text 22 | #[structopt(long, default_value = "#ffffff", parse(try_from_str = parse_color))] 23 | pub color3: u64, 24 | 25 | /// The color of the file scanning progress bar 26 | #[structopt(long, default_value = "#242222", parse(try_from_str = parse_color))] 27 | pub color4: u64, 28 | 29 | /// The height of the bar (in pixels) 30 | #[structopt(short, long, default_value = "22")] 31 | pub height: u32, 32 | 33 | /// Show the bar on the bottom of the screen 34 | #[structopt(short, long)] 35 | pub bottom: bool, 36 | 37 | /// The font used on the bar 38 | #[structopt(short, long, default_value = "DejaVu Sans Mono")] 39 | pub font: String, 40 | 41 | /// The terminal to use when launching applications that require a terminal 42 | #[structopt(short, long, default_value = "i3-sensible-terminal")] 43 | pub terminal: String, 44 | 45 | /// Scan the PATH variable. 46 | #[structopt(short, long)] 47 | pub path: bool, 48 | } 49 | 50 | pub fn get_args() -> Args { 51 | Args::from_args() 52 | } 53 | 54 | fn parse_color(string: &str) -> Result { 55 | if !string.starts_with('#') { 56 | return Err("Color hex code must start with a #"); 57 | } 58 | if string.len() != 7 { 59 | return Err("Color hex code format: #RRGGBB"); 60 | } 61 | 62 | u64::from_str_radix(&string[1..], 16).map_err(|_| "Couldn't parse color code") 63 | } 64 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod applications; 2 | mod arguments; 3 | mod x11; 4 | 5 | use applications::{read_applications, Apps}; 6 | use arguments::{get_args, Args}; 7 | use fuzzy_matcher::skim::SkimMatcherV2; 8 | use fuzzy_matcher::FuzzyMatcher; 9 | use std::cmp::{max, min}; 10 | use std::process::exit; 11 | use std::process::Command; 12 | use std::sync::{Arc, Mutex}; 13 | use std::thread; 14 | use std::time::Instant; 15 | use x11::{Action, GraphicsContext, TextRenderingContext, X11Context}; 16 | use x11_dl::xlib; 17 | 18 | const KEY_ESCAPE: u32 = 9; 19 | const KEY_LEFT: u32 = 113; 20 | const KEY_RIGHT: u32 = 114; 21 | const KEY_UP: u32 = 111; 22 | const KEY_DOWN: u32 = 116; 23 | const KEY_BACKSPACE: u32 = 22; 24 | const KEY_ENTER: u32 = 36; 25 | const KEY_TAB: u32 = 23; 26 | const KEY_K: u32 = 45; 27 | const KEY_U: u32 = 30; 28 | 29 | struct State { 30 | caret_pos: i32, 31 | text: String, 32 | last_text: String, 33 | suggestions: Vec<(i64, String)>, 34 | selected: u8, 35 | progress: f32, 36 | progress_finished: Option, 37 | } 38 | 39 | fn main() { 40 | let args = get_args(); 41 | // spawn a thread for reading all applications 42 | let apps = Arc::new(Mutex::new(Apps::new())); 43 | let apps_clone = apps.clone(); 44 | let path = args.path; 45 | let progress = Arc::new(Mutex::new((0, 1))); 46 | let progress_clone = progress.clone(); 47 | thread::spawn(move || read_applications(&apps_clone, path, &progress_clone)); 48 | 49 | let mut state = State { 50 | caret_pos: 0, 51 | text: String::new(), 52 | last_text: String::new(), 53 | suggestions: Vec::new(), 54 | selected: 0, 55 | progress: 0.0, 56 | progress_finished: None, 57 | }; 58 | 59 | // initialize xlib context 60 | let xc = match X11Context::new() { 61 | Ok(v) => v, 62 | Err(e) => { 63 | eprintln!("Error: {:?}", e); 64 | exit(1); 65 | } 66 | }; 67 | 68 | // get screen width and the position where to map window 69 | let mut screen_width = 0; 70 | let mut window_pos = (0, 0); 71 | 72 | let mouse_pos = xc.get_mouse_pos(); 73 | for screen in xc.get_screens() { 74 | // multiple monitors support 75 | if in_rect( 76 | (mouse_pos.0, mouse_pos.1), 77 | (screen.x_org, screen.y_org), 78 | (screen.width, screen.height), 79 | ) { 80 | screen_width = screen.width as u32; 81 | window_pos.0 = screen.x_org as i32; 82 | window_pos.1 = if args.bottom { 83 | screen.y_org as i32 + screen.height as i32 - args.height as i32 84 | } else { 85 | screen.y_org as i32 86 | }; 87 | break; 88 | } 89 | } 90 | 91 | // create the window 92 | let window = xc.create_window(window_pos, screen_width, args.height); 93 | 94 | xc.grab_keyboard(); 95 | 96 | let font_height = { 97 | let mut h = 12; 98 | for x in args.font.split(':') { 99 | if x.starts_with("size=") { 100 | h = (&x[5..]).parse().expect("couldn't parse font size"); 101 | break; 102 | } 103 | } 104 | h 105 | }; 106 | let mut trc = xc.init_trc(&window, &format!("{}:size=12:antialias=true", args.font)); 107 | xc.add_color_to_trc(&mut trc, args.color2); 108 | xc.add_color_to_trc(&mut trc, args.color3); 109 | 110 | let gc = xc.init_gc(&window); 111 | 112 | // show window 113 | xc.map_window(&window); 114 | 115 | xc.run(|xc, event| { 116 | update_suggestions(&xc, &trc, &mut state, screen_width, &apps); 117 | if state.progress_finished == None { 118 | let progress_lock = progress.lock().unwrap(); 119 | state.progress = progress_lock.0 as f32 / progress_lock.1 as f32; 120 | drop(progress_lock); 121 | if 1.0 - state.progress < 0.000_001 { 122 | state.progress_finished = Some(Instant::now()); 123 | } 124 | } 125 | render_bar(&xc, &trc, &gc, screen_width, &state, &args, font_height); 126 | match event { 127 | None => Action::Run, 128 | Some(e) => handle_event(&xc, e, &mut state, &apps, &args.terminal), 129 | } 130 | }); 131 | } 132 | 133 | fn render_bar( 134 | xc: &X11Context, 135 | trc: &TextRenderingContext, 136 | gc: &GraphicsContext, 137 | width: u32, 138 | state: &State, 139 | args: &Args, 140 | font_height: i32, 141 | ) { 142 | let text_y = args.height as i32 / 2 + font_height / 2; 143 | // clear 144 | xc.draw_rect(&gc, args.color0, 0, 0, width, args.height); 145 | 146 | // render the scanning progress bar 147 | if match state.progress_finished { 148 | Some(t) => t.elapsed().as_secs_f32() < 0.5, 149 | None => true, 150 | } { 151 | let progress_bar_width = ((width as f32 * 0.3).floor() * state.progress) as u32; 152 | let mut progress_bar_color = args.color4; 153 | if let Some(t) = state.progress_finished { 154 | let intensity = (0.5 - t.elapsed().as_secs_f32()).max(0.0) * 2.0; 155 | let (or, og, ob) = ( 156 | (progress_bar_color >> 16) as u8 as f32, 157 | (progress_bar_color >> 8) as u8 as f32, 158 | (progress_bar_color) as u8 as f32, 159 | ); 160 | let r = (((args.color0 >> 16) as u8) as f32 * (1.0 - intensity) + or * intensity) 161 | .round() as u64; 162 | let g = (((args.color0 >> 8) as u8) as f32 * (1.0 - intensity) + og * intensity).round() 163 | as u64; 164 | let b = 165 | (((args.color0) as u8) as f32 * (1.0 - intensity) + ob * intensity).round() as u64; 166 | progress_bar_color = (r << 16) + (g << 8) + b; 167 | } 168 | xc.draw_rect( 169 | &gc, 170 | progress_bar_color, 171 | 0, 172 | 0, 173 | progress_bar_width, 174 | args.height, 175 | ); 176 | } 177 | 178 | // render the typed text 179 | xc.render_text(&trc, 0, 0, text_y, &state.text); 180 | // and the caret 181 | xc.draw_rect( 182 | &gc, 183 | 0xFFFFFF, 184 | xc.get_text_dimensions(&trc, &state.text[0..state.caret_pos as usize]) 185 | .0 as i32, 186 | 2, 187 | 2, 188 | args.height - 4, 189 | ); 190 | 191 | // render suggestions 192 | let mut x = (width as f32 * 0.3).floor() as i32; 193 | for (i, suggestion) in state.suggestions.iter().enumerate() { 194 | let name_width = xc.get_text_dimensions(&trc, &suggestion.1).0 as i32; 195 | // if selected, render rectangle below 196 | if state.selected as usize == i { 197 | xc.draw_rect(&gc, args.color1, x, 0, name_width as u32 + 16, args.height); 198 | } 199 | 200 | xc.render_text(&trc, 1, x + 8, text_y, &suggestion.1); 201 | 202 | x += name_width + 16; 203 | } 204 | } 205 | 206 | fn update_suggestions( 207 | xc: &X11Context, 208 | trc: &TextRenderingContext, 209 | state: &mut State, 210 | width: u32, 211 | apps: &Mutex, 212 | ) { 213 | if state.text == state.last_text { 214 | return; 215 | } 216 | state.suggestions.clear(); 217 | // iterate over application names 218 | // and find those that match the typed text 219 | let mut x = 0; 220 | let max_width = (width as f32 * 0.7).floor() as i32; 221 | let apps_lock = apps.lock().unwrap(); 222 | for app in apps_lock.iter() { 223 | let name = &app.0; 224 | if let Some(mtch) = SkimMatcherV2::default() 225 | .fuzzy_match(name, &state.text.split_whitespace().collect::()) 226 | { 227 | state.suggestions.push((mtch, name.to_string())); 228 | } 229 | } 230 | // sort the suggestion by match scores (descending) and name (ascending) 231 | state.suggestions.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); 232 | 233 | for (i, suggestion) in state.suggestions.iter().enumerate() { 234 | let name = &suggestion.1; 235 | let width = xc.get_text_dimensions(&trc, &name).0 as i32; 236 | if x + width <= max_width { 237 | x += width + 16; 238 | } else { 239 | state.suggestions.truncate(i + 1); 240 | break; 241 | } 242 | } 243 | state.last_text = state.text.clone(); 244 | } 245 | 246 | fn handle_event( 247 | xc: &X11Context, 248 | event: &xlib::XEvent, 249 | state: &mut State, 250 | apps: &Mutex, 251 | terminal: &str, 252 | ) -> Action { 253 | if let Some(e) = xc.xevent_to_xkeyevent(*event) { 254 | let ctrl = (e.state & xlib::ControlMask) != 0; 255 | match e.keycode { 256 | KEY_ESCAPE => { 257 | return Action::Stop; 258 | } 259 | KEY_LEFT => { 260 | if state.selected == 0 { 261 | state.caret_pos = max(0, state.caret_pos - 1); 262 | } else { 263 | state.selected -= 1; 264 | } 265 | } 266 | KEY_UP => { 267 | state.selected = max(0, state.selected as i16 - 1) as u8; 268 | } 269 | KEY_DOWN => { 270 | state.selected = min(state.selected + 1, state.suggestions.len() as u8 - 1); 271 | } 272 | KEY_RIGHT => { 273 | if state.caret_pos == state.text.len() as i32 { 274 | state.selected = min(state.selected + 1, state.suggestions.len() as u8 - 1); 275 | } else { 276 | state.caret_pos += 1; 277 | } 278 | } 279 | KEY_BACKSPACE => { 280 | if state.caret_pos != 0 { 281 | state.text.remove(state.caret_pos as usize - 1); 282 | state.caret_pos -= 1; 283 | state.selected = 0; 284 | } 285 | } 286 | KEY_U if ctrl => { 287 | state.text = state.text.split_off(state.caret_pos as usize); 288 | state.caret_pos = 0; 289 | } 290 | KEY_K if ctrl => { 291 | state.text.truncate(state.caret_pos as usize); 292 | state.selected = 0; 293 | } 294 | KEY_ENTER => { 295 | // if no suggestions available, just run the text, otherwise launch selected application 296 | if state.suggestions.is_empty() { 297 | run_command(&state.text); 298 | } else { 299 | let apps_lock = apps.lock().unwrap(); 300 | let app = &apps_lock 301 | .get(&state.suggestions[state.selected as usize].1) 302 | .unwrap(); 303 | if app.show_terminal { 304 | run_command(&format!("{} -e \"{}\"", terminal, app.exec)); 305 | } else { 306 | run_command(&app.exec); 307 | } 308 | } 309 | return Action::Stop; 310 | } 311 | KEY_TAB => { 312 | if !state.suggestions.is_empty() { 313 | state.text = state.suggestions[state.selected as usize].1.to_string(); 314 | state.caret_pos = state.text.len() as i32; 315 | state.selected = 0; 316 | } 317 | } 318 | _ => { 319 | // some other key 320 | // try to interpret the key as a character 321 | let c = xc.keyevent_to_char(e); 322 | if !c.is_ascii_control() { 323 | state.text.insert(state.caret_pos as usize, c); 324 | state.caret_pos += 1; 325 | state.selected = 0; 326 | } 327 | } 328 | } 329 | } 330 | Action::Run 331 | } 332 | 333 | fn run_command(command: &str) { 334 | let mut parts = command.split(' '); 335 | if !command.is_empty() { 336 | let mut c = Command::new(parts.next().unwrap()); 337 | c.args(parts); 338 | let _ = c.spawn(); 339 | } 340 | } 341 | 342 | fn in_rect(point: (i32, i32), rect: (i16, i16), rect_size: (i16, i16)) -> bool { 343 | if point.0 >= rect.0 as i32 344 | && point.0 <= (rect.0 + rect_size.0) as i32 345 | && point.1 >= rect.1 as i32 346 | && point.1 <= (rect.1 + rect_size.1) as i32 347 | { 348 | return true; 349 | } 350 | false 351 | } 352 | -------------------------------------------------------------------------------- /src/x11.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | use std::mem::MaybeUninit; 3 | use std::os::raw::c_char; 4 | use std::ptr::{null, null_mut}; 5 | use std::thread::sleep; 6 | use std::time::Duration; 7 | use x11_dl::{xft, xinerama, xlib}; 8 | use std::ffi::c_ulong; 9 | 10 | #[derive(Debug, Copy, Clone, PartialEq)] 11 | pub enum Action { 12 | Run, 13 | Stop, 14 | } 15 | 16 | pub struct X11Context { 17 | xlib: xlib::Xlib, 18 | xin: xinerama::Xlib, 19 | xft: xft::Xft, 20 | display: *mut xlib::_XDisplay, 21 | root: c_ulong, 22 | } 23 | 24 | pub struct TextRenderingContext { 25 | visual: *mut xlib::Visual, 26 | cmap: c_ulong, 27 | font: *mut xft::XftFont, 28 | colors: Vec, 29 | draw: *mut xft::XftDraw, 30 | } 31 | 32 | pub struct GraphicsContext { 33 | gc: *mut xlib::_XGC, 34 | window: c_ulong, 35 | } 36 | 37 | pub struct Window { 38 | window: c_ulong, 39 | } 40 | 41 | pub struct Screens { 42 | screens: *mut xinerama::XineramaScreenInfo, 43 | screens_number: i32, 44 | i: i32, 45 | } 46 | 47 | impl Iterator for Screens { 48 | type Item = xinerama::XineramaScreenInfo; 49 | 50 | fn next(&mut self) -> Option { 51 | if self.i < self.screens_number { 52 | let screen = unsafe { *(self.screens.offset(self.i as isize)) }; 53 | self.i += 1; 54 | Some(screen) 55 | } else { 56 | None 57 | } 58 | } 59 | } 60 | 61 | impl X11Context { 62 | pub fn new() -> Result { 63 | unsafe { 64 | // Load Xlib library. 65 | let xlib = xlib::Xlib::open().map_err(|_| "Failed to load XLib.")?; 66 | 67 | // load xinerama 68 | let xin = xinerama::Xlib::open().map_err(|_| "Failed to load Xinerama.")?; 69 | 70 | // load xft 71 | let xft = xft::Xft::open().map_err(|_| "Failed to load XFT")?; 72 | 73 | // Open display connection. 74 | let display = (xlib.XOpenDisplay)(null()); 75 | 76 | if display.is_null() { 77 | return Err("XOpenDisplay failed"); 78 | } 79 | 80 | let screen = (xlib.XDefaultScreen)(display); 81 | let root = (xlib.XRootWindow)(display, screen); 82 | 83 | Ok(Self { 84 | xlib, 85 | xin, 86 | xft, 87 | display, 88 | root, 89 | }) 90 | } 91 | } 92 | pub fn get_screens(&self) -> Screens { 93 | let mut screens_number = 0; 94 | let screens = unsafe { (self.xin.XineramaQueryScreens)(self.display, &mut screens_number) }; 95 | Screens { 96 | screens, 97 | screens_number, 98 | i: 0, 99 | } 100 | } 101 | pub fn get_mouse_pos(&self) -> (i32, i32) { 102 | unsafe { 103 | let (mut x, mut y) = (0, 0); 104 | (self.xlib.XQueryPointer)( 105 | self.display, 106 | self.root, 107 | &mut 0, 108 | &mut 0, 109 | &mut x, 110 | &mut y, 111 | &mut 0, 112 | &mut 0, 113 | &mut 0, 114 | ); 115 | 116 | (x, y) 117 | } 118 | } 119 | pub fn create_window(&self, pos: (i32, i32), width: u32, height: u32) -> Window { 120 | unsafe { 121 | let mut attributes: xlib::XSetWindowAttributes = MaybeUninit::zeroed().assume_init(); 122 | attributes.override_redirect = xlib::True; 123 | 124 | Window { 125 | window: (self.xlib.XCreateWindow)( 126 | self.display, 127 | self.root, 128 | pos.0, 129 | pos.1, 130 | width, 131 | height, 132 | 0, 133 | xlib::CopyFromParent, 134 | xlib::InputOutput as u32, 135 | null_mut(), 136 | xlib::CWOverrideRedirect, 137 | &mut attributes, 138 | ), 139 | } 140 | } 141 | } 142 | pub fn map_window(&self, window: &Window) { 143 | unsafe { 144 | (self.xlib.XMapRaised)(self.display, window.window); 145 | } 146 | } 147 | pub fn grab_keyboard(&self) { 148 | for _ in 0..1000 { 149 | if unsafe { 150 | (self.xlib.XGrabKeyboard)( 151 | self.display, 152 | self.root, 153 | xlib::True, 154 | xlib::GrabModeAsync, 155 | xlib::GrabModeAsync, 156 | xlib::CurrentTime, 157 | ) 158 | } == 0 159 | { 160 | // Successfully grabbed keyboard 161 | break; 162 | } else { 163 | // Try again 164 | sleep(Duration::from_millis(1)); 165 | } 166 | } 167 | } 168 | pub fn init_trc(&self, window: &Window, font: &str) -> TextRenderingContext { 169 | unsafe { 170 | let cfontname = CString::new(font).unwrap(); 171 | 172 | let screen = (self.xlib.XDefaultScreen)(self.display); 173 | let visual = (self.xlib.XDefaultVisual)(self.display, screen); 174 | let cmap = (self.xlib.XDefaultColormap)(self.display, screen); 175 | 176 | let font = (self.xft.XftFontOpenName)(self.display, screen, cfontname.as_ptr()); 177 | let colors = Vec::new(); 178 | 179 | let draw = (self.xft.XftDrawCreate)(self.display, window.window, visual, cmap); 180 | TextRenderingContext { 181 | visual, 182 | cmap, 183 | font, 184 | colors, 185 | draw, 186 | } 187 | } 188 | } 189 | /// returns the index to use as the color argument in xc::render_text 190 | pub fn add_color_to_trc(&self, trc: &mut TextRenderingContext, color: u64) -> usize { 191 | unsafe { 192 | let color = CString::new(format!("#{:06X}", color)).unwrap(); 193 | let mut xftcolor: xft::XftColor = MaybeUninit::zeroed().assume_init(); 194 | (self.xft.XftColorAllocName)( 195 | self.display, 196 | trc.visual, 197 | trc.cmap, 198 | color.as_ptr(), 199 | &mut xftcolor, 200 | ); 201 | 202 | let index = trc.colors.len(); 203 | trc.colors.push(xftcolor); 204 | index 205 | } 206 | } 207 | pub fn init_gc(&self, window: &Window) -> GraphicsContext { 208 | unsafe { 209 | let mut xgc_values: xlib::XGCValues = MaybeUninit::zeroed().assume_init(); 210 | GraphicsContext { 211 | gc: (self.xlib.XCreateGC)(self.display, window.window, 0, &mut xgc_values), 212 | window: window.window, 213 | } 214 | } 215 | } 216 | pub fn run(&self, mut handle_events: F) 217 | where 218 | F: FnMut(&Self, Option<&xlib::XEvent>) -> Action, 219 | { 220 | let mut event = MaybeUninit::::uninit(); 221 | 222 | loop { 223 | if unsafe { 224 | (self.xlib.XCheckMaskEvent)(self.display, xlib::KeyPressMask, event.as_mut_ptr()) 225 | } == 0 226 | { 227 | // no events available 228 | // execute given closure and wait for the next frame 229 | if handle_events(&self, None) == Action::Stop { 230 | break; 231 | } 232 | 233 | sleep(Duration::from_nanos(1_000_000_000 / 60)); 234 | } else { 235 | // we got some events 236 | if handle_events(&self, Some(unsafe { &event.assume_init() })) == Action::Stop { 237 | break; 238 | } 239 | } 240 | } 241 | } 242 | pub fn draw_rect( 243 | &self, 244 | gc: &GraphicsContext, 245 | color: u64, 246 | x: i32, 247 | y: i32, 248 | width: u32, 249 | height: u32, 250 | ) { 251 | unsafe { 252 | (self.xlib.XSetForeground)(self.display, gc.gc, color as c_ulong); 253 | (self.xlib.XFillRectangle)(self.display, gc.window, gc.gc, x, y, width, height); 254 | } 255 | } 256 | pub fn render_text( 257 | &self, 258 | trc: &TextRenderingContext, 259 | color: usize, 260 | x: i32, 261 | y: i32, 262 | text: &str, 263 | ) { 264 | unsafe { 265 | let ctext = CString::new(text).unwrap(); 266 | 267 | // render the text 268 | let mut col = trc.colors[color]; 269 | (self.xft.XftDrawStringUtf8)( 270 | trc.draw, 271 | &mut col, 272 | trc.font, 273 | x, 274 | y, 275 | ctext.as_ptr() as *mut u8, 276 | text.len() as i32, 277 | ); 278 | } 279 | } 280 | pub fn get_text_dimensions(&self, trc: &TextRenderingContext, text: &str) -> (u16, u16) { 281 | unsafe { 282 | // Some fonts treat a single space at the end weirdly 283 | // which makes typing a bit confusing, so we will add a '/' 284 | // to the end and then remove it's width from the total width 285 | let dot = CString::new("/").unwrap(); 286 | let owned_text = text.to_owned(); 287 | let ctext = CString::new(owned_text + "/").unwrap(); 288 | 289 | let mut total_ext = MaybeUninit::zeroed().assume_init(); 290 | (self.xft.XftTextExtentsUtf8)( 291 | self.display, 292 | trc.font, 293 | ctext.as_ptr() as *mut u8, 294 | text.len() as i32 + 1, 295 | &mut total_ext, 296 | ); 297 | let mut dot_ext = MaybeUninit::zeroed().assume_init(); 298 | (self.xft.XftTextExtentsUtf8)( 299 | self.display, 300 | trc.font, 301 | dot.as_ptr() as *mut u8, 302 | 1, 303 | &mut dot_ext, 304 | ); 305 | 306 | (total_ext.width - dot_ext.width, total_ext.height) 307 | } 308 | } 309 | pub fn keyevent_to_char(&self, mut keyevent: xlib::XKeyEvent) -> char { 310 | unsafe { 311 | let mut c_char: c_char = 0; 312 | (self.xlib.XLookupString)(&mut keyevent, &mut c_char, 1, null_mut(), null_mut()); 313 | c_char as u8 as char 314 | } 315 | } 316 | pub fn xevent_to_xkeyevent(&self, xevent: xlib::XEvent) -> Option { 317 | match xevent.get_type() { 318 | xlib::KeyPress => Some(unsafe { xevent.key }), 319 | _ => None, 320 | } 321 | } 322 | } 323 | 324 | impl Drop for X11Context { 325 | fn drop(&mut self) { 326 | unsafe { 327 | (self.xlib.XCloseDisplay)(self.display); 328 | } 329 | } 330 | } 331 | --------------------------------------------------------------------------------