#!/usr/bin/env bash
#
# Pass picker
#
# Use a dmenu-like list selector to display and autofill your pass passwords.
# Can work with rofi, bemenu and dmenu, or a custom picker given as an option.
# Invoke it with `pass-pick`. You can set options through environment variables
# or through a configuration file.
#
# Keys:
# By default shows the available keys on rofi, but not on bemenu/dmenu.
# ROFI mapped keys (main password list):
#   return          autofill username/password combination
#   alt+return      enter entry submenu
#   alt+u           autofill username
#   alt+p           autofill password
#   alt+ctrl+u      send username to clipboard
#   alt+ctrl+p      send password to clipboard
# ROFI mapped keys (individual entry):
#   return          autofill selected field
#   alt+return      send selected field to clipboard
#   alt+s           reveal hidden password field
#   alt+backspace   back to main password menu
# Those options also work on bemenu, but have different (and fixed) mappings.
# BEMENU mapped keys (main password list):
#   return          autofill username/password combination
#   alt+2           send username to clipboard
#   alt+3           send password to clipboard
#   alt+4           autofill username
#   alt+5           autofill password
#   alt+6           enter entry submenu
# BEMENU mapped keys (individual entry):
#   return          autofill selected field
#   alt+2           send selected field to clipboard
#   alt+3           back to main password menu
#   alt+4           reveal hidden password field

# Selector wrapper
# Prefers rofi if found, otherwise bemenu or dmenu if found, complains if no selector available.
# Passes along any options given to main script.
rofi_opts=("$@")
_picker() {
    if [ -n "$PICKER" ]; then
        "${PICKER[@]}"
    elif command -v rofi 1>/dev/null 2>/dev/null; then
        rofi -dmenu -no-auto-select -i "${rofi_opts[@]}" "$@" -p "entry"
    elif command -v bemenu 1>/dev/null 2>/dev/null; then
        bemenu -l 20 -i -p "entry >"
    elif command -v dmenu 1>/dev/null 2>/dev/null; then
        dmenu -i -p "entry >"
    else
        printf "%s: 📦 %s must be installed for %s function.\n" "critical" "rofi/dmenu" "this" >&2
        notify-send "📦 rofi/dmenu" --urgency="critical" "must be installed for this function."
        exit 1
    fi
}

# parse, see https://unix.stackexchange.com/a/331965/8541
_parse_config() {
    (grep -e "^$2=" -m 1 "$1" 2>/dev/null || printf "var=__UNDEFINED__\n") | head -n1 | cut -d '=' -f 2-
}

# read config file
get_config() {
    local locations=(
        "$PP_CONFIGURATION_FILE"
        "${xdg_config_home:-$HOME/.config}/pass-picker/pass-picker.conf"
        "$HOME/.pass-picker.conf"
        "/etc/pass-picker.conf"
    )

    # return the first config file with a valid path
    for config in "${locations[@]}"; do
        if [[ -n "$config" && -f "$config" ]]; then
            # see if the config has been given a value
            local val
            val="$(_parse_config "$config" "$1")"
            break
        fi
    done

    # if there was a config file but no value
    # or there was no config file at all
    if [ "$val" = "__UNDEFINED__" ] || [ -z "$val" ]; then
        val="$2"
    fi
    printf -- "%s" "$val"
}

