├── .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 |
--------------------------------------------------------------------------------