auto-notify.plugin.zsh
export AUTO_NOTIFY_VERSION="0.11.1"

# Time it takes for a notification to expire
[[ -z "$AUTO_NOTIFY_EXPIRE_TIME" ]] &&
    export AUTO_NOTIFY_EXPIRE_TIME=8000
# Threshold in seconds for when to automatically show a notification
[[ -z "$AUTO_NOTIFY_THRESHOLD" ]] &&
    export AUTO_NOTIFY_THRESHOLD=10
# Enable or disable notifications for SSH sessions (0 = disabled, 1 = enabled)
[[ -z "$AUTO_NOTIFY_ENABLE_SSH" ]] &&
    export AUTO_NOTIFY_ENABLE_SSH=0
# Enable transient notifications to prevent them from being saved in the notification history
[[ -z "$AUTO_NOTIFY_ENABLE_TRANSIENT" ]] &&
    export AUTO_NOTIFY_ENABLE_TRANSIENT=1
# Configure whether notifications should be canceled when receiving a SIGINT (Ctrl+C)
[[ -z "$AUTO_NOTIFY_CANCEL_ON_SIGINT" ]] &&
    export AUTO_NOTIFY_CANCEL_ON_SIGINT=0


# List of commands/programs to ignore sending notifications for
[[ -z "$AUTO_NOTIFY_IGNORE" ]] &&
    export AUTO_NOTIFY_IGNORE=(
        'vim'
        'nvim'
        'less'
        'more'
        'man'
        'tig'
        'watch'
        'git commit'
        'top'
        'htop'
        'ssh'
        'nano'
    )

function _auto_notify_format() {
    local MESSAGE="$1"
    local command="$2"
    local elapsed="$3"
    local exit_code="$4"
    MESSAGE="${MESSAGE//\%command/$command}"
    MESSAGE="${MESSAGE//\%elapsed/$elapsed}"
    MESSAGE="${MESSAGE//\%exit_code/$exit_code}"
    printf "%s" "$MESSAGE"
}

function _auto_notify_message() {
    local command="$1"
    local elapsed="$2"
    local exit_code="$3"
    local platform="$(uname)"
    # Run using echo -e in order to make sure notify-send picks up new line
    local DEFAULT_TITLE="\"%command\" Completed"
    local DEFAULT_BODY="$(echo -e "Total time: %elapsed seconds\nExit code: %exit_code")"

    # Exit code 130 indicates termination by SIGINT (Ctrl+C).
    # If AUTO_NOTIFY_CANCEL_ON_SIGINT is enabled, suppress the notification.
    if [[ "$exit_code" -eq 130 ]] && [[ "${AUTO_NOTIFY_CANCEL_ON_SIGINT}" -eq 1 ]]; then
        return
    fi

    local title="${AUTO_NOTIFY_TITLE:-$DEFAULT_TITLE}"
    local text="${AUTO_NOTIFY_BODY:-$DEFAULT_BODY}"

    title="$(_auto_notify_format "$title" "$command" "$elapsed" "$exit_code")"
    body="$(_auto_notify_format "$text" "$command" "$elapsed" "$exit_code")"

    if [[ "$platform" == "Linux" ]]; then
        # Set default notification properties
        local urgency="normal"
        local transient="--hint=int:transient:$AUTO_NOTIFY_ENABLE_TRANSIENT"
        local icon="${AUTO_NOTIFY_ICON_SUCCESS:-""}"

        # Handle specific exit codes
        if [[ "$exit_code" -eq 130 ]]; then
            urgency="critical"
            transient="--hint=int:transient:1"
            icon="${AUTO_NOTIFY_ICON_FAILURE:-""}"
        elif [[ "$exit_code" -ne 0 ]]; then
            # For all other non-zero exit codes, mark the notification as critical.
            urgency="critical"
            icon="${AUTO_NOTIFY_ICON_FAILURE:-""}"
        fi

        local arguments=("$title" "$body" "--app-name=zsh" "$transient" "--urgency=$urgency" "--expire-time=$AUTO_NOTIFY_EXPIRE_TIME")

        if [[ -n "$icon" ]]; then
                arguments+=("--icon=$icon")
        fi

        # Check if the script is running over SSH
        if [[ -n "${SSH_CLIENT}" || -n "${SSH_CONNECTION}" ]]; then
            # Extract the client IP address from environment
            local client_ip="${SSH_CLIENT%% *}"
            [[ -z "$client_ip" ]] && client_ip="${SSH_CONNECTION%% *}"

            # Forward the notify-send command to the client machine via SSH
            ssh "${USER}@${client_ip}" "$(printf '%q ' notify-send "${arguments[@]}")"
        else
            # If not running over SSH, send notification locally
            notify-send "${arguments[@]}"
        fi

    elif [[ "$platform" == "Darwin" ]]; then
        osascript \
          -e 'on run argv' \
          -e 'display notification (item 1 of argv) with title (item 2 of argv)' \
          -e 'end run' \
          "$body" "$title" \
          2> >(grep -Ev 'ApplePersistence=(NO|YES)' >&2)
          # ^ osascript outputs "ApplePersistence=NO" to stderr on every run, which clutters the terminal.
          # ^ Filter stderr to remove only this message while preserving any legitimate error messages.
    else
        printf "Unknown platform for sending notifications: $platform\n"
        printf "Please post an issue on gitub.com/MichaelAquilina/zsh-auto-notify/issues/\n"
    fi
}

