#!/bin/bash

# passrofi -- Rofi script for pass (passwordstore.org)
# Version: 0.2.0
# Author: binaryDiv


# Strict mode
set -euo pipefail
IFS=$'\n\t'

# Set some generic variables
SCRIPTNAME=$0
BASENAME=$(basename $0)


# Usage
# -----

usage() {
    cat << END_OF_USAGE
Usage: $BASENAME [-d DIR] [-p PROMPT]

Simple rofi script to query passwords from pass (passwordstore.org).

Calling this script directly will start rofi in script mode.

General options:
  -h, --help        Print this help message
  -v, --verbose     Enables debug logging via stderr
  -d DIR            Sets the password-store directory (default: ~/.password-store)
  -p PROMPT         Sets the rofi prompt to PROMPT (default: "Password")
END_OF_USAGE
}


# Helper functions
# ----------------

# Logs a message to stderr if verbose mode is enabled (with a '#' prefix)
log() {
    if [[ -n $PASSROFI_VERBOSE ]]; then
        echo "# $@" >&2
    fi
}

# Prints a message to stderr and exits with status 1
fail() {
    echo "$@" >&2
    exit 1
}

# Copies a string to the clipboard
copy_to_clipboard() {
    if [[ $XDG_SESSION_TYPE == "wayland" ]]; then
        echo "$@" | wl-copy -n &>/dev/null
    else
        echo "$@" | xclip -selection clipboard -r -silent &>/dev/null
    fi
}


# Main entrypoint
# ---------------

# Main entrypoint: Checks if the script was called from the command line or as a rofi script, then
# calls either main_cli (first case) or main_rofi (second case).
main() {
    # Set defaults for environment variables
    set_default_env

    # If the script was called by rofi, this env variable will be defined
    if [[ -v ROFI_RETV ]]; then
        main_rofi "$@"
    else
        main_cli "$@"
    fi
}

set_default_env() {
    # Set default values for environment variables (unless already set)
    PASSWORD_STORE_DIR=${PASSWORD_STORE_DIR:-~/.password-store}
    PASSROFI_VERBOSE=${PASSROFI_VERBOSE:-}
    PASSROFI_PROMPT=${PASSROFI_PROMPT:-Password}
}


# CLI mode (called from command line)
# -----------------------------------

# Entrypoint when called from the command line: Parses command line arguments, sets some environment
# variables, and starts rofi (which then calls this script again, using main_rofi as entrypoint).
main_cli() {
    # Parse command line arguments (sets variables)
    parse_cli_args "$@"

    # Start rofi
    exec_rofi
}

# Parses command line arguments and sets variables accordingly
parse_cli_args() {
    # Use "-:" to sort of parse long options
    while getopts ":hvd:p:-:" option; do
        # getopts does not support long options, so we build a workaround
        if [[ $option == "-" ]]; then
            option="-$OPTARG"
            OPTARG=""
        fi
        
        case $option in
            h|-help)
                usage
                exit 0
                ;;

            v|-verbose)
                export PASSROFI_VERBOSE=1
                log "I can be very verbose if you want me to, HOOT HOOT"
                ;;

            d)
                export PASSWORD_STORE_DIR="$OPTARG"
                log "Setting password store directory to $PASSWORD_STORE_DIR"
                ;;

            p)
                export PASSROFI_PROMPT="$OPTARG"
                log "Setting rofi prompt to $PASSROFI_PROMPT"
                ;;

            :)
                fail "$BASENAME: option -$OPTARG requires an argument."
                ;;

            ?|*)
                fail "$BASENAME: invalid option: -${OPTARG:-$option}"
                ;;
        esac
    done
}

# Executes rofi with the needed parameters and environment variables
exec_rofi() {
    exec rofi -show pass \
        -modi "pass:$SCRIPTNAME" \
        -kb-custom-1 "Alt-h" \
        -kb-custom-2 "Alt-u" \
        -kb-custom-3 "Alt-v"
}


# Rofi script mode (called by rofi)
# ---------------------------------

# Entrypoint when called by rofi as a rofi script: Lists available password files and parses input.
main_rofi() {
    # Debug output
    log "Called by rofi: ROFI_RETV=${ROFI_RETV:-}"
    log "  ARGS: $@"
    log "  ROFI_INFO: ${ROFI_INFO:-}"
    log "  PASSWORD_STORE_DIR: ${PASSWORD_STORE_DIR:-}"

    # Ensure all environment variables are set correctly, directories exist etc.
    ensure_environment

    # Set rofi mode options (outputs specially formatted strings)
    set_rofi_mode_options

    # Get user selection from arguments
    USER_SELECTION="$@"

    # Check current script state and decide which action to take next
    handle_rofi_state
}