set_defaults() {
    # The location of the pass-picker config file
    # PP_CONFIGURATION_FILE="~/.config/pass-picker/pass-picker.conf"
    # set options, leaving already set environment variables intact
    # try to read any settings from config files
    PICKER="${PP_PICKER:-$(get_config PICKER)}"

    KEY_AUTOFILL="${PP_KEY_AUTOFILL:-$(get_config KEY_AUTOFILL Return)}"
    KEY_ENTRY_OPEN="${PP_KEY_ENTRY_OPEN:-$(get_config KEY_ENTRY_OPEN Alt+Return)}"
    KEY_FILL_USER="${PP_KEY_FILL_USER:-$(get_config KEY_FILL_USER Alt+u)}"
    KEY_CLIP_USER="${PP_KEY_CLIP_USER:-$(get_config KEY_CLIP_USER Ctrl+Alt+u)}"
    KEY_FILL_PASS="${PP_KEY_FILL_PASS:-$(get_config KEY_FILL_PASS Alt+p)}"
    KEY_CLIP_PASS="${PP_KEY_CLIP_PASS:-$(get_config KEY_CLIP_PASS Ctrl+Alt+p)}"
    KEY_ENTRYMENU_FILL="${PP_KEY_ENTRYMENU_FILL:-$(get_config KEY_ENTRYMENU_FILL Return)}"
    KEY_ENTRYMENU_CLIP="${PP_KEY_ENTRYMENU_CLIP:-$(get_config KEY_ENTRYMENU_CLIP Alt+Return)}"
    KEY_ENTRYMENU_SHOWFIELD="${KEY_ENTRYMENU_SHOWFIELD:-$(get_config KEY_ENTRYMENU_SHOWFIELD Alt+s)}"
    KEY_ENTRYMENU_QUIT="${PP_KEY_ENTRYMENU_QUIT:-$(get_config KEY_ENTRYMENU_QUIT Alt+BackSpace)}"

    AUTOFILL_BACKEND="${PP_AUTOFILL_BACKEND:-$(get_config AUTOFILL_BACKEND ydotool)}"
    AUTOFILL_CHAIN="${PP_AUTOENTRY_CHAIN:-$(get_config AUTOFILL_CHAIN 'username :tab password')}"
    AUTOFILL_DELAY="${PP_AUTOENTRY_DELAY:-$(get_config AUTOFILL_DELAY 30)}"
    PASS_USERNAME_FIELD="${PP_PASS_USERNAME_FIELD:-$(get_config PASS_USERNAME_FIELD 'username user login')}"
}

# exit on escape pressed
exit_check() {
    [ "$1" -eq 1 ] && exit
}

# simply return a list of all passwords in pass store
# TODO only show website names (+ folder names), and account names for multiple accounts on one site
list_passwords() {
    shopt -s nullglob globstar
    prefix=${PASSWORD_STORE_DIR:-~/.password-store}
    password_files=("$prefix"/**/*.gpg)
    password_files=("${password_files[@]#"$prefix"/}")
    password_files=("${password_files[@]%.gpg}")

    printf '%s\n' "${password_files[@]}"
}

# return password for argument passed
show_password() {
    pass show "$1" | head -n1
}

# send password to clipboard
clip_password() {
    pass show -c "$1"
}

# attempt to return the field specified
# attempts all (space separated) fields until the
# first one successfully returned
_p_get_field() {
    local gp_entry="$1"
    local gp_field="$2"
    local clip="$3"

    # return on first successfully returned key
    for key in $gp_field; do
        local value
        value=$(_p_get_key_value "$gp_entry" "$key")

        # found entry
        if [ -n "$value" ]; then

            if [ -n "$clip" ]; then
                # copies to clipboard, removes any trailing newlines,
                # and only keeps it in for 1 paste (1 loop to read in script, 1 to output)
                if command -v wl-copy; then
                    echo "$value" | wl-copy -o && break
                elif command -v xclip; then
                    echo "$value" | xclip -i -selection 'clipboard' -loops 2 -rmlastnl && break
                elif command -v xsel; then
                    echo "$value" | xsel -b && break
                else
                    notify-send "No clipboard utility" "Install wl-copy, xclip or xsel."
                fi
            else
                echo "$value" && break
            fi

        fi
    done
}

# returns the corresponding value for the key passed in
# arguments:
# $1: pass (file) entry to search through
# $2: string name of the containting key
_p_get_key_value() {
    local value
    value=$(list_fields "$1" | grep "$2")

    # get everything after first colon, remove whitespace
    echo "$value" | cut -d':' -f2- | tr -d '[:blank:]'
}

# return username for argument passed
show_username() {
    _p_get_field "$1" "${PASS_USERNAME_FIELD}"
}

clip_username() {
    _p_get_field "$1" "${PASS_USERNAME_FIELD}" "-c"
}

show_field() {
    _p_get_field "$1" "$2"
}

clip_field() {
    _p_get_field "$1" "$2" "-c"
}

list_fields() {
    pass show "$1" | tail -n+2
}

# invoke the dotool to type inputs
_type() {
    local tool="${AUTOFILL_BACKEND}"
    local toolmode="$1"
    local key="$2"

    if [ "$tool" = "xdotool" ]; then
        "$tool" "$toolmode" --delay "${AUTOFILL_DELAY}" "$key"
    elif [ "$tool" = "ydotool" ]; then
        "$tool" "$toolmode" --key-delay "${AUTOFILL_DELAY}" "$key"
    else
        "$tool" "$toolmode" "$key"
    fi
}