function _is_auto_notify_ignored() {
    local command="$1"
    # split the command if its been piped one or more times
    local command_list=("${(@s/|/)command}")
    local target_command="${command_list[-1]}"
    # Remove leading whitespace
    target_command="$(echo "$target_command" | sed -e 's/^ *//')"

    # Ignore the command if running over SSH and AUTO_NOTIFY_ENABLE_SSH is disabled
    if [[ -n ${SSH_CLIENT-} || -n ${SSH_TTY-} || -n ${SSH_CONNECTION-} ]] && [[ "${AUTO_NOTIFY_ENABLE_SSH-1}" == "0" ]]; then
        print "yes"
        return
    fi

    # Remove sudo prefix from command if detected
    if [[ "$target_command" == "sudo "* ]]; then
        target_command="${target_command/sudo /}"
    fi

    # If AUTO_NOTIFY_WHITELIST is defined, then auto-notify will ignore
    # any item not defined in the white list
    # Otherwise - the alternative (default) approach is used where the
    # AUTO_NOTIFY_IGNORE blacklist is used to ignore commands

    if [[ -n "$AUTO_NOTIFY_WHITELIST" ]]; then
        for allowed in $AUTO_NOTIFY_WHITELIST; do
            if [[ "$target_command" == "$allowed"* ]]; then
                print "no"
                return
            fi
        done
        print "yes"
    else
        for ignore in $AUTO_NOTIFY_IGNORE; do
            if [[ "$target_command" == "$ignore"* ]]; then
                print "yes"
                return
            fi
        done
        print "no"
    fi
}

function _auto_notify_send() {
    # Immediately store the exit code before it goes away
    local exit_code="$?"

    if [[ -z "$AUTO_COMMAND" && -z "$AUTO_COMMAND_START" ]]; then
        return
    fi

    if [[ "$(_is_auto_notify_ignored "$AUTO_COMMAND_FULL")" == "no" ]]; then
        local current="$(date +"%s")"
        let "elapsed = current - AUTO_COMMAND_START"

        if [[ $elapsed -gt $AUTO_NOTIFY_THRESHOLD ]]; then
            _auto_notify_message "$AUTO_COMMAND" "$elapsed" "$exit_code"
        fi
    fi

    # Empty tracking so that notifications are not
    # triggered for any commands not run (e.g ctrl+C when typing)
    _auto_notify_reset_tracking
}

function _auto_notify_track() {
    # $1 is the string the user typed, but only when history is enabled
    # $2 is a single-line, size-limited version of the command that is always available
    # To still do something useful when history is disabled, although with reduced functionality, fall back to $2 when $1 is empty
    AUTO_COMMAND="${1:-$2}"
    AUTO_COMMAND_FULL="$3"
    AUTO_COMMAND_START="$(date +"%s")"
}

function _auto_notify_reset_tracking() {
    # Command start time in seconds since epoch
    unset AUTO_COMMAND_START
    # Full command that the user has executed after alias expansion
    unset AUTO_COMMAND_FULL
    # Command that the user has executed
    unset AUTO_COMMAND
}

function disable_auto_notify() {
    add-zsh-hook -D preexec _auto_notify_track
    add-zsh-hook -D precmd _auto_notify_send
}

function enable_auto_notify() {
    autoload -Uz add-zsh-hook
    add-zsh-hook preexec _auto_notify_track
    add-zsh-hook precmd _auto_notify_send
}

_auto_notify_reset_tracking


platform="$(uname)"
if [[ "$platform" == "Linux" ]] && ! type notify-send > /dev/null; then
    printf "'notify-send' must be installed for zsh-auto-notify to work\n"
    printf "Please install it with your relevant package manager\n"
else
    enable_auto_notify
fi
