├── .gitignore ├── LICENSE ├── README.md ├── config.mk ├── makefile └── src ├── Cargo.toml ├── README.md ├── build ├── CargoSource.toml └── build.rs ├── config ├── Cargo.toml ├── build.rs └── src │ ├── checkdeps.sh │ ├── config.rs │ ├── list_plugins.rs │ └── util.rs ├── dmenu ├── README.md ├── additional_bindings.rs ├── clapflags.rs ├── cli_base.yml ├── config.rs ├── drw.rs ├── fnt.rs ├── globals.rs ├── init.rs ├── item.rs ├── main.rs ├── plugin_entry.rs ├── result.rs ├── run.rs ├── setup.rs └── util.rs ├── headers ├── Cargo.toml ├── README.md ├── build.rs └── src │ ├── fontconfig.h │ ├── main.rs │ ├── xinerama.h │ └── xlib.h ├── man ├── Cargo.toml ├── README.md ├── dmenu.1 └── src │ ├── lib.rs │ ├── see_also.1 │ ├── stest.1 │ └── usage.1 ├── plugins ├── README.md ├── autoselect │ ├── main.rs │ └── plugin.yml ├── calc │ ├── build.sh │ ├── deps.toml │ ├── main.rs │ └── plugin.yml ├── fuzzy │ ├── deps.toml │ ├── main.rs │ └── plugin.yml ├── lookup │ ├── build.sh │ ├── deps.toml │ ├── engines.rs │ ├── main.rs │ └── plugin.yml ├── maxlength │ ├── main.rs │ └── plugin.yml ├── password │ ├── main.rs │ └── plugin.yml └── spellcheck │ ├── build.sh │ ├── deps.toml │ ├── main.rs │ └── plugin.yml ├── sh ├── README.md ├── dmenu_path └── dmenu_run └── stest ├── Cargo.lock ├── Cargo.toml ├── README.md ├── src ├── config.rs ├── file.rs ├── lib.rs ├── main.rs └── semigroup.rs └── tests ├── integration_tests.rs ├── set-up-block-special-file ├── set-up-character-special-file ├── set-up-directory ├── set-up-directory-with-contents ├── set-up-executable-file ├── set-up-file ├── set-up-file-with-set-group-id ├── set-up-file-with-set-user-id ├── set-up-hidden-file ├── set-up-nonempty-file ├── set-up-nonexisting-file ├── set-up-pipe-file ├── set-up-readable-file ├── set-up-symbolic-link └── set-up-writable-file /.gitignore: -------------------------------------------------------------------------------- 1 | # editor files 2 | *~ 3 | \#*\# 4 | .\#* 5 | 6 | # Generated by Cargo 7 | # will have compiled files and executables 8 | **/target/ 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | vgcore.* 14 | massif* 15 | dmenu-* 16 | 17 | # m4 stuff 18 | src/build/Cargo.toml 19 | src/Cargo.lock 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dmenu-rs - dynamic menu 2 | dmenu is an efficient dynamic menu for X. 3 | dmenu-rs is a 1:1 port of dmenu rewritten in Rust. It looks, feels, and 4 | runs pixel-for-pixel exactly the same. 5 | It also has plugin support for easy modification. 6 | 7 | ## State of the project 8 | The master branch is a stable, feature complete product. It it not unmaintained; it's finished. 9 | 10 | There is a __small__ and ever-shrinking chance that this will be updated or overhauled in the distant future. It is much more likely that I will write a spiritual successor from scratch, but that won't happen until at least 2023. 11 | 12 | ## Why Rust? 13 | ### Inspiration 14 | This project started with [`dmenu-calc`](https://github.com/sumnerevans/menu-calc). 15 | Initially, I wanted much more function than what is provided by `bc`. However, I 16 | found the bottleneck to be a lack of functionality and modability in `dmenu(1)` 17 | itself. So, the choice I had was to either mod `dmenu(1)` or rewrite it. Because 18 | dmenu source is horrendously annoying to read, I decided to rewrite it in a 19 | language which lends itself to writing code that is easier to modify. There are 20 | other languages for this, but I like Rust. 21 | ### Improvements 22 | As mentioned earlier, `dmenu-rs` runs exactly the same as `dmenu`. However, there 23 | are some significant performance enhancements under the hood. The most impactful 24 | is memory usage: `dmenu-rs` uses 21.65% less memory[1], while managing it much 25 | more safely **without** any performance impacts. The other large improvement is 26 | plugin support; read below. 27 | 28 | ## Plugins 29 | dmenu-rs leverages rust crates `overrider` and `proc_use` to provide an easy to 30 | write and powerful plugin system. The end-result are plugins which are dead-simple 31 | to enable. 32 | For a list of available plugins and more info on 33 | enabling plugins, run `make plugins`. 34 | For more info on developing plugins, read the [plugin guide](src/plugins/README.md). 35 | 36 | ## Requirements 37 | - Xlib header files 38 | - Cargo / rustc 39 | - A working C compiler 40 | 41 | ## Installation 42 | ### Standalone 43 | Edit config.mk to match your local setup (dmenu is installed into 44 | the /usr/local namespace by default). 45 | 46 | Afterwards enter the following command to build dmenu: 47 | ```make``` 48 | Then, to install (if necessary as root): 49 | ```make install``` 50 | ### Distros 51 | dmenu-rs is available from the following sources: 52 | - [Arch AUR - stable branch](https://aur.archlinux.org/packages/dmenu-rs/) 53 | - [Arch AUR - development branch](https://aur.archlinux.org/packages/dmenu-rs-git/) 54 | 55 | If you'd like for this to be available on another distro, raise an issue 56 | or submit a pull request with a README change pointing to the released 57 | package. 58 | 59 | ## Running dmenu 60 | See the man page for details. For a quick test, run: 61 | ```make test``` 62 | 63 |

