Skip to content

Instantly share code, notes, and snippets.

@Ajnasz
Last active July 16, 2025 13:24
Show Gist options
  • Save Ajnasz/4f1586775d4c6edd3666fc376d4b3296 to your computer and use it in GitHub Desktop.
Save Ajnasz/4f1586775d4c6edd3666fc376d4b3296 to your computer and use it in GitHub Desktop.
#!/usr/bin/env sh
# This script extends the functionality of dmenu_run by allowing the user to
# include custom commands and executables. The custom commands are read from a
# file specified by the environment variable DMENU_RUN_COMMANDS, and the
# executables are read from a directory specified by the environment variable
# DMENU_RUN_BIN.
#
# Environment Variables:
# DMENU_RUN_COMMANDS: Path to a file containing custom commands to be included
# in the dmenu.
# Each line can be a regular command or an alias
#
# DMENU_RUN_BIN: Path to a directory containing executables.
# The executables should echo the commands to be included in
# the dmenu.
# If executable should print a command or an alias.
#
# Usage:
# Set the environment variables DMENU_RUN_COMMANDS and DMENU_RUN_BIN to point
# to your custom commands file and executables directory, respectively. Then,
# run the script. The script will add the custom commands and executables to
# the dmenu.
#
# Example:
# DMENU_RUN_COMMANDS=/path/to/commands.txt DMENU_RUN_BIN=/path/to/bin \
# ./dmenu_run2
#
# Aliases:
# If you want to include aliases in the dmenu, you can define them in the following
# forrmat:
# alias name="command --with-args"
# alias name with spaces="command2"
# The script will extract the alias name and command from the alias definition
# and include them in the dmenu.
#
# Note:
# The script assumes that all files in the DMENU_RUN_BIN directory are
# executable and that they will echo the correct commands. Ensure that these
# conditions are met to avoid unexpected behavior.
#
# The script runs in the background, and the selected command from the dmenu
# is executed in a subshell.
#
# FZF environment variable:
# The script sets the FZF environment variable to the original dmenu command
# This allows you to open dmenu with the same options as the original dmenu_run2
# example script to search for words in the dictionary with dmenu or fzf:
# > cat /usr/local/bin/words
# #/usr/bin/env sh
#
# fuzzy_find="${FZF:-fzf}"
# $fuzzy_find < /usr/share/dict/words
#
# If the `words` script called from dmenu_run2 the use dmenu to select a word
# otherwise it will use `fzf`
# Function to extract alias name from alias definition
extract_alias_name() {
printf '%s\n' "$1" | sed 's/^alias \([^=]*\)=.*/\1/'
}
# Function to extract alias command from alias definition
extract_alias_command() {
printf '%s\n' "$1" | sed 's/^alias [^=]*=//' | sed 's/^"\(.*\)"$/\1/'
}
# shellcheck disable=SC2317 # Don't warn about unreachable commands in this function
cleanup() {
rm -f "$DMENU_FIFO" "$DMENU_ALL_COMMANDS"
}
# Function to process commands and aliases
process_custom_commands() {
while IFS= read -r line || [ -n "$line" ]; do
# Skip comments and empty lines
case "$line" in
\#*|'') continue ;;
alias\ *=*)
# Extract and display alias name only
extract_alias_name "$line"
;;
*)
# Regular command
printf '%s\n' "$line"
;;
esac
done
}
# Function to resolve alias to its command
# shellcheck disable=SC2317 # Don't warn about unreachable commands in this function
resolve_alias() {
while IFS= read -r selected || [ -n "$selected" ]; do
# read $DMENU_RUN_COMMANDS and resolve aliases
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
alias\ *=*)
alias_name=$(extract_alias_name "$line")
if [ "$selected" = "$alias_name" ]; then
alias_command=$(extract_alias_command "$line")
# If selected item is an alias, print its command
printf '%s\n' "$alias_command"
break
fi
;;
*)
# If selected item is a regular command, print it as is
if [ "$selected" = "$line" ]; then
printf '%s\n' "$line"
break
fi
;;
esac
done < "$DMENU_ALL_COMMANDS"
done
}
if [ -z "$DMENU_RUN_BIN" ] || [ ! -d "$DMENU_RUN_BIN" ];then
echo "Warning: DMENU_RUN_BIN is not set or is not a directory" >&2
fi
if [ -z "$DMENU_RUN_COMMANDS" ] || [ ! -f "$DMENU_RUN_COMMANDS" ];then
echo "Warning: DMENU_RUN_COMMANDS is not set or is not a file" >&2
fi
DMENU="${DMENU:-dmenu}"
# DMENU="rofi -dmenu -i -no-fixed-num-lines -no-show-icons"
if [ -n "$WAYLAND_DISPLAY" ]; then
DMENU="bemenu"
fi
# Create FIFO for streaming to dmenu
DMENU_FIFO="$(mktemp -u -t dmenu_fifo_XXXXXX)"
mkfifo "$DMENU_FIFO"
# Create temp file for alias resolution (still needed)
DMENU_ALL_COMMANDS="$(mktemp -t dmenu_run2_XXXXXX)"
trap cleanup EXIT INT TERM
# Populate both FIFO and temp file simultaneously
{
# include output of executables from DMENU_RUN_BIN
if [ -n "$DMENU_RUN_BIN" ] && [ -d "$DMENU_RUN_BIN" ]; then
find "$DMENU_RUN_BIN" -maxdepth 1 -type f -executable -exec {} \;
fi
# include custom commands from DMENU_RUN_COMMANDS
if [ -n "$DMENU_RUN_COMMANDS" ] && [ -f "$DMENU_RUN_COMMANDS" ]; then
cat "$DMENU_RUN_COMMANDS"
fi
# always include dmenu_path
if [ "$DMENU_NO_PATH" != "true" ]; then
dmenu_path;
fi
} | tee "$DMENU_ALL_COMMANDS" > "$DMENU_FIFO" &
TEES_PID=$!
# Process and execute
if process_custom_commands < "$DMENU_FIFO" | $DMENU "$@" | resolve_alias | env FZF="$DMENU $*" "${SHELL:-"/usr/bin/env sh"}" &
then
DMENU_PID=$!
else
echo "Failed to start dmenu process" >&2
kill $TEES_PID 2>/dev/null
exit 1
fi
wait $TEES_PID
TEE_EXIT=$?
if [ $TEE_EXIT -ne 0 ]; then
echo "Warning: Data population process failed" >&2
fi
wait $DMENU_PID
DMENU_EXIT=$?
# Don't cleanup here since trap will handle it
exit $DMENU_EXIT
#!/bin/sh
# Interactive Dmenu Command Pipeline
#
# This script accepts an executable file as input and creates an interactive
# pipeline where the output of each command execution becomes input for dmenu,
# and the dmenu selection becomes input for the next command execution.
#
# Usage: ./dmenu_script <executable_file> <dmenu_options>
#
# The executable file should accept an optional argument and output selectable
# options.
# The loop continues until an empty selection is made or either the script or
# dmenu execution failed.
#
# Example:
# ./dmenu_script my_command.sh -l 15 -p "Select an option:" -fn 'monospace-10' -nb '#222222' -nf lightblue -sb black -sf pink
#
# Example of `my_command.sh`:
# #!/usr/bin/env sh
#
# case "$1" in
# "")
# printf 'foo\nnon existing command'
# ;;
# "foo")
# echo "bar"
# ;;
# "bar")
# # final command selected, perform some action
# exit 0
# ;;
# *)
# echo "Invalid argument: $1" >&2
# echo "Usage: $0 <foo|bar>" >&2
# exit 1
# esac
if [ $# -eq 0 ];then
exit 0
fi
if [ ! -e "$1" ];then
echo "File $1 does not exist." >&2
exit 1
fi
if [ ! -x "$1" ];then
echo "File $1 is not executable." >&2
exit 1
fi
script_name="$1"
shift
DMENU="${DMENU:-dmenu}"
if [ -n "$WAYLAND_DISPLAY" ]; then
DMENU="bemenu"
fi
while true;do
if [ -z "$ret" ];then
if ! ret="$($script_name)";then
echo "Command execution failed." >&2
exit 1
fi
else
if ! ret="$($script_name "$ret")";then
echo "Command execution failed." >&2
exit 1
fi
fi
if [ -z "$ret" ];then
exit 0
fi
if ! ret="$(echo "$ret" | $DMENU "$@")";then
echo "Dmenu selection failed or was cancelled." >&2
exit 1
fi
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment