#!/bin/bash # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2021 Ayush Agarwal # # tessen - a data selection interface for pass using bemenu or rofi on Wayland # ------------------------------------------------------------------------------ # shell "strict" mode set -uo pipefail readonly PATH="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin" export PATH umask 077 # initialize the global variables BACKEND="bemenu" CLIP_TIME=15 WTYPE="" readonly PASS_STORE="${PASSWORD_STORE_DIR:-$HOME/.password-store}" PASSFILE="" declare -A PASSDATA_ARR USERNAME="" PASSWORD="" CHOICE="" # display and get the shortened path of the password file get_pass_file() { local tmp_pass_1 tmp_pass_2 tmp_pass_3 # temporarily enable globbing to get the list of gpg files shopt -s nullglob globstar tmp_pass_1=("$PASS_STORE"/**/*.gpg) tmp_pass_2=("${tmp_pass_1[@]#"$PASS_STORE"/}") tmp_pass_3=("${tmp_pass_2[@]%.gpg}") shopt -u nullglob globstar PASSFILE="$(printf '%s\n' "${tmp_pass_3[@]}" | "${BACKEND[@]}")" if ! [[ -e "$PASS_STORE/$PASSFILE".gpg ]]; then exit 1 fi } # get the password data including username and other keys get_pass_data() { local passdata passdata_regex idx key val mapfile -t passdata < <(pass "$PASSFILE") # ASSUMPTION: the key can contain alphanumerics, spaces, hyphen, underscore # the value can contain anything but it has to follow after a space passdata_regex="^[[:alnum:][:blank:]_-]+:[[:blank:]].+$" # ASSUMPTION: the basename of the gpg file is the username although one can still # select a username field inside the file USERNAME="${PASSFILE##*/}" # ASSUMPTION: the first line of $PASSFILE will contain the password PASSWORD="${passdata[0]}" # skip the password, validate each entry against $passdata_regex, store valid results # ASSUMPTION: each key is unique otherwise, the value of the last non-unique key will be used for idx in "${passdata[@]:1}"; do if [[ "$idx" =~ $passdata_regex ]]; then key="${idx%%:*}" val="${idx#*: }" PASSDATA_ARR["$key"]="$val" else continue fi done } # get the key that the user chooses to copy or autotype choice_data() { local ch flag choice_arr flag="" choice_arr=("autotype" "username" "password" "${!PASSDATA_ARR[@]}") if [[ "$WTYPE" -eq 1 ]]; then CHOICE="$(printf '%s\n' "${choice_arr[@]}" | "${BACKEND[@]}")" else CHOICE="$(printf '%s\n' "${choice_arr[@]:1}" | "${BACKEND[@]}")" fi for ch in "${choice_arr[@]}"; do if [[ "$CHOICE" == "$ch" ]]; then flag=1 fi done if [[ "$flag" -ne 1 ]]; then exit 1 fi } # the menu for selecting and copying the decrypted data key_menu_copy() { if [[ "$CHOICE" == "username" ]]; then wl-copy "$USERNAME" notify-send "username copied, clearing in $CLIP_TIME seconds ..." clean elif [[ "$CHOICE" == "password" ]]; then wl-copy "$PASSWORD" notify-send "password copied, clearing in $CLIP_TIME seconds ..." clean elif [[ -n "${PASSDATA_ARR[$CHOICE]}" ]]; then wl-copy "${PASSDATA_ARR[$CHOICE]}" notify-send "$CHOICE copied, clearing in $CLIP_TIME seconds ..." clean else exit 1 fi } # the menu for selecting and autotyping the decrypted data key_menu_autotype() { if [[ "$CHOICE" == "autotype" ]]; then wtype -s 100 "$USERNAME" && wtype -s 100 -k Tab -- && wtype -s 100 "$PASSWORD" exit 0 elif [[ "$CHOICE" == "username" ]]; then wtype "$USERNAME" exit 0 elif [[ "$CHOICE" == "password" ]]; then wtype "$PASSWORD" exit 0 elif [[ -n "${PASSDATA_ARR[$CHOICE]}" ]]; then wtype "${PASSDATA_ARR[$CHOICE]}" exit 0 else exit 1 fi } print_help() { printf '%s\n' "${0##*/} - data selection interface for password-store on wayland" "" printf '%s\n' "Usage: ${0##*/} [options]" "" printf '%s\n' " tessen use bemenu and either autotype or copy data" printf '%s\n' " tessen -b rofi -t use rofi and always autotype data" printf '%s\n' " tessen -c use bemenu and always copy data" "" printf '%s\n' " -b, --backend, --backend= choose 'bemenu' or 'rofi' as backend (default: bemenu)" printf '%s\n' " -t, --autotype always autotype data" printf '%s\n' " -c, --clipboard always copy data" printf '%s\n' " -h, --help print this help menu" printf '%s\n' " -v, --version print the version of tessen" "" printf '%s\n' "For more details, visit https://github.com/ayushnix/pass-tessen" } validate_backend() { if [[ "$BACKEND" == "bemenu" ]]; then bmn_opt=("-i -l 10 -w --scrollbar=autohide -n") readonly BEMENU_OPTS="${BEMENU_OPTS:-${bmn_opt[*]}}" export BEMENU_OPTS unset -v bmn_opt elif [[ "$BACKEND" == "rofi" ]]; then BACKEND=(rofi -dmenu) else exit 1 fi } validate_clip_time() { local clip_regex clip_regex="^[[:digit:]]+$" if [[ "$CLIP_TIME" =~ $clip_regex ]]; then return 0 else notify-send "invalid clipboard time provided" exit 1 fi } clean() { { sleep "$CLIP_TIME" wl-copy --clear } > /dev/null 2>&1 & disown unset -v PASSFILE USERNAME PASSWORD PASSDATA_ARR CHOICE } die() { wl-copy --clear unset -v PASSFILE USERNAME PASSWORD PASSDATA_ARR CHOICE } main() { local _opt # exit if the password store directory doesn't exist if ! [[ -d "$PASS_STORE" ]]; then notify-send "password store not found" exit 1 fi # parse any options given by the user while [[ "$#" -gt 0 ]]; do _opt="${1-}" case "$_opt" in -b | --backend) [[ "$#" -lt 2 ]] && { printf '%s\n' "Please specify a backend: bemenu|rofi" >&2 exit 1 } BACKEND="${2-}" validate_backend shift ;; --backend=*) BACKEND="${_opt##--backend=}" validate_backend ;; -t | --autotype) unset -v AT_TYPE 2> /dev/null || { printf '%s\n' "Please use either -t|--autotype or -c|--clipboard, not both" >&2 exit 1 } readonly AT_TYPE=true ;; -c | --clipboard) unset -v AT_TYPE 2> /dev/null || { printf '%s\n' "Please use either -t|--autotype or -c|--clipboard, not both" >&2 exit 1 } readonly AT_TYPE=false ;; -h | --help) print_help exit 0 ;; -v | --version) printf '%s\n' "tessen version $VERSION" exit 0 ;; --) shift break ;; *) printf '%s\n' "invalid argument detected" >&2 exit 1 ;; esac shift done unset -v _opt validate_backend validate_clip_time readonly WTYPE readonly BACKEND readonly CLIP_TIME trap 'die' EXIT TERM get_pass_file get_pass_data choice_data if [[ "$WTYPE" -eq 1 ]]; then key_menu_autotype else key_menu_copy fi trap - EXIT TERM } main "$@"