64 | [1]: According to `valgrind(1)` 65 | -------------------------------------------------------------------------------- /config.mk: -------------------------------------------------------------------------------- 1 | VERSION = 5.5.3 2 | 3 | # paths 4 | PREFIX = /usr/local 5 | MANPREFIX = $(PREFIX)/share/man 6 | 7 | # Xinerama, set to false/empty if you don't want it 8 | XINERAMA=true 9 | 10 | # compiler and linker for non-rust files, blank for system default (cc) 11 | CC = 12 | 13 | # additional flags to be passed to rustc 14 | RUSTFLAGS = 15 | 16 | # additional flags to be passed to cargo 17 | # only used on the final build 18 | CARGOFLAGS = 19 | 20 | # space seperated list of plugins to be compiled in 21 | # run `make plugins` to see a list of available plugins 22 | PLUGINS = 23 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # dmenu-rs - dynamic menu 2 | # See LICENSE file for copyright and license details. 3 | 4 | include config.mk 5 | 6 | ifeq ($(XINERAMA),true) 7 | XINERAMA_FLAGS = --all-features # idk if there will ever be a workaround 8 | endif 9 | 10 | ifeq ($(CC),) 11 | CC = cc 12 | endif 13 | 14 | export CARGOFLAGS 15 | export RUSTFLAGS 16 | export PLUGINS 17 | export VERSION 18 | export XINERAMA 19 | export depcheck 20 | export CC 21 | 22 | all: options dmenu stest 23 | 24 | options: 25 | @echo "dmenu ($(VERSION)) build options:" 26 | @echo "CC = $(CC)" 27 | @echo "RUSTFLAGS = $(RUSTFLAGS)" 28 | @echo "PLUGINS = $(PLUGINS)" 29 | 30 | config: scaffold 31 | cd src && cargo run --release -p config --bin config 32 | $(MAKE) m4 33 | 34 | dmenu: config 35 | cd src && cargo run --release -p headers 36 | cd src && cargo build -p dmenu-build --release $(XINERAMA_FLAGS) $(CARGOFLAGS) 37 | cp src/target/release/dmenu target/ 38 | 39 | man: config 40 | man target/dmenu.1 41 | 42 | test: all 43 | cd src && cargo test 44 | seq 1 100 | target/dmenu $(ARGS) 45 | 46 | debug: config 47 | cd src && cargo build -p dmenu-build $(XINERAMA_FLAGS) $(CARGOFLAGS) 48 | cp src/target/debug/dmenu target 49 | seq 1 100 | target/dmenu $(ARGS) 50 | 51 | plugins: 52 | cd src && cargo run --release -p config --bin list-plugins 53 | 54 | stest: 55 | cd src && cargo build -p stest --release $(XINERAMA_FLAGS) $(CARGOFLAGS) 56 | cp src/target/release/stest target/ 57 | cp src/man/src/stest.1 target/ 58 | 59 | scaffold: 60 | mkdir -p target 61 | mkdir -p target/build 62 | touch target/build/deps.toml 63 | $(MAKE) m4 # second round will finish deps 64 | 65 | m4: 66 | m4 src/build/CargoSource.toml > target/build/Cargo.toml 67 | test -f src/build/Cargo.toml || cp target/build/Cargo.toml src/build/Cargo.toml 68 | cmp -s -- target/build/Cargo.toml src/build/Cargo.toml || cp target/build/Cargo.toml src/build/Cargo.toml 69 | 70 | clean: scaffold 71 | cd src && cargo clean -p config -p dmenu-build -p headers 72 | rm -rf src/target 73 | rm -f vgcore* massif* src/build/Cargo.toml 74 | rm -rf target 75 | rm -rf dmenu-* # distribution files 76 | rm -f src/Cargo.lock 77 | 78 | fmt: config 79 | cd src && cargo fmt 80 | 81 | version: 82 | @echo -n "${VERSION}" 83 | 84 | dist: 85 | mkdir -p dmenu-$(VERSION) 86 | cp -r LICENSE README.md makefile config.mk src dmenu-$(VERSION) 87 | tar -cf dmenu-$(VERSION).tar dmenu-$(VERSION) 88 | gzip dmenu-$(VERSION).tar 89 | rm -rf dmenu-$(VERSION) 90 | 91 | # may need sudo 92 | install: all 93 | mkdir -p $(DESTDIR)$(PREFIX)/bin 94 | cp -f target/dmenu src/sh/dmenu_path src/sh/dmenu_run target/stest $(DESTDIR)$(PREFIX)/bin/ 95 | chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu 96 | chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu_path 97 | chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu_run 98 | chmod 755 $(DESTDIR)$(PREFIX)/bin/stest 99 | mkdir -p $(DESTDIR)$(MANPREFIX)/man1 100 | cp target/dmenu.1 $(DESTDIR)$(MANPREFIX)/man1/dmenu.1 101 | sed "s/VERSION/$(VERSION)/g" < target/stest.1 > $(DESTDIR)$(MANPREFIX)/man1/stest.1 102 | chmod 644 $(DESTDIR)$(MANPREFIX)/man1/dmenu.1 103 | chmod 644 $(DESTDIR)$(MANPREFIX)/man1/stest.1 104 | 105 | uninstall: 106 | rm -f $(DESTDIR)$(PREFIX)/bin/dmenu\ 107 | $(DESTDIR)$(PREFIX)/bin/dmenu_path\ 108 | $(DESTDIR)$(PREFIX)/bin/dmenu_run\ 109 | $(DESTDIR)$(PREFIX)/bin/stest\ 110 | $(DESTDIR)$(MANPREFIX)/man1/dmenu.1\ 111 | $(DESTDIR)$(MANPREFIX)/man1/stest.1 112 | -------------------------------------------------------------------------------- /src/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "build", 5 | "config", 6 | "headers", 7 | "stest" 8 | ] 9 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # src 2 | 3 | Source code directory. The folders contained here, as well as their 4 | functions are as follows: 5 | - dmenu 6 | The main `dmenu(1)` program 7 | - plugins 8 | Plugin files that can be conditionally compiled in 9 | - stest 10 | The `stest(1)` program 11 | - headers 12 | C headers used by bindgen to create Rust bindings for external libraries 13 | - man 14 | Man page contents 15 | - sh 16 | Shell scripts for drop-in compatibility with dmenu 17 | -------------------------------------------------------------------------------- /src/build/CargoSource.toml: -------------------------------------------------------------------------------- 1 | # Cargo.toml is generated by m4, see makefile 2 | 3 | [package] 4 | name = "dmenu-build" 5 | version = "0.0.0" 6 | authors = ["Shizcow "] 7 | edition = "2018" 8 | links = "X11" 9 | build = "build.rs" 10 | default-run="dmenu" 11 | 12 | [[bin]] 13 | name = "dmenu" 14 | path = "../dmenu/main.rs" 15 | 16 | [dependencies] 17 | clap = { version = "2.33.3", features = ["yaml"]} 18 | clipboard = "0.5" 19 | itertools = "0.9" 20 | lazy_static = "1.4.0" 21 | libc = "0.2.69" 22 | overrider = "^0.7.0" 23 | pledge = "0.4.0" 24 | regex = "1.3.7" 25 | rustc_version_runtime = "0.2.0" 26 | servo-fontconfig = "0.5.0" 27 | unicode-segmentation = "1.6.0" 28 | yaml-rust = "^0.3" # clap uses yaml-rust too, so Cargo will figure out the proper version 29 | x11 = "2.18.2" 30 | include(target/build/deps.toml) #m4 31 | 32 | [build-dependencies] 33 | overrider_build = "^0.7.0" 34 | proc_use = "^0.2.1" 35 | yaml-rust = "^0.3" 36 | termcolor = "1.1" 37 | 38 | [features] 39 | Xinerama = [] 40 | default = ["Xinerama"] -------------------------------------------------------------------------------- /src/build/build.rs: -------------------------------------------------------------------------------- 1 | use proc_use::UseBuilder; 2 | use std::env; 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::path::PathBuf; 6 | 7 | #[path = "../config/src/util.rs"] 8 | mod util; 9 | 10 | fn main() { 11 | let build_path_str = "../../target/build"; 12 | 13 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 14 | let build_path = PathBuf::from(build_path_str); 15 | println!("cargo:rustc-env=BUILD_DIR={}", build_path_str); 16 | 17 | println!( 18 | "cargo:rerun-if-changed={}", 19 | build_path 20 | .join("watch_files") 21 | .canonicalize() 22 | .unwrap() 23 | .display() 24 | .to_string() 25 | ); 26 | println!("cargo:rerun-if-env-changed=PLUGINS"); 27 | 28 | // grab the list of plugins and aliases 29 | let mut plugin_file = File::open(build_path.join("watch_files")).unwrap(); 30 | let mut plugin_str = String::new(); 31 | if let Err(err) = plugin_file.read_to_string(&mut plugin_str) { 32 | panic!("Could not read plugin file {}", err); 33 | } 34 | 35 | let mut lines = plugin_str.split("\n"); 36 | let mut watch_globs = Vec::new(); 37 | 38 | while let (Some(path), Some(alias), Some("")) = (lines.next(), lines.next(), lines.next()) { 39 | watch_globs.push((path, alias)); 40 | } 41 | 42 | // finalize overrider and proc_use initilization 43 | let mut usebuilder = UseBuilder::new(); 44 | let mut overrider_watch = vec!["../dmenu/plugin_entry.rs"]; 45 | for file in &watch_globs { 46 | overrider_watch.push(&file.0); 47 | usebuilder.mod_glob_alias(&file.0, &file.1); 48 | } 49 | 50 | // Write overrider and proc_use 51 | overrider_build::watch_files(overrider_watch); 52 | usebuilder.write_to_file_all(out_path.join("proc_mod_plugin.rs")); 53 | 54 | // if plugin files are changed without modifying anything else, 55 | // sometimes overrider needs to be ran again 56 | let plugins = util::get_selected_plugin_list(); 57 | for plugin in plugins { 58 | let mut plugin_yaml = 59 | util::get_yaml(&format!("../plugins/{}/plugin.yml", plugin), Some(&plugin)); 60 | println!( 61 | "cargo:rerun-if-changed=../plugins/{}/{}", 62 | plugin, 63 | util::get_yaml_top_level(&mut plugin_yaml, "entry").unwrap() 64 | ); 65 | } 66 | 67 | // link libs 68 | if cfg!(feature = "Xinerama") { 69 | println!("cargo:rustc-link-lib=Xinerama"); 70 | } 71 | println!("cargo:rustc-link-lib=X11"); 72 | println!("cargo:rustc-link-lib=Xft"); 73 | } 74 | -------------------------------------------------------------------------------- /src/config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "config" 3 | version = "0.0.0" 4 | authors = ["Shizcow "] 5 | edition = "2018" 6 | 7 | [[bin]] 8 | name = "config" 9 | path = "src/config.rs" 10 | 11 | [[bin]] 12 | name = "list-plugins" 13 | path = "src/list_plugins.rs" 14 | 15 | [dependencies] 16 | glob = "0.3.0" 17 | itertools = "0.10.5" 18 | man_dmenu = { path = "../man" } 19 | prettytable-rs = "0.10.0" 20 | termcolor = "1.1" 21 | yaml-rust = "0.4.5" -------------------------------------------------------------------------------- /src/config/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | // bindgen is pretty slow, so we add a layer of indirection, 4 | // making sure it's only ran when needed. build.rs has great 5 | // support for that, so here it is 6 | fn main() { 7 | let mut target_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); 8 | target_path.pop(); 9 | target_path.pop(); 10 | target_path = target_path.join("target"); 11 | let build_path = target_path.join("build"); 12 | 13 | println!( 14 | "cargo:rustc-env=BUILD_TARGET_PATH={}", 15 | target_path.display().to_string() 16 | ); 17 | println!( 18 | "cargo:rustc-env=BUILD_PATH={}", 19 | build_path.display().to_string() 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/config/src/checkdeps.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | FAILED=0 4 | 5 | # we can assume sh is installed or else we wouldn't be here 6 | 7 | printf "Checking for $CC... " 8 | if command -v $CC &> /dev/null 9 | then 10 | echo "yes" 11 | else 12 | echo "no" 13 | >&2 echo "Build-time dependency $CC not installed. Install it or change the C compiler used in config.mk" 14 | FAILED=1 15 | fi 16 | 17 | printf "Checking for X11 headers... " 18 | if $CC -c ../../headers/src/xlib.h -o tmp.gch; 19 | then 20 | rm tmp.gch 21 | echo "yes" 22 | else 23 | echo "no" 24 | >&2 echo "Build-time dependency is not present. Install the xorg development packages" 25 | rm -f tmp.gch 26 | FAILED=1 27 | fi 28 | 29 | printf "Checking for fontconfig headers... " 30 | if $CC -c ../../headers/src/fontconfig.h -o tmp.gch; 31 | then 32 | rm tmp.gch 33 | echo "yes" 34 | else 35 | echo "no" 36 | >&2 echo "Build-time dependency is not present. Install fontconfig packages" 37 | rm -f tmp.gch 38 | FAILED=1 39 | fi 40 | 41 | if [ "$XINERAMA" = "true" ]; then 42 | printf "Checking for xinerama headers... " 43 | if $CC -c ../../headers/src/xinerama.h -o tmp.gch; 44 | then 45 | rm tmp.gch 46 | echo "yes" 47 | else 48 | echo "no" 49 | >&2 echo "Build-time dependency is not present. Install xinerama package(s) or disable the feature in config.mk" 50 | rm -f tmp.gch 51 | FAILED=1 52 | fi 53 | fi 54 | 55 | if [ $FAILED != 0 ]; then 56 | exit 1 57 | fi 58 | -------------------------------------------------------------------------------- /src/config/src/config.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use man_dmenu::*; 3 | use std::env; 4 | use std::fs::File; 5 | use std::io::{Read, Write}; 6 | use std::path::PathBuf; 7 | use yaml_rust::{yaml, Yaml, YamlEmitter}; 8 | 9 | mod util; 10 | use crate::util::*; 11 | 12 | fn main() { 13 | let target_path = PathBuf::from(env!("BUILD_TARGET_PATH")); 14 | let build_path = PathBuf::from(env!("BUILD_PATH")); 15 | let mut build_failed = false; 16 | 17 | // Check for dependencies 18 | if run_build_command( 19 | &format!("sh checkdeps.sh"), 20 | &"config/src/", 21 | &format!("dependency check"), 22 | ) 23 | .unwrap() 24 | { 25 | build_failed = true; 26 | } 27 | 28 | // On to plugins 29 | // First, figure out what plugins we are using 30 | let plugins = get_selected_plugin_list(); 31 | 32 | // Next, set up the following for plugin files: 33 | // 1) clap command line yaml file 34 | // 2) proc_use import files 35 | // 3) overrider watch files 36 | // 4) Cargo.toml plugin dependencies 37 | // 5) manpage (used later) 38 | let mut watch_globs = Vec::new(); 39 | let mut deps_vec = Vec::new(); 40 | let mut manpage = Manpage::new("dmenu", &env::var("VERSION").unwrap(), 1); 41 | 42 | // prepare to edit cli_base args 43 | let mut yaml = get_yaml("dmenu/cli_base.yml", None); 44 | let yaml_args: &mut Vec = get_yaml_args(&mut yaml).unwrap(); 45 | 46 | // For every plugin, check if it has arguements. If so, add them to clap and overrider 47 | // While we're here, set proc_use to watch the plugin entry points 48 | for plugin in plugins { 49 | let mut plugin_yaml = get_yaml(&format!("plugins/{}/plugin.yml", plugin), Some(&plugin)); 50 | 51 | if let Some(plugin_yaml_args) = get_yaml_args(&mut plugin_yaml) { 52 | yaml_args.append(plugin_yaml_args); 53 | } 54 | 55 | watch_globs.push(( 56 | format!( 57 | "../plugins/{}/{}", 58 | plugin, 59 | get_yaml_top_level(&mut plugin_yaml, "entry") 60 | .expect("No args found in yaml object") 61 | ), //relative to other build script 62 | format!("plugin_{}", plugin), 63 | )); 64 | 65 | if let Some(deps_name) = get_yaml_top_level(&mut plugin_yaml, "cargo_dependencies") { 66 | let deps_file = format!("plugins/{}/{}", plugin, deps_name); 67 | let mut deps_base = File::open(deps_file).unwrap(); 68 | let mut deps_read_str = String::new(); 69 | if let Err(err) = deps_base.read_to_string(&mut deps_read_str) { 70 | panic!("Could not read dependency base file {}", err); 71 | } 72 | deps_vec.push(deps_read_str); 73 | } 74 | 75 | if let Some(build_command) = get_yaml_top_level(&mut plugin_yaml, "build") { 76 | if run_build_command( 77 | build_command, 78 | &format!("plugins/{}/", plugin), 79 | &format!("plugin {}", plugin), 80 | ) 81 | .unwrap() 82 | { 83 | build_failed = true; 84 | } 85 | } 86 | 87 | if let Some(desc) = get_yaml_top_level(&mut plugin_yaml, "about") { 88 | manpage.plugin(plugin, desc.to_string()); 89 | } 90 | } 91 | if build_failed { 92 | std::process::exit(1); 93 | } 94 | 95 | // Write additional dependency list 96 | let mut deps_finished_file = File::create(build_path.join("deps.toml")).unwrap(); 97 | if let Err(err) = deps_finished_file.write_all(deps_vec.join("\n").as_bytes()) { 98 | panic!( 99 | "Could not write generated dependency file to OUT_DIR: {}", 100 | err 101 | ); 102 | } 103 | 104 | // Now that cli is built, generate manpage 105 | manpage 106 | .desc_short("dynamic menu") 107 | .description( 108 | "dmenu", 109 | "is a dynamic menu for X, which reads a list of newline\\-separated \ 110 | items from stdin. When the user selects an item and presses \ 111 | Return, their choice is printed to stdout and dmenu terminates. \ 112 | Entering text will narrow the items to those matching the tokens \ 113 | in the input.", 114 | ) 115 | .description( 116 | "dmenu_run", 117 | "is a script used by\n\ 118 | .IR dwm (1)\n\ 119 | which lists programs in the user's $PATH and runs the result in \ 120 | their $SHELL. It is kept here for compatibility; j4-dmenu-desktop \ 121 | is the recommended alternative.", 122 | ) 123 | .build( 124 | "This dmenu is dmenu-rs, a rewrite of dmenu in rust. It's faster and more \ 125 | flexible.", 126 | ); 127 | 128 | for arg in yaml_args { 129 | let hash = match arg { 130 | Yaml::Hash(hash) => hash, 131 | _ => panic!("yaml arg must be hash"), 132 | }; 133 | let keys: Vec<_> = hash.keys().cloned().collect(); 134 | let mut short = None; 135 | let mut long = None; 136 | let mut help = None; 137 | let mut inputs = Vec::new(); 138 | match hash.get(&keys[0]) { 139 | Some(Yaml::Hash(hash)) => { 140 | let keys: Vec<_> = hash.keys().cloned().collect(); 141 | for key in &keys { 142 | let keyname = match &key { 143 | Yaml::String(string) => string, 144 | _ => panic!("yaml arg name must be string"), 145 | }; 146 | let keyvalue = match hash.get(key) { 147 | Some(Yaml::String(string)) => string, 148 | _ => continue, 149 | }; 150 | if keyname == "long_help" { 151 | help = Some(keyvalue); 152 | } else if keyname == "help" && help.is_none() { 153 | help = Some(keyvalue); 154 | } else if keyname == "short" { 155 | short = Some(keyvalue); 156 | } else if keyname == "long" { 157 | long = Some(keyvalue); 158 | } else if keyname == "value_name" { 159 | inputs = vec![keyvalue.clone()]; 160 | } else if keyname == "value_names" { 161 | inputs = keyvalue.split(" ").map(|c| c.to_string()).collect(); 162 | } 163 | } 164 | } 165 | _ => panic!("Invalid yaml format"), 166 | } 167 | if short.is_some() || long.is_some() { 168 | manpage.arg( 169 | short.map(|s| s.chars().nth(0).unwrap()), 170 | long.map(|s| s.to_string()), 171 | inputs, 172 | help.expect("yaml: help must be provided").to_string(), 173 | ); 174 | } 175 | } 176 | 177 | manpage.write_to_file(target_path.join("dmenu.1")); 178 | 179 | // Dump yaml, clap will parse this later. 180 | let mut yaml_out = String::new(); 181 | let mut emitter = YamlEmitter::new(&mut yaml_out); 182 | emitter.dump(&mut yaml).unwrap(); 183 | write_to_file_protected(build_path.join("cli.yml"), yaml_out); 184 | 185 | // dump plugin watch files to target/build so src/build/build.rs can pick up on them 186 | let watch_indicator_string = watch_globs 187 | .into_iter() 188 | .map(|(glob, alias)| format!("{}\n{}\n", glob, alias)) 189 | .join("\n"); 190 | write_to_file_protected(build_path.join("watch_files"), watch_indicator_string); 191 | } 192 | 193 | // This is done not to trigger final build script if not needed -- speeds up recompile 194 | fn write_to_file_protected(path: PathBuf, string: String) { 195 | let file_current = std::fs::read_to_string(path.clone()); 196 | if file_current.is_err() || file_current.unwrap() != string { 197 | let mut cli_finished_file = File::create(path).unwrap(); 198 | if let Err(err) = cli_finished_file.write_all(string.as_bytes()) { 199 | panic!("Could not write generated file to OUT_DIR: {}", err); 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/config/src/list_plugins.rs: -------------------------------------------------------------------------------- 1 | use glob::glob; 2 | use prettytable::{color, format::Alignment, Attr, Cell, Row, Table}; 3 | use std::path::PathBuf; 4 | 5 | mod util; 6 | use util::*; 7 | 8 | // list all the available plugins, along with their descriptions, args, etc 9 | fn main() { 10 | let selected_plugins = get_selected_plugin_list(); 11 | 12 | let mut table_head = Table::new(); 13 | table_head.add_row(Row::new(vec![Cell::new("Available Plugins") 14 | .with_style(Attr::Bold) 15 | .with_style(Attr::ForegroundColor(color::BLUE))])); 16 | table_head.printstd(); 17 | 18 | let mut table = Table::new(); 19 | 20 | table.set_titles(Row::new(vec![ 21 | Cell::new_align("Sel", Alignment::CENTER).with_style(Attr::Bold), 22 | Cell::new_align("Name", Alignment::CENTER).with_style(Attr::Bold), 23 | Cell::new_align("Description", Alignment::CENTER).with_style(Attr::Bold), 24 | ])); 25 | 26 | let mut plugin_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); 27 | plugin_path.pop(); 28 | plugin_path = plugin_path.join("plugins").join("*").join("plugin.yml"); 29 | 30 | for entry in glob(&plugin_path.display().to_string()).expect("Failed to read glob pattern") { 31 | match entry { 32 | Err(e) => println!("{:?}", e), 33 | Ok(path) => { 34 | let plugin_name = path 35 | .parent() 36 | .unwrap() 37 | .file_name() 38 | .unwrap() 39 | .to_str() 40 | .unwrap() 41 | .to_string(); 42 | let mut plugin_yaml = get_yaml(&path.display().to_string(), None); 43 | let about = get_yaml_top_level(&mut plugin_yaml, "about").unwrap(); 44 | 45 | table.add_row(Row::new(vec![ 46 | Cell::new_align( 47 | if selected_plugins.contains(&plugin_name) { 48 | "X" 49 | } else { 50 | " " 51 | }, 52 | Alignment::CENTER, 53 | ), 54 | Cell::new(&plugin_name) 55 | .with_style(Attr::Bold) 56 | .with_style(Attr::ForegroundColor(color::GREEN)), 57 | Cell::new(about).with_style(Attr::Italic(true)), 58 | ])); 59 | } 60 | } 61 | } 62 | 63 | table.printstd(); 64 | 65 | let mut table_footer = Table::new(); 66 | table_footer.add_row(Row::new(vec![Cell::new( 67 | "To enable plugins, read the PLUGINS section in config.mk", 68 | ) 69 | .with_style(Attr::Bold) 70 | .with_style(Attr::ForegroundColor(color::BLUE))])); 71 | table_footer.printstd(); 72 | } 73 | -------------------------------------------------------------------------------- /src/config/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::{Error, Read}; 4 | use std::process::Command; 5 | use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; 6 | use yaml_rust::{yaml, Yaml, YamlLoader}; 7 | 8 | #[allow(unused)] 9 | pub fn run_build_command(build_command: &str, dir: &str, heading: &str) -> Result { 10 | let mut failed = false; 11 | let mut stdout = StandardStream::stdout(ColorChoice::Always); 12 | let mut command = Command::new("sh"); 13 | command.current_dir(dir); 14 | let output = command.arg("-c").arg(build_command).output()?; 15 | let stdout_cmd = String::from_utf8_lossy(&output.stdout); 16 | let stderr_cmd = String::from_utf8_lossy(&output.stderr); 17 | let stdout_ref = stdout_cmd.trim_end(); 18 | let stderr_ref = stderr_cmd.trim_end(); 19 | if stdout_ref.len() > 0 { 20 | println!("{}", stdout_ref); 21 | } 22 | if stderr_ref.len() > 0 { 23 | stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?; 24 | println!("{}", stderr_ref); 25 | } 26 | if output.status.success() { 27 | stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?; 28 | print!("PASS"); 29 | } else { 30 | stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?; 31 | print!("FAIL"); 32 | failed = true; 33 | } 34 | stdout.set_color(ColorSpec::new().set_bold(true))?; 35 | print!(" Running build command for {}", heading); 36 | stdout.set_color(&ColorSpec::new())?; 37 | println!(""); // make sure colors are flushed 38 | Ok(failed) 39 | } 40 | 41 | pub fn get_selected_plugin_list() -> Vec { 42 | let plugins_str = env::var("PLUGINS").expect( 43 | "\n\n\ 44 | ┌─────────────────────────────────┐\n\ 45 | │ BUILD FAILED │\n\ 46 | │PLUGINS environment variable not found. │\n\ 47 | │Help: You should call make instead of cargo│\n\ 48 | └─────────────────────────────────┘\ 49 | \n\n", 50 | ); 51 | if plugins_str.len() > 0 { 52 | plugins_str 53 | .trim() 54 | .split(" ") 55 | .map(|s| s.to_string()) 56 | .collect() 57 | } else { 58 | Vec::new() 59 | } 60 | } 61 | 62 | pub fn get_yaml(file: &str, plugin: Option<&str>) -> Yaml { 63 | match File::open(file) { 64 | Ok(mut base) => { 65 | let mut yaml_str = String::new(); 66 | if let Err(err) = base.read_to_string(&mut yaml_str) { 67 | panic!("Could not read yaml base file {}", err); 68 | } 69 | yaml_str = yaml_str.replace("$VERSION", &env!("VERSION")); 70 | YamlLoader::load_from_str(&yaml_str).unwrap().swap_remove(0) 71 | } 72 | Err(err) => { 73 | if let Some(plugin_name) = plugin { 74 | let mut stdout = StandardStream::stdout(ColorChoice::Always); 75 | stdout 76 | .set_color(ColorSpec::new().set_fg(Some(Color::Red))) 77 | .expect("Could not get stdout"); 78 | println!( 79 | "Could not find plugin '{}'. Perhaps it's invalid? \ 80 | Double check config.mk", 81 | plugin_name 82 | ); 83 | stdout 84 | .set_color(&ColorSpec::new()) 85 | .expect("Could not get stdout"); 86 | println!(""); // make sure colors are flushed 87 | std::process::exit(1); 88 | } else { 89 | panic!("{}", err); 90 | } 91 | } 92 | } 93 | } 94 | 95 | #[allow(unused)] 96 | pub fn get_yaml_top_level<'a>(yaml: &'a mut Yaml, fieldsearch: &str) -> Option<&'a mut String> { 97 | match yaml { 98 | Yaml::Hash(hash) => { 99 | for field in hash { 100 | if let Yaml::String(fieldname) = field.0 { 101 | if fieldname == fieldsearch { 102 | match field.1 { 103 | Yaml::String(arr) => { 104 | return Some(arr); 105 | } 106 | _ => panic!("Incorrect arg format on cli_base"), 107 | } 108 | } 109 | } 110 | } 111 | } 112 | _ => panic!("Incorrect yaml format on cli_base"), 113 | } 114 | None 115 | } 116 | 117 | #[allow(unused)] 118 | pub fn get_yaml_args(yaml: &mut Yaml) -> Option<&mut Vec> { 119 | match yaml { 120 | Yaml::Hash(hash) => { 121 | for field in hash { 122 | if let Yaml::String(fieldname) = field.0 { 123 | if fieldname == "args" { 124 | match field.1 { 125 | Yaml::Array(arr) => { 126 | sanitize_args(arr); 127 | return Some(arr); 128 | } 129 | _ => panic!("Incorrect arg format on cli_base"), 130 | } 131 | } 132 | } 133 | } 134 | } 135 | _ => panic!("Incorrect yaml format on cli_base"), 136 | } 137 | None 138 | } 139 | 140 | fn sanitize_args(args: &mut Vec) { 141 | *args = args 142 | .drain(..) 143 | .map(|yml| { 144 | if let Yaml::Hash(mut hash) = yml { 145 | for (_, arg) in hash.iter_mut() { 146 | if let Yaml::Hash(ref mut properties) = arg { 147 | let name_visible_aliases = Yaml::String("visible_aliases".to_owned()); 148 | let name_visible_short_aliases = 149 | Yaml::String("visible_short_aliases".to_owned()); 150 | let visible_aliases = properties.remove(&name_visible_aliases); 151 | let visible_short_aliases = properties.remove(&name_visible_short_aliases); 152 | 153 | let mut alias_help = Vec::new(); 154 | 155 | if let Some(Yaml::String(visible_aliases)) = visible_aliases { 156 | let name_aliases = Yaml::String("aliases".to_owned()); 157 | let aliases = properties.remove(&name_aliases); 158 | let mut new_aliases = visible_aliases; 159 | if let Some(Yaml::String(aliases)) = aliases { 160 | new_aliases.push(' '); 161 | new_aliases.push_str(&aliases); 162 | }; 163 | for alias in new_aliases.split(' ') { 164 | alias_help.push(format!("--{}", alias)); 165 | } 166 | properties.insert(name_aliases, Yaml::String(new_aliases)); 167 | } 168 | 169 | if let Some(Yaml::String(visible_short_aliases)) = visible_short_aliases { 170 | let name_short_aliases = Yaml::String("short_aliases".to_owned()); 171 | let short_aliases = properties.remove(&name_short_aliases); 172 | let mut new_short_aliases = visible_short_aliases; 173 | if let Some(Yaml::String(short_aliases)) = short_aliases { 174 | new_short_aliases.push(' '); 175 | new_short_aliases.push_str(&short_aliases); 176 | }; 177 | for alias in new_short_aliases.split(' ') { 178 | alias_help.push(format!("-{}", alias)); 179 | } 180 | properties.insert(name_short_aliases, Yaml::String(new_short_aliases)); 181 | } 182 | 183 | if !alias_help.is_empty() { 184 | let alias_string = format!("\n [aliases: {}]", alias_help.join(", ")); 185 | if let Some(Yaml::String(ref mut long_help)) = properties 186 | .get_mut(&Yaml::String("long_help".to_owned())) 187 | .as_mut() 188 | { 189 | long_help.push_str(&alias_string); 190 | } else if let Some(Yaml::String(ref mut help)) = properties 191 | .get_mut(&Yaml::String("help".to_owned())) 192 | .as_mut() 193 | { 194 | help.push_str(&alias_string); 195 | } 196 | } 197 | } 198 | } 199 | return Yaml::Hash(hash); 200 | } 201 | yml 202 | }) 203 | .collect(); 204 | } 205 | -------------------------------------------------------------------------------- /src/dmenu/README.md: -------------------------------------------------------------------------------- 1 | # dmenu 2 | 3 | This folder contains the source code for the main dmenu functionality. 4 | Files, along with their functions, are as follows: 5 | - additional_bindings.rs 6 | Module configuration for generated bindings taken from the `headers` 7 | - config.rs 8 | `Config` object and it's default values 9 | - drw.rs 10 | Main file for the Drw object, which controls the menu -- focuses on 11 | highly used methods 12 | - fnt.rs 13 | Initialization and handling of xfonts 14 | - globals.rs 15 | Hub file for global variables. As globals in Rust are bad, they are 16 | attached to an object and passed around that way. 17 | - init.rs 18 | Drw initialization (new method) 19 | - item.rs 20 | Deals with menu items 21 | - main.rs 22 | Entry point, command line arguement parsing 23 | - setup.rs 24 | Setup for X windowing 25 | - util.rs 26 | Miscellaneous useful functions 27 | -------------------------------------------------------------------------------- /src/dmenu/additional_bindings.rs: -------------------------------------------------------------------------------- 1 | mod raw { 2 | pub mod main { 3 | #![allow(non_upper_case_globals)] 4 | #![allow(non_camel_case_types)] 5 | #![allow(non_snake_case)] 6 | #![allow(unused)] 7 | include!(concat!(env!("BUILD_DIR"), "/bindings_main.rs")); 8 | } 9 | pub mod xlib { 10 | #![allow(non_upper_case_globals)] 11 | #![allow(non_camel_case_types)] 12 | #![allow(non_snake_case)] 13 | #![allow(unused)] 14 | include!(concat!(env!("BUILD_DIR"), "/bindings_xlib.rs")); 15 | } 16 | } 17 | pub mod fontconfig { 18 | #![allow(non_upper_case_globals)] 19 | #![allow(non_camel_case_types)] 20 | #![allow(non_snake_case)] 21 | use super::raw::main; 22 | pub const FcTrue: main::FcBool = main::FcTrue as main::FcBool; 23 | pub const FcFalse: main::FcBool = main::FcFalse as main::FcBool; 24 | pub const FC_SCALABLE: *const i8 = main::FC_SCALABLE.as_ptr() as *const i8; 25 | pub const FC_CHARSET: *const i8 = main::FC_CHARSET.as_ptr() as *const i8; 26 | pub const FC_COLOR: *const i8 = main::FC_COLOR.as_ptr() as *const i8; 27 | pub const FC_FAMILY: *mut i8 = main::FC_FAMILY.as_ptr() as *mut i8; 28 | } 29 | pub mod xlib { 30 | #![allow(non_upper_case_globals)] 31 | #![allow(non_camel_case_types)] 32 | #![allow(non_snake_case)] 33 | use super::raw::xlib; 34 | pub use xlib::{XNClientWindow, XNFocusWindow, XNInputStyle}; 35 | } 36 | -------------------------------------------------------------------------------- /src/dmenu/clapflags.rs: -------------------------------------------------------------------------------- 1 | use clap::{App, ArgMatches}; 2 | use itertools::Itertools; 3 | use regex::RegexBuilder; 4 | use yaml_rust::yaml::Yaml; 5 | 6 | use crate::config::{Clrs::*, Config, DefaultWidth, Schemes::*}; 7 | use crate::result::*; 8 | 9 | lazy_static::lazy_static! { 10 | static ref YAML: Yaml = { 11 | clap::YamlLoader::load_from_str(include_str!(concat!(env!("BUILD_DIR"), "/cli.yml"))) 12 | .expect("failed to load YAML file") 13 | .pop() 14 | .unwrap() 15 | }; 16 | pub static ref CLAP_FLAGS: ArgMatches<'static> = App::from_yaml(&YAML).get_matches(); 17 | } 18 | 19 | pub fn validate(config: &mut Config) -> CompResult<()> { 20 | if CLAP_FLAGS.occurrences_of("version") > 2 { 21 | eprintln!("More than 2 version flags do nothing special"); 22 | } 23 | if CLAP_FLAGS.occurrences_of("version") == 1 { 24 | return Die::stdout(format!("dmenu-rs {}", env!("VERSION"))); 25 | } 26 | if CLAP_FLAGS.occurrences_of("version") >= 2 { 27 | let plugins = env!("PLUGINS"); 28 | if plugins.len() == 0 { 29 | return Die::stdout(format!( 30 | "dmenu-rs {}\n\ 31 | Compiled with rustc {}\n\ 32 | Compiled without plugins", 33 | env!("VERSION"), 34 | rustc_version_runtime::version(), 35 | )); 36 | } else { 37 | return Die::stdout(format!( 38 | "dmenu-rs {}\n\ 39 | Compiled with rustc {}\n\ 40 | Compiled with plugins:\n\ 41 | {}", 42 | env!("VERSION"), 43 | rustc_version_runtime::version(), 44 | plugins.split(" ").map(|p| format!("- {}", p)).join("\n"), 45 | )); 46 | } 47 | } 48 | 49 | let color_regex = RegexBuilder::new("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\0$") 50 | .case_insensitive(true) 51 | .build() 52 | .map_err(|_| Die::Stderr("Could not build regex".to_owned()))?; 53 | 54 | // bottom 55 | if CLAP_FLAGS.occurrences_of("bottom") == 1 { 56 | config.topbar = false; 57 | } 58 | 59 | // fast 60 | if CLAP_FLAGS.occurrences_of("fast") == 1 { 61 | config.fast = true; 62 | } 63 | 64 | // insensitive 65 | if CLAP_FLAGS.occurrences_of("insensitive") == 1 { 66 | config.case_sensitive = false; 67 | } 68 | 69 | // lines 70 | if let Some(lines) = CLAP_FLAGS.value_of("lines") { 71 | config.lines = lines 72 | .parse::() 73 | .map_err(|_| Die::Stderr("-l: Lines must be a non-negaitve integer".to_owned()))?; 74 | } 75 | 76 | // monitor 77 | if let Some(monitor) = CLAP_FLAGS.value_of("monitor") { 78 | config.mon = monitor 79 | .parse::() 80 | .map_err(|_| Die::Stderr("-m: Monitor must be a non-negaitve integer".to_owned()))?; 81 | } 82 | 83 | // prompt 84 | if let Some(prompt) = CLAP_FLAGS.value_of("prompt") { 85 | config.prompt = prompt.to_string(); 86 | } 87 | 88 | // font 89 | if let Some(fonts) = CLAP_FLAGS.values_of("font") { 90 | let default = config.fontstrings.pop().unwrap(); 91 | config.fontstrings = fonts.map(|f| f.to_string()).collect(); 92 | config.fontstrings.push(default); 93 | } 94 | 95 | // color_normal_background 96 | if let Some(color) = CLAP_FLAGS.value_of("color_normal_background") { 97 | let mut color = color.to_string(); 98 | color.push('\0'); 99 | color_regex.find_iter(&color).nth(0).ok_or(Die::Stderr( 100 | "--nb: Color must be in hex format (#123456 or #123)".to_owned(), 101 | ))?; 102 | config.colors[SchemeNorm as usize][ColBg as usize][..color.len()] 103 | .copy_from_slice(color.as_bytes()); 104 | } 105 | 106 | // color_normal_foreground 107 | if let Some(color) = CLAP_FLAGS.value_of("color_normal_foreground") { 108 | let mut color = color.to_string(); 109 | color.push('\0'); 110 | color_regex.find_iter(&color).nth(0).ok_or(Die::Stderr( 111 | "--nf: Color must be in hex format (#123456 or #123)".to_owned(), 112 | ))?; 113 | config.colors[SchemeNorm as usize][ColFg as usize][..color.len()] 114 | .copy_from_slice(color.as_bytes()); 115 | } 116 | 117 | // color_selected_background 118 | if let Some(color) = CLAP_FLAGS.value_of("color_selected_background") { 119 | let mut color = color.to_string(); 120 | color.push('\0'); 121 | color_regex.find_iter(&color).nth(0).ok_or(Die::Stderr( 122 | "--sb: Color must be in hex format (#123456 or #123)".to_owned(), 123 | ))?; 124 | config.colors[SchemeSel as usize][ColBg as usize][..color.len()] 125 | .copy_from_slice(color.as_bytes()); 126 | } 127 | 128 | // color_selected_foreground 129 | if let Some(color) = CLAP_FLAGS.value_of("color_selected_foreground") { 130 | let mut color = color.to_string(); 131 | color.push('\0'); 132 | color_regex.find_iter(&color).nth(0).ok_or(Die::Stderr( 133 | "--sf: Color must be in hex format (#123456 or #123)".to_owned(), 134 | ))?; 135 | config.colors[SchemeSel as usize][ColFg as usize][..color.len()] 136 | .copy_from_slice(color.as_bytes()); 137 | } 138 | 139 | // window 140 | if let Some(window) = CLAP_FLAGS.value_of("window") { 141 | config.embed = window.parse::().map_err(|_| { 142 | Die::Stderr("-w: Window ID must be a valid X window ID string".to_owned()) 143 | })?; 144 | } 145 | 146 | // nostdin 147 | if CLAP_FLAGS.occurrences_of("nostdin") == 1 { 148 | config.nostdin = true; 149 | } 150 | 151 | // render_minheight 152 | if let Some(minheight) = CLAP_FLAGS.value_of("render_minheight") { 153 | config.render_minheight = minheight.parse::().map_err(|_| { 154 | Die::Stderr( 155 | "--render_minheight: Height must be an integet number of \ 156 | pixels" 157 | .to_owned(), 158 | ) 159 | })?; 160 | } 161 | 162 | // render_overrun 163 | if CLAP_FLAGS.occurrences_of("render_overrun") == 1 { 164 | config.render_overrun = true; 165 | config.render_flex = true; 166 | } 167 | 168 | // render_flex 169 | if CLAP_FLAGS.occurrences_of("render_flex") == 1 { 170 | config.render_flex = true; 171 | } 172 | 173 | // render_rightalign 174 | if CLAP_FLAGS.occurrences_of("render_rightalign") == 1 { 175 | config.render_rightalign = true; 176 | } 177 | 178 | // render_default_width 179 | if let Some(arg) = CLAP_FLAGS.value_of("render_default_width") { 180 | if !arg.contains("=") { 181 | config.render_default_width = match arg { 182 | "min" => DefaultWidth::Min, 183 | "items" => DefaultWidth::Items, 184 | "max" => { 185 | config.render_rightalign = true; 186 | DefaultWidth::Max 187 | } 188 | _ => return Die::stderr("--render_default_width: invalid arguement".to_owned()), 189 | } 190 | } else { 191 | let vec: Vec<&str> = arg.split("=").collect(); 192 | if vec.len() != 2 || (vec.len() > 0 && vec[0] != "custom") { 193 | return Die::stderr( 194 | "Incorrect format for --render_default_width, \ 195 | see help for details" 196 | .to_owned(), 197 | ); 198 | } 199 | let width = vec[1].parse::(); 200 | if width.is_err() || *width.as_ref().unwrap() > 100 { 201 | return Die::stderr( 202 | "--render_default_width: custom width \ 203 | must be a positive integer" 204 | .to_owned(), 205 | ); 206 | } 207 | config.render_default_width = DefaultWidth::Custom(width.unwrap()); 208 | } 209 | } 210 | 211 | Ok(()) 212 | } 213 | -------------------------------------------------------------------------------- /src/dmenu/cli_base.yml: -------------------------------------------------------------------------------- 1 | name: dmenu 2 | version: $VERSION # filled in by build.rs 3 | about: dynamic menu 4 | 5 | args: 6 | - version: 7 | help: Prints version and build information. 8 | Specify twice for additional info. 9 | short: V 10 | long: version 11 | multiple: true 12 | - bottom: 13 | help: Places menu at bottom of the screen 14 | short: b 15 | long: bottom 16 | - fast: 17 | help: Grabs keyboard before reading stdin 18 | short: f 19 | long: fast 20 | - insensitive: 21 | help: Case insensitive item matching 22 | short: i 23 | long: insensitive 24 | - lines: 25 | help: Number of vertical listing lines 26 | short: l 27 | long: lines 28 | takes_value: true 29 | value_name: LINES 30 | - monitor: 31 | help: X monitor to display on 32 | short: m 33 | long: monitor 34 | takes_value: true 35 | value_name: MONITOR 36 | - prompt: 37 | help: Display a prompt 38 | short: p 39 | long: prompt 40 | takes_value: true 41 | value_name: PROMPT 42 | - font: 43 | help: Add menu font 44 | long_help: "Add menu font. Can be specified multiple times to give fallback fonts. \ 45 | For example, --font Terminus --font 'Font Awesome' would draw everything \ 46 | with Terminus, and fall back to Font Awesome for symbols.\n\ 47 | If a glyph is not found in any of the supplied fonts, it will be provided \ 48 | by the default font (:mono). \n\ 49 | If a glyph is not found in any fonts, it will \ 50 | render as the no-character box." 51 | long: font 52 | visible_aliases: fn 53 | takes_value: true 54 | value_name: FONT 55 | multiple: true 56 | - color_normal_background: 57 | help: Normal Background Color 58 | long: nb 59 | takes_value: true 60 | value_name: COLOR 61 | - color_normal_foreground: 62 | help: Normal Foreground Color 63 | long: nf 64 | takes_value: true 65 | value_name: COLOR 66 | - color_selected_background: 67 | help: Selected Background Color 68 | long: sb 69 | takes_value: true 70 | value_name: COLOR 71 | - color_selected_foreground: 72 | help: Selected Foreground Color 73 | long: sf 74 | takes_value: true 75 | value_name: COLOR 76 | - window: 77 | help: Embed into window ID 78 | short: w 79 | long: window 80 | takes_value: true 81 | value_name: ID 82 | - render_minheight: 83 | help: Minimum menu height 84 | long_help: Minimum menu draw height. Normally, the menu height is decided by the font size, 85 | however this option overrides that. Useful for getting things aligned with elements always 86 | on screen, such as i3 statusbar. 87 | long: render_minheight 88 | takes_value: true 89 | value_name: PIXELS 90 | - render_overrun: 91 | help: Draw behavior of input box. If specified will draw input 92 | over the top of items when input exceeds the width of input box 93 | long: render_overrun 94 | - render_flex: 95 | help: Draw behavior of input box. If specified will expand input 96 | box when input exceeds the width of input box, gracefully moving items 97 | out of the way 98 | long: render_flex 99 | - render_rightalign: 100 | help: Draw behavior of menu items. If specified will right align 101 | long: render_rightalign 102 | - render_default_width: 103 | help: | 104 | Default size of input box. Options are: 105 | min - input box remains as small as possible 106 | items - same size as the largest menu item (default) 107 | yields the most static layout 108 | max - only one menu item at a time is displayed, right aligned 109 | custom=WIDTH - fixed width, percentage of total menu width 110 | ranges from 0 (min) to 100 (max) 111 | long: render_default_width 112 | takes_value: true 113 | value_name: DEFAULT_WIDTH 114 | - nostdin: 115 | help: Do not read from stdin. Probably not useful unless compiled with plugins 116 | long: nostdin 117 | -------------------------------------------------------------------------------- /src/dmenu/config.rs: -------------------------------------------------------------------------------- 1 | use libc::{c_int, c_uint}; 2 | use x11::xlib::Window; 3 | 4 | pub enum Schemes { 5 | SchemeNorm, 6 | SchemeSel, 7 | SchemeOut, 8 | SchemeLast, 9 | } 10 | pub enum Clrs { 11 | ColFg, 12 | ColBg, 13 | } 14 | pub use Clrs::*; 15 | pub use Schemes::*; 16 | 17 | #[derive(Debug, PartialEq)] 18 | pub enum DefaultWidth { 19 | Min, 20 | Items, 21 | Max, 22 | Custom(u8), 23 | } 24 | 25 | #[derive(Debug)] 26 | pub struct Config { 27 | pub lines: c_uint, 28 | pub topbar: bool, 29 | pub prompt: String, 30 | pub promptw: c_int, 31 | pub fontstrings: Vec, 32 | pub fast: bool, 33 | pub embed: Window, 34 | pub case_sensitive: bool, 35 | pub mon: c_int, 36 | pub colors: [[[u8; 8]; 2]; SchemeLast as usize], 37 | pub render_minheight: u32, 38 | pub render_overrun: bool, 39 | pub render_flex: bool, 40 | pub render_rightalign: bool, 41 | pub render_default_width: DefaultWidth, 42 | pub nostdin: bool, 43 | } 44 | 45 | pub struct ConfigDefault {} 46 | 47 | impl Default for Config { 48 | fn default() -> Self { 49 | Self { 50 | lines: ConfigDefault::lines(), 51 | topbar: ConfigDefault::topbar(), 52 | prompt: ConfigDefault::prompt(), 53 | promptw: 0, 54 | fontstrings: ConfigDefault::fontstrings(), 55 | fast: ConfigDefault::fast(), 56 | embed: ConfigDefault::embed(), 57 | case_sensitive: ConfigDefault::case_sensitive(), 58 | mon: ConfigDefault::mon(), 59 | colors: ConfigDefault::colors(), 60 | render_minheight: ConfigDefault::render_minheight(), 61 | render_overrun: ConfigDefault::render_overrun(), 62 | render_flex: ConfigDefault::render_flex(), 63 | render_rightalign: ConfigDefault::render_rightalign(), 64 | render_default_width: ConfigDefault::render_default_width(), 65 | nostdin: ConfigDefault::nostdin(), 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/dmenu/fnt.rs: -------------------------------------------------------------------------------- 1 | use crate::additional_bindings::fontconfig::{FC_COLOR, FC_FAMILY}; 2 | use fontconfig::fontconfig::{ 3 | FcBool, FcChar8, FcFontList, FcFontSet, FcFontSetDestroy, FcNameParse, FcObjectSetBuild, 4 | FcObjectSetDestroy, FcPatternDestroy, FcPatternGetBool, FcResultMatch, 5 | }; 6 | use libc::c_uint; 7 | use std::ffi::c_void; 8 | use std::mem::MaybeUninit; 9 | use std::ptr; 10 | use x11::xft::{ 11 | FcPattern, XftFont, XftFontClose, XftFontOpenName, XftFontOpenPattern, XftNameParse, 12 | }; 13 | use x11::xlib::Display; 14 | 15 | use crate::drw::Drw; 16 | use crate::result::*; 17 | 18 | #[derive(Debug)] 19 | pub struct Fnt { 20 | pub xfont: *mut XftFont, 21 | pub pattern_pointer: *mut FcPattern, 22 | pub height: c_uint, 23 | } 24 | 25 | impl PartialEq for Fnt { 26 | fn eq(&self, other: &Self) -> bool { 27 | self.xfont == other.xfont 28 | } 29 | } 30 | 31 | impl Fnt { 32 | // xfont_create 33 | pub fn new( 34 | drw: &Drw, 35 | fontopt: Option<&String>, 36 | mut pattern: *mut FcPattern, 37 | ) -> CompResult { 38 | let __blank = "".to_owned(); // fighting the borrow checker 39 | let fontname = fontopt.unwrap_or(&__blank); 40 | let fontptr = if fontname.len() > 0 { 41 | fontname.as_ptr() as *mut i8 42 | } else { 43 | ptr::null_mut() 44 | }; 45 | unsafe { 46 | let xfont; 47 | if fontptr != ptr::null_mut() { 48 | if let Err(warning) = Self::find_font_sys(&fontname) { 49 | eprintln!("{}", warning); 50 | } 51 | 52 | /* Using the pattern found at font->xfont->pattern does not yield the 53 | * same substitution results as using the pattern returned by 54 | * FcNameParse; using the latter results in the desired fallback 55 | * behaviour whereas the former just results in missing-character 56 | * rectangles being drawn, at least with some fonts. */ 57 | xfont = XftFontOpenName(drw.dpy, drw.screen, fontptr); 58 | if xfont == ptr::null_mut() { 59 | return Die::stderr(format!( 60 | "error, cannot load font from name: '{}'", 61 | fontname 62 | )); 63 | } 64 | 65 | pattern = XftNameParse(fontptr); 66 | if pattern == ptr::null_mut() { 67 | XftFontClose(drw.dpy, xfont); 68 | return Die::stderr(format!( 69 | "error, cannot parse font name to pattern: '{}'", 70 | fontname 71 | )); 72 | } 73 | } else if pattern != ptr::null_mut() { 74 | xfont = XftFontOpenPattern(drw.dpy, pattern); 75 | if xfont == ptr::null_mut() { 76 | return Die::stderr(format!( 77 | "error, cannot load font '{}' from pattern.", 78 | fontname 79 | )); 80 | } 81 | } else { 82 | return Die::stderr("No font specified.".to_owned()); 83 | } 84 | 85 | /* Do not allow using color fonts. This is a workaround for a BadLength 86 | * error from Xft with color glyphs. Modelled on the Xterm workaround. See 87 | * https://bugzilla.redhat.com/show_bug.cgi?id=1498269 88 | * https://lists.suckless.org/dev/1701/30932.html 89 | * https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=916349 90 | * and lots more all over the internet. 91 | */ 92 | let mut iscol = MaybeUninit::::uninit(); 93 | if FcPatternGetBool( 94 | (*xfont).pattern as *mut c_void, 95 | FC_COLOR, 96 | 0, 97 | iscol.as_mut_ptr(), 98 | ) == FcResultMatch 99 | && iscol.assume_init() != 0 100 | { 101 | XftFontClose(drw.dpy, xfont); 102 | return Die::stderr("Cannot load color fonts".to_owned()); 103 | } 104 | 105 | let height = (*xfont).ascent + (*xfont).descent; 106 | 107 | return Ok(Self { 108 | xfont, 109 | pattern_pointer: pattern, 110 | height: height as c_uint, 111 | }); 112 | } 113 | } 114 | // xfont_free 115 | pub fn free(&mut self, dpy: *mut Display) { 116 | unsafe { 117 | if self.pattern_pointer != ptr::null_mut() { 118 | FcPatternDestroy(self.pattern_pointer as *mut c_void); 119 | } 120 | XftFontClose(dpy, self.xfont); 121 | } 122 | } 123 | 124 | fn find_font_sys(needle: &String) -> Result<(), String> { 125 | // First, validate the font name 126 | let parts = needle 127 | .split(":") 128 | .nth(0) 129 | .unwrap_or("") 130 | .split("-") 131 | .collect::>(); 132 | let searchterm = { 133 | let len = parts.len(); 134 | parts 135 | .into_iter() 136 | .take(if len <= 1 { len } else { len - 1 }) 137 | .fold(String::new(), |mut acc, g| { 138 | acc.push_str(g); 139 | acc 140 | }) 141 | }; 142 | 143 | // Then search for it on the system 144 | unsafe { 145 | let all_fonts = Self::op_pattern("".to_owned())?; 146 | let mut fs = Self::op_pattern(searchterm.clone())?; 147 | if (*fs).nfont == 0 { 148 | // user may have searched for an attribute instead of family 149 | FcFontSetDestroy(fs); 150 | fs = Self::op_pattern(format!(":{}", searchterm))?; 151 | } 152 | let ret = if (*fs).nfont == 0 || (*fs).nfont == (*all_fonts).nfont { 153 | Err(format!( 154 | "Warning: font '{}' not found on the system", 155 | searchterm 156 | )) 157 | } else { 158 | Ok(()) 159 | }; 160 | FcFontSetDestroy(fs); 161 | FcFontSetDestroy(all_fonts); 162 | ret 163 | } 164 | } 165 | 166 | fn op_pattern(mut pattern: String) -> Result<*mut FcFontSet, String> { 167 | unsafe { 168 | pattern.push('\0'); 169 | let os = FcObjectSetBuild(FC_FAMILY, ptr::null_mut::()); 170 | let pat = FcNameParse(format!("{}\0", pattern).as_ptr() as *mut FcChar8); 171 | let fs = FcFontList(ptr::null_mut(), pat, os); 172 | FcPatternDestroy(pat); 173 | FcObjectSetDestroy(os); 174 | if fs == ptr::null_mut() { 175 | return Err("Could not get system fonts".to_owned()); 176 | } 177 | Ok(fs) 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/dmenu/globals.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Schemes::*; 2 | use libc::c_int; 3 | use std::ptr; 4 | use x11::xft::XftColor; 5 | use x11::xlib::{Window, XIC}; 6 | 7 | #[derive(Debug)] 8 | pub struct PseudoGlobals { 9 | pub promptw: c_int, 10 | pub inputw: c_int, 11 | pub lrpad: c_int, 12 | pub schemeset: [[*mut XftColor; 2]; SchemeLast as usize], 13 | pub bh: u32, 14 | pub win: Window, 15 | pub cursor: usize, 16 | pub xic: XIC, 17 | } 18 | 19 | impl Default for PseudoGlobals { 20 | fn default() -> Self { 21 | Self { 22 | promptw: 0, 23 | inputw: 0, 24 | schemeset: [[ptr::null_mut(); 2]; SchemeLast as usize], 25 | lrpad: 0, 26 | bh: 0, 27 | win: 0, 28 | cursor: 0, 29 | xic: ptr::null_mut(), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/dmenu/init.rs: -------------------------------------------------------------------------------- 1 | use libc::{c_char, c_int, isatty}; 2 | use std::{ffi::CStr, mem::MaybeUninit, ptr}; 3 | use x11::xft::{XftColor, XftColorAllocName}; 4 | use x11::xlib::{ 5 | CapButt, Display, JoinMiter, LineSolid, Window, XCreateGC, XCreatePixmap, XDefaultColormap, 6 | XDefaultDepth, XDefaultVisual, XSetLineAttributes, XWindowAttributes, 7 | }; 8 | 9 | use crate::config::{Config, Schemes::*}; 10 | use crate::drw::Drw; 11 | use crate::fnt::*; 12 | use crate::globals::*; 13 | use crate::item::Items; 14 | use crate::result::*; 15 | use crate::util::*; 16 | 17 | impl Drw { 18 | pub fn new( 19 | dpy: *mut Display, 20 | screen: c_int, 21 | root: Window, 22 | wa: XWindowAttributes, 23 | pseudo_globals: PseudoGlobals, 24 | config: Config, 25 | ) -> CompResult { 26 | unsafe { 27 | let drawable = XCreatePixmap( 28 | dpy, 29 | root, 30 | wa.width as u32, 31 | wa.height as u32, 32 | XDefaultDepth(dpy, screen) as u32, 33 | ); 34 | let gc = XCreateGC(dpy, root, 0, ptr::null_mut()); 35 | XSetLineAttributes(dpy, gc, 1, LineSolid, CapButt, JoinMiter); 36 | let mut ret = Self { 37 | wa, 38 | dpy, 39 | screen, 40 | root, 41 | drawable, 42 | gc, 43 | fonts: Vec::new(), 44 | pseudo_globals, 45 | config, 46 | scheme: [ptr::null_mut(), ptr::null_mut()], 47 | w: 0, 48 | h: 0, 49 | input: "".to_string(), 50 | items: None, 51 | }; 52 | 53 | ret.fontset_create()?; 54 | ret.pseudo_globals.lrpad = ret.fonts[0].height as i32; 55 | 56 | ret.items = if ret.config.nostdin { 57 | ret.format_stdin(vec![])?; 58 | grabkeyboard(ret.dpy, ret.config.embed)?; 59 | Some(Items::new(Vec::new())) 60 | } else { 61 | Some(Items::new(if ret.config.fast && isatty(0) == 0 { 62 | grabkeyboard(ret.dpy, ret.config.embed)?; 63 | readstdin(&mut ret)? 64 | } else { 65 | let tmp = readstdin(&mut ret)?; 66 | grabkeyboard(ret.dpy, ret.config.embed)?; 67 | tmp 68 | })) 69 | }; 70 | 71 | for j in 0..SchemeLast as usize { 72 | ret.pseudo_globals.schemeset[j] = ret.scm_create(ret.config.colors[j])?; 73 | } 74 | 75 | ret.config.lines = ret.config.lines.min(ret.get_items().len() as u32); 76 | 77 | Ok(ret) 78 | } 79 | } 80 | 81 | fn scm_create(&self, clrnames: [[u8; 8]; 2]) -> CompResult<[*mut XftColor; 2]> { 82 | let blank_val_1 = MaybeUninit::::uninit(); 83 | let blank_val_2 = MaybeUninit::::uninit(); 84 | let ret: [*mut XftColor; 2] = unsafe { 85 | [ 86 | Box::into_raw(Box::new(blank_val_1.assume_init())), 87 | Box::into_raw(Box::new(blank_val_2.assume_init())), 88 | ] 89 | }; 90 | self.clr_create(ret[0], clrnames[0].as_ptr() as *const c_char)?; 91 | self.clr_create(ret[1], clrnames[1].as_ptr() as *const c_char)?; 92 | Ok(ret) 93 | } 94 | 95 | fn clr_create(&self, dest: *mut XftColor, clrname: *const c_char) -> CompResult<()> { 96 | unsafe { 97 | if XftColorAllocName( 98 | self.dpy, 99 | XDefaultVisual(self.dpy, self.screen), 100 | XDefaultColormap(self.dpy, self.screen), 101 | clrname, 102 | dest, 103 | ) == 0 104 | { 105 | Die::stderr(format!( 106 | "error, cannot allocate color {:?}", 107 | CStr::from_ptr(clrname) 108 | )) 109 | } else { 110 | Ok(()) 111 | } 112 | } 113 | } 114 | 115 | fn fontset_create(&mut self) -> CompResult<()> { 116 | for font in self.config.fontstrings.iter_mut() { 117 | font.push('\0'); 118 | } 119 | for font in self.config.fontstrings.iter() { 120 | self.fonts 121 | .push(Fnt::new(self, Some(font), ptr::null_mut())?); 122 | } 123 | 124 | Ok(()) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/dmenu/item.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{DefaultWidth, Schemes::*}; 2 | use crate::drw::{Drw, TextOption::*}; 3 | use crate::result::*; 4 | 5 | use libc::c_int; 6 | use regex::Regex; 7 | 8 | #[allow(unused_imports)] 9 | pub enum MatchCode { 10 | Exact, 11 | Prefix, 12 | Substring, 13 | None, 14 | } 15 | pub use MatchCode::*; 16 | #[derive(Debug)] 17 | pub enum Direction { 18 | Vertical, 19 | Horizontal, 20 | } 21 | pub use Direction::*; 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct Item { 25 | // dmenu entry 26 | pub text: String, 27 | pub out: bool, 28 | pub width: c_int, 29 | } 30 | 31 | impl Item { 32 | pub fn new(text: String, out: bool, drw: &mut Drw) -> CompResult { 33 | Ok(Self { 34 | out, 35 | width: drw.textw(Other(&text))?, 36 | text, 37 | }) 38 | } 39 | pub fn draw(&self, x: c_int, y: c_int, w: c_int, drw: &mut Drw) -> CompResult { 40 | drw.text( 41 | x, 42 | y, 43 | w as u32, 44 | drw.pseudo_globals.bh as u32, 45 | drw.pseudo_globals.lrpad as u32 / 2, 46 | Other(&self.text), 47 | false, 48 | ) 49 | .map(|o| o.0) 50 | } 51 | #[allow(unused)] // won't be used if overriden 52 | pub fn matches(&self, re: &Regex) -> MatchCode { 53 | match re 54 | .find_iter(&self.text) 55 | .nth(0) 56 | .map(|m| (m.start(), m.end())) 57 | .unwrap_or((1, 0)) 58 | { 59 | (1, 0) => MatchCode::None, // don't expect zero length matches... 60 | (0, end) => 61 | // unless search is empty 62 | { 63 | if end == self.text.len() { 64 | MatchCode::Exact 65 | } else { 66 | MatchCode::Prefix 67 | } 68 | } 69 | _ => MatchCode::Substring, 70 | } 71 | } 72 | } 73 | 74 | #[derive(Debug)] 75 | pub struct Partition { 76 | pub data: Vec, 77 | pub leftover: i32, // leftover padding on right side 78 | } 79 | 80 | impl Partition { 81 | pub fn new(data: Vec, leftover: i32) -> Self { 82 | Self { data, leftover } 83 | } 84 | #[inline(always)] 85 | pub fn len(&self) -> usize { 86 | self.data.len() 87 | } 88 | pub fn decompose(haystack: &Vec, needle: &Drw) -> (usize, usize) { 89 | let mut partition_i = needle.items.as_ref().unwrap().curr; 90 | let mut partition = 0; 91 | for p in haystack { 92 | if partition_i >= p.len() { 93 | partition_i -= p.len(); 94 | partition += 1; 95 | } else { 96 | break; 97 | } 98 | } 99 | (partition_i, partition) 100 | } 101 | } 102 | 103 | impl std::ops::Index for Partition { 104 | type Output = Item; 105 | 106 | fn index(&self, index: usize) -> &Item { 107 | &self.data[index] 108 | } 109 | } 110 | 111 | #[derive(Debug)] 112 | pub struct Items { 113 | pub data: Vec, 114 | pub cached_partitions: Vec, // seperated into screens 115 | pub curr: usize, 116 | } 117 | 118 | impl Items { 119 | pub fn new(data: Vec) -> Self { 120 | Self { 121 | data, 122 | cached_partitions: Vec::new(), 123 | curr: 0, 124 | } 125 | } 126 | pub fn match_len(&self) -> usize { 127 | self.cached_partitions.len() 128 | } 129 | pub fn draw(drw: &mut Drw, direction: Direction) -> CompResult { 130 | // gets an apropriate vec of matches 131 | let pre_processed_items = drw.gen_matches()?; 132 | let items_to_draw = drw.postprocess_matches(pre_processed_items)?; 133 | let rangle = ">".to_string(); 134 | let rangle_width = drw.textw(Other(&rangle))?; 135 | let langle = "<".to_string(); 136 | let langle_width = drw.textw(Other(&langle))?; 137 | 138 | drw.pseudo_globals.inputw = match drw.config.render_default_width { 139 | DefaultWidth::Min => items_to_draw 140 | .iter() 141 | .fold(0, |acc, w| acc.max(w.width)) 142 | .min(drw.w / 3) 143 | .min(drw.textw(Input)?), 144 | DefaultWidth::Items => drw 145 | .get_items() 146 | .iter() 147 | .fold(0, |acc, w| acc.max(w.width)) 148 | .min(drw.w / 3), 149 | DefaultWidth::Max => { 150 | let curr = drw.items.as_ref().unwrap().curr; 151 | let data = drw.get_items(); 152 | let mut w = drw.w - drw.pseudo_globals.promptw - data[curr].width; 153 | if curr < data.len() - 1 { 154 | w -= rangle_width; 155 | } 156 | if curr > 0 { 157 | w -= langle_width; 158 | } 159 | w 160 | } 161 | DefaultWidth::Custom(width) => (drw.w as f32 * (width as f32) / 100.0) as i32, 162 | }; 163 | 164 | let matched_partitions = Self::partition_matches( 165 | items_to_draw, 166 | &direction, 167 | drw, 168 | if !(drw.config.render_default_width == DefaultWidth::Min) 169 | || drw.config.render_default_width == DefaultWidth::Items 170 | { 171 | langle_width 172 | } else { 173 | 0 174 | }, 175 | rangle_width, 176 | )?; 177 | 178 | if matched_partitions.len() == 0 { 179 | drw.items.as_mut().unwrap().cached_partitions = matched_partitions; 180 | return Ok(false); // nothing to draw 181 | } 182 | 183 | let (partition_i, partition) = Partition::decompose(&matched_partitions, drw); 184 | 185 | let mut coord = match direction { 186 | Horizontal => { 187 | if drw.config.render_rightalign { 188 | matched_partitions[partition].leftover 189 | } else { 190 | 0 191 | } 192 | } 193 | Vertical => drw.pseudo_globals.bh as c_int, 194 | }; 195 | 196 | if let Horizontal = direction { 197 | if drw.config.render_flex { 198 | let inputw_desired = drw.textw(Input)?; 199 | if inputw_desired > drw.pseudo_globals.inputw { 200 | let delta = inputw_desired 201 | - drw.pseudo_globals.inputw 202 | - matched_partitions[partition].leftover; 203 | if delta < 0 { 204 | drw.pseudo_globals.inputw = inputw_desired; 205 | } else { 206 | drw.pseudo_globals.inputw = inputw_desired - delta; 207 | } 208 | } 209 | } 210 | coord += drw.pseudo_globals.promptw + drw.pseudo_globals.inputw; 211 | if partition > 0 { 212 | // draw langle if required 213 | drw.setscheme(SchemeNorm); 214 | coord = drw 215 | .text( 216 | coord, 217 | 0, 218 | langle_width as u32, 219 | drw.pseudo_globals.bh as u32, 220 | drw.pseudo_globals.lrpad as u32 / 2, 221 | Other(&langle), 222 | false, 223 | )? 224 | .0; 225 | if drw.config.render_default_width == DefaultWidth::Max { 226 | // This is here due do an optical illusion 227 | // It's not pedantically correct alignment, but makes sense on Max 228 | drw.pseudo_globals.inputw += drw.pseudo_globals.lrpad / 2; 229 | } 230 | } else { 231 | // now, do we give phantom space? 232 | if drw.config.render_default_width == DefaultWidth::Items { 233 | coord += langle_width; 234 | } 235 | } 236 | } 237 | 238 | for index in 0..matched_partitions[partition].len() { 239 | if index == partition_i { 240 | drw.setscheme(SchemeSel); 241 | } else if matched_partitions[partition][index].out { 242 | drw.setscheme(SchemeOut); 243 | } else { 244 | drw.setscheme(SchemeNorm); 245 | } 246 | match direction { 247 | Horizontal => { 248 | if partition + 1 < matched_partitions.len() { 249 | // draw rangle 250 | coord = matched_partitions[partition][index].draw( 251 | coord, 252 | 0, 253 | matched_partitions[partition][index] 254 | .width 255 | .min(drw.w - coord - rangle_width), 256 | drw, 257 | )?; 258 | drw.setscheme(SchemeNorm); 259 | drw.text( 260 | drw.w - rangle_width, 261 | 0, 262 | rangle_width as u32, 263 | drw.pseudo_globals.bh as u32, 264 | drw.pseudo_globals.lrpad as u32 / 2, 265 | Other(&rangle), 266 | false, 267 | )?; 268 | } else { 269 | // no rangle 270 | coord = matched_partitions[partition][index].draw( 271 | coord, 272 | 0, 273 | matched_partitions[partition][index] 274 | .width 275 | .min(drw.w - coord), 276 | drw, 277 | )?; 278 | } 279 | } 280 | Vertical => { 281 | matched_partitions[partition][index].draw(0, coord, drw.w, drw)?; 282 | coord += drw.pseudo_globals.bh as i32; 283 | } 284 | } 285 | } 286 | 287 | drw.items.as_mut().unwrap().cached_partitions = matched_partitions; 288 | 289 | Ok(true) 290 | } 291 | 292 | fn partition_matches( 293 | input: Vec, 294 | direction: &Direction, 295 | drw: &mut Drw, 296 | langle_width: i32, 297 | rangle_width: i32, 298 | ) -> CompResult> { 299 | // matches come in, partitions come out 300 | match direction { 301 | Horizontal => { 302 | let mut partitions = Vec::new(); 303 | let mut partition_build = Vec::new(); 304 | let mut x = if drw.config.render_default_width == DefaultWidth::Items { 305 | drw.pseudo_globals.promptw + drw.pseudo_globals.inputw + langle_width 306 | } else { 307 | drw.pseudo_globals.promptw + drw.pseudo_globals.inputw 308 | }; 309 | let mut item_iter = input.into_iter().peekable(); 310 | while let Some(item) = item_iter.next() { 311 | let precomp_width = x; 312 | let leftover; 313 | x += item.width; 314 | if x > { 315 | let width_comp = if item_iter.peek().is_some() { 316 | drw.w - rangle_width 317 | } else { 318 | drw.w 319 | }; 320 | leftover = width_comp - precomp_width; 321 | width_comp 322 | } || drw.config.render_default_width == DefaultWidth::Max 323 | { 324 | // not enough room, create new partition, but what if: 325 | if !( 326 | partitions.len() == 0 // if there's only one page 327 | && item_iter.peek().is_none() // there will only be one page 328 | && x < drw.w + rangle_width 329 | // and everything could fit if it wasn't for the '>' 330 | ) && partition_build.len() > 0 331 | { 332 | // (make sure no empties) 333 | partitions.push(Partition::new(partition_build, leftover)); 334 | partition_build = Vec::new(); 335 | x = drw.pseudo_globals.promptw 336 | + drw.pseudo_globals.inputw 337 | + langle_width 338 | + item.width; 339 | } 340 | } 341 | partition_build.push(item); 342 | } 343 | if partition_build.len() > 0 { 344 | // grab any extras from the last page 345 | let leftover = if partitions.len() == 0 { 346 | drw.w - x 347 | } else { 348 | drw.w - x - langle_width 349 | }; 350 | partitions.push(Partition::new(partition_build, leftover)); 351 | } 352 | Ok(partitions) 353 | } 354 | Vertical => Ok(input 355 | .chunks(drw.config.lines as usize) 356 | .map(|p| Partition::new(p.to_vec(), 0)) 357 | .collect()), 358 | } 359 | } 360 | } 361 | 362 | impl Drw { 363 | #[inline(always)] 364 | pub fn get_items(&self) -> &Vec { 365 | &self.items.as_ref().unwrap().data 366 | } 367 | #[allow(unused)] // for plugins 368 | #[inline(always)] 369 | pub fn get_items_mut(&mut self) -> &mut Vec { 370 | &mut self.items.as_mut().unwrap().data 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/dmenu/main.rs: -------------------------------------------------------------------------------- 1 | mod additional_bindings; 2 | mod clapflags; 3 | mod config; 4 | mod drw; 5 | mod fnt; 6 | mod globals; 7 | mod init; 8 | mod item; 9 | mod plugin_entry; 10 | mod result; 11 | mod run; 12 | mod setup; 13 | mod util; 14 | mod plugins { 15 | include!(concat!(env!("OUT_DIR"), "/proc_mod_plugin.rs")); 16 | } 17 | 18 | use libc::{setlocale, LC_CTYPE}; 19 | #[cfg(target_os = "openbsd")] 20 | use pledge; 21 | use std::mem::MaybeUninit; 22 | use std::ptr; 23 | use x11::xlib::*; 24 | 25 | use config::*; 26 | use drw::Drw; 27 | use globals::*; 28 | use result::*; 29 | 30 | fn main() { 31 | // just a wrapper to ensure a clean death in the event of error 32 | std::process::exit(match try_main() { 33 | Ok(_) => 0, 34 | Err(Die::Stdout(msg)) => { 35 | if msg.len() > 0 { 36 | println!("{}", msg) 37 | } 38 | 0 39 | } 40 | Err(Die::Stderr(msg)) => { 41 | if msg.len() > 0 { 42 | eprintln!("{}", msg) 43 | } 44 | 1 45 | } 46 | }); 47 | } 48 | 49 | fn try_main() -> CompResult<()> { 50 | let mut config = Config::default(); 51 | let pseudo_globals = PseudoGlobals::default(); 52 | 53 | clapflags::validate(&mut config)?; 54 | 55 | unsafe { 56 | if setlocale(LC_CTYPE, ptr::null()) == ptr::null_mut() || XSupportsLocale() == 0 { 57 | return Die::stderr("warning: no locale support".to_owned()); 58 | } 59 | let dpy = XOpenDisplay(ptr::null_mut()); 60 | if dpy == ptr::null_mut() { 61 | return Die::stderr("cannot open display".to_owned()); 62 | } 63 | let screen = XDefaultScreen(dpy); 64 | let root = XRootWindow(dpy, screen); 65 | let parentwin = root.max(config.embed); 66 | let mut wa = MaybeUninit::::uninit(); 67 | XGetWindowAttributes(dpy, parentwin, wa.as_mut_ptr()); 68 | 69 | let mut drw = Drw::new(dpy, screen, root, wa.assume_init(), pseudo_globals, config)?; 70 | if cfg!(target_os = "openbsd") { 71 | pledge::pledge("stdio rpath", None) 72 | .map_err(|_| Die::Stderr("Could not pledge".to_owned()))?; 73 | } 74 | 75 | drw.setup(parentwin, root)?; 76 | drw.run() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/dmenu/plugin_entry.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use crate::clapflags::CLAP_FLAGS; 3 | use crate::drw::Drw; 4 | #[allow(unused_imports)] 5 | use crate::item::{Item, MatchCode}; 6 | #[allow(unused_imports)] 7 | use crate::result::*; 8 | 9 | use overrider::*; 10 | #[allow(unused_imports)] 11 | use regex::{Regex, RegexBuilder}; 12 | 13 | use crate::config::ConfigDefault; 14 | use crate::config::DefaultWidth; 15 | use crate::config::Schemes::*; 16 | 17 | #[default] 18 | impl Drw { 19 | /** 20 | * When taking input from stdin, apply post-processing 21 | */ 22 | pub fn format_stdin(&mut self, lines: Vec) -> CompResult> { 23 | Ok(lines) 24 | } 25 | 26 | /** 27 | * Every time the input is drawn, how should it be presented? 28 | * Does it need additional processing? 29 | */ 30 | pub fn format_input(&mut self) -> CompResult { 31 | Ok(self.input.clone()) 32 | } 33 | 34 | /** 35 | * What to do when printing to stdout / program termination? 36 | * 37 | * Args: 38 | * - output: what's being processed 39 | * - recommendation: is exiting recommended? C-Enter will not normally exit 40 | * 41 | * Returns - true if program should exit 42 | */ 43 | pub fn dispose(&mut self, output: String, recommendation: bool) -> CompResult { 44 | println!("{}", output); 45 | Ok(recommendation) 46 | } 47 | 48 | /** 49 | * The following is called immediatly after gen_matches, taking its unwrapped output 50 | * 51 | * This is particularly useful for doing something based on a match method defined 52 | * elsewhere. For example, if any matched items contain a key, highlight them, 53 | * but still allow a custom matching algorithm (such as from the fuzzy plugin) 54 | */ 55 | pub fn postprocess_matches(&mut self, items: Vec) -> CompResult> { 56 | Ok(items) 57 | } 58 | 59 | /** 60 | * Every time the input changes, what items should be shown 61 | * And, how should they be shown? 62 | * 63 | * Returns - Vector of items to be drawn 64 | */ 65 | pub fn gen_matches(&mut self) -> CompResult> { 66 | let re = RegexBuilder::new(®ex::escape(&self.input)) 67 | .case_insensitive(!self.config.case_sensitive) 68 | .build() 69 | .map_err(|_| Die::Stderr("Could not build regex".to_owned()))?; 70 | let mut exact: Vec = Vec::new(); 71 | let mut prefix: Vec = Vec::new(); 72 | let mut substring: Vec = Vec::new(); 73 | for item in self.get_items() { 74 | match item.matches(&re) { 75 | MatchCode::Exact => exact.push(item.clone()), 76 | MatchCode::Prefix => prefix.push(item.clone()), 77 | MatchCode::Substring => substring.push(item.clone()), 78 | MatchCode::None => {} 79 | } 80 | } 81 | exact.reserve(prefix.len() + substring.len()); 82 | for item in prefix { 83 | // extend is broken for pointers 84 | exact.push(item); 85 | } 86 | for item in substring { 87 | exact.push(item); 88 | } 89 | Ok(exact) 90 | } 91 | } 92 | 93 | /// The following are the default config values, loaded just after program init 94 | #[default] 95 | impl ConfigDefault { 96 | pub fn lines() -> u32 { 97 | 0 98 | } 99 | pub fn topbar() -> bool { 100 | true 101 | } 102 | pub fn prompt() -> String { 103 | String::new() 104 | } 105 | pub fn fontstrings() -> Vec { 106 | vec!["mono:size=10".to_owned()] 107 | } 108 | pub fn fast() -> bool { 109 | false 110 | } 111 | pub fn embed() -> u64 { 112 | 0 113 | } 114 | pub fn case_sensitive() -> bool { 115 | true 116 | } 117 | pub fn mon() -> i32 { 118 | -1 119 | } 120 | pub fn colors() -> [[[u8; 8]; 2]; SchemeLast as usize] { 121 | /* [ fg bg ]*/ 122 | let mut arr = [[[0; 8]; 2]; SchemeLast as usize]; 123 | arr[SchemeNorm as usize] = [*b"#bbbbbb\0", *b"#222222\0"]; 124 | arr[SchemeSel as usize] = [*b"#eeeeee\0", *b"#005577\0"]; 125 | arr[SchemeOut as usize] = [*b"#000000\0", *b"#00ffff\0"]; 126 | arr 127 | } 128 | pub fn nostdin() -> bool { 129 | false 130 | } 131 | pub fn render_minheight() -> u32 { 132 | 4 133 | } 134 | pub fn render_overrun() -> bool { 135 | false 136 | } 137 | pub fn render_flex() -> bool { 138 | false 139 | } 140 | pub fn render_rightalign() -> bool { 141 | false 142 | } 143 | pub fn render_default_width() -> DefaultWidth { 144 | DefaultWidth::Items 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/dmenu/result.rs: -------------------------------------------------------------------------------- 1 | /// This is the CompResult type 2 | /// It is used across the crate to propogate errors more easily 3 | pub type CompResult = Result; 4 | 5 | /// Its error is a Die 6 | /// When dieing, the the following options are given: 7 | /// - Stdout: print to stdout, exit with code 0 8 | /// - Stderr: print to stderr, exit with code 1 9 | /// If an empty string is returned, nothing is printed 10 | /// but return codes are obeyed 11 | pub enum Die { 12 | Stdout(String), 13 | Stderr(String), 14 | } 15 | 16 | /// The following are convienence methods for creating a Die 17 | /// For example, instead of the following: 18 | /// `return Err(Die::Stderr("fatal flaw".to_owned))` 19 | /// Use this instead: 20 | /// `return Die::stderr("fatal flaw".to_owned)` 21 | impl Die { 22 | pub fn stdout(msg: String) -> CompResult { 23 | Self::Stdout(msg).into() 24 | } 25 | pub fn stderr(msg: String) -> CompResult { 26 | Self::Stderr(msg).into() 27 | } 28 | } 29 | 30 | impl From for CompResult { 31 | fn from(error: Die) -> Self { 32 | Err(error) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/dmenu/setup.rs: -------------------------------------------------------------------------------- 1 | use libc::{c_char, c_int, c_long, c_void}; 2 | use std::mem::MaybeUninit; 3 | use std::ptr; 4 | use x11::xinerama::{XineramaQueryScreens, XineramaScreenInfo}; 5 | use x11::xlib::{ 6 | CWBackPixel, CWEventMask, CWOverrideRedirect, ExposureMask, FocusChangeMask, KeyPressMask, 7 | PointerRoot, SubstructureNotifyMask, VisibilityChangeMask, Window, XClassHint, XCreateIC, 8 | XCreateWindow, XFree, XGetInputFocus, XGetWindowAttributes, XIMPreeditNothing, 9 | XIMStatusNothing, XMapRaised, XOpenIM, XQueryPointer, XQueryTree, XSelectInput, XSetClassHint, 10 | XSetWindowAttributes, 11 | }; 12 | 13 | use crate::additional_bindings::xlib::{XNClientWindow, XNFocusWindow, XNInputStyle}; 14 | use crate::config::{Clrs::*, Schemes::*}; 15 | use crate::drw::Drw; 16 | use crate::result::*; 17 | use crate::util::grabfocus; 18 | 19 | #[inline] 20 | fn intersect(x: c_int, y: c_int, w: c_int, h: c_int, r: *mut XineramaScreenInfo) -> c_int { 21 | unsafe { 22 | 0.max((x + w).min(((*r).x_org + (*r).width) as c_int) - x.max((*r).x_org as c_int)) 23 | * 0.max((y + h).min(((*r).y_org + (*r).height) as c_int) - y.max((*r).y_org as c_int)) 24 | } 25 | } 26 | 27 | impl Drw { 28 | pub fn setup(&mut self, parentwin: u64, root: u64) -> CompResult<()> { 29 | unsafe { 30 | let mut x: c_int; 31 | let mut y: c_int; 32 | 33 | let mut ch: XClassHint = XClassHint { 34 | res_name: (*b"dmenu\0").as_ptr() as *mut c_char, 35 | res_class: (*b"dmenu\0").as_ptr() as *mut c_char, 36 | }; 37 | 38 | // appearances are set up in constructor 39 | 40 | self.pseudo_globals.bh = (self.fonts.iter().map(|f| f.height).max().unwrap() + 4) 41 | .max(self.config.render_minheight); 42 | self.h = ((self.config.lines + 1) * self.pseudo_globals.bh) as c_int; 43 | 44 | let mut dws: *mut Window = ptr::null_mut(); 45 | let mut w = MaybeUninit::::uninit(); 46 | let mut dw = MaybeUninit::::uninit(); 47 | let n: c_int; 48 | let info = if cfg!(feature = "Xinerama") && parentwin == root { 49 | let mut __n = MaybeUninit::uninit(); 50 | let ret = XineramaQueryScreens(self.dpy, __n.as_mut_ptr()); 51 | n = __n.assume_init(); 52 | ret 53 | } else { 54 | // Setting n=0 isn't required here, but rustc isn't smart enough 55 | // to realize that the only use of n can't occur if this branch is taken 56 | n = 0; 57 | ptr::null_mut() 58 | }; 59 | if cfg!(feature = "Xinerama") && info != ptr::null_mut() { 60 | let mut i = 0; 61 | let mut area = 0; 62 | let mut di = MaybeUninit::::uninit(); 63 | let mut a; 64 | let mut pw; 65 | 66 | XGetInputFocus(self.dpy, w.as_mut_ptr(), di.as_mut_ptr()); 67 | if self.config.mon >= 0 && self.config.mon < n { 68 | i = self.config.mon; 69 | } else if w.assume_init() != root 70 | && w.assume_init() != PointerRoot as u64 71 | && w.assume_init() != 0 72 | { 73 | /* find top-level window containing current input focus */ 74 | while { 75 | pw = w.assume_init(); 76 | let mut _du = MaybeUninit::uninit(); 77 | if XQueryTree( 78 | self.dpy, 79 | pw, 80 | dw.as_mut_ptr(), 81 | w.as_mut_ptr(), 82 | &mut dws, 83 | _du.as_mut_ptr(), 84 | ) != 0 85 | && dws != ptr::null_mut() 86 | { 87 | XFree(dws as *mut c_void); 88 | } 89 | w.assume_init() != root && w.assume_init() != pw 90 | } {} // do-while 91 | /* find xinerama screen with which the window intersects most */ 92 | if XGetWindowAttributes(self.dpy, pw, &mut self.wa) != 0 { 93 | for j in 0..n { 94 | a = intersect( 95 | self.wa.x, 96 | self.wa.y, 97 | self.wa.width, 98 | self.wa.height, 99 | info.offset(j as isize), 100 | ); 101 | if a > area { 102 | area = a; 103 | i = j; 104 | } 105 | } 106 | } 107 | } 108 | /* no focused window is on screen, so use pointer location instead */ 109 | let mut _du = MaybeUninit::uninit(); 110 | let mut __x = MaybeUninit::uninit(); 111 | let mut __y = MaybeUninit::uninit(); 112 | if self.config.mon < 0 113 | && area == 0 114 | && XQueryPointer( 115 | self.dpy, 116 | root, 117 | dw.as_mut_ptr(), 118 | dw.as_mut_ptr(), 119 | __x.as_mut_ptr(), 120 | __y.as_mut_ptr(), 121 | di.as_mut_ptr(), 122 | di.as_mut_ptr(), 123 | _du.as_mut_ptr(), 124 | ) != 0 125 | { 126 | x = __x.assume_init(); 127 | y = __y.assume_init(); 128 | for j in 0..n { 129 | i = j; // this is here to bypass rust's shadowing rules in an efficient way 130 | if intersect(x, y, 1, 1, info.offset(i as isize)) != 0 { 131 | break; 132 | } 133 | } 134 | } 135 | x = (*info.offset(i as isize)).x_org as c_int; 136 | y = (*info.offset(i as isize)).y_org as c_int 137 | + (if self.config.topbar { 138 | 0 139 | } else { 140 | (*info.offset(i as isize)).height as c_int - self.h as c_int 141 | }); 142 | self.w = (*info.offset(i as isize)).width as c_int; 143 | XFree(info as *mut c_void); 144 | } else { 145 | if XGetWindowAttributes(self.dpy, parentwin, &mut self.wa) == 0 { 146 | return Die::stderr(format!( 147 | "could not get embedding window attributes: 0x{:?}", 148 | parentwin 149 | )); 150 | } 151 | x = 0; 152 | y = if self.config.topbar { 153 | 0 154 | } else { 155 | self.wa.height - self.h as c_int 156 | }; 157 | self.w = self.wa.width; 158 | } 159 | 160 | let mut swa = XSetWindowAttributes { 161 | override_redirect: true as i32, 162 | background_pixel: (*self.pseudo_globals.schemeset[SchemeNorm as usize] 163 | [ColBg as usize]) 164 | .pixel, 165 | event_mask: ExposureMask | KeyPressMask | VisibilityChangeMask, 166 | background_pixmap: 0, 167 | backing_pixel: 0, 168 | backing_store: 0, 169 | backing_planes: 0, 170 | bit_gravity: 0, 171 | border_pixel: 0, 172 | border_pixmap: 0, 173 | colormap: 0, 174 | cursor: 0, 175 | do_not_propagate_mask: false as c_long, 176 | save_under: 0, 177 | win_gravity: 0, 178 | }; 179 | self.pseudo_globals.win = XCreateWindow( 180 | self.dpy, 181 | parentwin, 182 | x, 183 | y, 184 | self.w as u32, 185 | self.h as u32, 186 | 0, 187 | 0, 188 | 0, 189 | ptr::null_mut(), 190 | CWOverrideRedirect | CWBackPixel | CWEventMask, 191 | &mut swa, 192 | ); 193 | XSetClassHint(self.dpy, self.pseudo_globals.win, &mut ch); 194 | 195 | /* input methods */ 196 | let xim = XOpenIM(self.dpy, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()); 197 | if xim == ptr::null_mut() { 198 | return Die::stderr("XOpenIM failed: could not open input device".to_owned()); 199 | } 200 | 201 | self.pseudo_globals.xic = XCreateIC( 202 | xim, 203 | XNInputStyle, 204 | XIMPreeditNothing | XIMStatusNothing, 205 | XNClientWindow, 206 | self.pseudo_globals.win, 207 | XNFocusWindow, 208 | self.pseudo_globals.win, 209 | ptr::null_mut::(), 210 | ); 211 | // void* makes sure the value is large enough for varargs to properly stop 212 | // parsing. Any smaller and it will skip over, causing a segfault 213 | 214 | XMapRaised(self.dpy, self.pseudo_globals.win); 215 | 216 | if self.config.embed != 0 { 217 | XSelectInput( 218 | self.dpy, 219 | parentwin, 220 | FocusChangeMask | SubstructureNotifyMask, 221 | ); 222 | let mut du = MaybeUninit::uninit(); 223 | if XQueryTree( 224 | self.dpy, 225 | parentwin, 226 | dw.as_mut_ptr(), 227 | w.as_mut_ptr(), 228 | &mut dws, 229 | du.as_mut_ptr(), 230 | ) != 0 231 | && dws != ptr::null_mut() 232 | { 233 | for i in 0..du.assume_init() { 234 | if *dws.offset(i as isize) == self.pseudo_globals.win { 235 | break; 236 | } 237 | XSelectInput(self.dpy, *dws.offset(i as isize), FocusChangeMask); 238 | } 239 | XFree(dws as *mut c_void); 240 | } 241 | grabfocus(self)?; 242 | } 243 | 244 | self.draw() 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/dmenu/util.rs: -------------------------------------------------------------------------------- 1 | use crate::drw::Drw; 2 | use crate::item::Item; 3 | use crate::result::*; 4 | use std::io::{self, BufRead}; 5 | use std::mem::MaybeUninit; 6 | use std::thread::sleep; 7 | use std::time::Duration; 8 | use x11::xlib::{ 9 | CurrentTime, Display, GrabModeAsync, GrabSuccess, RevertToParent, True, Window, 10 | XDefaultRootWindow, XGetInputFocus, XGrabKeyboard, XSetInputFocus, 11 | }; 12 | 13 | pub fn readstdin(drw: &mut Drw) -> CompResult> { 14 | let mut lines: Vec = Vec::new(); 15 | for line in io::stdin().lock().lines() { 16 | match line { 17 | Ok(l) => lines.push(l), 18 | Err(e) => return Die::stderr(format!("Could not read from stdin: {}", e)), 19 | } 20 | } 21 | let mut ret = Vec::new(); 22 | for line in drw.format_stdin(lines)?.into_iter() { 23 | let item = Item::new(line, false, drw)?; 24 | if item.width as i32 > drw.pseudo_globals.inputw { 25 | drw.pseudo_globals.inputw = item.width as i32; 26 | } 27 | ret.push(item) 28 | } 29 | Ok(ret) 30 | } 31 | 32 | pub fn grabkeyboard(dpy: *mut Display, embed: Window) -> CompResult<()> { 33 | let ts = Duration::from_millis(1); 34 | 35 | if embed != 0 { 36 | return Ok(()); 37 | } 38 | /* try to grab keyboard, we may have to wait for another process to ungrab */ 39 | for _ in 0..1000 { 40 | if unsafe { 41 | XGrabKeyboard( 42 | dpy, 43 | XDefaultRootWindow(dpy), 44 | True, 45 | GrabModeAsync, 46 | GrabModeAsync, 47 | CurrentTime, 48 | ) == GrabSuccess 49 | } { 50 | return Ok(()); 51 | } 52 | sleep(ts); 53 | } 54 | Die::stderr("cannot grab keyboard".to_owned()) 55 | } 56 | 57 | pub fn grabfocus(drw: &Drw) -> CompResult<()> { 58 | unsafe { 59 | let ts = Duration::from_millis(1); 60 | let mut focuswin = MaybeUninit::::uninit(); 61 | let mut revertwin = MaybeUninit::uninit(); 62 | 63 | for _ in 0..100 { 64 | XGetInputFocus(drw.dpy, focuswin.as_mut_ptr(), revertwin.as_mut_ptr()); 65 | if focuswin.assume_init() == drw.pseudo_globals.win { 66 | return Ok(()); 67 | } 68 | XSetInputFocus(drw.dpy, drw.pseudo_globals.win, RevertToParent, CurrentTime); 69 | sleep(ts); 70 | } 71 | Die::stderr("cannot grab focus".to_owned()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/headers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "headers" 3 | version = "0.1.0" 4 | authors = ["Shizcow "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | termcolor = "1.1" 9 | 10 | [build-dependencies] 11 | bindgen = "0.64.0" -------------------------------------------------------------------------------- /src/headers/README.md: -------------------------------------------------------------------------------- 1 | # headers 2 | 3 | These files contain some basic includes for C++ libraries: 4 | - fontconfig.h 5 | - xinerama.h 6 | - xlib.h 7 | 8 | These files are parsed by bindgen, providing rust bindings 9 | for some additional functions which the default x11 and 10 | fontconfig crates don't provide or get incorrect. 11 | -------------------------------------------------------------------------------- /src/headers/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | // bindgen is pretty slow, so we add a layer of indirection, 4 | // making sure it's only ran when needed. build.rs has great 5 | // support for that, so here it is 6 | fn main() { 7 | let mut target_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); 8 | target_path.pop(); 9 | target_path.pop(); 10 | target_path = target_path.join("target"); 11 | let build_path = target_path.join("build"); 12 | 13 | // servo-fontconfig does a good job for 99% of fontconfig, 14 | // but doesn't quite get everything we need. 15 | // So, generate bindings here. 16 | let mut builder_main = bindgen::Builder::default(); 17 | builder_main = builder_main.header("src/fontconfig.h"); 18 | builder_main = builder_main.header("src/xinerama.h"); 19 | 20 | builder_main 21 | .parse_callbacks(Box::new(bindgen::CargoCallbacks)) 22 | .generate() 23 | .expect("Unable to generate bindings_main") 24 | .write_to_file(build_path.join("bindings_main.rs")) 25 | .expect("Couldn't write bindings_main!"); 26 | 27 | // Additionally, the x11 crate doesn't null terminate its strings for some 28 | // strange reason, so a bit of extra work is required 29 | bindgen::Builder::default() 30 | .header("src/xlib.h") 31 | .ignore_functions() // strip out unused and warning-prone functions 32 | .parse_callbacks(Box::new(bindgen::CargoCallbacks)) 33 | .generate() 34 | .expect("Unable to generate bindings_xlib") 35 | .write_to_file(build_path.join("bindings_xlib.rs")) 36 | .expect("Couldn't write bindings_xlib!"); 37 | } 38 | -------------------------------------------------------------------------------- /src/headers/src/fontconfig.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define __redef_tmp FC_COLOR 4 | #undef FC_COLOR 5 | #define FC_COLOR (__redef_tmp "\0") 6 | #undef __redef_tmp 7 | -------------------------------------------------------------------------------- /src/headers/src/main.rs: -------------------------------------------------------------------------------- 1 | use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; 2 | 3 | fn main() { 4 | let mut stdout = StandardStream::stdout(ColorChoice::Always); 5 | stdout 6 | .set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true)) 7 | .expect("Could not grab stdout!"); 8 | print!("SUCCESS "); 9 | stdout 10 | .set_color(ColorSpec::new().set_bold(true)) 11 | .expect("Could not grab stdout!"); 12 | println!("Headers generated successfully"); 13 | } 14 | -------------------------------------------------------------------------------- /src/headers/src/xinerama.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /src/headers/src/xlib.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // The following just add null chars to their strings 4 | // This is implicit in C, but bindgen doesn't include 5 | // these characters. Doing this makes code much cleaner 6 | 7 | #define __redef_tmp XNInputStyle 8 | #undef XNInputStyle 9 | #define XNInputStyle (__redef_tmp "\0") 10 | #undef __redef_tmp 11 | 12 | #define __redef_tmp XNClientWindow 13 | #undef XNClientWindow 14 | #define XNClientWindow (__redef_tmp "\0") 15 | #undef __redef_tmp 16 | 17 | #define __redef_tmp XNFocusWindow 18 | #undef XNFocusWindow 19 | #define XNFocusWindow (__redef_tmp "\0") 20 | #undef __redef_tmp 21 | -------------------------------------------------------------------------------- /src/man/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "man_dmenu" 3 | version = "0.1.0" 4 | authors = ["Shizcow "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | itertools = "0.10.5" 9 | -------------------------------------------------------------------------------- /src/man/README.md: -------------------------------------------------------------------------------- 1 | # man 2 | 3 | This folder contains man pages for `dmenu(1)` and `stest(1)`. 4 | 5 | While the `stest(1)` manpage is complete, the `dmenu(1)` page is not. 6 | It is constructed during a release build from `build.rs`. 7 | -------------------------------------------------------------------------------- /src/man/dmenu.1: -------------------------------------------------------------------------------- 1 | .TH DMENU 1 dmenu\-VERSION 2 | .SH NAME 3 | dmenu \- dynamic menu 4 | .SH SYNOPSIS 5 | .B dmenu 6 | .RB [ \-bfiv ] 7 | .RB [ \-l 8 | .IR lines ] 9 | .RB [ \-m 10 | .IR monitor ] 11 | .RB [ \-p 12 | .IR prompt ] 13 | .RB [ \-fn 14 | .IR font ] 15 | .RB [ \-nb 16 | .IR color ] 17 | .RB [ \-nf 18 | .IR color ] 19 | .RB [ \-sb 20 | .IR color ] 21 | .RB [ \-sf 22 | .IR color ] 23 | .RB [ \-w 24 | .IR windowid ] 25 | .P 26 | .BR dmenu_run " ..." 27 | .SH DESCRIPTION 28 | .B dmenu 29 | is a dynamic menu for X, which reads a list of newline\-separated items from 30 | stdin. When the user selects an item and presses Return, their choice is printed 31 | to stdout and dmenu terminates. Entering text will narrow the items to those 32 | matching the tokens in the input. 33 | .P 34 | .B dmenu_run 35 | is a script used by 36 | .IR dwm (1) 37 | which lists programs in the user's $PATH and runs the result in their $SHELL. 38 | .SH OPTIONS 39 | .TP 40 | .B \-b 41 | dmenu appears at the bottom of the screen. 42 | .TP 43 | .B \-f 44 | dmenu grabs the keyboard before reading stdin if not reading from a tty. This 45 | is faster, but will lock up X until stdin reaches end\-of\-file. 46 | .TP 47 | .B \-i 48 | dmenu matches menu items case insensitively. 49 | .TP 50 | .BI \-l " lines" 51 | dmenu lists items vertically, with the given number of lines. 52 | .TP 53 | .BI \-m " monitor" 54 | dmenu is displayed on the monitor number supplied. Monitor numbers are starting 55 | from 0. 56 | .TP 57 | .BI \-p " prompt" 58 | defines the prompt to be displayed to the left of the input field. 59 | .TP 60 | .BI \-fn " font" 61 | defines the font or font set used. 62 | .TP 63 | .BI \-nb " color" 64 | defines the normal background color. 65 | .IR #RGB , 66 | .IR #RRGGBB , 67 | and X color names are supported. 68 | .TP 69 | .BI \-nf " color" 70 | defines the normal foreground color. 71 | .TP 72 | .BI \-sb " color" 73 | defines the selected background color. 74 | .TP 75 | .BI \-sf " color" 76 | defines the selected foreground color. 77 | .TP 78 | .B \-v 79 | prints version information to stdout, then exits. 80 | .TP 81 | .BI \-w " windowid" 82 | embed into windowid. 83 | -------------------------------------------------------------------------------- /src/man/src/lib.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use std::fs::File; 3 | use std::io::{Read, Write}; 4 | use std::path::PathBuf; 5 | 6 | struct Arg { 7 | short: Option, 8 | long: Option, 9 | inputs: Vec, 10 | info: String, 11 | } 12 | 13 | impl Arg { 14 | pub fn new( 15 | short: Option, 16 | long: Option, 17 | inputs: Vec, 18 | info: String, 19 | ) -> Self { 20 | Self { 21 | short, 22 | long, 23 | inputs, 24 | info, 25 | } 26 | } 27 | } 28 | 29 | pub struct Manpage { 30 | name: String, 31 | version: String, 32 | section: u32, 33 | desc_short: String, 34 | descriptions: Vec<(String, String)>, 35 | args: Vec, 36 | buildmsg: Option, 37 | plugins: Vec<(String, String)>, 38 | } 39 | 40 | impl Manpage { 41 | pub fn new(name: &str, version: &str, section: u32) -> Self { 42 | Self { 43 | name: name.to_string(), 44 | version: version.to_string(), 45 | section, 46 | desc_short: String::new(), 47 | descriptions: Vec::new(), 48 | args: Vec::new(), 49 | buildmsg: None, 50 | plugins: Vec::new(), 51 | } 52 | } 53 | 54 | pub fn desc_short(&mut self, desc_short: &str) -> &mut Self { 55 | self.desc_short = desc_short.to_string(); 56 | self 57 | } 58 | 59 | pub fn description(&mut self, name: &str, desc: &str) -> &mut Self { 60 | self.descriptions.push((name.to_string(), desc.to_string())); 61 | self 62 | } 63 | 64 | pub fn build(&mut self, desc: &str) -> &mut Self { 65 | self.buildmsg = Some(desc.to_string()); 66 | self 67 | } 68 | 69 | pub fn arg( 70 | &mut self, 71 | short: Option, 72 | long: Option, 73 | inputs: Vec, 74 | info: String, 75 | ) -> &mut Self { 76 | self.args.push(Arg::new(short, long, inputs, info)); 77 | self 78 | } 79 | 80 | pub fn plugin(&mut self, name: String, desc: String) -> &mut Self { 81 | self.plugins.push((name, desc)); 82 | self 83 | } 84 | 85 | pub fn write_to_file(&self, path: PathBuf) { 86 | let heading = format!( 87 | ".TH {} {} {}\\-{}", 88 | self.name.to_uppercase(), 89 | self.section, 90 | self.name, 91 | self.version 92 | ); 93 | let name = format!(".SH NAME\n{} \\- {}", self.name, self.desc_short); 94 | 95 | let description = format!( 96 | ".SH DESCRIPTION\n{}", 97 | self.descriptions 98 | .iter() 99 | .map(|(name, description)| { format!(".B {}\n{}", name, description) }) 100 | .join("\n.P\n") 101 | ); 102 | let (synopsis, options) = self.gen_argstrs(); 103 | 104 | let mut usage_file = 105 | File::open(concat!(env!("CARGO_MANIFEST_DIR"), "/src/usage.1")).unwrap(); 106 | let mut usage = String::new(); 107 | if let Err(err) = usage_file.read_to_string(&mut usage) { 108 | panic!("Could not read usage man file {}", err); 109 | } 110 | let mut see_also_file = 111 | File::open(concat!(env!("CARGO_MANIFEST_DIR"), "/src/see_also.1")).unwrap(); 112 | let mut see_also = String::new(); 113 | if let Err(err) = see_also_file.read_to_string(&mut see_also) { 114 | panic!("Could not read see_also man file {}", err); 115 | } 116 | 117 | let build = if let Some(build) = &self.buildmsg { 118 | let mut ret = format!(".SH BUILD\n{}", build); 119 | if self.plugins.len() > 0 { 120 | ret.push_str("\ndmenu-rs has been compiled with the following plugins:\n"); 121 | let descs = self 122 | .plugins 123 | .iter() 124 | .map(|(name, description)| { 125 | format!(".TP\n.B {}\n{}", name, description.replace("\n", "\n.br\n")) 126 | }) 127 | .join("\n"); 128 | ret.push_str(&descs); 129 | } 130 | ret 131 | } else { 132 | "".to_string() 133 | }; 134 | 135 | let manpage = vec![ 136 | heading, 137 | name, 138 | synopsis, 139 | build, 140 | description, 141 | options, 142 | usage, 143 | see_also, 144 | ] 145 | .join("\n"); 146 | match File::create(&path) { 147 | Ok(mut file) => { 148 | if let Err(err) = file.write_all(manpage.as_bytes()) { 149 | panic!( 150 | "Could not write to file '{}': {}", 151 | path.to_string_lossy(), 152 | err 153 | ); 154 | } 155 | } 156 | Err(err) => panic!( 157 | "Could not open file '{}' for writing: {}", 158 | path.to_string_lossy(), 159 | err 160 | ), 161 | } 162 | } 163 | 164 | fn gen_argstrs(&self) -> (String, String) { 165 | let mut arg_shorts = Vec::new(); 166 | let mut arg_other_short = Vec::new(); 167 | let mut arg_other_long = Vec::new(); 168 | let mut arg_other_both = Vec::new(); 169 | for arg in &self.args { 170 | match (arg.short, arg.long.as_ref(), arg.inputs.len()) { 171 | (Some(_), None, 0) => arg_shorts.push(arg), 172 | (Some(_), None, _) => arg_other_short.push(arg), 173 | (None, Some(_), _) => arg_other_long.push(arg), 174 | (Some(_), Some(_), _) => arg_other_both.push(arg), 175 | (None, None, _) => panic!("yaml arguement must have some flag"), 176 | } 177 | } 178 | 179 | let synopsis_shorts_str = if arg_shorts.len() == 0 { 180 | String::new() 181 | } else { 182 | format!( 183 | ".RB [ \\-{} ]\n", 184 | arg_shorts 185 | .iter() 186 | .map(|arg| arg.short.unwrap()) 187 | .collect::() 188 | ) 189 | }; 190 | 191 | let synopsis_other_short_str = if arg_other_short.len() == 0 { 192 | String::new() 193 | } else { 194 | format!( 195 | "{}\n", 196 | arg_other_short 197 | .iter() 198 | .map(|syn| { 199 | format!( 200 | ".RB [ \\-{}\n{} ]", 201 | syn.short.unwrap(), 202 | syn.inputs 203 | .iter() 204 | .map(|name| { format!(".IR {}", name) }) 205 | .join("\n") 206 | ) 207 | }) 208 | .join("\n") 209 | ) 210 | }; 211 | 212 | let synopsis_other_long_str = if arg_other_long.len() == 0 { 213 | String::new() 214 | } else { 215 | format!( 216 | "{}\n", 217 | arg_other_long 218 | .iter() 219 | .map(|syn| { 220 | if syn.inputs.len() > 0 { 221 | format!( 222 | ".RB [ \\-\\-{}\n{} ]", 223 | syn.long.as_ref().unwrap(), 224 | syn.inputs 225 | .iter() 226 | .map(|name| { format!(".IR {}", name) }) 227 | .join("\n") 228 | ) 229 | } else { 230 | format!(".RB [ \\-\\-{} ]", syn.long.as_ref().unwrap()) 231 | } 232 | }) 233 | .join("\n") 234 | ) 235 | }; 236 | 237 | let synopsis_other_both_str = if arg_other_both.len() == 0 { 238 | String::new() 239 | } else { 240 | format!( 241 | "{}\n", 242 | arg_other_both 243 | .iter() 244 | .map(|syn| { 245 | if syn.inputs.len() > 0 { 246 | format!( 247 | ".RB [ \\-{}|\\-\\-{}\n{} ]", 248 | syn.short.unwrap(), 249 | syn.long.as_ref().unwrap(), 250 | syn.inputs 251 | .iter() 252 | .map(|name| { format!(".IR {}", name) }) 253 | .join("\n") 254 | ) 255 | } else { 256 | format!( 257 | ".RB [ \\-{}|\\-\\-{} ]", 258 | syn.short.unwrap(), 259 | syn.long.as_ref().unwrap() 260 | ) 261 | } 262 | }) 263 | .join("\n") 264 | ) 265 | }; 266 | 267 | let synopsis = format!( 268 | ".SH SYNOPSIS\n\ 269 | .B {}\n\ 270 | {}{}{}{}\n\ 271 | .P\n\ 272 | .BR dmenu_run \" ...\"", 273 | self.name, 274 | synopsis_shorts_str, 275 | synopsis_other_short_str, 276 | synopsis_other_long_str, 277 | synopsis_other_both_str 278 | ); 279 | 280 | let options_short = if arg_shorts.len() == 0 { 281 | String::new() 282 | } else { 283 | format!( 284 | "{}\n", 285 | arg_shorts 286 | .into_iter() 287 | .map(|arg| { format!(".TP\n.B \\-{}\n{}", arg.short.unwrap(), arg.info) }) 288 | .join("\n") 289 | ) 290 | }; 291 | 292 | let options_other_short = if arg_other_short.len() == 0 { 293 | String::new() 294 | } else { 295 | format!( 296 | "{}\n", 297 | arg_other_short 298 | .into_iter() 299 | .map(|arg| { 300 | format!( 301 | ".TP\n.BI \\-{} \" {}\"\n{}", 302 | arg.short.unwrap(), 303 | arg.inputs.join(" "), 304 | arg.info 305 | ) 306 | }) 307 | .join("\n") 308 | ) 309 | }; 310 | 311 | let options_other_long = if arg_other_long.len() == 0 { 312 | String::new() 313 | } else { 314 | format!( 315 | "{}\n", 316 | arg_other_long 317 | .into_iter() 318 | .map(|arg| { 319 | format!( 320 | ".TP\n.BI \\-\\-{} \" {}\"\n{}", 321 | arg.long.as_ref().unwrap(), 322 | arg.inputs.join(" "), 323 | arg.info 324 | ) 325 | }) 326 | .join("\n") 327 | ) 328 | }; 329 | 330 | let options_other_both = if arg_other_both.len() == 0 { 331 | String::new() 332 | } else { 333 | format!( 334 | "{}\n", 335 | arg_other_both 336 | .into_iter() 337 | .map(|arg| { 338 | format!( 339 | ".TP\n\ 340 | \\fB\\-{}\\fP{}\\fI{}\\/\\fP, \ 341 | \\fB\\-\\-\\fP{} \\fI{}\\/\\fP \ 342 | \n{}", 343 | arg.short.unwrap(), 344 | if arg.inputs.len() == 0 { "" } else { " " }, 345 | arg.inputs.join(" "), 346 | arg.long.as_ref().unwrap(), 347 | arg.inputs.join(" "), 348 | arg.info 349 | ) 350 | }) 351 | .join("\n") 352 | ) 353 | }; 354 | 355 | let options = format!( 356 | ".SH OPTIONS\n{}{}{}{}", 357 | options_short, options_other_short, options_other_long, options_other_both 358 | ); 359 | 360 | (synopsis, options) 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/man/src/see_also.1: -------------------------------------------------------------------------------- 1 | .SH SEE ALSO 2 | .IR dwm (1), 3 | .IR stest (1) 4 | -------------------------------------------------------------------------------- /src/man/src/stest.1: -------------------------------------------------------------------------------- 1 | .TH STEST 1 dmenu\-VERSION 2 | .SH NAME 3 | stest \- filter a list of files by properties 4 | .SH SYNOPSIS 5 | .B stest 6 | .RB [ -abcdefghlpqrsuwx ] 7 | .RB [ -n 8 | .IR file ] 9 | .RB [ -o 10 | .IR file ] 11 | .RI [ file ...] 12 | .SH DESCRIPTION 13 | .B stest 14 | takes a list of files and filters by the files' properties, analogous to 15 | .IR test (1). 16 | Files which pass all tests are printed to stdout. If no files are given, stest 17 | reads files from stdin. 18 | .SH OPTIONS 19 | .TP 20 | .B \-a 21 | Test hidden files. 22 | .TP 23 | .B \-b 24 | Test that files are block specials. 25 | .TP 26 | .B \-c 27 | Test that files are character specials. 28 | .TP 29 | .B \-d 30 | Test that files are directories. 31 | .TP 32 | .B \-e 33 | Test that files exist. 34 | .TP 35 | .B \-f 36 | Test that files are regular files. 37 | .TP 38 | .B \-g 39 | Test that files have their set-group-ID flag set. 40 | .TP 41 | .B \-h 42 | Test that files are symbolic links. 43 | .TP 44 | .B \-l 45 | Test the contents of a directory given as an argument. 46 | .TP 47 | .BI \-n " file" 48 | Test that files are newer than 49 | .IR file . 50 | .TP 51 | .BI \-o " file" 52 | Test that files are older than 53 | .IR file . 54 | .TP 55 | .B \-p 56 | Test that files are named pipes. 57 | .TP 58 | .B \-q 59 | No files are printed, only the exit status is returned. 60 | .TP 61 | .B \-r 62 | Test that files are readable. 63 | .TP 64 | .B \-s 65 | Test that files are not empty. 66 | .TP 67 | .B \-u 68 | Test that files have their set-user-ID flag set. 69 | .TP 70 | .B \-v 71 | Invert the sense of tests, only failing files pass. 72 | .TP 73 | .B \-w 74 | Test that files are writable. 75 | .TP 76 | .B \-x 77 | Test that files are executable. 78 | .SH EXIT STATUS 79 | .TP 80 | .B 0 81 | At least one file passed all tests. 82 | .TP 83 | .B 1 84 | No files passed all tests. 85 | .TP 86 | .B 2 87 | An error occurred. 88 | .SH SEE ALSO 89 | .IR dmenu (1), 90 | .IR test (1) 91 | -------------------------------------------------------------------------------- /src/man/src/usage.1: -------------------------------------------------------------------------------- 1 | .SH USAGE 2 | dmenu is completely controlled by the keyboard. Items are selected using the 3 | arrow keys, page up, page down, home, and end. 4 | .TP 5 | .B Tab 6 | Copy the selected item to the input field. 7 | .TP 8 | .B Return 9 | Confirm selection. Prints the selected item to stdout and exits, returning 10 | success. 11 | .TP 12 | .B Ctrl-Return 13 | Confirm selection. Prints the selected item to stdout and continues. 14 | .TP 15 | .B Shift\-Return 16 | Confirm input. Prints the input text to stdout and exits, returning success. 17 | .TP 18 | .B Escape 19 | Exit without selecting an item, returning failure. 20 | .TP 21 | .B Ctrl-Left 22 | Move cursor to the start of the current word 23 | .TP 24 | .B Ctrl-Right 25 | Move cursor to the end of the current word 26 | .TP 27 | .B C\-a 28 | Home 29 | .TP 30 | .B C\-b 31 | Left 32 | .TP 33 | .B C\-c 34 | Escape 35 | .TP 36 | .B C\-d 37 | Delete 38 | .TP 39 | .B C\-e 40 | End 41 | .TP 42 | .B C\-f 43 | Right 44 | .TP 45 | .B C\-g 46 | Escape 47 | .TP 48 | .B C\-h 49 | Backspace 50 | .TP 51 | .B C\-i 52 | Tab 53 | .TP 54 | .B C\-j 55 | Return 56 | .TP 57 | .B C\-J 58 | Shift-Return 59 | .TP 60 | .B C\-k 61 | Delete line right 62 | .TP 63 | .B C\-m 64 | Return 65 | .TP 66 | .B C\-M 67 | Shift-Return 68 | .TP 69 | .B C\-n 70 | Down 71 | .TP 72 | .B C\-p 73 | Up 74 | .TP 75 | .B C\-u 76 | Delete line left 77 | .TP 78 | .B C\-w 79 | Delete word left 80 | .TP 81 | .B C\-y 82 | Paste from primary X selection 83 | .TP 84 | .B C\-Y 85 | Paste from X clipboard 86 | .TP 87 | .B M\-b 88 | Move cursor to the start of the current word 89 | .TP 90 | .B M\-f 91 | Move cursor to the end of the current word 92 | .TP 93 | .B M\-g 94 | Home 95 | .TP 96 | .B M\-G 97 | End 98 | .TP 99 | .B M\-h 100 | Up 101 | .TP 102 | .B M\-j 103 | Page down 104 | .TP 105 | .B M\-k 106 | Page up 107 | .TP 108 | .B M\-l 109 | Down 110 | -------------------------------------------------------------------------------- /src/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Plugin development 2 | 3 | 0. [Introduction](#introduction) 4 | 1. [Directory Structure](#directory-structure) 5 | 2. [Building](#building) 6 | a. [Testing Changes](#testing-changes) 7 | 3. [Files](#files) 8 | a. [plugin.yml](#pluginyml) 9 | b. [main.rs](#mainrs) 10 | c. [deps.toml](#depstoml) 11 | 4. [Manpage Generation](#manpage-generation) 12 | 5. [What Functionality Can Be Changed](#what-functionality-can-be-changed) 13 | 6. [Quickstart](#quickstart) 14 | a. [Cloning The Project](#cloning-the-project) 15 | b. [Setting Up Files](#setting-up-files) 16 | c. [Compiling The Plugin](#compiling-the-plugin) 17 | d. [CompResult](#compresult) 18 | 19 | ## Introduction 20 | 21 | Developing plugins for dmenu-rs is a relatively simple process. Because this project is still 22 | young, only a small part of the internal API is exposed. Raise an issue if you'd like 23 | something specific exposed, or if instructions here are unclear. 24 | 25 | If you write a decent plugin, feel welcome to submit a pull request to have it included upstream. 26 | 27 | ## Directory structure 28 | The basic structure of a plugin is as follows, starting from project root: 29 | ``` 30 | dmenu-rs 31 | └─src 32 | └─plugins 33 | └─PLUGIN_NAME 34 | └─plugin.yml 35 | └─main.rs 36 | └─deps.toml 37 | ``` 38 | To start developing a new plugin, simply add a new folder to `src/plugins` with the name the 39 | same as the plugin name. The individual files are described in the **Files** section below. 40 | 41 | ## Building 42 | The build process for plugins is dead-simple. During the process of running `make`, a few 43 | things will be automatically configured: 44 | - `config.mk` is read to determine which plugins should be compiled in 45 | - Command line arguments are added to `clap`, so they will work out of the box 46 | - Command line arguments are added to man pages 47 | - Additional Rust crate dependencies are added to `Cargo.toml` for final compilation 48 | - Plugin files are watched by `overrider` and `proc_use` so `rustc` compiles them in 49 | This all happens automatically, so no build script configuration is required. 50 | 51 | ### Testing Changes 52 | As mentioned above, `config.mk` controls what plugins are loaded. Add your plugin name to the 53 | `PLUGINS` field to have it compiled in. 54 | 55 | ## Files 56 | Above described is the directory structure of a plugin. A more thorough explanation on each 57 | is as follows: 58 | ### plugin.yml 59 | dmenu-rs configures plugins using YAML. Why YAML and not TOML? Because `clap` uses YAML, and 60 | work has not yet been done to change that yet. 61 | The structure of a `plugin.yml` file is as follows: 62 | ```yaml 63 | about: A short description of this plugin 64 | entry: main.rs 65 | cargo_dependencies: deps.toml 66 | build: "sh build.sh" 67 | 68 | args: 69 | $CLAP_ARGS 70 | ``` 71 | For each field: 72 | - about: This text is shown when `make plugins` is ran 73 | - entry: This file is where all top-level overrides should occur. Utility functions can 74 | be defined elsewhere, but anything with `#[override_default]` must be here. The name 75 | is left up to the developer. 76 | - cargo_dependencies: This field is optional. If additional crate dependencies are required, 77 | this field points to this file. 78 | - build: This field is optional. If included, runs a command during configuration. This is 79 | useful for dependency checks or downloading external libraries that will be referenced 80 | during compile time. When checking dependencies, check `"$depcheck" != "false"` for 81 | runtime dependencies. An example of this check can be found in the `spellcheck` plugin. 82 | - args: These are command line arguments. They have the same syntax as arguments for `clap`, 83 | and are more or less copy-pasted into a `cli.yml` file down the line. 84 | Support for `visible_aliases` has been added in, so these work out of the box while `clap` 85 | does not yet support them. 86 | 87 | ### main.rs 88 | This file's actual name is set by the `entry` field in `plugin.yml`. 89 | 90 | This is where all top-level overrides reside. Utility functions may be defined elsewhere, 91 | and `mod`'d into compilation. Or, utility functions may be defined directly in this file, if 92 | they are required at all. 93 | 94 | Plugins function by overriding certain features. These can be overridden for all cases, or 95 | only when specific flags are called. For more information on syntax, see the 96 | [`overrider`](https://docs.rs/overrider/0.6.1/overrider/) crate. 97 | 98 | For a list of functions which can be overridden, see the `src/dmenu/plugin_entry.rs` file. 99 | Viewing examples of pre-existing plugins will be highly helpful. 100 | 101 | ### deps.toml 102 | This file is optional, and it's actual name is set by the `cargo_dependencies` field in 103 | `plugin.yml`. 104 | 105 | This file contains additional crate dependencies required by a plugin. The text in this 106 | file is literally copy-pasted directly into the `[dependencies]` section of `Cargo.toml`, 107 | so all syntax is allowed. 108 | 109 | 110 | ## Manpage Generation 111 | Manpages are automatically generated including info for plugin-based flags. The flag 112 | long/short names are included if present. `help` is required, unless `long_help` is 113 | provided. If both `help` and `long_help` are provided, `long_help` will be included 114 | in manpage generation. 115 | 116 | ## What Functionality Can Be Changed 117 | Because dmenu-rs is still young, not all functionality is changable yet. 118 | 119 | For a living list of everything that can be overriden, see 120 | [the plugin entry](../dmenu/plugin_entry.rs). Every method and funciton here is exposed 121 | to `overrider`. 122 | 123 | Some examples include: 124 | - `gen_matches` for displaying dynamic menu content 125 | - `format_input` for changing how the input renders on screen 126 | - `ConfigDefault` methods, which set the default values of config variables 127 | More are on their way. 128 | 129 | ## Quickstart 130 | Here's a short walkthrough on how to write a plugin, get the build system to recognize it, 131 | and get changes working correctly. 132 | 133 | This example will show how to make a `hello` plugin which replaces all menu items with 134 | the phrase `Hello world!`. 135 | 136 | ### Cloning The Project 137 | The first thing to do it get a working copy of this repo, and to make sure a clean build 138 | is working. To do so, either fork and clone or run the following: 139 | ``` 140 | git clone https://github.com/Shizcow/dmenu-rs/ 141 | cd dmenu-rs 142 | ``` 143 | Now, switch to the `develop` branch. This branch has the latest features, so any build 144 | conflicts will be more easily resolved here: 145 | ``` 146 | git checkout develop 147 | ``` 148 | Finally, make sure you have all the build tools installed: 149 | ``` 150 | make 151 | ``` 152 | This will check dependencies and attempt a build. If it doesn't succeed, you're likely 153 | missing dependencies. 154 | 155 | ### Setting Up Files 156 | The next thing to do is make plugin files. Switch to the plugin directory: 157 | ``` 158 | cd src/plugins 159 | ``` 160 | And make a plugin folder. The name of the folder is the name of the plugin. In this example, 161 | we're making a `hello` plugin, so the command is as follows: 162 | ``` 163 | mkdir hello 164 | cd hello 165 | ``` 166 | Now for the actual content. Create the following files: 167 | ```yaml 168 | #plugin.yml 169 | 170 | about: Replaces all menu items with the phrase "Hello world!" 171 | entry: main.rs 172 | ``` 173 | ```rust 174 | #main.rs 175 | 176 | use overrider::*; 177 | use crate::drw::Drw; 178 | use crate::item::Item; 179 | use crate::result::*; 180 | 181 | #[override_default] 182 | impl Drw { 183 | pub fn gen_matches(&mut self) -> CompResult> { 184 | let mut ret = Vec::new(); 185 | for _ in 0..self.get_items().len() { 186 | ret.push(Item::new("Hello world!".to_owned(), false, self)?); 187 | } 188 | Ok(ret) 189 | } 190 | } 191 | ``` 192 | As far as a basic plugin goes, that's all that's required. 193 | 194 | ### Compiling The Plugin 195 | Now return back to the root of the project: 196 | ``` 197 | cd ../../ 198 | ``` 199 | And open the `config.mk` file. Scroll to the bottom and you'll see the following: 200 | ```mk 201 | PLUGINS = 202 | ``` 203 | To compile with the new `hello` plugin, change that line to the following: 204 | ```mk 205 | PLUGINS = hello 206 | ``` 207 | Finally, build the project: 208 | ``` 209 | make 210 | ``` 211 | Hopefully, everything will build correctly. Now to test changes: 212 | ``` 213 | make test 214 | ``` 215 | And voilà, menu output should be different. For more customization, see the rest of 216 | the guide above. 217 | 218 | ### CompResult 219 | `CompResult` is a type defined as follows: 220 | ```rust 221 | pub type CompResult = Result; 222 | ``` 223 | Where `Die` is defined as follows: 224 | ```rust 225 | pub enum Die { 226 | Stdout(String), 227 | Stderr(String), 228 | } 229 | ``` 230 | More info can be found in the [result.rs](../dmenu/result.rs) file. 231 | 232 | The purpose of this type is stopping the program quickly. Be it in error, 233 | or some reason that requires a quick-exit (such as auto-selection). Most 234 | plugin_entry methods return `CompResult`, so quickly exiting from any point 235 | is possible. 236 | -------------------------------------------------------------------------------- /src/plugins/autoselect/main.rs: -------------------------------------------------------------------------------- 1 | use overrider::*; 2 | 3 | use crate::drw::Drw; 4 | use crate::item::Item; 5 | use crate::result::*; 6 | 7 | #[override_flag(flag = autoselect)] 8 | impl Drw { 9 | pub fn postprocess_matches(&mut self, mut current_matches: Vec) -> CompResult> { 10 | if current_matches.len() == 1 { 11 | self.dispose(current_matches.swap_remove(0).text, true)?; 12 | Err(Die::Stdout("".to_owned())) 13 | } else { 14 | Ok(current_matches) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/plugins/autoselect/plugin.yml: -------------------------------------------------------------------------------- 1 | about: | 2 | Auto select if there is only one match with the current input 3 | Pass --autoselect to enable 4 | entry: main.rs # entry has all overriding and pub items 5 | 6 | args: 7 | - autoselect: 8 | help: Auto select if there is only one match with the current input 9 | long: autoselect 10 | # short: 11 | -------------------------------------------------------------------------------- /src/plugins/calc/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | if [ "$depcheck" != "false" ]; then 4 | printf "Checking for xclip... " 5 | if command -v xclip &> /dev/null 6 | then 7 | echo "yes" 8 | else 9 | echo "no" 10 | >&2 echo "Install xclip to use the spellcheck plugin. Install it or run make depcheck=false to continue anyway" 11 | exit 1 12 | fi 13 | fi 14 | -------------------------------------------------------------------------------- /src/plugins/calc/deps.toml: -------------------------------------------------------------------------------- 1 | rink-core = "0.5.1" 2 | async-std = {version = "1.7.0", features = ["unstable"]} -------------------------------------------------------------------------------- /src/plugins/calc/main.rs: -------------------------------------------------------------------------------- 1 | use overrider::*; 2 | use rink_core::{one_line, simple_context, Context}; 3 | use std::io::Write; 4 | use std::process::{Command, Stdio}; 5 | use std::sync::Mutex; 6 | use std::time::Duration; 7 | use async_std::prelude::*; 8 | use async_std::task::block_on; 9 | 10 | use crate::drw::Drw; 11 | use crate::item::Item; 12 | use crate::result::*; 13 | 14 | lazy_static::lazy_static! { 15 | static ref CTX: Mutex = Mutex::new(simple_context().unwrap()); 16 | } 17 | 18 | async fn timed_eval(expr: String) -> Result { 19 | async { 20 | async_std::task::spawn(async { 21 | let xpr = expr; 22 | one_line(&mut CTX.lock().unwrap(), &xpr) 23 | }).await 24 | }.timeout(Duration::from_millis(250)) 25 | .await.unwrap_or(Ok("Calculation Timeout".to_string())) 26 | } 27 | 28 | #[override_flag(flag = calc)] 29 | impl Drw { 30 | pub fn gen_matches(&mut self) -> CompResult> { 31 | let eval = self.config.prompt.clone() + " " + &self.input; 32 | if let Ok(evaluated) = block_on(timed_eval(eval)) { 33 | Ok(vec![Item::new(evaluated, false, self)?]) 34 | } else { 35 | Ok(vec![]) 36 | } 37 | } 38 | pub fn dispose(&mut self, _output: String, recommendation: bool) -> CompResult { 39 | let eval = self.config.prompt.clone() + " " + &self.input; 40 | let output = if let Ok(evaluated) = block_on(timed_eval(eval)) { 41 | evaluated 42 | } else { 43 | return Ok(false) 44 | }; 45 | 46 | self.input = "".to_owned(); 47 | self.pseudo_globals.cursor = 0; 48 | self.config.prompt = output.clone(); 49 | if output.len() > 0 { 50 | // Wow making sure keyboard content sticks around after exit is a pain in the neck 51 | 52 | let mut child = Command::new("xclip") 53 | .arg("-sel") 54 | .arg("clip") 55 | .stdin(Stdio::piped()) 56 | .spawn() 57 | .map_err(|_| Die::Stderr("Failed to spawn child process".to_owned()))?; 58 | 59 | child.stdin.as_mut().ok_or(Die::Stderr("Failed to open stdin of child process" 60 | .to_owned()))? 61 | .write_all(output.as_bytes()) 62 | .map_err(|_| Die::Stderr("Failed to write to stdin of child process" 63 | .to_owned()))?; 64 | } 65 | self.draw()?; 66 | Ok(!recommendation) 67 | } 68 | } 69 | 70 | use crate::config::{ConfigDefault, DefaultWidth}; 71 | #[override_flag(flag = calc)] 72 | impl ConfigDefault { 73 | pub fn nostdin() -> bool { 74 | true 75 | } 76 | pub fn render_flex() -> bool { 77 | true 78 | } 79 | pub fn render_default_width() -> DefaultWidth { 80 | DefaultWidth::Custom(25) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/plugins/calc/plugin.yml: -------------------------------------------------------------------------------- 1 | about: | 2 | Calculator -- unit aware calculation for physics and engineering 3 | Pass --calc to enable. Uses rink-rs syntax 4 | entry: main.rs 5 | cargo_dependencies: deps.toml 6 | build: "sh build.sh" 7 | 8 | args: 9 | - calc: 10 | help: Enter calc mode 11 | long_help: > 12 | Enter calc mode. Type a calculator query to be evaluated. Querys follow syntax 13 | for rink-rs, which include basic operations, unit conversions, and simple functions. 14 | Pressing Enter loads the output of the current command into the prompt field, 15 | chaining the expression for further evaluation. Every time Enter is pressed, the result 16 | of the computation is coppied to the clipboard. Ctrl-Enter coppies to clipboard and 17 | exits. 18 | short: = 19 | long: calc 20 | conflicts_with: prompt 21 | -------------------------------------------------------------------------------- /src/plugins/fuzzy/deps.toml: -------------------------------------------------------------------------------- 1 | fuzzy-matcher = "0.3.4" -------------------------------------------------------------------------------- /src/plugins/fuzzy/main.rs: -------------------------------------------------------------------------------- 1 | use overrider::*; 2 | 3 | #[allow(unused_imports)] 4 | use crate::clapflags::CLAP_FLAGS; 5 | 6 | use fuzzy_matcher::FuzzyMatcher; 7 | use fuzzy_matcher::skim::SkimMatcherV2; 8 | 9 | use crate::drw::Drw; 10 | use crate::item::Item; 11 | use crate::result::*; 12 | 13 | #[override_flag(flag = nofuzz, invert = true)] 14 | impl Drw { 15 | pub fn gen_matches(&mut self) -> CompResult> { 16 | let searchterm = self.input.clone(); 17 | let matcher: Box = Box::new(SkimMatcherV2::default()); 18 | let mut items: Vec<(Item, i64)> = 19 | self.get_items().iter().map(|item| { 20 | (item.clone(), 21 | if let Some(score) = matcher.fuzzy_match(&item.text, &searchterm) { 22 | -score 23 | } else { 24 | 1 25 | }) 26 | }).collect(); 27 | items.retain(|(_, score)| *score <= 0); 28 | if searchterm.len() > 0 { 29 | items.sort_by_key(|(item, _)| item.text.len()); // this prioritizes exact matches 30 | items.sort_by_key(|(_, score)| *score); 31 | } 32 | 33 | Ok(items.into_iter().map(|(item, _)| item).collect()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/plugins/fuzzy/plugin.yml: -------------------------------------------------------------------------------- 1 | about: | 2 | Fuzzy string matching for searches. 3 | Enabled by default; pass --nofuzz to disable 4 | entry: main.rs 5 | cargo_dependencies: deps.toml 6 | 7 | args: 8 | - nofuzz: 9 | help: Disable fuzzy search, reverting to the default algorithm 10 | long: nofuzz 11 | -------------------------------------------------------------------------------- /src/plugins/lookup/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | if [ "$depcheck" != "false" ]; then 4 | printf "Checking for xdg-open... " 5 | if command -v xdg-open &> /dev/null 6 | then 7 | echo "yes" 8 | else 9 | echo "no" 10 | >&2 echo "xdg-open required for plugin 'lookup' but not available. Install xdg or run make depcheck=false to continue anyway" 11 | exit 1 12 | fi 13 | fi 14 | -------------------------------------------------------------------------------- /src/plugins/lookup/deps.toml: -------------------------------------------------------------------------------- 1 | phf = { version = "0.8.0", features = ["macros"] } 2 | -------------------------------------------------------------------------------- /src/plugins/lookup/engines.rs: -------------------------------------------------------------------------------- 1 | // Edit the following to add/remove/change search engines 2 | // The keys are the parameters for the --engine arguement 3 | // The values are the URLs. "%s" will be replaced with the search string. 4 | pub static ENGINES: phf::Map<&'static str, &'static str> = phf::phf_map! { 5 | "ddg" => "https://duckduckgo.com/%s", 6 | "crates" => "https://crates.io/crates/%s", 7 | "docs" => "https://docs.rs/%s", 8 | "rust" => "https://doc.rust-lang.org/std/?search=%s", 9 | "github" => "https://github.com/search?q=%s", 10 | "archwiki" => "https://wiki.archlinux.org/index.php?search=%s", 11 | "dictionary" => "https://www.merriam-webster.com/dictionary/%s", 12 | "thesaurus" => "https://www.merriam-webster.com/thesaurus/%s", 13 | }; 14 | -------------------------------------------------------------------------------- /src/plugins/lookup/main.rs: -------------------------------------------------------------------------------- 1 | use overrider::*; 2 | use std::process::Command; 3 | 4 | use crate::clapflags::CLAP_FLAGS; 5 | use crate::config::ConfigDefault; 6 | use crate::drw::Drw; 7 | use crate::result::*; 8 | use itertools::Itertools; 9 | 10 | mod engines; 11 | use engines::ENGINES; 12 | 13 | // Format engine as prompt 14 | // eg "ddg" -> "[Search ddg]" 15 | fn create_search_input(engine: &str) -> CompResult { 16 | // fail early if engine is wrong 17 | match ENGINES.get(engine) { 18 | Some(_) => Ok(format!("[Search {}]", engine)), 19 | None => { 20 | return Err(Die::Stderr(format!( 21 | "Invalid search search engine {}. Valid options are: {}", 22 | engine, 23 | ENGINES.keys().map(|e| format!("\"{}\"", e)).join(", ") 24 | ))) 25 | } 26 | } 27 | } 28 | 29 | // Take the output of create_search_input as prompt 30 | // It's not very clean but hey it works 31 | fn do_dispose(output: &str, prompt: &str) -> CompResult<()> { 32 | let mut engine: String = prompt.chars().skip("[Search ".len()).collect(); 33 | engine.pop(); 34 | 35 | // just unwrap since the check was performed before 36 | let search_prompt = ENGINES.get(engine.as_str()) 37 | .unwrap().to_string().replace("%s", output); 38 | 39 | // TODO: consider user defined open command for cross-platform awareness 40 | Command::new("xdg-open") 41 | .arg(search_prompt) 42 | .spawn() 43 | .map_err(|_| Die::Stderr("Failed to spawn child process".to_owned()))?; 44 | Ok(()) 45 | } 46 | 47 | // Important: engine must become before lookup. It's a bug in overrider. 48 | #[override_flag(flag = engine, priority = 2)] 49 | impl Drw { 50 | pub fn dispose(&mut self, output: String, recommendation: bool) -> CompResult { 51 | do_dispose(&output, &self.config.prompt)?; 52 | Ok(recommendation) 53 | } 54 | pub fn format_stdin(&mut self, _lines: Vec) -> CompResult> { 55 | self.config.prompt = create_search_input(CLAP_FLAGS.value_of("engine").unwrap())?; 56 | Ok(vec![]) // turns into prompt 57 | } 58 | } 59 | 60 | #[override_flag(flag = listEngines, priority = 2)] 61 | impl Drw { 62 | pub fn format_stdin(&mut self, _: Vec) -> CompResult> { 63 | Err(Die::Stdout(ENGINES.keys().join("\n"))) 64 | } 65 | } 66 | #[override_flag(flag = listEngines, priority = 2)] 67 | impl ConfigDefault { 68 | pub fn nostdin() -> bool { 69 | true // if called with --list-engines, takes no stdin (only prints) 70 | } 71 | } 72 | 73 | #[override_flag(flag = engine, priority = 2)] 74 | impl ConfigDefault { 75 | pub fn nostdin() -> bool { 76 | true // if called with --engine ENGINE, takes no stdin 77 | } 78 | } 79 | 80 | #[override_flag(flag = lookup, priority = 1)] 81 | impl Drw { 82 | pub fn dispose(&mut self, output: String, recommendation: bool) -> CompResult { 83 | do_dispose(&output, &self.config.prompt)?; 84 | Ok(recommendation) 85 | } 86 | pub fn format_stdin(&mut self, lines: Vec) -> CompResult> { 87 | self.config.prompt = create_search_input(&lines[0])?; 88 | Ok(vec![]) // turns into prompt 89 | } 90 | } 91 | 92 | #[override_flag(flag = lookup, priority = 1)] 93 | impl ConfigDefault { 94 | pub fn nostdin() -> bool { 95 | false // if called without --engine, takes stdin 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/plugins/lookup/plugin.yml: -------------------------------------------------------------------------------- 1 | about: | 2 | Open a search query in the browser. Pass --lookup to enable. 3 | Use --engine to specify a search engine or pipe through stdin. 4 | See src/plugins/lookup/engines.rs for custom engines at build time. 5 | entry: main.rs 6 | cargo_dependencies: deps.toml 7 | build: "sh build.sh" 8 | 9 | args: 10 | - lookup: 11 | help: Enter lookup mode 12 | long_help: > 13 | The input in the prompt will be used as the search term in the selected 14 | engine. XDG-OPEN will be used for opening generated links. 15 | short: L 16 | long: lookup 17 | conflicts_with: prompt 18 | - engine: 19 | help: Engine to use 20 | long_help: > 21 | Engine to lookup with. Run `dmenu --lookup --list-engines` 22 | to show available engines. More engines can be added at 23 | src/plugins/lookup/engines.rs during build time. 24 | long: engine 25 | takes_value: true 26 | requires: lookup 27 | - listEngines: # overrider doesn't like underscores 28 | help: List available engines 29 | long_help: > 30 | List available engines for lookup. Prints a newline seperated list to stdout. 31 | long: list-engines 32 | requires: lookup 33 | conflicts_with: engine 34 | -------------------------------------------------------------------------------- /src/plugins/maxlength/main.rs: -------------------------------------------------------------------------------- 1 | use overrider::*; 2 | 3 | use crate::config::ConfigDefault; 4 | use crate::drw::Drw; 5 | use crate::item::Item; 6 | use crate::result::*; 7 | use crate::clapflags::CLAP_FLAGS; 8 | 9 | use unicode_segmentation::UnicodeSegmentation; 10 | 11 | #[override_flag(flag = maxlength)] 12 | impl Drw { 13 | pub fn postprocess_matches(&mut self, current_matches: Vec) -> CompResult> { 14 | let max_length_str_option = CLAP_FLAGS.value_of( "maxlength" ); 15 | 16 | match max_length_str_option 17 | { 18 | None => { 19 | Err(Die::Stderr("Please specificy max length".to_owned())) 20 | }, 21 | Some( max_length_str ) => 22 | { 23 | let max_length_result = max_length_str.parse::(); 24 | match max_length_result 25 | { 26 | Err( _ ) => Err(Die::Stderr("Please specificy a positive integer for max length".to_owned())), 27 | Ok( 0 ) => Ok( current_matches ), 28 | Ok( max_length ) => { 29 | // >= in place of = in case someoen pastes stuff in 30 | // when there is a paste functionality. 31 | if self.input.graphemes(true).count() >= max_length 32 | { 33 | self.dispose( self.input.graphemes(true).take( max_length ).collect(), true )?; 34 | Err(Die::Stdout("".to_owned())) 35 | } 36 | else 37 | { 38 | Ok(current_matches) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | #[override_flag(flag = maxlength)] 48 | impl ConfigDefault { 49 | pub fn nostdin() -> bool { 50 | true 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/plugins/maxlength/plugin.yml: -------------------------------------------------------------------------------- 1 | about: | 2 | Specify a maximum length of input, 3 | usually used without menu items. 4 | Acts as a replacement for `i3-input -l` 5 | Pass --maxlength= to use 6 | entry: main.rs 7 | 8 | args: 9 | - maxlength: 10 | help: Limit maximum length of input 11 | long: maxlength 12 | takes_value: true 13 | value_name: MAXLENGTH 14 | # short: 15 | -------------------------------------------------------------------------------- /src/plugins/password/main.rs: -------------------------------------------------------------------------------- 1 | use overrider::*; 2 | 3 | use crate::drw::Drw; 4 | use crate::result::*; 5 | 6 | #[override_flag(flag = password)] 7 | impl Drw { 8 | pub fn format_input(&self) -> CompResult { 9 | Ok((0..self.input.len()).map(|_| "*").collect()) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/password/plugin.yml: -------------------------------------------------------------------------------- 1 | about: Provides a -P command line switch to replace input with *** 2 | entry: main.rs # entry has all overriding and pub items 3 | 4 | args: 5 | - password: 6 | help: Hides input field with *** 7 | short: P 8 | long: password 9 | -------------------------------------------------------------------------------- /src/plugins/spellcheck/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | if [ "$depcheck" != "false" ]; then 4 | printf "Checking for aspell... " 5 | if command -v aspell &> /dev/null 6 | then 7 | echo "yes" 8 | else 9 | echo "no" 10 | >&2 echo "Runtime dependency aspell missing. Install it or run make depcheck=false to continue anyway" 11 | exit 1 12 | fi 13 | 14 | printf "Checking for xclip... " 15 | if command -v xclip &> /dev/null 16 | then 17 | echo "yes" 18 | else 19 | echo "no" 20 | >&2 echo "Install xclip to use the spellcheck plugin. Install it or run make depcheck=false to continue anyway" 21 | exit 1 22 | fi 23 | fi 24 | -------------------------------------------------------------------------------- /src/plugins/spellcheck/deps.toml: -------------------------------------------------------------------------------- 1 | ispell = "0.3.1" -------------------------------------------------------------------------------- /src/plugins/spellcheck/main.rs: -------------------------------------------------------------------------------- 1 | use overrider::*; 2 | 3 | use ispell::{SpellLauncher}; 4 | use std::process::{Command, Stdio}; 5 | use std::io::Write; 6 | 7 | use crate::drw::Drw; 8 | use crate::item::Item; 9 | use crate::result::*; 10 | 11 | #[override_flag(flag = spellcheck)] 12 | impl Drw { 13 | pub fn gen_matches(&mut self) -> CompResult> { 14 | let checker = SpellLauncher::new() 15 | .aspell() 16 | .launch(); 17 | 18 | let (first, second) = self.input.split_at(self.pseudo_globals.cursor); 19 | let first_replaced = first.replace(" ", ""); 20 | let second_replaced = second.replace(" ", ""); 21 | self.pseudo_globals.cursor = first_replaced.chars().count(); 22 | self.input = first_replaced+&second_replaced; 23 | 24 | match checker { 25 | Ok(mut checker) => { 26 | match checker.check(&self.input) { 27 | Ok(mut res) => { 28 | if res.is_empty() { 29 | Ok(vec![Item::new(self.input.clone(), false, self)?]) 30 | } else { 31 | let mut ret = Vec::new(); 32 | for word in res.swap_remove(0).suggestions.into_iter() { 33 | ret.push(Item::new(word, false, self)?); 34 | } 35 | Ok(ret) 36 | } 37 | }, 38 | Err(err) => Die::stderr(format!("Error: could not run aspell: {}", err)) 39 | } 40 | }, 41 | Err(err) => Die::stderr(format!("Error: could not start aspell: {}", err)) 42 | } 43 | } 44 | pub fn dispose(&mut self, output: String, recommendation: bool) -> CompResult { 45 | if output.len() > 0 { 46 | let mut child = Command::new("xclip") 47 | .arg("-sel") 48 | .arg("clip") 49 | .stdin(Stdio::piped()) 50 | .spawn() 51 | .map_err(|_| Die::Stderr("Failed to spawn child process".to_owned()))?; 52 | 53 | child.stdin.as_mut().ok_or(Die::Stderr("Failed to open stdin of child process" 54 | .to_owned()))? 55 | .write_all(output.as_bytes()) 56 | .map_err(|_| Die::Stderr("Failed to write to stdin of child process" 57 | .to_owned()))?; 58 | } 59 | Ok(recommendation) 60 | } 61 | } 62 | 63 | use crate::config::{ConfigDefault, DefaultWidth}; 64 | #[override_flag(flag = spellcheck)] 65 | impl ConfigDefault { 66 | pub fn nostdin() -> bool { 67 | true 68 | } 69 | pub fn render_flex() -> bool { 70 | true 71 | } 72 | pub fn render_default_width() -> DefaultWidth { 73 | DefaultWidth::Custom(10) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/plugins/spellcheck/plugin.yml: -------------------------------------------------------------------------------- 1 | about: | 2 | Single word spellcheck. Backed by the GNU aspell library. 3 | Pass --spellcheck to enable 4 | entry: main.rs 5 | cargo_dependencies: deps.toml 6 | build: "sh build.sh" 7 | args: 8 | - spellcheck: 9 | help: Enter spellcheck mode 10 | long_help: "Enter spellcheck mode. Begin typing a word for spelling suggestions.\nOnly accepts 11 | single word queries.\nPressing Enter will copy to clipboard before exiting." 12 | long: spellcheck 13 | visible_aliases: sc 14 | -------------------------------------------------------------------------------- /src/sh/README.md: -------------------------------------------------------------------------------- 1 | # sh 2 | 3 | This directory contains the following shell scripts: 4 | - dmenu_path 5 | - dmenu_run 6 | 7 | These scripts simplify some common dmenu-functions, 8 | such as launching programs. 9 | -------------------------------------------------------------------------------- /src/sh/dmenu_path: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cachedir="${XDG_CACHE_HOME:-"$HOME/.cache"}" 4 | cache="$cachedir/dmenu_run" 5 | 6 | [ ! -e "$cachedir" ] && mkdir -p "$cachedir" 7 | 8 | IFS=: 9 | if stest -dqr -n "$cache" $PATH; then 10 | stest -flx $PATH | sort -u | tee "$cache" 11 | else 12 | cat "$cache" 13 | fi 14 | -------------------------------------------------------------------------------- /src/sh/dmenu_run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | dmenu_path | dmenu "$@" | ${SHELL:-"/bin/sh"} & 3 | -------------------------------------------------------------------------------- /src/stest/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 = "bitflags" 7 | version = "1.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 10 | 11 | [[package]] 12 | name = "cc" 13 | version = "1.0.78" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" 16 | 17 | [[package]] 18 | name = "clap" 19 | version = "4.0.32" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" 22 | dependencies = [ 23 | "bitflags", 24 | "clap_derive", 25 | "clap_lex", 26 | "is-terminal", 27 | "once_cell", 28 | "strsim", 29 | "termcolor", 30 | ] 31 | 32 | [[package]] 33 | name = "clap_derive" 34 | version = "4.0.21" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" 37 | dependencies = [ 38 | "heck", 39 | "proc-macro-error", 40 | "proc-macro2", 41 | "quote", 42 | "syn", 43 | ] 44 | 45 | [[package]] 46 | name = "clap_lex" 47 | version = "0.3.0" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" 50 | dependencies = [ 51 | "os_str_bytes", 52 | ] 53 | 54 | [[package]] 55 | name = "errno" 56 | version = "0.2.8" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 59 | dependencies = [ 60 | "errno-dragonfly", 61 | "libc", 62 | "winapi", 63 | ] 64 | 65 | [[package]] 66 | name = "errno-dragonfly" 67 | version = "0.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 70 | dependencies = [ 71 | "cc", 72 | "libc", 73 | ] 74 | 75 | [[package]] 76 | name = "heck" 77 | version = "0.4.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 80 | 81 | [[package]] 82 | name = "hermit-abi" 83 | version = "0.2.6" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 86 | dependencies = [ 87 | "libc", 88 | ] 89 | 90 | [[package]] 91 | name = "io-lifetimes" 92 | version = "1.0.3" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" 95 | dependencies = [ 96 | "libc", 97 | "windows-sys", 98 | ] 99 | 100 | [[package]] 101 | name = "is-terminal" 102 | version = "0.4.2" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" 105 | dependencies = [ 106 | "hermit-abi", 107 | "io-lifetimes", 108 | "rustix", 109 | "windows-sys", 110 | ] 111 | 112 | [[package]] 113 | name = "libc" 114 | version = "0.2.139" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 117 | 118 | [[package]] 119 | name = "linux-raw-sys" 120 | version = "0.1.4" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 123 | 124 | [[package]] 125 | name = "once_cell" 126 | version = "1.16.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 129 | 130 | [[package]] 131 | name = "os_str_bytes" 132 | version = "6.4.1" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 135 | 136 | [[package]] 137 | name = "proc-macro-error" 138 | version = "1.0.4" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 141 | dependencies = [ 142 | "proc-macro-error-attr", 143 | "proc-macro2", 144 | "quote", 145 | "syn", 146 | "version_check", 147 | ] 148 | 149 | [[package]] 150 | name = "proc-macro-error-attr" 151 | version = "1.0.4" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 154 | dependencies = [ 155 | "proc-macro2", 156 | "quote", 157 | "version_check", 158 | ] 159 | 160 | [[package]] 161 | name = "proc-macro2" 162 | version = "1.0.49" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" 165 | dependencies = [ 166 | "unicode-ident", 167 | ] 168 | 169 | [[package]] 170 | name = "quote" 171 | version = "1.0.23" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 174 | dependencies = [ 175 | "proc-macro2", 176 | ] 177 | 178 | [[package]] 179 | name = "rustix" 180 | version = "0.36.6" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" 183 | dependencies = [ 184 | "bitflags", 185 | "errno", 186 | "io-lifetimes", 187 | "libc", 188 | "linux-raw-sys", 189 | "windows-sys", 190 | ] 191 | 192 | [[package]] 193 | name = "stest" 194 | version = "0.1.0" 195 | dependencies = [ 196 | "clap", 197 | ] 198 | 199 | [[package]] 200 | name = "strsim" 201 | version = "0.10.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 204 | 205 | [[package]] 206 | name = "syn" 207 | version = "1.0.107" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 210 | dependencies = [ 211 | "proc-macro2", 212 | "quote", 213 | "unicode-ident", 214 | ] 215 | 216 | [[package]] 217 | name = "termcolor" 218 | version = "1.1.3" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 221 | dependencies = [ 222 | "winapi-util", 223 | ] 224 | 225 | [[package]] 226 | name = "unicode-ident" 227 | version = "1.0.6" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 230 | 231 | [[package]] 232 | name = "version_check" 233 | version = "0.9.4" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 236 | 237 | [[package]] 238 | name = "winapi" 239 | version = "0.3.9" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 242 | dependencies = [ 243 | "winapi-i686-pc-windows-gnu", 244 | "winapi-x86_64-pc-windows-gnu", 245 | ] 246 | 247 | [[package]] 248 | name = "winapi-i686-pc-windows-gnu" 249 | version = "0.4.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 252 | 253 | [[package]] 254 | name = "winapi-util" 255 | version = "0.1.5" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 258 | dependencies = [ 259 | "winapi", 260 | ] 261 | 262 | [[package]] 263 | name = "winapi-x86_64-pc-windows-gnu" 264 | version = "0.4.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 267 | 268 | [[package]] 269 | name = "windows-sys" 270 | version = "0.42.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 273 | dependencies = [ 274 | "windows_aarch64_gnullvm", 275 | "windows_aarch64_msvc", 276 | "windows_i686_gnu", 277 | "windows_i686_msvc", 278 | "windows_x86_64_gnu", 279 | "windows_x86_64_gnullvm", 280 | "windows_x86_64_msvc", 281 | ] 282 | 283 | [[package]] 284 | name = "windows_aarch64_gnullvm" 285 | version = "0.42.0" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 288 | 289 | [[package]] 290 | name = "windows_aarch64_msvc" 291 | version = "0.42.0" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 294 | 295 | [[package]] 296 | name = "windows_i686_gnu" 297 | version = "0.42.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 300 | 301 | [[package]] 302 | name = "windows_i686_msvc" 303 | version = "0.42.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 306 | 307 | [[package]] 308 | name = "windows_x86_64_gnu" 309 | version = "0.42.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 312 | 313 | [[package]] 314 | name = "windows_x86_64_gnullvm" 315 | version = "0.42.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 318 | 319 | [[package]] 320 | name = "windows_x86_64_msvc" 321 | version = "0.42.0" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 324 | -------------------------------------------------------------------------------- /src/stest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stest" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | clap = { version = "4.1.8", features = ["derive"] } 8 | -------------------------------------------------------------------------------- /src/stest/README.md: -------------------------------------------------------------------------------- 1 | # stest 2 | 3 | This folder contains code for `stest(1)`. 4 | 5 | It's been ported to rust and, like the original stest written in C, it has a single dependency that is used for parsing command line arguments. 6 | -------------------------------------------------------------------------------- /src/stest/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::file::File; 2 | use clap::Parser; 3 | 4 | /// filter a list of files by properties 5 | /// 6 | /// stest takes a list of files and filters by the files' properties, analogous to test. Files 7 | /// which pass all tests are printed to stdout. If no files are given, stest reads files from 8 | /// stdin. 9 | #[derive(Clone, Debug, Parser)] 10 | #[command(author, version = env!("VERSION"), about, long_about)] 11 | pub struct Config { 12 | /// Test hidden files. 13 | #[arg(short = 'a')] 14 | pub requires_each_file_is_hidden: bool, 15 | /// Test that files are block specials. 16 | #[arg(short = 'b')] 17 | pub requires_each_file_is_block_special: bool, 18 | /// Test that files are character specials. 19 | #[arg(short = 'c')] 20 | pub requires_each_file_is_character_special: bool, 21 | /// Test that files are directories. 22 | #[arg(short = 'd')] 23 | pub requires_each_file_is_directory: bool, 24 | /// Test that files exist. 25 | /// 26 | /// This option does not actually alter the behavior of stest. By default, stest will always 27 | /// test that a file exists. 28 | /// 29 | /// This behavior deviates from the behavior of the POSIX test utility, which requires the -e 30 | /// option in order to perform an existence test on a file. This deviation, together with the 31 | /// fact that it's counterintuitive to provide an option that has no effect, implies that this 32 | /// may be a bug in the C implementation of stest in the original dmenu repository. The 33 | /// behavior is reproduced here because this rust implementation strives to be a drop-in 34 | /// replacement for the original and because it's unlikely that this behavior could 35 | /// meaningfully impact the experience of using dmenu_run: non-existing files on your PATH 36 | /// cannot be executed successfully, so it's always reasonable to filter them out. 37 | #[arg(short = 'e')] 38 | pub requires_each_file_exists: bool, 39 | /// Test that files are regular files. 40 | #[arg(short = 'f')] 41 | pub requires_each_file_is_file: bool, 42 | /// Test that files have their set-group-ID flag set. 43 | #[arg(short = 'g')] 44 | pub requires_each_file_has_set_group_id: bool, 45 | // Using -h here works and matches the original stest, however it overrides one of clap's 46 | // autogenerated help flags and displays confusing help information when calling with the long 47 | // option --help. We can disable clap's help flags if we want to but it complicates usage. 48 | /// Test that files are symbolic links. 49 | #[arg(short = 'h')] 50 | pub requires_each_file_is_symbolic_link: bool, 51 | /// Test the contents of a directory given as an argument. 52 | #[arg(short = 'l')] 53 | pub test_contents_of_directories: bool, 54 | /// Test that files are newer (by modification time) than file. 55 | /// 56 | /// If this option is given a non-existing file as its argument, the test is ignored. 57 | /// 58 | /// This behavior is similar to assuming the last modification time of a non-existent file is a 59 | /// lower bound like the unix epoch or zero, and is consistent with the behavior of GNU 60 | /// coreutils' test. 61 | #[arg(short = 'n')] 62 | pub oldest_file: Option, 63 | /// Test that files are older (by modification time) than file. 64 | /// 65 | /// If this option is given a non-existing file as its argument, the test is ignored. 66 | /// 67 | /// This behavior is similar to assuming the last modification time of a non-existent file is 68 | /// an upper bound like infinity. Note that this behavior is not consistent with the behavior 69 | /// of GNU coreutils' test: checking that a file is older than (-ot) a non-existent file with 70 | /// GNU coreutils' test always fails, while checking the same with stest (-o) always passes. 71 | #[arg(short = 'o')] 72 | pub newest_file: Option, 73 | /// Test that files are named pipes. 74 | #[arg(short = 'p')] 75 | pub requires_each_file_is_pipe: bool, 76 | /// No files are printed, only the exit status is returned. 77 | #[arg(short = 'q')] 78 | pub quiet: bool, 79 | /// Test that files are readable. 80 | #[arg(short = 'r')] 81 | pub requires_each_file_is_readable: bool, 82 | /// Test that files are not empty. 83 | #[arg(short = 's')] 84 | pub requires_each_file_has_size_greater_than_zero: bool, 85 | /// Test that files have their set-user-ID flag set. 86 | #[arg(short = 'u')] 87 | pub requires_each_file_has_set_user_id: bool, 88 | /// Invert the sense of tests, only failing files pass. 89 | #[arg(short = 'v')] 90 | pub has_inverted_tests: bool, 91 | /// Test that files are writable. 92 | #[arg(short = 'w')] 93 | pub requires_each_file_is_writable: bool, 94 | /// Test that files are executable. 95 | #[arg(short = 'x')] 96 | pub requires_each_file_is_executable: bool, 97 | /// files to filter on 98 | pub files: Vec, 99 | } 100 | -------------------------------------------------------------------------------- /src/stest/src/file.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::clone::Clone; 3 | use std::fmt::Error as FmtError; 4 | use std::fmt::{Display, Formatter}; 5 | use std::fs::read_dir; 6 | use std::fs::DirEntry; 7 | use std::fs::Metadata; 8 | use std::io; 9 | use std::os::unix::fs::{FileTypeExt, PermissionsExt}; 10 | use std::path::Component; 11 | use std::path::PathBuf; 12 | use std::result::Result; 13 | use std::str::FromStr; 14 | use std::time::SystemTime; 15 | 16 | /// Wraps a [PathBuf] for use in stest. 17 | /// 18 | /// These files are assumed to be on a unix system. 19 | /// 20 | /// Error-handling here employs two approaches: system errors (e.g., file IO) induce panic, user 21 | /// erros (e.g., trying to process a file the user does not have access to) return a helpful error 22 | /// message wrapped in a result, which composes up to the outer shell of the program. 23 | #[derive(Clone, Debug, Parser, PartialEq)] 24 | pub struct File { 25 | path_buf: PathBuf, 26 | } 27 | 28 | impl File { 29 | pub fn new(path_buf: PathBuf) -> File { 30 | File { path_buf } 31 | } 32 | 33 | pub fn from(string: String) -> File { 34 | let path_buf = PathBuf::from(string); 35 | File { path_buf } 36 | } 37 | 38 | pub fn read_directory(&self) -> Result, std::io::Error> { 39 | fn dir_entry_to_file(dir_entry: DirEntry) -> File { 40 | File::new(dir_entry.path()) 41 | } 42 | let iterator = read_dir(&self.path_buf)?; 43 | iterator 44 | .map(|result| result.map(dir_entry_to_file)) 45 | .collect() 46 | } 47 | 48 | pub fn is_hidden(&self) -> bool { 49 | fn is_hidden(component: Component) -> bool { 50 | fn component_starts_with_dot(component: Component) -> bool { 51 | component 52 | .as_os_str() 53 | .to_string_lossy() // Ignore invalid unicode characters. 54 | .starts_with('.') 55 | } 56 | component != Component::CurDir 57 | && component != Component::ParentDir 58 | && component_starts_with_dot(component) 59 | } 60 | let iterator = self.path_buf.as_path().components(); 61 | // If a file's path is empty, it cannot be hidden. 62 | let option = iterator.last(); 63 | option 64 | .map(|component| is_hidden(component)) 65 | .unwrap_or(false) 66 | } 67 | 68 | pub fn is_block_special(&self) -> Result { 69 | fn is_block_special(metadata: Metadata) -> bool { 70 | metadata.file_type().is_block_device() 71 | } 72 | self.metadata().map(is_block_special) 73 | } 74 | 75 | pub fn is_character_special(&self) -> Result { 76 | fn is_character_special(metadata: Metadata) -> bool { 77 | metadata.file_type().is_char_device() 78 | } 79 | self.metadata().map(is_character_special) 80 | } 81 | 82 | pub fn is_directory(&self) -> bool { 83 | self.path_buf.is_dir() 84 | } 85 | 86 | pub fn exists(&self) -> Result { 87 | self.path_buf.try_exists() 88 | } 89 | 90 | pub fn is_file(&self) -> bool { 91 | self.path_buf.is_file() 92 | } 93 | 94 | /// Check if the file has the set-group-ID bit set. 95 | /// See: https://stackoverflow.com/a/50045872/8732788 96 | /// See: https://en.wikipedia.org/wiki/Setuid 97 | pub fn has_set_group_id(&self) -> Result { 98 | fn has_set_group_id(mode: u32) -> bool { 99 | mode & 0o2000 != 0 100 | } 101 | self.mode().map(has_set_group_id) 102 | } 103 | 104 | pub fn is_symbolic_link(&self) -> bool { 105 | self.path_buf.is_symlink() 106 | } 107 | 108 | pub fn is_newer_than(&self, file: &File) -> Result { 109 | let modified_time = self.last_modified()?; 110 | let oldest_modified_time = file.last_modified()?; 111 | let bool = modified_time > oldest_modified_time; 112 | Ok(bool) 113 | } 114 | 115 | pub fn is_older_than(&self, file: &File) -> Result { 116 | let modified_time = self.last_modified()?; 117 | // If the file we're comparing against doesn't exist, then the result of the comparision is 118 | // considered true. See comments on the newest_file option in config.rs for more details. 119 | match file.last_modified_without_default() { 120 | Ok(newest_modified_time) => Ok(modified_time < newest_modified_time), 121 | Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(true), 122 | Err(error) => Err(error), 123 | } 124 | } 125 | 126 | pub fn is_pipe(&self) -> Result { 127 | fn is_pipe(metadata: Metadata) -> bool { 128 | metadata.file_type().is_fifo() 129 | } 130 | self.path_buf.metadata().map(is_pipe) 131 | } 132 | 133 | /// Check if a unix file has any readable bit set (user, group, or other). 134 | /// See: https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation 135 | /// See: https://en.wikipedia.org/wiki/Bitwise_operation#AND 136 | pub fn is_readable(&self) -> Result { 137 | fn is_readable(mode: u32) -> bool { 138 | mode & 0o444 != 0 139 | } 140 | self.mode().map(is_readable) 141 | } 142 | 143 | pub fn has_size_greater_than_zero(&self) -> Result { 144 | fn has_size_greater_than_zero(metadata: Metadata) -> bool { 145 | let len = metadata.len(); 146 | len > 0 147 | } 148 | self.metadata().map(has_size_greater_than_zero) 149 | } 150 | 151 | /// Check if the file has the set-user-ID bit set. 152 | /// See: https://stackoverflow.com/a/50045872/8732788 153 | /// See: https://en.wikipedia.org/wiki/Setuid 154 | pub fn has_set_user_id(&self) -> Result { 155 | fn has_set_user_id(mode: u32) -> bool { 156 | mode & 0o4000 != 0 157 | } 158 | self.mode().map(has_set_user_id) 159 | } 160 | 161 | /// Check if the file has any writable bit set (user, group, or other). 162 | /// See: https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation 163 | /// See: https://en.wikipedia.org/wiki/Bitwise_operation#AND 164 | pub fn is_writable(&self) -> Result { 165 | fn is_writable(mode: u32) -> bool { 166 | mode & 0o222 != 0 167 | } 168 | self.mode().map(is_writable) 169 | } 170 | 171 | /// Check if the file has any executable bit set (user, group, or other). 172 | /// See: https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation 173 | /// See: https://en.wikipedia.org/wiki/Bitwise_operation#AND 174 | pub fn is_executable(&self) -> Result { 175 | fn is_executable(mode: u32) -> bool { 176 | mode & 0o111 != 0 177 | } 178 | self.mode().map(is_executable) 179 | } 180 | 181 | pub fn clone_with_path_as_file_name(&self) -> Option { 182 | self.path_buf 183 | .file_name() 184 | .map(|os_str| PathBuf::from(os_str)) 185 | .map(|path_buf| File::new(path_buf)) 186 | } 187 | 188 | /// Return the file's last modification time. 189 | /// 190 | /// If a file does not exist, it's last modified time defaults to the unix epoch. This is 191 | /// effectively a lower bound and is consistent with the behavior of GNU coreutils' test. See 192 | /// comments in config.rs for more details. 193 | fn last_modified(&self) -> Result { 194 | let result = self.last_modified_without_default(); 195 | match result { 196 | Ok(system_time) => Ok(system_time), 197 | Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(SystemTime::UNIX_EPOCH), 198 | Err(error) => Err(error), 199 | } 200 | } 201 | 202 | /// Return the file's last modification time without a default for non-existing files. 203 | fn last_modified_without_default(&self) -> Result { 204 | self.path_buf.metadata()?.modified() 205 | } 206 | 207 | fn metadata(&self) -> Result { 208 | self.path_buf.metadata() 209 | } 210 | 211 | fn mode(&self) -> Result { 212 | fn metadata_to_mode(metadata: Metadata) -> u32 { 213 | metadata.permissions().mode() 214 | } 215 | self.metadata().map(metadata_to_mode) 216 | } 217 | } 218 | 219 | impl Display for File { 220 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { 221 | let display = self.path_buf.display(); 222 | write!(f, "{display}") 223 | } 224 | } 225 | 226 | impl FromStr for File { 227 | type Err = std::convert::Infallible; 228 | 229 | fn from_str(str: &str) -> Result { 230 | let result = PathBuf::from_str(str); 231 | result.map(&File::new) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/stest/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod file; 3 | pub mod semigroup; 4 | 5 | use crate::config::Config; 6 | use crate::file::File; 7 | use crate::semigroup::Semigroup; 8 | use std::io; 9 | use std::io::{BufRead, Write}; 10 | 11 | pub struct App { 12 | pub config: Config, 13 | } 14 | 15 | impl App { 16 | pub fn new(config: Config) -> Self { 17 | Self { config } 18 | } 19 | 20 | pub fn run(self, stdin: &mut dyn BufRead, stdout: &mut dyn Write) -> Result { 21 | let files = self.files(stdin)?; 22 | let passing_files = self.passing_files(files, stdout)?; 23 | if passing_files.is_empty() { 24 | Ok(false) 25 | } else { 26 | Ok(true) 27 | } 28 | } 29 | 30 | pub fn files(&self, stdin: &mut dyn BufRead) -> Result, io::Error> { 31 | let files = self.config.files.clone(); 32 | 33 | let files = if files.is_empty() { 34 | stdin 35 | .lines() 36 | .map(|result| result.map(File::from)) 37 | .collect::>()? 38 | } else { 39 | files 40 | }; 41 | 42 | let files = if self.config.test_contents_of_directories { 43 | self.expand_directories(files)? 44 | } else { 45 | files 46 | }; 47 | 48 | Ok(files) 49 | } 50 | 51 | fn expand_directories(&self, files: Vec) -> Result, io::Error> { 52 | files 53 | .into_iter() 54 | .map(|file| self.expand_directory(file)) 55 | .reduce(|x, y| x.combine(y)) 56 | .unwrap_or(Ok(vec![])) 57 | } 58 | 59 | fn expand_directory(&self, file: File) -> Result, io::Error> { 60 | if file.is_directory() { 61 | file.read_directory() 62 | } else { 63 | Ok(vec![file]) 64 | } 65 | } 66 | 67 | pub fn passing_files( 68 | &self, 69 | files: Vec, 70 | stdout: &mut dyn Write, 71 | ) -> Result, io::Error> { 72 | fn by_test(result: &Result) -> bool { 73 | *result 74 | .as_ref() 75 | .map(|tested_file| { 76 | let TestedFile { file: _, passes } = tested_file; 77 | passes 78 | }) 79 | .unwrap_or(&true) // Pass errors through the filter for later. 80 | } 81 | 82 | let passing_files: Vec = files 83 | .into_iter() 84 | .map(|file| self.test_file(file)) 85 | .filter(by_test) 86 | .map(|result| result.map(|tested_file| tested_file.file)) 87 | .map(|result| result.and_then(|file| self.write(stdout, file))) 88 | .collect::>()?; 89 | 90 | stdout.flush()?; 91 | 92 | Ok(passing_files) 93 | } 94 | 95 | fn test_file(&self, file: File) -> Result { 96 | let passes = self.test(&file)?; 97 | let tested_file = TestedFile { file, passes }; 98 | Ok(tested_file) 99 | } 100 | 101 | /// Test that a file passes all configured tests. 102 | /// 103 | /// Each test is optional. If a test is disabled, the result is true by default. Only if it's 104 | /// enabled do we test the given file. A natural expression of this in boolean logic is 105 | /// 106 | /// !config.is_test_enabled || (config.is_test_enabled && test) 107 | /// 108 | /// This simplifies to the following shorter expression that we use for each test. 109 | /// 110 | /// !config.is_test_enabled || test 111 | /// 112 | /// We combine the result of each individual test with logical AND and return the result, 113 | /// inverting it if necessary. 114 | fn test(&self, file: &File) -> Result { 115 | let passes_all_tests = (!self.config.requires_each_file_is_hidden || file.is_hidden()) 116 | && (!self.config.requires_each_file_is_block_special || file.is_block_special()?) 117 | && (!self.config.requires_each_file_is_character_special 118 | || file.is_character_special()?) 119 | && (!self.config.requires_each_file_is_directory || file.is_directory()) 120 | && file.exists()? // See comments on the requires_each_file_exists option in config.rs. 121 | && (!self.config.requires_each_file_is_file || file.is_file()) 122 | && (!self.config.requires_each_file_has_set_group_id || file.has_set_group_id()?) 123 | && (!self.config.requires_each_file_is_symbolic_link || file.is_symbolic_link()) 124 | && (self.optionally_test_if_newer_than_oldest_file(file)?) 125 | && (self.optionally_test_if_older_than_newest_file(file)?) 126 | && (!self.config.requires_each_file_is_pipe || file.is_pipe()?) 127 | && (!self.config.requires_each_file_is_readable || file.is_readable()?) 128 | && (!self.config.requires_each_file_has_size_greater_than_zero 129 | || file.has_size_greater_than_zero()?) 130 | && (!self.config.requires_each_file_has_set_user_id || file.has_set_user_id()?) 131 | && (!self.config.requires_each_file_is_writable || file.is_writable()?) 132 | && (!self.config.requires_each_file_is_executable || file.is_executable()?); 133 | 134 | if self.config.has_inverted_tests { 135 | Ok(!passes_all_tests) 136 | } else { 137 | Ok(passes_all_tests) 138 | } 139 | } 140 | 141 | fn optionally_test_if_newer_than_oldest_file(&self, file: &File) -> Result { 142 | let default = Ok(true); 143 | self.config 144 | .oldest_file 145 | .as_ref() 146 | .map(|oldest_file| file.is_newer_than(oldest_file)) 147 | .unwrap_or(default) 148 | } 149 | 150 | fn optionally_test_if_older_than_newest_file(&self, file: &File) -> Result { 151 | let default = Ok(true); 152 | self.config 153 | .newest_file 154 | .as_ref() 155 | .map(|newest_file| file.is_older_than(newest_file)) 156 | .unwrap_or(default) 157 | } 158 | 159 | fn write(&self, stdout: &mut dyn Write, file: File) -> Result { 160 | if self.config.quiet { 161 | () 162 | } else { 163 | let mut string = if self.config.test_contents_of_directories { 164 | file.clone_with_path_as_file_name() 165 | .map(|f| f.to_string()) 166 | .expect("contents of the directory won't include '..'") 167 | } else { 168 | file.to_string() 169 | }; 170 | string.push('\n'); 171 | let bytes = string.as_bytes(); 172 | stdout.write_all(bytes)? 173 | } 174 | Ok(file) 175 | } 176 | } 177 | 178 | struct TestedFile { 179 | file: File, 180 | passes: bool, 181 | } 182 | -------------------------------------------------------------------------------- /src/stest/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io; 3 | use std::io::{stdin, stdout}; 4 | use std::process::ExitCode; 5 | use stest::config::Config; 6 | use stest::App; 7 | 8 | /// The stest program filters a list of files by their properties, in a way that is analogous to 9 | /// bash's builtin test, except that files that pass all tests are printed to stdout. 10 | fn main() -> ExitCode { 11 | let config = Config::parse(); 12 | let mut stdin = stdin().lock(); 13 | let mut stdout = stdout().lock(); 14 | let result = App::new(config).run(&mut stdin, &mut stdout); 15 | match result { 16 | Ok(true) => ExitCode::SUCCESS, 17 | Ok(false) => ExitCode::FAILURE, 18 | Err(error) => match error { 19 | io::Error { .. } => { 20 | print!("{}", error); 21 | ExitCode::FAILURE 22 | } 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/stest/src/semigroup.rs: -------------------------------------------------------------------------------- 1 | //! This module implements a Semigroup trait for several types used by stest. 2 | //! 3 | //! Semigroups are a common abstraction in functional program. They effectively mean that a type is 4 | //! combinable. A combinable type can easily be folded or reduced in the context of an iterator. 5 | //! 6 | //! See: https://en.wikipedia.org/wiki/Semigroup 7 | //! See: https://typelevel.org/cats/typeclasses/semigroup.html 8 | //! See: https://hackage.haskell.org/package/base-4.15.0.0/docs/Data-Semigroup.html 9 | 10 | /// Proves a type is combinable. 11 | /// 12 | /// A combinable type can be easily folded or reduced in the context of an iterator. 13 | pub trait Semigroup { 14 | fn combine(self, other: Self) -> Self; 15 | } 16 | 17 | /// Proves Result is a semigroup if A is a semigroup. 18 | /// 19 | /// Results are combined by sequencing them and either combining their successful, "left" side if 20 | /// they're both successful or else returning the first error in the sequence. 21 | impl Semigroup for Result { 22 | fn combine(self, other: Result) -> Result { 23 | self.and_then(|a| other.map(|other_a| a.combine(other_a))) 24 | } 25 | } 26 | 27 | /// Proves Vec is a semigroup. 28 | /// 29 | /// Vectors are combined by appending the other vector onto the one whose combine method is called. 30 | impl Semigroup for Vec { 31 | fn combine(mut self: Vec, mut other: Vec) -> Vec { 32 | self.append(&mut other); 33 | self 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/stest/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | //! These stest integration tests use executable bash files to set up their test fixtures (files 2 | //! and directories on the local filesystem). The bash is written to try to be portable across 3 | //! systems, but it may not be. 4 | //! 5 | //! Most tests share roughly the same structure. They set up both positive and negative test cases 6 | //! (i.e., a file that will pass stest under a given configuration and a file that will not), then 7 | //! generate a config struct and expected output, then run the stest app, and then assert that the 8 | //! actual output equals the expected output. Comparisons are done using the File struct rather 9 | //! than raw stdout byte streams, because it makes test failures easier to read. 10 | 11 | use std::io; 12 | use std::path::PathBuf; 13 | use std::process::Command; 14 | use std::str; 15 | use stest::config::Config; 16 | use stest::file::File; 17 | use stest::App; 18 | 19 | /// Test stest in its default configuration without any options. 20 | /// 21 | /// Note that this test's expectations are predicated on the fact that stest always tests that a 22 | /// file exists, both with and without the exists option (-e) being explicitly configured. See 23 | /// comments on this option in config.rs for more details. 24 | #[test] 25 | fn test() -> () { 26 | let config = EMPTY.clone(); 27 | let (actual, expected) = set_up_test(config, "set-up-file", "set-up-nonexisting-file"); 28 | assert_eq!(actual, expected); 29 | } 30 | 31 | #[test] 32 | fn test_hidden_file() -> () { 33 | let config = { 34 | let mut config = EMPTY.clone(); 35 | config.requires_each_file_is_hidden = true; 36 | config 37 | }; 38 | let (actual, expected) = set_up_test(config, "set-up-hidden-file", "set-up-file"); 39 | assert_eq!(actual, expected); 40 | } 41 | 42 | #[test] 43 | fn test_block_special_file() -> () { 44 | let config = { 45 | let mut config = EMPTY.clone(); 46 | config.requires_each_file_is_block_special = true; 47 | config 48 | }; 49 | let (actual, expected) = set_up_test(config, "set-up-block-special-file", "set-up-file"); 50 | assert_eq!(actual, expected); 51 | } 52 | 53 | #[test] 54 | fn test_character_special_file() -> () { 55 | let config = { 56 | let mut config = EMPTY.clone(); 57 | config.requires_each_file_is_character_special = true; 58 | config 59 | }; 60 | let (actual, expected) = set_up_test(config, "set-up-character-special-file", "set-up-file"); 61 | assert_eq!(actual, expected); 62 | } 63 | 64 | #[test] 65 | fn test_directory() -> () { 66 | let config = { 67 | let mut config = EMPTY.clone(); 68 | config.requires_each_file_is_directory = true; 69 | config 70 | }; 71 | let (actual, expected) = set_up_test(config, "set-up-directory", "set-up-file"); 72 | assert_eq!(actual, expected); 73 | } 74 | 75 | #[test] 76 | fn test_exists() -> () { 77 | let config = { 78 | let mut config = EMPTY.clone(); 79 | config.requires_each_file_exists = true; 80 | config 81 | }; 82 | let (actual, expected) = set_up_test(config, "set-up-file", "set-up-nonexisting-file"); 83 | assert_eq!(actual, expected); 84 | } 85 | 86 | #[test] 87 | fn test_file() -> () { 88 | let config = { 89 | let mut config = EMPTY.clone(); 90 | config.requires_each_file_is_file = true; 91 | config 92 | }; 93 | let (actual, expected) = set_up_test(config, "set-up-file", "set-up-directory"); 94 | assert_eq!(actual, expected); 95 | } 96 | 97 | #[test] 98 | fn test_set_group_id_file() -> () { 99 | let config = { 100 | let mut config = EMPTY.clone(); 101 | config.requires_each_file_has_set_group_id = true; 102 | config 103 | }; 104 | let (actual, expected) = set_up_test(config, "set-up-file-with-set-group-id", "set-up-file"); 105 | assert_eq!(actual, expected); 106 | } 107 | 108 | #[test] 109 | fn test_symbolic_link() -> () { 110 | let config = { 111 | let mut config = EMPTY.clone(); 112 | config.requires_each_file_is_symbolic_link = true; 113 | config 114 | }; 115 | let (actual, expected) = set_up_test(config, "set-up-symbolic-link", "set-up-file"); 116 | assert_eq!(actual, expected); 117 | } 118 | 119 | #[test] 120 | fn test_contents_of_directories() -> () { 121 | let (input, output) = { 122 | let directory = run_script("set-up-directory-with-contents"); 123 | let contents = { 124 | directory 125 | .clone() 126 | .into_iter() 127 | .map(|file| file.read_directory().unwrap()) 128 | .flatten() 129 | .map(|f| f.clone_with_path_as_file_name().unwrap()) 130 | .collect() 131 | }; 132 | (directory, contents) 133 | }; 134 | 135 | let config = { 136 | let mut config = EMPTY.clone(); 137 | config.test_contents_of_directories = true; 138 | config.files = input; 139 | config 140 | }; 141 | let mut stdin: &[u8] = &[]; 142 | let mut stdout: Vec = vec![]; 143 | 144 | let app = App::new(config); 145 | let result = app.run(&mut stdin, &mut stdout); 146 | let actual = StestResult::new(result, stdout.to_files()); 147 | 148 | let expected: StestResult = { 149 | let stdout = output; 150 | StestResult::new(Ok(true), stdout) 151 | }; 152 | 153 | assert_eq!(actual, expected); 154 | } 155 | 156 | #[test] 157 | fn test_newer_than_oldest_file() -> () { 158 | let negative_case = run_script("set-up-file"); 159 | let mut oldest_file = run_script("set-up-file"); 160 | let positive_case = run_script("set-up-file"); 161 | 162 | let (input, output) = positive_and_negative_to_input_and_output(positive_case, negative_case); 163 | 164 | let config = { 165 | let mut config = EMPTY.clone(); 166 | config.oldest_file = oldest_file.pop(); 167 | config.files = input; 168 | config 169 | }; 170 | let mut stdin: &[u8] = &[]; 171 | let mut stdout: Vec = vec![]; 172 | 173 | let app = App::new(config); 174 | let result = app.run(&mut stdin, &mut stdout); 175 | let actual = StestResult::new(result, stdout.to_files()); 176 | 177 | let expected: StestResult = { 178 | let stdout = output; 179 | StestResult::new(Ok(true), stdout) 180 | }; 181 | 182 | assert_eq!(actual, expected); 183 | } 184 | 185 | /// Test stest configured with the newer than option (-n) that is passed a file that does not 186 | /// exist. 187 | /// 188 | /// This test expects stest to ignore the newer than check when passed a file that does not exist. 189 | /// See comments on this option in config.rs for more details. 190 | #[test] 191 | fn test_newer_than_oldest_file_that_does_not_exist() -> () { 192 | let positive_case_1 = run_script("set-up-file"); 193 | let mut oldest_file = run_script("set-up-nonexisting-file"); 194 | let positive_case_2 = run_script("set-up-file"); 195 | 196 | let mut positive_cases = positive_case_1.clone(); 197 | positive_cases.extend(positive_case_2.clone()); 198 | let negative_cases = vec![]; 199 | 200 | let (input, output) = positive_and_negative_to_input_and_output(positive_cases, negative_cases); 201 | 202 | let config = { 203 | let mut config = EMPTY.clone(); 204 | config.oldest_file = oldest_file.pop(); 205 | config.files = input; 206 | config 207 | }; 208 | let mut stdin: &[u8] = &[]; 209 | let mut stdout: Vec = vec![]; 210 | 211 | let app = App::new(config); 212 | let result = app.run(&mut stdin, &mut stdout); 213 | let actual = StestResult::new(result, stdout.to_files()); 214 | 215 | let expected: StestResult = { 216 | let stdout = output; 217 | StestResult::new(Ok(true), stdout) 218 | }; 219 | 220 | assert_eq!(actual, expected); 221 | } 222 | 223 | #[test] 224 | fn test_older_than_newest_file() -> () { 225 | let positive_case = run_script("set-up-file"); 226 | let mut newest_file = run_script("set-up-file"); 227 | let negative_case = run_script("set-up-file"); 228 | 229 | let (input, output) = positive_and_negative_to_input_and_output(positive_case, negative_case); 230 | 231 | let config = { 232 | let mut config = EMPTY.clone(); 233 | config.newest_file = newest_file.pop(); 234 | config.files = input; 235 | config 236 | }; 237 | let mut stdin: &[u8] = &[]; 238 | let mut stdout: Vec = vec![]; 239 | 240 | let app = App::new(config); 241 | let result = app.run(&mut stdin, &mut stdout); 242 | let actual = StestResult::new(result, stdout.to_files()); 243 | 244 | let expected: StestResult = { 245 | let stdout = output; 246 | StestResult::new(Ok(true), stdout) 247 | }; 248 | 249 | assert_eq!(actual, expected); 250 | } 251 | 252 | /// Test stest configured with the older than option (-o) that is passed a file that does not 253 | /// exist. 254 | /// 255 | /// This test expects stest to ignore the older than check when passed a file that does not exist. 256 | /// See comments on this option in config.rs for more details. 257 | #[test] 258 | fn test_older_than_newest_file_that_does_not_exist() -> () { 259 | let positive_case_1 = run_script("set-up-file"); 260 | let mut newest_file = run_script("set-up-nonexisting-file"); 261 | let positive_case_2 = run_script("set-up-file"); 262 | 263 | let mut positive_cases = positive_case_1.clone(); 264 | positive_cases.extend(positive_case_2.clone()); 265 | let negative_cases = vec![]; 266 | 267 | let (input, output) = positive_and_negative_to_input_and_output(positive_cases, negative_cases); 268 | 269 | let config = { 270 | let mut config = EMPTY.clone(); 271 | config.newest_file = newest_file.pop(); 272 | config.files = input; 273 | config 274 | }; 275 | let mut stdin: &[u8] = &[]; 276 | let mut stdout: Vec = vec![]; 277 | 278 | let app = App::new(config); 279 | let result = app.run(&mut stdin, &mut stdout); 280 | let actual = StestResult::new(result, stdout.to_files()); 281 | 282 | let expected: StestResult = { 283 | let stdout = output; 284 | StestResult::new(Ok(true), stdout) 285 | }; 286 | 287 | assert_eq!(actual, expected); 288 | } 289 | 290 | #[test] 291 | fn test_pipe_file() -> () { 292 | let config = { 293 | let mut config = EMPTY.clone(); 294 | config.requires_each_file_is_pipe = true; 295 | config 296 | }; 297 | let (actual, expected) = set_up_test(config, "set-up-pipe-file", "set-up-file"); 298 | assert_eq!(actual, expected); 299 | } 300 | 301 | #[test] 302 | fn test_readable_file() -> () { 303 | let config = { 304 | let mut config = EMPTY.clone(); 305 | config.requires_each_file_is_readable = true; 306 | config 307 | }; 308 | let (actual, expected) = set_up_test(config, "set-up-readable-file", "set-up-file"); 309 | assert_eq!(actual, expected); 310 | } 311 | 312 | #[test] 313 | fn test_nonempty_file() -> () { 314 | let config = { 315 | let mut config = EMPTY.clone(); 316 | config.requires_each_file_has_size_greater_than_zero = true; 317 | config 318 | }; 319 | let (actual, expected) = set_up_test(config, "set-up-nonempty-file", "set-up-file"); 320 | assert_eq!(actual, expected); 321 | } 322 | 323 | #[test] 324 | fn test_set_user_id_file() -> () { 325 | let config = { 326 | let mut config = EMPTY.clone(); 327 | config.requires_each_file_has_set_user_id = true; 328 | config 329 | }; 330 | let (actual, expected) = set_up_test(config, "set-up-file-with-set-user-id", "set-up-file"); 331 | assert_eq!(actual, expected); 332 | } 333 | 334 | /// Test stest configured with the inverted option (-v). 335 | /// 336 | /// Note that this test's expectations are predicated on the fact that stest always tests that a 337 | /// file exists, both with and without the exists option (-e) being explicitly configured. See 338 | /// comments on the requires_each_file_exists option in config.rs for more details. 339 | #[test] 340 | fn test_inverted() -> () { 341 | let config = { 342 | let mut config = EMPTY.clone(); 343 | config.has_inverted_tests = true; 344 | config 345 | }; 346 | let (actual, expected) = set_up_test(config, "set-up-nonexisting-file", "set-up-file"); 347 | assert_eq!(actual, expected); 348 | } 349 | 350 | #[test] 351 | fn test_writable_file() -> () { 352 | let config = { 353 | let mut config = EMPTY.clone(); 354 | config.requires_each_file_is_writable = true; 355 | config 356 | }; 357 | let (actual, expected) = set_up_test(config, "set-up-writable-file", "set-up-file"); 358 | assert_eq!(actual, expected); 359 | } 360 | 361 | #[test] 362 | fn test_executable_file() -> () { 363 | let config = { 364 | let mut config = EMPTY.clone(); 365 | config.requires_each_file_is_executable = true; 366 | config 367 | }; 368 | let (actual, expected) = set_up_test(config, "set-up-executable-file", "set-up-file"); 369 | assert_eq!(actual, expected); 370 | } 371 | 372 | static EMPTY: Config = Config { 373 | requires_each_file_is_hidden: false, 374 | requires_each_file_is_block_special: false, 375 | requires_each_file_is_character_special: false, 376 | requires_each_file_is_directory: false, 377 | requires_each_file_exists: false, 378 | requires_each_file_is_file: false, 379 | requires_each_file_has_set_group_id: false, 380 | requires_each_file_is_symbolic_link: false, 381 | test_contents_of_directories: false, 382 | oldest_file: None, 383 | newest_file: None, 384 | requires_each_file_is_pipe: false, 385 | quiet: false, 386 | requires_each_file_is_readable: false, 387 | requires_each_file_has_size_greater_than_zero: false, 388 | requires_each_file_has_set_user_id: false, 389 | has_inverted_tests: false, 390 | requires_each_file_is_writable: false, 391 | requires_each_file_is_executable: false, 392 | files: Vec::new(), 393 | }; 394 | 395 | #[derive(Debug, PartialEq)] 396 | struct StestResult { 397 | result: Result, 398 | stdout: Vec, 399 | } 400 | 401 | impl StestResult { 402 | fn new(result: Result, stdout: Vec) -> Self { 403 | let result = result.map_err(|io_error| IOErrorWithPartialEq(io_error)); 404 | StestResult { result, stdout } 405 | } 406 | } 407 | 408 | /// io::Error does not define PartialEq for a good reason: what are the semantics of equality 409 | /// between IO errors? Aren't they always (or nearly always) unique? Instead, we define a newtype 410 | /// that has the semantics we want when comparing IO errors when integration testing here: they're 411 | /// all equivalent. 412 | /// See: https://users.rust-lang.org/t/help-understanding-io-error-and-lack-of-partialeq/13212/2 413 | #[derive(Debug)] 414 | struct IOErrorWithPartialEq(io::Error); 415 | 416 | impl PartialEq for IOErrorWithPartialEq { 417 | fn eq(&self, _other: &Self) -> bool { 418 | true 419 | } 420 | } 421 | 422 | fn set_up_test( 423 | config: Config, 424 | positive_case: &str, 425 | negative_case: &str, 426 | ) -> (StestResult, StestResult) { 427 | let (input, output): (Vec, Vec) = 428 | set_up_positive_and_negative_tests(positive_case, negative_case); 429 | 430 | let config = { 431 | let mut config = config.clone(); 432 | config.files = input; 433 | config 434 | }; 435 | let mut stdin: &[u8] = &[]; 436 | let mut stdout: Vec = vec![]; 437 | 438 | let app = App::new(config); 439 | let result = app.run(&mut stdin, &mut stdout); 440 | let actual = StestResult::new(result, stdout.to_files()); 441 | 442 | let expected: StestResult = { 443 | let stdout = output; 444 | StestResult::new(Ok(true), stdout) 445 | }; 446 | 447 | (actual, expected) 448 | } 449 | 450 | fn set_up_positive_and_negative_tests( 451 | positive_script_filename: &str, 452 | negative_script_filename: &str, 453 | ) -> (Vec, Vec) { 454 | let positive_cases = run_script(positive_script_filename); 455 | let negative_cases = run_script(negative_script_filename); 456 | positive_and_negative_to_input_and_output(positive_cases, negative_cases) 457 | } 458 | 459 | fn positive_and_negative_to_input_and_output( 460 | positive_cases: Vec, 461 | negative_cases: Vec, 462 | ) -> (Vec, Vec) { 463 | let input = { 464 | let mut vec = positive_cases.clone(); 465 | vec.extend(negative_cases.clone()); 466 | vec 467 | }; 468 | let output = positive_cases; 469 | (input, output) 470 | } 471 | 472 | fn run_script(script: &str) -> Vec { 473 | let path_buf: PathBuf = { 474 | let mut path_buf = { 475 | let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); 476 | PathBuf::from(cargo_manifest_dir) 477 | }; 478 | let string = format!("tests/{script}"); 479 | path_buf.push(string); 480 | path_buf 481 | }; 482 | let os_str = path_buf.as_path().as_os_str(); 483 | Command::new(os_str) 484 | .output() 485 | .map(|output| output.stdout) 486 | .map(|vec| vec.to_files()) 487 | .unwrap() 488 | } 489 | 490 | trait ToFiles { 491 | fn to_files(&self) -> Vec; 492 | } 493 | 494 | impl ToFiles for Vec { 495 | fn to_files(&self) -> Vec { 496 | let slice = self.as_slice(); 497 | let str = str::from_utf8(slice).unwrap(); 498 | // Note, we trim the final newline before splitting. 499 | let vec: Vec<&str> = str.trim_end().split('\n').collect(); 500 | vec.into_iter() 501 | .map(|str| PathBuf::from(str)) 502 | .map(|path_buf| File::new(path_buf)) 503 | .collect() 504 | } 505 | } 506 | 507 | trait ToBytes { 508 | fn to_bytes(&self) -> Vec; 509 | } 510 | 511 | impl ToBytes for Vec { 512 | fn to_bytes(&self) -> Vec { 513 | self.into_iter() 514 | .map(|file| { 515 | let mut string = file.to_string(); 516 | // Note, this appends a newline at the end of the list of files. 517 | string.push('\n'); 518 | string 519 | }) 520 | .map(|string| string.into_bytes()) 521 | .flatten() 522 | .collect() 523 | } 524 | } 525 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-block-special-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Return a path to a block special file that exists on most systems. 4 | function main { 5 | echo "/dev/disk/by-diskseq/1" || return 1 6 | return 0 7 | } 8 | 9 | main "$@" 10 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-character-special-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Return a path to a character special file that exists on most systems. 4 | function main { 5 | echo "/dev/tty" || return 1 6 | return 0 7 | } 8 | 9 | main "$@" 10 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-directory: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main: () -> string 3 | # Create a directory. Return the path to the directory. 4 | function main { 5 | local -r tmp=$(mktemp --directory) || return 1 6 | local -r directory="$tmp/directory" || return 1 7 | mkdir "$directory" || return 1 8 | echo "$directory" || return 1 9 | return 0 10 | } 11 | 12 | main "$@" 13 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-directory-with-contents: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a directory that contains multiple files. Return its path. 4 | function main { 5 | local -r directory=$(mktemp --directory) || return 1 6 | touch "$directory/file1" || return 1 7 | touch "$directory/file2" || return 1 8 | touch "$directory/file3" || return 1 9 | echo "$directory" || return 1 10 | return 0 11 | } 12 | 13 | main "$@" 14 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-executable-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a file that is only executable. Return the path to the file. 4 | function main { 5 | local -r tmp=$(mktemp --directory) || return 1 6 | local -r file="$tmp/executable-file" || return 1 7 | touch "$file" || return 1 8 | chmod ugo-srwx "$file" || return 1 9 | chmod ugo+x "$file" || return 1 10 | echo "$file" || return 1 11 | return 0 12 | } 13 | 14 | main "$@" 15 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a file. Return the path to the file. 4 | function main { 5 | local -r tmp=$(mktemp --directory) || return 1 6 | local -r file="$tmp/file" || return 1 7 | touch "$file" || return 1 8 | chmod ugo-srwx "$file" || return 1 9 | echo "$file" || return 1 10 | return 0 11 | } 12 | 13 | main "$@" 14 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-file-with-set-group-id: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a file with the set-group-ID bit set. Return the path to this file. 4 | function main { 5 | local -r tmp=$(mktemp --directory) || return 1 6 | local -r file="$tmp/file-with-set-group-id" || return 1 7 | touch "$file" || return 1 8 | chmod ugo-srwx "$file" || return 1 9 | chmod g+s "$file" || return 1 10 | echo "$file" || return 1 11 | return 0 12 | } 13 | 14 | main "$@" 15 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-file-with-set-user-id: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a file with the set-user-ID bit set. Return the path to the file. 4 | function main { 5 | local -r tmp=$(mktemp --directory) || return 1 6 | local -r file="$tmp/file-with-set-user-id" || return 1 7 | touch "$file" || return 1 8 | chmod ugo-srwx "$file" || return 1 9 | chmod u+s "$file" || return 1 10 | echo "$file" || return 1 11 | return 0 12 | } 13 | 14 | main "$@" 15 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-hidden-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a hidden file. Return the path to the file. 4 | function main { 5 | local -r tmp=$(mktemp --directory) || return 1 6 | local -r file="$tmp/.hidden-file" || return 1 7 | touch "$file" || return 1 8 | echo "$file" || return 1 9 | return 0 10 | } 11 | 12 | main "$@" 13 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-nonempty-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a file. Return the path to the file. 4 | function main { 5 | local -r tmp=$(mktemp --directory) || return 1 6 | local -r file="$tmp/nonempty-file" || return 1 7 | echo "This file is not empty." > "$file" || return 1 8 | chmod ugo-srwx "$file" || return 1 9 | echo "$file" || return 1 10 | return 0 11 | } 12 | 13 | main "$@" 14 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-nonexisting-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Return a file that we can reasonably guarantee does not exist. 4 | function main { 5 | local -r file=$(mktemp --dry-run) || return 1 6 | echo "$file" || return 1 7 | return 0 8 | } 9 | 10 | main "$@" 11 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-pipe-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a named pipe. Return the path to the file. 4 | function main { 5 | local -r tmp=$(mktemp --directory) || return 1 6 | local -r file="$tmp/pipe-file" || return 1 7 | mkfifo --mode=0000 "$file" || return 1 8 | echo "$file" || return 1 9 | return 0 10 | } 11 | 12 | main "$@" 13 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-readable-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a file that is only readable. Return the path to the file. 4 | function main { 5 | local -r tmp=$(mktemp --directory) || return 1 6 | local -r file="$tmp/readable-file" || return 1 7 | touch "$file" || return 1 8 | chmod ugo-srwx "$file" || return 1 9 | chmod ugo+r "$file" || return 1 10 | echo "$file" || return 1 11 | return 0 12 | } 13 | 14 | main "$@" 15 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-symbolic-link: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a symbolic link to a file. Return the path to the link. 4 | function main { 5 | local -r file=$(mktemp) || return 1 6 | local -r tmp=$(mktemp --directory) || return 1 7 | local -r link="$tmp/symbolic-link" || return 1 8 | ln -s "$file" "$link" || return 1 9 | echo "$link" || return 1 10 | return 0 11 | } 12 | 13 | main "$@" 14 | -------------------------------------------------------------------------------- /src/stest/tests/set-up-writable-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # main : () -> string 3 | # Create a file that is only writable. Return the path to the file. 4 | function main { 5 | local -r tmp=$(mktemp --directory) || return 1 6 | local -r file="$tmp/writable-file" || return 1 7 | touch "$file" || return 1 8 | chmod ugo-srwx "$file" || return 1 9 | chmod ugo+w "$file" || return 1 10 | echo "$file" || return 1 11 | return 0 12 | } 13 | 14 | main "$@" 15 | --------------------------------------------------------------------------------