# automatically fill out fields
# transform special chain entries into valid dotool commands
autofill() {
    local selected="${1}"
    local autoentry_chain="${2}"

    for part in $autoentry_chain; do
        case "$part" in
        ":tab") _type key Tab ;;
        ":return") _type key Return ;;
        ":space") _type key space ;;
        "username") _type type "$(show_username "$selected")" ;;
        "password") _type type "$(show_password "$selected")" ;;
        ":direct") _type type "$selected" ;;
        *) printf '%s' "$selected" ;;
        esac
    done
}

# opens a menu for the specified pass entry, containing its individual fields
entrymenu() {
    local entry="$1"
    local deobfuscate="$2"
    local k_entrymenu_fill="${KEY_ENTRYMENU_FILL}"
    local k_entrymenu_clip="${KEY_ENTRYMENU_CLIP}"
    local k_entrymenu_showfield="${KEY_ENTRYMENU_SHOWFIELD}"
    local k_entrymenu_quit="${KEY_ENTRYMENU_QUIT}"

    local pass
    if [ "$deobfuscate" = "true" ]; then
        pass="$(show_password "$entry")"
    else
        pass="(hidden)"
    fi

    local field
    field=$(
        printf "password: %s\n%s" "$pass" "$(list_fields "$entry")" |
            _picker \
                -kb-accept-entry "" \
                -kb-custom-1 "$k_entrymenu_fill" \
                -kb-custom-2 "$k_entrymenu_clip" \
                -kb-custom-3 "$k_entrymenu_quit" \
                -kb-custom-4 "$k_entrymenu_showfield" \
                -mesg " ᐊ $k_entrymenu_quit ᐊ | $k_entrymenu_fill: fill selection | $k_entrymenu_clip: clip selection | $k_entrymenu_showfield: reveal password"
    )
    exit_value=$?
    exit_check "$exit_value"

    # get field name
    field=${field%%:*}
    case "$exit_value" in
    "0" | "10")
        if [ "$field" = "password" ]; then
            autofill "$entry" "password"
        else
            autofill "$(show_field "$entry" "$field")" ":direct"
        fi
        exit 0
        ;;
    "11")
        if [ "$field" = "password" ]; then
            clip_password "$entry"
        else
            clip_field "$entry" "$field"
        fi
        exit 0
        ;;
    "12")
        main
        ;;
    "13")
        local toggle
        if [ "$deobfuscate" = "true" ]; then
            toggle=false
        else
            toggle=true
        fi
        entrymenu "$entry" "$toggle"
        ;;
    esac
}

main() {
    local autoentry_chain="${AUTOFILL_CHAIN}"
    local k_autofill="${KEY_AUTOFILL}"
    local k_fill_user="${KEY_FILL_USER}"
    local k_clip_user="${KEY_CLIP_USER}"
    local k_fill_pass="${KEY_FILL_PASS}"
    local k_clip_pass="${KEY_CLIP_PASS}"
    local k_submenu="${KEY_ENTRY_OPEN}"

    entry="$(
        list_passwords |
            _picker -kb-accept-entry "" \
                -kb-custom-1 "$k_autofill" \
                -kb-custom-2 "$k_clip_user" \
                -kb-custom-3 "$k_clip_pass" \
                -kb-custom-4 "$k_fill_user" \
                -kb-custom-5 "$k_fill_pass" \
                -kb-custom-6 "$k_submenu" \
                -mesg "| $k_autofill: fill credentials | $k_submenu: open entry | $k_fill_user: fill username | $k_fill_pass: fill password | $k_clip_user: clip username | $k_clip_pass: clip password |"
    )"
    exit_value=$?

    echo "$entry"
    exit_check "$exit_value"
    case "$exit_value" in
    "0" | "10")
        autofill "$entry" "$autoentry_chain"
        exit 0
        ;;
    "11")
        clip_username "$entry"
        exit 0
        ;;
    "12")
        clip_password "$entry"
        exit
        ;;
    "13")
        autofill "$entry" "username"
        exit
        ;;
    "14")
        autofill "$entry" "password"
        exit
        ;;
    "15")
        entrymenu "$entry"
        exit
        ;;
    esac
}

set_defaults
main