dots - my configuration files
tessen (29020B) - raw
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 | #!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-only # Copyright (C) 2021-2022 Ayush Agarwal <ayushnix at fastmail dot com> # # 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 readonly tsn_version="2.1.2" 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 -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[@]}")" 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 == 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 == mount* ]]; 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#mount *}" mount_name="${mount_name% =>*}" 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[@]}")" 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 # the key can contain alphanumerics, spaces, hyphen, underscore, plus, at, # and hash # # the value can contain anything but `key:` and `val` should be separated # with a whitespace keyval_regex='^[[:alnum:][:blank:]+#@_-]+:[[:blank:]].+$' # parse the 'otpauth://' URI # this regex is borrowed from pass-otp at commit 3ba564c # # note that OTP support in gopass has been deprecated and for good reasons # I, for one, don't see how storing OTPs in the same place as storing your # passwords is a sane idea # https://github.com/gopasspw/gopass/blob/master/docs/features.md#adding-otp-secrets otp_regex='^otpauth:\/\/(totp|hotp)(\/(([^:?]+)?(:([^:?]*))?)(:([0-9]+))?)?\?(.+)$' # the first line should contain the only the password # this assumes the caveat highlighted earlier about gopass' behavior tsn_password="${passdata[0]}" # each key should be unique # if non-unique keys are present, the value of the first non-unique key will # be considered # in addition, the 'username', 'autotype', 'url', and 'password' keys are # considered as case insensitive for idx in "${passdata[@]:1}"; do key="${idx%%:*}" val="${idx#*: }" # keys with the case insensitive name 'password' are ignored 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 } # 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 a parameter called "url", autotype becomes equivalent to # opening the url in the 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 # note that OTP support in gopass is deprecated and if they end up removing # support for it, we'll have to make changes here as well 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 } # open the url using either xdg-open or tsn_web_browser # if tsn_web_browser is defined, xdg-open won't be used 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" - } # POTENTIAL IMPROVEMENT: We could restore the clipboard as it was before pass # was used. This is done by default by pass. 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 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)) \ "pass" "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 } # fuzzel and bemenu do not support config files so their backend options won't # be changed after this function, rofi and wofi do support config files and # those will be appended to the dmenu_backend_opts array by sourcing the config # file of tessen # # of course, this limits customization when using fuzzel or bemenu and when # multiple contexts are invovled, which I don't like myself, but I won't accept # arbitrary input as arguments and use hacks like # [this](https://github.com/ayushnix/tessen/pull/13) # # if [this](https://codeberg.org/dnkl/fuzzel/issues/3) issue gets resolved, # fuzzel will be promoted as the recommended and the default dmenu backend for # tessen 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() { # 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 key_menu trap - EXIT TERM INT } main "$@" |