#!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-only # Copyright (C) Ayush Agarwal # # vim: set expandtab ts=2 sw=2 sts=2: # # tessen - a data selection interface for pass and gopass on Wayland # ------------------------------------------------------------------------------ # don't leak password data if debug mode is enabled set +x # GLOBAL VARIABLES declare pass_backend dmenu_backend tsn_action tsn_config declare -a dmenu_backend_opts tmp_tofi_opts tmp_rofi_opts tmp_wofi_opts declare tsn_userkey tsn_urlkey tsn_autokey tsn_delay tsn_web_browser # show both actions, 'autotype' and 'copy', to choose from by default tsn_action="default" tsn_otp=false # initialize default values for keys tsn_userkey="user" tsn_urlkey="url" tsn_autokey="autotype" tsn_delay=100 # initialize the default location of the config file tsn_config="${XDG_CONFIG_HOME:-$HOME/.config}"/tessen/config # variables with sensitive data which will be manually unset using _clear declare tsn_passfile tsn_username tsn_password tsn_url tsn_autotype chosen_key declare -i _EXIT_STATUS declare -A tsn_passdata # FIRST MENU: generate a list of pass files, let the user select one get_pass_files() { local tmp_prefix="${PASSWORD_STORE_DIR:-$HOME/.password-store}" if ! [[ -d $tmp_prefix ]]; then _die "password store directory not found" fi local -a tmp_pass_files # temporarily enable globbing, get the list of all gpg files recursively, # remove PASSWORD_STORE_DIR from the file names, and remove the '.gpg' suffix shopt -s nullglob globstar tmp_pass_files=("$tmp_prefix"/**/*.gpg) tmp_pass_files=("${tmp_pass_files[@]#"$tmp_prefix"/}") tmp_pass_files=("${tmp_pass_files[@]%.gpg}") shopt -u nullglob globstar tsn_passfile="$(printf "%s\n" "${tmp_pass_files[@]}" \ | "$dmenu_backend" "${dmenu_backend_opts[@]}")" _EXIT_STATUS="$?" if ! [[ -f "$tmp_prefix/$tsn_passfile".gpg ]]; then _die fi unset -v tmp_pass_files tmp_prefix } # FIRST MENU: generate a list of gopass files, let the user select one get_gopass_files() { local line path_files file mount_name tmp_tsn_passfile local -A tmp_gopass_files local -a mount_name_arr # why not use `gopass config path` and `gopass ls -f`? shopt -s nullglob globstar while read -r line || [[ -n $line ]]; do # we assume that we'll encounter `path: ...` only once and as soon as we # do, we parse the list of all the files inside the dir and store them in # an associative array with the name of the files as the index and the path # as the value if [[ $line == mounts.path* ]] && [[ -d ${line#*= } ]]; then path_files=("${line#*= }"/**/*.gpg) path_files=("${path_files[@]#"${line#*= }"/}") path_files=("${path_files[@]%.gpg}") for file in "${path_files[@]}"; do tmp_gopass_files["$file"]="${line#*= }" done fi # similarly, we go through the mount points, generate the list of files # inside those mount points, add those files to the associative array with # the file names as the index and the location of the mount point as the # value if [[ $line == mounts.*.path* ]]; then # remove the quotes from the parsed line line="${line//\"/}" # the mount name needs to be extracted to distinguish files with # potentially identical names mount_name="${line#mounts*.}" mount_name="${mount_name%.path*}" mount_name_arr+=("$mount_name") if [[ -d ${line#*= } ]]; then path_files=("${line#*= }"/**/*.gpg) path_files=("${path_files[@]#"${line#*= }"/}") path_files=("${path_files[@]%.gpg}") path_files=("${path_files[@]/#/"$mount_name/"}") for file in "${path_files[@]}"; do tmp_gopass_files["$file"]="${line#*= }" done fi fi done < <(gopass config) shopt -u nullglob globstar # the actual menu tsn_passfile="$(printf "%s\n" "${!tmp_gopass_files[@]}" \ | "$dmenu_backend" "${dmenu_backend_opts[@]}")" _EXIT_STATUS="$?" if [[ -z $tsn_passfile ]]; then _die fi # remove the mount name for the path check to be successful # initialize the temp variable with the value of tsn_passfile in case an # entry from the gopass path is chosen tmp_tsn_passfile="$tsn_passfile" for idx in "${mount_name_arr[@]}"; do if [[ ${tsn_passfile%%/*} == "$idx" ]]; then tmp_tsn_passfile="${tsn_passfile#*/}" fi done # we had to use an associative array to keep track of the absolute path of # the selected file because it is possible to give invalid input to dmenu # while making a selection and tessen should exit in that case if [[ -n ${tmp_gopass_files["$tsn_passfile"]} ]]; then if ! [[ -f "${tmp_gopass_files["$tsn_passfile"]}"/"$tmp_tsn_passfile".gpg ]]; then _die "the selected file was not found" fi fi unset -v tmp_gopass_files line path_files file mount_name mount_name_arr tmp_tsn_passfile } # parse the password store file for username, password, otp, custom autotype, # and other key value pairs get_pass_data() { local -a passdata local keyval_regex otp_regex idx key val if [[ $pass_backend == "pass" ]]; then mapfile -t passdata < <(pass show "$tsn_passfile" 2> /dev/null) if [[ ${#passdata[@]} -eq 0 ]]; then _die "$tsn_passfile is empty" fi elif [[ $pass_backend == "gopass" ]]; then # gopass show -n -f is weird because it emits a first line 'Secret: # truncated-file-name' and that doesn't get assigned to a variable. but if # I redirect stdout to /dev/null, that first line gets redirected as well. # there doesn't seem to be any way to disable printing this first line. mapfile -t passdata < <(gopass show -n -f "$tsn_passfile" 2> /dev/null) if [[ ${#passdata[@]} -eq 0 ]]; then _die "$tsn_passfile is empty" fi fi keyval_regex='^[[:alnum:][:blank:]+#@_-]+:[[:blank:]].+$' # for parsing the 'otpauth://' URI # this regex is borrowed from pass-otp at commit 3ba564c otp_regex='^otpauth:\/\/(totp|hotp)(\/(([^:?]+)?(:([^:?]*))?)(:([0-9]+))?)?\?(.+)$' tsn_password="${passdata[0]}" for idx in "${passdata[@]:1}"; do key="${idx%%:*}" val="${idx#*: }" if [[ ${key,,} == "password" ]]; then continue elif [[ ${key,,} =~ ^$tsn_userkey$ ]] && [[ -z ${tsn_username} ]]; then tsn_userkey="${key,,}" tsn_username="$val" elif [[ ${key,,} =~ ^$tsn_autokey$ ]] && [[ -z ${tsn_autotype} ]]; then tsn_autokey="${key,,}" tsn_autotype="$val" elif [[ ${key,,} =~ ^$tsn_urlkey$ ]] && [[ -z ${tsn_url} ]]; then tsn_urlkey="${key,,}" tsn_url="$val" elif [[ $idx =~ $otp_regex ]] && [[ $tsn_otp == "false" ]]; then tsn_otp=true elif [[ $idx =~ $keyval_regex ]] && [[ -z ${tsn_passdata["$key"]} ]]; then tsn_passdata["$key"]="$val" fi done # if $tsn_userkey isn't found, use the basename of file as username # also set the value of the tsn_userkey to the default value # this prevents the userkey from showing up as a regex in case a user has set # it in the config file # the same goes for other custom key variables if [[ -z $tsn_username ]]; then tsn_username="${tsn_passfile##*/}" tsn_userkey="user" fi if [[ -z $tsn_autotype ]]; then tsn_autokey="autotype" fi if [[ -z $tsn_url ]]; then tsn_urlkey="url" fi unset -v passdata keyval_regex otp_regex idx key val } # map custom actions to specific dmenu exit codes custom_keyb_action() { case "$_EXIT_STATUS" in 10) auto_type_def ;; 11) auto_type "$tsn_username" ;; 12) auto_type "$tsn_password" ;; 13) key_open_url ;; 14) wld_copy "$tsn_username" ;; 15) wld_copy "$tsn_password" ;; 16) wld_copy "$tsn_url" ;; *) _die "unmapped exit code detected" ;; esac } # SECOND MENU: show a list of possible keys to choose from for autotyping or # copying, depending on the value of tsn_action # THIRD MENU: optional, this will show up if tsn_action is blank get_key() { local -a key_arr local ch flag=false # the 2nd menu for autotype, both, and the default actions will be the same # and the autotype key will be present in these cases # when tsn_action is set to copy, the autotype key shouldn't be shown in the 2nd menu case "$tsn_action" in autotype | both | default) if [[ $1 == "key_list" ]]; then if [[ $tsn_otp == "false" ]] && [[ -z $tsn_url ]]; then key_arr=("$tsn_autokey" "$tsn_userkey" "password" "${!tsn_passdata[@]}") elif [[ $tsn_otp == "false" ]] && [[ -n $tsn_url ]]; then key_arr=("$tsn_autokey" "$tsn_userkey" "password" "$tsn_urlkey" "${!tsn_passdata[@]}") elif [[ $tsn_otp == "true" ]] && [[ -z $tsn_url ]]; then key_arr=("$tsn_autokey" "$tsn_userkey" "password" "otp" "${!tsn_passdata[@]}") elif [[ $tsn_otp == "true" ]] && [[ -n $tsn_url ]]; then key_arr=("$tsn_autokey" "$tsn_userkey" "password" "otp" "$tsn_urlkey" "${!tsn_passdata[@]}") fi fi # the (optional) third menu, its appearance depends on tsn_action being default if [[ $tsn_action == "default" ]] && [[ $1 == "option" ]]; then key_arr=("$tsn_autokey" "copy") # the (optional) third menu if tsn_urlkey is chosen, it depends on # tsn_action being default elif [[ $tsn_action == "default" ]] && [[ $1 == "$tsn_urlkey" ]]; then key_arr=("open" "copy") fi ;; copy) if [[ $1 == "key_list" ]]; then if [[ $tsn_otp == "false" ]] && [[ -z $tsn_url ]]; then key_arr=("$tsn_userkey" "password" "${!tsn_passdata[@]}") elif [[ $tsn_otp == "false" ]] && [[ -n $tsn_url ]]; then key_arr=("$tsn_userkey" "password" "$tsn_urlkey" "${!tsn_passdata[@]}") elif [[ $tsn_otp == "true" ]] && [[ -z $tsn_url ]]; then key_arr=("$tsn_userkey" "password" "otp" "${!tsn_passdata[@]}") elif [[ $tsn_otp == "true" ]] && [[ -n $tsn_url ]]; then key_arr=("$tsn_userkey" "password" "otp" "$tsn_urlkey" "${!tsn_passdata[@]}") fi fi ;; esac # a global variable to hold the selected key for key_menu chosen_key="$(printf "%s\n" "${key_arr[@]}" | "$dmenu_backend" "${dmenu_backend_opts[@]}")" # validate the chosen key, if it doesn't exist, exit for ch in "${key_arr[@]}"; do if [[ $chosen_key == "$ch" ]]; then flag=true break fi done if [[ $flag == "false" ]]; then _die fi unset -v key_arr ch flag } # SECOND MENU: use 'get_key()' to show a list of possible keys to choose from key_menu() { get_key key_list case "$chosen_key" in "$tsn_autokey") auto_type_def ;; "$tsn_userkey") key_action "$tsn_username" ;; password) key_action "$tsn_password" ;; otp) key_otp ;; "$tsn_urlkey") key_action "$tsn_urlkey" ;; *) key_action "${tsn_passdata["$chosen_key"]}" ;; esac } # this function checks the value of tsn_action and decides if the third menu # should be presented or not # in case it receives "url", autotype becomes equivalent to opening the url in # a web browser key_action() { local arg="$1" case "$tsn_action" in autotype) if [[ $arg == "$tsn_urlkey" ]]; then key_open_url || _die return 0 fi auto_type "$arg" ;; copy) if [[ $arg == "$tsn_urlkey" ]]; then wld_copy "$tsn_url" || _die return 0 fi wld_copy "$arg" ;; both) if [[ $arg == "$tsn_urlkey" ]]; then key_open_url wld_copy "$tsn_url" else printf "%s" "$arg" | wtype -s "$tsn_delay" - wld_copy "$arg" fi ;; default) if [[ $arg == "$tsn_urlkey" ]]; then get_key "$tsn_urlkey" if [[ $chosen_key == "open" ]]; then key_open_url || _die return 0 else wld_copy "$tsn_url" fi else get_key option if [[ $chosen_key == "$tsn_autokey" ]]; then auto_type "$arg" else wld_copy "$arg" fi fi ;; esac unset -v arg } # THIRD MENU: optional, this function is used if an 'otpauth://' URI is found # OTP support in gopass is deprecated key_otp() { local tmp_otp if [[ $pass_backend == "pass" ]] && ! pass otp -h > /dev/null 2>&1; then _die "pass-otp is not installed" fi if [[ $pass_backend == "pass" ]]; then tmp_otp="$(pass otp "$tsn_passfile")" elif [[ $pass_backend == "gopass" ]]; then tmp_otp="$(gopass otp -o "$tsn_passfile")" fi if ! [[ $tmp_otp =~ ^[[:digit:]]+$ ]]; then _die "invalid OTP detected" fi key_action "$tmp_otp" unset -v tmp_otp } key_open_url() { if [[ -n $tsn_web_browser ]]; then "$tsn_web_browser" "$tsn_url" > /dev/null 2>&1 || { printf "%s\n" "$tsn_web_browser was unable to open '$tsn_url'" >&2 return 1 } elif is_installed xdg-open; then xdg-open "$tsn_url" 2> /dev/null || { printf "%s\n" "xdg-open was unable to open '$tsn_url'" >&2 return 1 } else _die "failed to open '$tsn_urlkey'" fi } # SECOND MENU: the default autotype function, either autotype the username and # password or the custom autotype defined by the user auto_type_def() { local word tmp_otp if [[ -z $tsn_autotype ]]; then printf "%s" "$tsn_username" | wtype -s "$tsn_delay" - wtype -s "$tsn_delay" -k Tab -- printf "%s" "$tsn_password" | wtype -s "$tsn_delay" - else for word in $tsn_autotype; do case "$word" in ":delay") sleep 1 ;; ":tab") wtype -s "$tsn_delay" -k Tab -- ;; ":space") wtype -s "$tsn_delay" -k space -- ;; ":enter") wtype -s "$tsn_delay" -k Return -- ;; ":otp") key_otp ;; path | basename | filename) printf "%s" "${tsn_passfile##*/}" | wtype -s "$tsn_delay" - ;; "$tsn_userkey") printf "%s" "$tsn_username" | wtype -s "$tsn_delay" - ;; pass | password) printf "%s" "$tsn_password" | wtype -s "$tsn_delay" - ;; *) if [[ -n ${tsn_passdata["$word"]} ]]; then printf "%s" "${tsn_passdata["$word"]}" | wtype -s "$tsn_delay" - else wtype -s "$tsn_delay" -k space -- fi ;; esac done fi } auto_type() { printf "%s" "$1" | wtype -s "$tsn_delay" - } wld_copy() { local tsn_cliptime if [[ $pass_backend == "pass" ]]; then tsn_cliptime="${PASSWORD_STORE_CLIP_TIME:-15}" if ! are_digits "$tsn_cliptime"; then printf "%s\n" "invalid clipboard timeout value in PASSWORD_STORE_CLIP_TIME" >&2 return 1 fi elif [[ $pass_backend == "gopass" ]]; then tsn_cliptime="$(gopass config core.cliptimeout)" tsn_cliptime="${tsn_cliptime##*: }" if ! are_digits "$tsn_cliptime"; then printf "%s\n" "invalid clipboard timeout value in cliptimeout" >&2 return 1 fi fi # it would've been better to use, or at least provide an option, to paste # only once using `wl-copy -o` but web browsers don't work well with this # feature # https://github.com/bugaevc/wl-clipboard/issues/107 printf "%s" "$1" | wl-copy if is_installed notify-send; then notify-send -t $((tsn_cliptime * 1000)) \ "data has been copied and will be cleared from the clipboard after $tsn_cliptime seconds" fi { sleep "$tsn_cliptime" || kill 0 wl-copy --clear } > /dev/null 2>&1 & unset -v tsn_cliptime unset -v tsn_passfile tsn_passdata tsn_username tsn_password chosen_key } are_digits() { if [[ $1 =~ ^[[:digit:]]+$ ]]; then return 0 else return 1 fi } validate_pass_backend() { if ! is_installed "$1"; then _die "please install a valid password store backend: pass | gopass" fi if [[ $1 == "pass" ]] || [[ $1 == "gopass" ]]; then pass_backend="$1" else _die "please specify a valid password store backend: pass | gopass" fi } validate_dmenu_backend() { if ! is_installed "$1"; then _die "please install a valid dmenu backend: fuzzel | tofi | bemenu | wofi | rofi | dmenu" fi local -a bemenu_opts case "$1" in rofi) dmenu_backend="rofi" dmenu_backend_opts=('-dmenu') ;; fuzzel) dmenu_backend="fuzzel" dmenu_backend_opts=('-d' '--log-level=warning') ;; tofi) dmenu_backend="tofi" dmenu_backend_opts=() ;; bemenu) dmenu_backend="bemenu" dmenu_backend_opts=() bemenu_opts=('-l' '10' '-n') if [[ -z ${BEMENU_OPTS[*]} ]]; then export BEMENU_OPTS="${bemenu_opts[*]}" fi ;; wofi) dmenu_backend="wofi" dmenu_backend_opts=('-d' '-k' '/dev/null') ;; dmenu) dmenu_backend="dmenu" dmenu_backend_opts=() ;; *) _die "please install a valid dmenu backend: fuzzel | tofi | bemenu | wofi | rofi | dmenu" ;; esac unset -v bemenu_opts } validate_action() { case "$1" in autotype) if ! is_installed "wtype"; then _die "wtype is not installed, unable to autotype pass data" fi tsn_action="autotype" ;; copy) if ! is_installed "wl-copy"; then _die "wl-clipboard is not installed, unable to copy-paste pass data" fi tsn_action="copy" ;; both) if ! is_installed "wtype"; then _die "wtype is not installed, unable to autotype pass data" elif ! is_installed "wl-copy"; then _die "wl-clipboard is not installed, unable to copy-paste pass data" fi tsn_action="both" ;; default) if is_installed "wtype" && is_installed "wl-copy"; then tsn_action="default" elif is_installed "wtype" && ! is_installed "wl-copy"; then printf "%s\n" "wl-clipboard is not installed, unable to copy-paste pass data" >&2 tsn_action="autotype" elif ! is_installed "wtype" && is_installed "wl-copy"; then printf "%s\n" "wtype is not installed, unable to autotype pass data" >&2 tsn_action="copy" elif ! is_installed "wtype" && ! is_installed "wl-copy"; then _die "please install at least one the following backends to use tessen: wtype | wl-clipboard " fi ;; *) _die "please specify a valid action: autotype | copy | both" ;; esac } find_pass_backend() { local -a tmp_pass_arr=('pass' 'gopass') local idx for idx in "${tmp_pass_arr[@]}"; do if is_installed "$idx"; then pass_backend="$idx" break fi done if [[ -z $pass_backend ]]; then _die "please install a valid password store backend: pass | gopass" fi unset -v idx tmp_pass_arr } find_dmenu_backend() { local -a tmp_dmenu_arr=('fuzzel' 'tofi' 'bemenu' 'wofi' 'rofi' 'dmenu') local idx for idx in "${tmp_dmenu_arr[@]}"; do if is_installed "$idx"; then dmenu_backend="$idx" break fi done if [[ -z $dmenu_backend ]]; then _die "please install a valid dmenu backend: fuzzel | tofi | bemenu | wofi | rofi | dmenu" fi unset -v idx tmp_dmenu_arr } is_installed() { if command -v "$1" > /dev/null 2>&1; then return 0 else return 1 fi } _clear() { unset -v tsn_passfile tsn_passdata tsn_username tsn_password tsn_url unset -v tsn_autotype chosen_key } _die() { if [[ -n $1 ]]; then printf "%s\n" "$1" >&2 fi exit 1 } print_help() { local prog="tessen" printf "%s" "\ $prog - autotype and copy data from password-store and gopass on wayland usage: $prog [options] $prog find a dmenu and pass backend, look for a config file in \$XDG_CONFIG_HOME/tessen/config, and either autotype OR copy data $prog -p gopass use gopass as the pass backend $prog -d fuzzel use fuzzel as the dmenu backend $prog -d fuzzel -a autotype use fuzzel and always autotype data $prog -d fuzzel -a copy use fuzzel and always copy data $prog -d fuzzel -a both use fuzzel and always autotype AND copy data $prog -c \$HOME/tsncfg specify a custom location for the $prog config file -p, --pass, --pass= choose either 'pass' or 'gopass' -d, --dmenu, --dmenu= specify a dmenu backend - 'fuzzel', 'tofi', 'bemenu', 'wofi', 'rofi', and 'dmenu' are supported -a, --action, --action= choose either 'autotype', 'copy', or 'both' omit this option to use the default behavior -c, --config, --config= use a config file on a custom path -h, --help print this help menu -v, --version print the version of $prog for more details and additional features, please read the man page of $prog(1) for reporting bugs or feedback, visit one of the following git forge providers https://sr.ht/~ayushnix/tessen https://codeberg.org/ayushnix/tessen https://github.com/ayushnix/tessen " unset -v prog } # this block of code is needed because we can't source the file and execute # arbitrary input parse_config() { local line idx key val local -a config_arr local config_regex='^[[:alpha:]_]+="[[:alnum:]~_./^$|()-]+"$' # in case the user hasn't provided an explicit location, we'll have to check # if the default file exists before we parse it if [[ -s $tsn_config ]]; then while read -r line || [[ -n $line ]]; do if [[ $line == \#* ]]; then continue elif [[ $line =~ $config_regex ]]; then config_arr+=("$line") fi done < "$tsn_config" for idx in "${config_arr[@]}"; do key="${idx%=*}" val="${idx#*\"}" val="${val%*\"}" # here comes the ladder # the -p, -d, and -a options will be parsed and set only if they're not # already set, i.e., from the argparse if [[ $key == "pass_backend" ]] && [[ -z $pass_backend ]]; then validate_pass_backend "$val" readonly pass_backend elif [[ $key == "dmenu_backend" ]] && [[ -z $dmenu_backend ]]; then validate_dmenu_backend "$val" readonly dmenu_backend elif [[ $key == "action" ]] && unset -v tsn_action 2> /dev/null; then validate_action "$val" readonly tsn_action elif [[ $key == "tofi_config_file" ]] && [[ -f ${val@P} ]]; then tmp_tofi_opts+=("--config" "${val@P}") elif [[ $key == "rofi_config_file" ]] && [[ -f ${val@P} ]]; then tmp_rofi_opts+=("-config" "${val@P}") elif [[ $key == "wofi_config_file" ]] && [[ -f ${val@P} ]]; then tmp_wofi_opts+=("-c" "${val@P}") elif [[ $key == "wofi_style_file" ]] && [[ -f ${val@P} ]]; then tmp_wofi_opts+=("-s" "${val@P}") elif [[ $key == "wofi_color_file" ]] && [[ -f ${val@P} ]]; then tmp_wofi_opts+=("-C" "${val@P}") elif [[ $key == "userkey" ]]; then tsn_userkey="$val" elif [[ $key == "urlkey" ]]; then tsn_urlkey="$val" elif [[ $key == "autotype_key" ]]; then tsn_autokey="$val" elif [[ $key == "delay" ]]; then tsn_delay="$val" elif [[ $key == "web_browser" ]] && is_installed "$val"; then tsn_web_browser="$val" fi done fi unset -v line key val idx config_arr config_regex } main() { local -r tsn_version="2.2.0-dev" # parse arguments because they have the highest priority # make the values supplied to -p, -d, and -a as readonly local _opt while [[ $# -gt 0 ]]; do _opt="$1" case "$_opt" in -p | --pass) if [[ $# -lt 2 ]]; then _die "please specify a valid password store backend: pass | gopass" fi validate_pass_backend "$2" readonly pass_backend shift ;; --pass=*) if [[ -z ${_opt##--pass=} ]]; then _die "please specify a valid password store backend: pass | gopass" fi validate_pass_backend "${_opt##--pass=}" readonly pass_backend ;; -d | --dmenu) if [[ $# -lt 2 ]]; then _die "please install a valid dmenu backend: fuzzel | tofi | bemenu | wofi | rofi | dmenu" fi validate_dmenu_backend "$2" readonly dmenu_backend # since there's a possibility that a user may mention config files for # rofi and wofi, we will make dmenu_backend_opts readonly only if # dmenu_backend is fuzzel and bemenu, the dmenu programs which don't # support configuration files if [[ $dmenu_backend == "fuzzel" ]] || [[ $dmenu_backend == "bemenu" ]] \ || [[ $dmenu_backend == "dmenu" ]]; then readonly dmenu_backend_opts fi shift ;; --dmenu=*) if [[ -z ${_opt##--dmenu=} ]]; then _die "please install a valid dmenu backend: fuzzel | tofi | bemenu | wofi | rofi | dmenu" fi validate_dmenu_backend "${_opt##--dmenu=}" readonly dmenu_backend if [[ $dmenu_backend == "fuzzel" ]] || [[ $dmenu_backend == "bemenu" ]] \ || [[ $dmenu_backend == "dmenu" ]]; then readonly dmenu_backend_opts fi ;; -a | --action) if [[ $# -lt 2 ]]; then _die "please specify a valid action: autotype | copy | both" fi validate_action "$2" readonly tsn_action shift ;; --action=*) if [[ -z ${_opt##--action=} ]]; then _die "please specify a valid action: autotype | copy | both" fi validate_action "${_opt##--action=}" readonly tsn_action ;; -c | --config) if [[ $# -lt 2 ]] || ! [[ -f $2 ]]; then _die "please specify a valid path for the configuration file of tessen" fi tsn_config="$2" shift ;; --config=*) if ! [[ -f ${_opt##--config=} ]]; then _die "please specify a valid path for the configuration file of tessen" fi tsn_config="${_opt##--config=}" ;; -h | --help) print_help exit 0 ;; -v | --version) printf "%s\n" "$tsn_version" exit 0 ;; --) shift break ;; *) _die "invalid argument detected" ;; esac shift done unset -v _opt # parse the config file # the config file comes AFTER the argparse because the config file has some # options that argparse doesn't offer # the options which are mutual between the argparse and the config file will # be considered in the config file only if those options aren't already set parse_config if [[ $dmenu_backend == "tofi" ]]; then dmenu_backend_opts+=("${tmp_tofi_opts[@]}") readonly dmenu_backend_opts elif [[ $dmenu_backend == "rofi" ]]; then dmenu_backend_opts+=("${tmp_rofi_opts[@]}") readonly dmenu_backend_opts elif [[ $dmenu_backend == "wofi" ]]; then dmenu_backend_opts+=("${tmp_wofi_opts[@]}") readonly dmenu_backend_opts fi # initialize basic options for users who expect sane defaults and don't use # either the config file or args if [[ -z $pass_backend ]]; then find_pass_backend readonly pass_backend fi if [[ -z $dmenu_backend ]]; then find_dmenu_backend validate_dmenu_backend "$dmenu_backend" readonly dmenu_backend fi if unset -v tsn_action 2> /dev/null; then validate_action "default" readonly tsn_action fi trap '_clear' EXIT TERM INT if [[ $pass_backend == "pass" ]]; then get_pass_files elif [[ $pass_backend == "gopass" ]]; then get_gopass_files fi get_pass_data if [[ $_EXIT_STATUS -ge 10 ]]; then custom_keyb_action exit 0 fi key_menu trap - EXIT TERM INT } main "$@"