diff --git a/Cargo.lock b/Cargo.lock index 0765f34..e9d65c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,6 +400,11 @@ name = "redox_syscall" version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "rust-ini" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "rusttype" version = "0.7.7" @@ -430,6 +435,11 @@ dependencies = [ "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "smithay-client-toolkit" version = "0.6.3" @@ -599,7 +609,9 @@ dependencies = [ "nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", "os_pipe 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "rcalc_lib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-ini 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusttype 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", + "shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "smithay-client-toolkit 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-client 0.23.5 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-protocols 0.23.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -667,9 +679,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" "checksum rcalc_lib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2497fc2d7578e75282922918d96de8d4790714312900d95b6d5f577168f2656e" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +"checksum rust-ini 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" "checksum rusttype 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)" = "654103d61a05074b268a107cf6581ce120f0fc0115f2610ed9dfea363bb81139" "checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" "checksum sha-1 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "23962131a91661d643c98940b20fcaffe62d776a823247be80a48fcb8b6fce68" +"checksum shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" "checksum smithay-client-toolkit 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8c23093ebdaac1ad67558c7aef153522c6b3be7e0257820909e3a25c4519e78d" "checksum stb_truetype 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "69b7df505db8e81d54ff8be4693421e5b543e08214bd8d99eb761fcb4d5668ba" "checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" diff --git a/Cargo.toml b/Cargo.toml index 4d0489d..63e4a74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,5 @@ fuzzy-matcher = "0.2" lazy_static = "1.3.0" libivy = { git = "https://github.com/kennylevinsen/libivy.git", optional = true } rcalc_lib = { version = "0.9.3", optional = true } +rust-ini = "0.13" +shlex = "0.1.1" \ No newline at end of file diff --git a/README.md b/README.md index d52c208..531ad9d 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,13 @@ To display wldash on all outputs, set `WLDASH_ALL_OUTPUTS=1`. ## How to use launcher -Assuming https://github.com/kennylevinsen/dot-desktop is installed: `eval "$(dot-desktop "$(dot-desktop | wldash)")"` +The launcher for wldash is built-in, based on https://github.com/kennylevinsen/dot-desktop. Pluggable systems may come back in the future. -wldash simply takes a new-line separated list of strings, and returns the selection. dot-desktop generates this list, and afterwards processes the selection to return the proper instantiation string. Eval then executes it. +Configuration currently happen through environment variables: + +- WLDASH_APP_OPENER: The command used to open normal applications. For sway, the recommended value is "swaymsg exec". +- WLDASH_TERM_OPENER: The command used to open terminal applications. +- WLDASH_URL_OPENER: The command used to open a URL. ## Features @@ -43,7 +47,7 @@ Using backlight sys file, only visible of backlight is detected. Scroll to adjus ### Launcher -Works like bemenu: pipe a list into wldash, and it will print out the selection made. Use https://github.com/kennylevinsen/dot-desktop if you want to launch using desktop files. +Loads desktop files from the usual locations. The launcher also accepts prefix operators to change its mode: - `!`: Arbitrary command @@ -54,3 +58,4 @@ The launcher also accepts prefix operators to change its mode: - Scaling of any kind - all sizes are currently hardcoded in pixels - Configurability - Cleanup and reorganization +- Proper line editor for the launcher \ No newline at end of file diff --git a/src/desktop.rs b/src/desktop.rs new file mode 100644 index 0000000..641435a --- /dev/null +++ b/src/desktop.rs @@ -0,0 +1,93 @@ +extern crate ini; +use ini::{Ini, ParseOption}; +use std::cmp::Ordering; +use std::env; +use std::error::Error; +use std::fs; +use std::io::Error as io_error; +use std::io::ErrorKind; + +#[derive(Clone, Debug, Eq)] +pub struct Desktop { + pub entry_type: String, + pub name: String, + pub no_display: bool, + pub hidden: bool, + pub exec: Option, + pub url: Option, + pub term: bool, +} + +impl Desktop { + fn parse(f: &str) -> Result> { + let file = Ini::load_from_file_opt(f, ParseOption{ enabled_quote: false, enabled_escape: false })?; + match file.section(Some("Desktop Entry").to_owned()) { + Some(desktop) => Ok(Desktop { + entry_type: desktop.get("Type").unwrap_or(&"".to_string()).to_string(), + name: desktop.get("Name").unwrap_or(&"".to_string()).to_string(), + term: desktop.get("Terminal").unwrap_or(&"".to_string()) == "true", + no_display: desktop.get("NoDisplay").unwrap_or(&"".to_string()) == "true", + hidden: desktop.get("Hidden").unwrap_or(&"".to_string()) == "true", + exec: desktop.get("Exec").map(|x| x.to_string()), + url: desktop.get("URL").map(|x| x.to_string()), + }), + None => Err(Box::new(io_error::new( + ErrorKind::NotFound, + "no desktop entry in file", + ))), + } + } + + fn parse_dir(d: &str) -> Result, Box> { + let mut files: Vec = Vec::new(); + for entry in fs::read_dir(d)? { + let entry = entry?; + let path = entry.path(); + + if let Ok(d) = Desktop::parse(path.to_str().unwrap()) { + files.push(d) + } + } + + Ok(files) + } +} + +impl Ord for Desktop { + fn cmp(&self, other: &Self) -> Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialOrd for Desktop { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Desktop { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +pub fn load_desktop_files() -> Vec { + let home = env::var_os("HOME").unwrap().into_string().unwrap(); + let paths = [ + "/usr/share/applications".to_string(), + "/usr/local/share/applications".to_string(), + "/var/lib/flatpak/exports/share/applications".to_string(), + format!("{}/.local/share/applications", home).to_string(), + format!("{}/.local/share/flatpak/exports/share/applications", home).to_string(), + ]; + + paths + .into_iter() + .map(|p| Desktop::parse_dir(&p)) + .filter_map(Result::ok) + .flatten() + .filter(|d| { + !d.hidden && !d.no_display && (d.entry_type == "Application" || d.entry_type == "Link") + }) + .collect() +} diff --git a/src/main.rs b/src/main.rs index 72d5509..d881104 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ mod cmd; mod color; mod draw; mod modules; +mod desktop; use app::{App, OutputMode}; use cmd::Cmd; diff --git a/src/modules/launcher.rs b/src/modules/launcher.rs index 64a2632..872b07d 100644 --- a/src/modules/launcher.rs +++ b/src/modules/launcher.rs @@ -3,20 +3,24 @@ use crate::cmd::Cmd; use crate::color::Color; use crate::draw::{Font, ROBOTO_REGULAR}; use crate::modules::module::{Input, ModuleImpl}; +use crate::desktop::{Desktop, load_desktop_files}; +use std::env; use std::cell::RefCell; use std::cmp::Ordering; -use std::io::Read; use std::sync::mpsc::Sender; +use std::process::Command; -use atty::Stream; use chrono::{DateTime, Local}; use fuzzy_matcher::skim::{fuzzy_indices, fuzzy_match}; use smithay_client_toolkit::keyboard::keysyms; pub struct Launcher { - options: Vec, - matches: Vec, + options: Vec, + term_opener: String, + app_opener: String, + url_opener: String, + matches: Vec, input: String, result: Option, offset: usize, @@ -27,38 +31,32 @@ pub struct Launcher { impl Launcher { pub fn new(listener: Sender) -> Result { - if !atty::is(Stream::Stdin) { - let mut inbuf = String::new(); - std::io::stdin().read_to_string(&mut inbuf)?; - let options = inbuf - .split("\n") - .into_iter() - .filter(|s| s.len() > 0) - .map(|s| s.to_string()) - .collect(); + let term = match env::var_os("WLDASH_TERM_OPENER") { + Some(s) => format!("{:} ", s.into_string().unwrap()).to_string(), + None => "".to_string(), + }; + let app = match env::var_os("WLDASH_APP_OPENER") { + Some(s) => format!("{:} ", s.into_string().unwrap()).to_string(), + None => "".to_string(), + }; + let url = match env::var_os("WLDASH_URL_OPENER") { + Some(s) => format!("{:} ", s.into_string().unwrap()).to_string(), + None => "xdg-open ".to_string(), + }; - Ok(Launcher { - options: options, - matches: vec![], - input: "".to_string(), - result: None, - offset: 0, - font: RefCell::new(Font::new(&ROBOTO_REGULAR, 32.0)), - dirty: true, - tx: listener, - }) - } else { - Ok(Launcher { - options: vec![], - matches: vec![], - input: "".to_string(), - result: None, - offset: 0, - font: RefCell::new(Font::new(&ROBOTO_REGULAR, 32.0)), - dirty: true, - tx: listener, - }) - } + Ok(Launcher { + options: load_desktop_files(), + term_opener: term, + app_opener: app, + url_opener: url, + matches: vec![], + input: "".to_string(), + result: None, + offset: 0, + font: RefCell::new(Font::new(&ROBOTO_REGULAR, 32.0)), + dirty: true, + tx: listener, + }) } fn draw_launcher( @@ -94,9 +92,9 @@ impl Launcher { }; let size = if idx == self.offset && self.input.len() > 0 { let (_, indices) = - fuzzy_indices(&m.to_lowercase(), &self.input.to_lowercase()).unwrap(); - let mut colors = Vec::with_capacity(m.len()); - for pos in 0..m.len() { + fuzzy_indices(&m.name.to_lowercase(), &self.input.to_lowercase()).unwrap(); + let mut colors = Vec::with_capacity(m.name.len()); + for pos in 0..m.name.len() { if indices.contains(&pos) { colors.push(Color::new(1.0, 1.0, 1.0, 1.0)); } else { @@ -105,13 +103,13 @@ impl Launcher { } self.font .borrow_mut() - .auto_draw_text_individual_colors(&mut b, bg, &colors, &m)? + .auto_draw_text_individual_colors(&mut b, bg, &colors, &m.name)? } else { self.font.borrow_mut().auto_draw_text( &mut b, bg, &Color::new(0.5, 0.5, 0.5, 1.0), - m, + &m.name, )? }; @@ -297,22 +295,22 @@ impl ModuleImpl for Launcher { .iter() .map(|x| { ( - fuzzy_match(&x.to_lowercase(), &self.input.to_lowercase()), + fuzzy_match(&x.name.to_lowercase(), &self.input.to_lowercase()), x, ) }) .filter(|(x, _)| x.is_some()) - .map(|(x, y)| (x.unwrap(), y.to_string())) - .collect::>(); + .map(|(x, y)| (x.unwrap(), y.clone())) + .collect::>(); m.sort_by(|(x1, y1), (x2, y2)| { if x1 > x2 { Ordering::Less } else if x1 < x2 { Ordering::Greater - } else if y1.len() < y2.len() { + } else if y1.name.len() < y2.name.len() { Ordering::Less - } else if y1.len() > y2.len() { + } else if y1.name.len() > y2.name.len() { Ordering::Greater } else { y1.cmp(y2) @@ -350,13 +348,53 @@ impl ModuleImpl for Launcher { None => (), }, Some('!') => { - println!("{}", self.input.chars().skip(1).collect::()); + let _ = Command::new("sh") + .arg("-c") + .arg(self.input.chars().skip(1).collect::()) + .spawn(); self.tx.send(Cmd::Exit).unwrap(); } _ => { if self.matches.len() > self.offset { - println!("{}", self.matches[self.offset]); - self.tx.send(Cmd::Exit).unwrap(); + let d = &self.matches[self.offset]; + if let Some(exec) = &d.exec { + let exec = exec + .replace("%f", "") + .replace("%F", "") + .replace("%u", "") + .replace("%U", ""); + let prefix = if d.term { + &self.term_opener + } else { + &self.app_opener + }; + + let lexed = if prefix.len() > 0 { + let mut prefix = shlex::split(prefix).unwrap(); + prefix.push(exec); + prefix + } else { + shlex::split(&exec).unwrap() + }; + if lexed.len() > 0 { + let _ = Command::new(lexed[0].clone()) + .args(&lexed[1..]) + .spawn(); + self.tx.send(Cmd::Exit).unwrap(); + } + } + if let Some(url) = &d.url { + if self.url_opener.len() > 0 { + let mut lexed = shlex::split(&self.url_opener).unwrap(); + lexed.push(url.to_string()); + if lexed.len() > 0 { + let _ = Command::new(lexed[0].clone()) + .args(&lexed[1..]) + .spawn(); + self.tx.send(Cmd::Exit).unwrap(); + } + } + } } } };