# Checks the current script state and executes the next action
handle_rofi_state() {
    # First, check if we're coming from the help screen
    if [[ ${ROFI_INFO:-} == 'help_menu' ]]; then
        # Change state so that we show the password list again
        ROFI_RETV=0
        ROFI_INFO=""
    fi

    # Check state to determine what to do
    case $ROFI_RETV in
        # Initial call of script
        0)
            # Generate a list of password files for rofi to show
            generate_password_list
            ;;

        # User selected an entry from the list (1) or a custom entry (2)
        1|2)
            # Use pass to decrypt and copy the password
            handle_user_selection
            ;;

        # Custom keybinding 1 (Alt-h): Show help for keybindings
        10)
            show_help_menu
            ;;

        # Custom keybinding 2 (Alt-u): Copy username
        11)
            # Copy username to clipboard
            handle_copy_username
            ;;

        # Custom keybinding 3 (Alt-v): View password file in terminal
        12)
            # Open password file in a less inside a terminal
            handle_view_file
            ;;

        # Unused custom keybindings: Show help menu
        1[3-9]|2[0-8])
            log "Unused keybinding $((ROFI_RETV - 9)), showing help instead."
            show_help_menu
            ;;

        # Unknown state
        *)
            fail "Unknown ROFI_RETV state: $ROFI_RETV"
            ;;
    esac
}

# Ensures all environment variables are set correctly
ensure_environment() {
    # Check if the password store directory exists
    [[ -d $PASSWORD_STORE_DIR ]] || fail "Invalid password store directory: $PASSWORD_STORE_DIR"
}

# Sets (outputs) rofi mode options
set_rofi_mode_options() {
    # This enables custom keybindings for the script
    echo -en "\0use-hot-keys\x1ftrue\n"

    # Disable custom input
    echo -en "\0no-custom\x1ftrue\n"

    # Set default prompt and message
    echo -en "\0prompt\x1f$PASSROFI_PROMPT\n"
    echo -en "\0message\x1f\n"
}

# Custom keybinding (Alt-h) to show a help menu with keybindings
show_help_menu() {
    # Set prompt and message
    echo -en "\0prompt\x1fHelp\n"
    echo -en "\0message\x1fThis is a list of custom keybindings for passrofi.\n"

    # Helper function that adds an info string to the entry
    _echo() {
        echo -en "$@\0info\x1fhelp_menu\n"
    }

    # Print help page
    _echo "Alt-h: Shows this help page"
    _echo " "
    _echo "Alt-u: Copy username"
    _echo "Alt-v: View password file"
}

# Lists all password files as input for rofi
generate_password_list() {
    # Get list of all .gpg files in password-store
    password_store_dir="${PASSWORD_STORE_DIR%/}"
    for password_file in $(find "$password_store_dir" -type f -name '*.gpg' | sort); do
        # Strip prefix and suffix
        password_file="${password_file#"$password_store_dir"/}"
        password_file="${password_file%.gpg}"

        # Output filename
        echo $password_file
    done
}

# Parse user selection: Use pass to decrypt and copy password
handle_user_selection() {
    log "User selected entry: $USER_SELECTION"

    # External programs need to be launched asynchronously, otherwise rofi blocks
    coproc (
        # Copy password to clipboard (pass will automatically clear the clipboard after 45 seconds)
        pass show -c "$USER_SELECTION" &>/dev/null
    )

    # End here
    exit 0
}

# Copys the username of a selected password file to the clipboard
handle_copy_username() {
    log "User wants to copy username of: $USER_SELECTION"

    # We can get the username directly from the filename
    username=${USER_SELECTION##*/}

    log "Username is: $username"

    # Copy to clipboard
    coproc (
        copy_to_clipboard "$username"
    )

    # End here
    exit 0
}

# Views the selected password file in a pager inside a terminal
handle_view_file() {
    log "User wants to view file: $USER_SELECTION"

    # Get full filename
    password_store_dir="${PASSWORD_STORE_DIR%/}"
    filename="$password_store_dir/$USER_SELECTION.gpg"

    # Open file in a pager inside a terminal
    coproc (
        #rofi-sensible-terminal -e "bash -ic 'pass show \"$USER_SELECTION\" | less'" &>/dev/null
        {
            echo "Password file: $USER_SELECTION"
            echo "------------------------------------------------------------"
            pass show "$USER_SELECTION" 
            echo
        } | view_stdin_in_terminal &>/dev/null
    )
}

# Opens a terminal that shows data from stdin. Uses 'view_in_terminal' if it exists in $PATH.
view_stdin_in_terminal() {
    if which view_in_terminal &>/dev/null; then
        view_in_terminal
    else
        rofi-sensible-terminal -e "bash -ic 'less < /proc/$BASHPID/fd/0'"
    fi
}


# Run script
main "$@"


# --------------------------------------------------------------------------------
#
# CHANGELOG
# =========
#
# Version 0.2.0:
#
# - Add wayland support for copying the username to the clipboard.
#   (Requires wl-copy from wl-clipboard.)
#
# Version 0.1.1:
# 
# - Use "view_in_terminal" to view a password file if an executable with that
#   name exists in the PATH.
#
# Version 0.1.0:
#
# - First version
# - Built-in help page
# - List password files, copy password, copy username, view file in terminal
#
# --------------------------------------------------------------------------------
