#!/bin/bash
#
# bwm - Bitwarden menu for Wayland
#
# © 2021, Pierre-Albéric TROUPLIN (palb91)
set -e
# Environments variables, default values
declare -A credentials
cmd=wtype
asset=pass
url=
selection=
# Utils
help() {
cat <<HELP
bwm - Type a password from bitwarden in a Wayland environment.
Usage:
${0##*/} [-h]
${0##*/} [-u|-b|-o] [URL]
${0##*/} -c [-u|-o] [URL]
Options:
-u, --user Type the username instead of the password.
-b, --both Type the username and the password separated by a <Tab>.
-o, --otp Type the otp code if exists.
-c, --copy Copy instead of type.
-h, --help Print this help.
Argument:
URL If any, must be the last parameter. ${0##*/} will try to
find only matching values from \`bw\`.
HELP
exit "${1:-0}"
}
die() {
printf '%s\n\n' "${*}" >&2
help 1
}
check_dep() {
if [[ -z "$(command -v "${1}")" ]]; then
printf '%s is not installed\n' "${1}" >&2
exit 1
fi
}
menu() {
column -ts$'\t' -o' :: ' -R2 \
| bemenu -pselect: \
| sed 's/ .*// ; q'
}
use_Xwayland() {
query='recurse(.nodes[]?, .floating_nodes[]?) | select(.focused).shell'
[[ "$(swaymsg -t get_tree | jq -r "${query}")" = 'xwayland' ]]
}
# Parse arguments, check dependancies
parse_args() {
while [[ "${#}" -gt 0 ]]; do
case "${1}" in
-h|--help) help ;;
-c|--copy) cmd=wl-copy ;;
-u|--user) asset=user ;;
-b|--both) asset=both ;;
-o|--otp) asset=totp ;;
-cu) cmd=wl-copy asset=user ;;
-co) cmd=wl-copy asset=totp ;;
-*) die "Wrong option: ${1}" ;;
*) url="${1}"; break ;;
esac
shift
done
[[ "${#}" -gt 1 ]] \
&& die "Wrong number of arguments."
[[ "${cmd}" = 'wl-copy' ]] && [[ "${asset}" = 'both' ]] \
&& die 'Cannot copy both username and password.'
# Use xdotool for Xwayland windows
[[ "${cmd}" = 'wtype' ]] && use_Xwayland \
&& cmd=xdotool
check_dep "${cmd}"
}
get_credentials() {
# Gather bitwarden entries (no url means all entries)
entries="$( bw list items --url "${url}" )"
# Exit if not entry in bitwarden
if [[ "${entries}" = '[]' ]]; then
printf 'No credentials in bitwarden...\n' >&2
exit 2
fi
# Gather matching entries
_jq='if length == 1 then
.[0] | .id + "\t" + .name + "\t" + .login.username
else
sort_by(.favorite == false)
| .[]
| .id + "\t" + .name + "\t" + .login.username
end'
mapfile -t match < <(printf '%s' "${entries}" | jq -r "${_jq}")
# Get id, filter if there are several matchs
id="${match[0]%% *}"
if [[ "${#match[@]}" -gt 1 ]]; then
check_dep bemenu
id="$(printf "%s\n" "${match[@]}" | menu)"
fi
id="${id%%$'\t'*}"
# Get corresponding json ojbect
selection="$(printf '%s' "${entries}" \
| jq '.[] | select(.id == "'"${id}"'")')"
# Exit if no values
if [[ -z "${selection}" ]]; then
printf 'Wrong selection. Exiting...\n' >&2
exit 3
fi
# Gather credentials
_jq='.login | .username + "\n" + .password + "\n" + .totp'
item=()
mapfile -t item < <(printf '%s' "${selection}" | jq -r "${_jq}")
credentials[user]="${item[0]}"
credentials[pass]="${item[1]}"
credentials[totp]="${item[2]:+$(bw get totp "${id}")}"
}
# Run wtype or wl-copy with asked content
type_or_copy() {
set -- "${credentials[${asset}]}"
if [[ "${cmd}" = 'wtype' ]]; then
[[ "${asset}" = 'both' ]] \
&& set -- "${credentials[user]}" -k Tab "${credentials[pass]}"
# Add a short delay before typing
set -- -s 150 "${@}"
elif [[ "${cmd}" = 'xdotool' ]]; then
[[ "${asset}" = 'both' ]] \
&& set -- "${credentials[user]}"$'\t'"${credentials[pass]}"
set -- sleep 0.15 type "${@}"
fi
exec "${cmd}" "${@}"
}
main() {
parse_args "${@}"
get_credentials
type_or_copy
}
main "${@}"