# gpaste-client(1) completion                              -*- shell-script -*-
#
# SPDX-FileCopyrightText: 2010-2026 Marc-Antoine Perennou <Marc-Antoine@Perennou.com>
# SPDX-License-Identifier: BSD-2-Clause

# Is a daemon already there to answer us? Completion must never be what
# activates one: D-Bus would start it behind the user's back and block the shell
# until it finished loading the history. Owning the name is not enough either:
# a daemon still settling its storage, or a GPaste 45 or 50 an update left
# running, serves none of this interface, and gpaste-client would wait on it.
# So the owner is asked by its unique name, which nothing activates, for one of
# that interface's properties.
_gpaste_client_daemon_running()
{
    local owner

    owner=$(gdbus call --session --dest org.freedesktop.DBus \
                                 --object-path /org/freedesktop/DBus \
                                 --method org.freedesktop.DBus.GetNameOwner org.gnome.GPaste 2>/dev/null) || return
    owner=${owner#(\'}
    owner=${owner%\',)}
    gdbus call --session --dest "${owner}" \
                         --object-path /org/gnome/GPaste \
                         --method org.freedesktop.DBus.Properties.Get org.gnome.GPaste3 Active >/dev/null 2>&1
}

# One file per history, named <history>.<extension>. Listing them off disk keeps
# history completion working (and instant) with no daemon running at all. Each
# name is ended with a NUL: a history's name may hold a line break.
_gpaste_client_histories()
{
    local dir=${XDG_DATA_HOME:-${HOME}/.local/share}
    local f name

    # The daemon stores under PACKAGE_NAME ("GPaste") when that directory
    # already exists, and under PACKAGE ("gpaste") otherwise.
    if [[ -d ${dir}/GPaste ]]; then
        dir=${dir}/GPaste
    else
        dir=${dir}/gpaste
    fi

    # De-duplicate in the shell rather than forking sort(1): the same history
    # can have a file per flavour, but this is the one completion that promises
    # to be instant because it asks no daemon, and a fork per tab is not that.
    local -A seen=()

    for f in "${dir}"/*.xml "${dir}"/*.xmls "${dir}"/*.db "${dir}"/*.dbs; do
        [[ -f ${f} ]] || continue
        name=${f##*/}
        name=${name%.*}
        # Through a parameter expansion: bash before 5.2 expands the subscript
        # of [[ -v ]] a second time, running a $(...) a history's name holds.
        [[ -n ${seen[${name}]+set} ]] && continue
        seen[${name}]=1
        printf '%s\0' "${name}"
    done
}

# The history names matching the word being completed, into COMPREPLY quoted
# for the shell. Not through compgen -W, which expands every word it is given
# once more: a $(...) or a backquote in a name would run, a $VAR or a ~ expand,
# and a space split it in two -- and a history's name may hold any of them.
#
# A word quoting nothing is the start of a name as it is, matched against the
# name itself: %q writes a name holding a control character (or, outside a
# UTF-8 locale, any non-ASCII byte) as $'...', which no letter of it starts. A
# word quoting something is matched against the names as %q quotes them.
_gpaste_client_complete_histories()
{
    local name quoted literal=0

    [[ ${cur} != *[\\\'\"\$\`]* ]] && literal=1

    while IFS= read -r -d '' name; do
        printf -v quoted '%q' "${name}"
        if ((literal)); then
            [[ ${name} == "${cur}"* ]] && COMPREPLY+=("${quoted}")
        else
            [[ ${quoted} == "${cur}"* ]] && COMPREPLY+=("${quoted}")
        fi
    done < <(_gpaste_client_histories)
}

# Both of these ask the daemon for the history and keep only its first column.
# `history` reads nothing from stdin, but an older gpaste-client first on PATH
# (a local build, say) read it whenever it was not a tty, and would block on it
# where completion runs without a terminal: fed /dev/null, it cannot.
_gpaste_client_uuids()
{
    _gpaste_client_daemon_running || return

    gpaste-client history --oneline </dev/null 2>/dev/null | cut -d: -f1
}

# With --use-index the item arguments are indexes into the history, not uuids.
_gpaste_client_indexes()
{
    _gpaste_client_daemon_running || return

    gpaste-client history --oneline --use-index </dev/null 2>/dev/null | cut -d: -f1
}

_gpaste_client_options()
{
    local common="--help -h"

    case ${1} in
        # No verb at all: the history is printed, and --version is still to come.
        '')
            printf '%s' "${common} --version -v --oneline -o --raw -r --reverse -e --zero -z --use-index -i --favourites -f"
            ;;
        history|h|search)
            printf '%s' "${common} --oneline -o --raw -r --reverse -e --zero -z --use-index -i --favourites -f"
            ;;
        get|g|select|set|s|delete|del|d|remove|rm|replace|strip-rich-text|srt|upload|u|upload-and-copy|uc|favourite|fav|unfavourite|unfav)
            printf '%s' "${common} --use-index -i"
            ;;
        make-password|mp)
            printf '%s' "${common} --use-index -i --timeout -t"
            ;;
        add-password|ap)
            printf '%s' "${common} --timeout -t"
            ;;
        list-histories|lh)
            printf '%s' "${common} --zero -z"
            ;;
        merge|m)
            printf '%s' "${common} --use-index -i --decoration -d --separator -s"
            ;;
        *)
            printf '%s' "${common}"
            ;;
    esac
}

_gpaste-client()
{
    local cur prev verb items
    local -i i argpos=0 use_index=0

    cur=${COMP_WORDS[COMP_CWORD]}
    prev=${COMP_WORDS[COMP_CWORD-1]}
    COMPREPLY=()

    # Both take a free-form value we have no way to guess -- typed after the
    # option, or after its "=", which bash makes a word of its own.
    [[ ${prev} == = ]] && prev=${COMP_WORDS[COMP_CWORD-2]}
    case ${prev} in
        -d|--decoration|-s|--separator|-t|--timeout)
            return
            ;;
    esac

    # Options may appear anywhere (getopt_long permutes), so walk the whole line:
    # the first bare word is the verb, the ones after it are its arguments, and
    # @argpos ends up being the index of the argument now being completed.
    for ((i = 1; i < COMP_CWORD; ++i)); do
        case ${COMP_WORDS[i]} in
            --decoration|--separator|--timeout)
                # Its value: the next word, or the one after an "=".
                ((++i))
                [[ ${COMP_WORDS[i]} == = ]] && ((++i))
                ;;
            --use-index)
                use_index=1
                ;;
            --*)
                ;;
            -?*)
                # Short options, bundled or not: -i among them, and one taking a
                # value ending the bundle -- with the rest of it, or the next word
                # when it is last.
                local flags=${COMP_WORDS[i]#-} j

                for ((j = 0; j < ${#flags}; ++j)); do
                    case ${flags:j:1} in
                        i)
                            use_index=1
                            ;;
                        d|s|t)
                            ((j == ${#flags} - 1)) && ((++i))
                            break
                            ;;
                    esac
                done
                ;;
            *)
                if [[ -z ${verb} ]]; then
                    verb=${COMP_WORDS[i]}
                else
                    ((++argpos))
                fi
                ;;
        esac
    done

    if [[ ${cur} == -* ]]; then
        COMPREPLY=($(compgen -W "$(_gpaste_client_options "${verb}")" -- "${cur}"))
        return
    fi

    if [[ -z ${verb} ]]; then
        # Every verb the dispatch table accepts, aliases included: the three
        # completions have to agree on what the CLI takes, and zsh and fish
        # offer the short forms too.
        local verbs="a about add add-password ap backup-history bh change-passphrase d
                     daemon daemon-reexec daemon-version del delete delete-history
                     delete-password dh doctor dp dr dv e empty f fav favourite file g get
                     get-history gh h help history history-size hs lh list-histories m
                     make-password merge migrate mp p preferences q quit remove replace
                     rm s search select set settings sh show-history srt start stop
                     strip-rich-text switch-history u uc ui unfav unfavourite upload
                     upload-and-copy v version"

        COMPREPLY=($(compgen -W "${verbs}" -- "${cur}"))
        return
    fi

    # Which of the two an item argument is spelled as depends on --use-index.
    if ((use_index)); then
        items=_gpaste_client_indexes
    else
        items=_gpaste_client_uuids
    fi

    case ${verb} in
        empty|e|delete-history|dh|switch-history|sh|backup-history|bh)
            # backup-history takes <history> <backup name>, the history being
            # optional; either way only its first argument names a history.
            ((argpos == 0)) && _gpaste_client_complete_histories
            ;;
        file|f)
            COMPREPLY=($(compgen -f -- "${cur}"))
            compopt -o filenames 2>/dev/null
            ;;
        get|g|select|set|s|delete|del|d|remove|rm|upload|u|upload-and-copy|uc|replace|strip-rich-text|srt|make-password|mp|favourite|fav|unfavourite|unfav)
            ((argpos == 0)) && COMPREPLY=($(compgen -W "$(${items})" -- "${cur}"))
            ;;
        merge|m)
            COMPREPLY=($(compgen -W "$(${items})" -- "${cur}"))
            ;;
    esac
}

complete -F _gpaste-client gpaste-client
