Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add previous or next on gnome notification #1228

Open
legacychimera247 opened this issue Sep 3, 2024 · 2 comments
Open

add previous or next on gnome notification #1228

legacychimera247 opened this issue Sep 3, 2024 · 2 comments
Labels
enhancement Accepted functionality to be added

Comments

@legacychimera247
Copy link
Contributor

would be nice to have the ability to go to previous song or skip to the next one directly in the notification that appear in gnome/mpris when a new song is played

(it would also be nice to have one click to repeat, two clicks for previous but i guess this is for another issue, right?)

@Taiko2k
Copy link
Owner

Taiko2k commented Sep 3, 2024

Interesting request, ill look into it.

For the one click to repeat there is a config key in the config file, set back-restarts to true.

@C0rn3j
Copy link
Collaborator

C0rn3j commented Dec 3, 2024

Spec - https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html

KDE implementation - https://invent.kde.org/frameworks/knotifications

So, the state of this ability in Python is currently bad.

The end result can look something similar to:

image
This is Plasma

We can (but REALLY shouldn't) yoink this script:

notify-send.sh
#!/usr/bin/env bash

# Merged:
#   https://github.com/vlevit/notify-send.sh/blob/master/notify-send.sh
#   https://github.com/vlevit/notify-send.sh/blob/master/notify-action.sh

# notify-send.sh - drop-in replacement for notify-send with more features
# Copyright (C) 2015-2021 notify-send.sh authors (see AUTHORS file)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Desktop Notifications Specification
# https://developer.gnome.org/notification-spec/
set -euo pipefail
#set -x
VERSION=1.2
NOTIFY_ARGS=(--session
             --dest org.freedesktop.Notifications
             --object-path /org/freedesktop/Notifications)
EXPIRE_TIME=-1
APP_NAME="${0##*/}"
REPLACE_ID=0
URGENCY=1
HINTS=()
SUMMARY_SET=n

help() {
    cat <<EOF
Usage:
  notify-send.sh [OPTION...] <SUMMARY> [BODY] - create a notification

Help Options:
  -?|--help                         Show help options

Application Options:
  -u, --urgency=LEVEL               Specifies the urgency level (low, normal, critical).
  -t, --expire-time=TIME            Specifies the timeout in milliseconds at which to expire the notification.
  -f, --force-expire                Forcefully closes the notification when the notification has expired.
  -a, --app-name=APP_NAME           Specifies the app name for the icon.
  -i, --icon=ICON[,ICON...]         Specifies an icon filename or stock icon to display.
  -c, --category=TYPE[,TYPE...]     Specifies the notification category.
  -h, --hint=TYPE:NAME:VALUE        Specifies basic extra data to pass. Valid types are int, double, string and byte.
  -o, --action=COMMAND=LABEL        Specifies an action. Can be passed multiple times. LABEL is usually a button's label. COMMAND is a shell command executed when action is invoked.
  -d, --default-action=COMMAND      Specifies the default action which is usually invoked by clicking the notification.
  -l, --close-action=COMMAND        Specifies the action invoked when notification is closed.
  -p, --print-id                    Print the notification ID to the standard output.
  -r, --replace=ID                  Replace existing notification.
  -R, --replace-file=FILE           Store and load notification replace ID to/from this file.
  -s, --close=ID                    Close notification.
  -v, --version                     Version of the package.
EOF
}

cleanup() {
    rm -f "$GDBUS_MONITOR_PID"
}

create_pid_file(){
    rm -f "$GDBUS_MONITOR_PID"
    umask 077
    touch "$GDBUS_MONITOR_PID"
}

invoke_action() {
    invoked_action_id="$1"
    local action="" cmd=""
    for index in "${!ACTION_COMMANDS[@]}"; do
        if [[ $((index % 2)) == 0 ]]; then
            action="${ACTION_COMMANDS[$index]}"
        else
            cmd="${ACTION_COMMANDS[$index]}"
            if [[ "$action" == "$invoked_action_id" ]]; then
                echo "${cmd}"
#                bash -c "${cmd}" &
            fi
        fi
    done
}

monitor() {
    create_pid_file
    ( "${GDBUS_MONITOR[@]}" & echo $! >&3 ) 3>"$GDBUS_MONITOR_PID" | while read -r line; do
        local closed_notification_id="$(sed '/^\/org\/freedesktop\/Notifications: org.freedesktop.Notifications.NotificationClosed (uint32 \([0-9]\+\), uint32 [0-9]\+)$/!d;s//\1/' <<< "$line")"
        if [[ -n "$closed_notification_id" ]]; then
            if [[ "$closed_notification_id" == "$NOTIFICATION_ID" ]]; then
                invoke_action close
                break
            fi
        else
            local action_invoked="$(sed '/\/org\/freedesktop\/Notifications: org.freedesktop.Notifications.ActionInvoked (uint32 \([0-9]\+\), '\''\(.*\)'\'')$/!d;s//\1:\2/' <<< "$line")"
            IFS=: read invoked_id action_id <<< "$action_invoked"
            if [[ "$invoked_id" == "$NOTIFICATION_ID" ]]; then
                invoke_action "$action_id"
                break
            fi
        fi
    done
    kill $(<"$GDBUS_MONITOR_PID")
    if [[ -n ${NOTIFY_PID-} ]]; then
#        echo "killing ${NOTIFY_PID} and its children"
        procChildren=$(cat /proc/${NOTIFY_PID}/task/${NOTIFY_PID}/children)
        kill -SIGTERM "${NOTIFY_PID}" "${procChildren}"
    fi
    cleanup
}

actionTime() {
    GDBUS_MONITOR_PID=/tmp/notify-action-dbus-monitor.$$.pid
    GDBUS_MONITOR=(gdbus monitor --session --dest org.freedesktop.Notifications --object-path /org/freedesktop/Notifications)
    NOTIFICATION_ID="$1"
    if [[ -z "$NOTIFICATION_ID" ]]; then
        echo "no notification id passed: $@"
        exit 1
    fi
    shift

    ACTION_COMMANDS=("$@")
    if [[ -z "$ACTION_COMMANDS" ]]; then
        echo "no action commands passed: $@"
        exit 1
    fi
    monitor
}

convert_type() {
    case "$1" in
        int) echo int32 ;;
        double|string|byte) echo "$1" ;;
        *) echo error; return 1 ;;
    esac
}

make_action_key() {
    echo "$(tr -dc _A-Z-a-z-0-9 <<< \"$1\")${RANDOM}"
}

make_action() {
    local action_key="$1"
    printf -v text "%q" "$2"
    echo "\"$action_key\", \"$text\""
}

make_hint() {
    if ! type=$(convert_type "$1"); then
        return 1
    fi
    name="$2"
    [[ "$type" = string ]] && command="\"$3\"" || command="$3"
    echo "\"$name\": <$type $command>"
}

concat_actions() {
    local result="$1"
    shift
    for s in "$@"; do
        result="$result, $s"
    done
    echo "[$result]"
}

concat_hints() {
    local result="$1"
    shift
    for s in "$@"; do
        result="$result, $s"
    done
    echo "{$result}"
}

parse_notification_id() {
    sed 's/(uint32 \([0-9]\+\),)/\1/g'
}

notify() {
    local actions
    if [[ -z ${ACTIONS-} ]]; then
        actions='[]'
    else
        actions="$(concat_actions "${ACTIONS[@]}")"
    fi
    local hints="$(concat_hints "${HINTS[@]}")"

    NOTIFICATION_ID=$(gdbus call "${NOTIFY_ARGS[@]}" \
                            --method org.freedesktop.Notifications.Notify \
                            -- \
                            "$APP_NAME" "$REPLACE_ID" "$ICON" "$SUMMARY" "$BODY" \
                            "${actions}" "${hints}" "int32 $EXPIRE_TIME" \
                          | parse_notification_id)

    if [[ -n "${STORE_ID-}" ]]; then
        echo "$NOTIFICATION_ID" > "$STORE_ID"
    fi
    if [[ -n "$PRINT_ID" ]]; then
        echo "$NOTIFICATION_ID"
    fi

    if [[ $FORCE_EXPIRE -eq 1 ]]; then
        SLEEP_TIME="$( LC_NUMERIC=C printf %f "${EXPIRE_TIME}e-3" )"
        ( sleep "$SLEEP_TIME" ; notify_close "$NOTIFICATION_ID" ) &
        NOTIFY_PID=$!
    fi

    maybe_run_action_handler
}

notify_close () {
    gdbus call "${NOTIFY_ARGS[@]}"  --method org.freedesktop.Notifications.CloseNotification "$1" >/dev/null
}

process_urgency() {
    case "$1" in
        low) URGENCY=0 ;;
        normal) URGENCY=1 ;;
        critical) URGENCY=2 ;;
        *) echo "Unknown urgency $URGENCY specified. Known urgency levels: low, normal, critical."
           exit 1
           ;;
    esac
}

process_category() {
    IFS=, read -a categories <<< "$1"
    for category in "${categories[@]}"; do
        hint="$(make_hint string category "$category")"
        HINTS=("${HINTS[@]}" "$hint")
    done
}

process_hint() {
    IFS=: read type name command <<< "$1"
    if [[ -z "$name" ]] || [[ -z "$command" ]]; then
        echo "Invalid hint syntax specified. Use TYPE:NAME:VALUE."
        exit 1
    fi
    hint="$(make_hint "$type" "$name" "$command")"
    if [[ ! $? = 0 ]]; then
        echo "Invalid hint type \"$type\". Valid types are int, double, string and byte."
        exit 1
    fi
    HINTS=("${HINTS[@]}" "$hint")
}

maybe_run_action_handler() {
    if [[ -n "$NOTIFICATION_ID" ]] && [[ -n "${ACTION_COMMANDS-}" ]]; then
        actionTime "$NOTIFICATION_ID" "${ACTION_COMMANDS[@]}" &
        exit 0
    fi
}

process_action() {
    IFS='=' read command name <<<"$1"
    if [[ -z "$name" ]] || [[ -z "$command" ]]; then
        echo "Invalid action syntax '${1}' specified. Use NAME=COMMAND."
        exit 1
    fi

    local action_key="$(make_action_key "$name")"
    ACTION_COMMANDS=("${ACTION_COMMANDS[@]}" "$action_key" "$command")

    local action="$(make_action "$action_key" "$name")"
    ACTIONS=("${ACTIONS[@]}" "$action")
}

process_special_action() {
    action_key="$1"
    command="$2"

    if [[ -z "$action_key" ]] || [[ -z "$command" ]]; then
        echo "Command must not be empty"
        exit 1
    fi

    ACTION_COMMANDS=("${ACTION_COMMANDS[@]}" "$action_key" "$command")

    if [[ "$action_key" != close ]]; then
        local action="$(make_action "$action_key" "$name")"
        ACTIONS=("${ACTIONS[@]}" "$action")
    fi
}

process_posargs() {
    if [[ "$1" = -* ]] && ! [[ "$positional" = yes ]]; then
        echo "Unknown option $1"
        exit 1
    else
        if [[ "$SUMMARY_SET" = n ]]; then
            SUMMARY="$1"
            SUMMARY_SET=y
        else
            BODY="$1"
        fi
    fi
}

FORCE_EXPIRE=0
while (( $# > 0 )) ; do
    case "$1" in
        -\?|--help)
            help
            exit 0
            ;;
        -v|--version)
            echo "${0##*/} $VERSION"
            exit 0
            ;;
        -u|--urgency|--urgency=*)
            [[ "$1" = --urgency=* ]] && urgency="${1#*=}" || { shift; urgency="$1"; }
            process_urgency "$urgency"
            ;;
        -t|--expire-time|--expire-time=*)
            [[ "$1" = --expire-time=* ]] && EXPIRE_TIME="${1#*=}" || { shift; EXPIRE_TIME="$1"; }
            if ! [[ "$EXPIRE_TIME" =~ ^-?[0-9]+$ ]]; then
                echo "Invalid expire time: ${EXPIRE_TIME}"
                exit 1
            fi
            ;;
        -f|--force-expire)
            FORCE_EXPIRE=1
            ;;
        -a|--app-name|--app-name=*)
            [[ "$1" = --app-name=* ]] && APP_NAME="${1#*=}" || { shift; APP_NAME="$1"; }
            ;;
        -i|--icon|--icon=*)
            [[ "$1" = --icon=* ]] && ICON="${1#*=}" || { shift; ICON="$1"; }
            ;;
        -c|--category|--category=*)
            [[ "$1" = --category=* ]] && category="${1#*=}" || { shift; category="$1"; }
            process_category "$category"
            ;;
        -h|--hint|--hint=*)
            [[ "$1" = --hint=* ]] && hint="${1#*=}" || { shift; hint="$1"; }
            process_hint "$hint"
            ;;
        -o | --action | --action=*)
            [[ "$1" == --action=* ]] && action="${1#*=}" || { shift; action="$1"; }
            process_action "$action"
            ;;
        -d | --default-action | --default-action=*)
            [[ "$1" == --default-action=* ]] && default_action="${1#*=}" || { shift; default_action="$1"; }
            process_special_action default "$default_action"
            ;;
        -l | --close-action | --close-action=*)
            [[ "$1" == --close-action=* ]] && close_action="${1#*=}" || { shift; close_action="$1"; }
            process_special_action close "$close_action"
            ;;
        -p|--print-id)
            PRINT_ID=yes
            ;;
        -r|--replace|--replace=*)
            [[ "$1" = --replace=* ]] && REPLACE_ID="${1#*=}" || { shift; REPLACE_ID="$1"; }
            ;;
        -R|--replace-file|--replace-file=*)
            [[ "$1" = --replace-file=* ]] && filename="${1#*=}" || { shift; filename="$1"; }
            if [[ -s "$filename" ]]; then
                REPLACE_ID="$(< "$filename")"
            fi
            STORE_ID="$filename"
            ;;
        -s|--close|--close=*)
            [[ "$1" = --close=* ]] && close_id="${1#*=}" || { shift; close_id="$1"; }
            # always check that --close provides a numeric value
            if [[ -z "$close_id" || ! "$close_id" =~ ^[0-9]+$ ]]; then
              echo "Invalid close id: '$close_id'"
              exit 1
            fi
            notify_close "$close_id"
            exit $?
            ;;
        --)
            positional=yes
            ;;
        *)
            process_posargs "$1"
            ;;
    esac
    shift
done

# always force --replace and --replace-file to provide a numeric value; 0 means no id provided
if [[ -z "$REPLACE_ID" || ! "$REPLACE_ID" =~ ^[0-9]+$ ]]; then
    REPLACE_ID=0
fi

# urgency is always set
HINTS=("$(make_hint byte urgency "$URGENCY")" "${HINTS[@]}")

if [[ "$SUMMARY_SET" = n ]]; then
    help
    exit 1
else
    notify
fi

And do something like this on the Python side(shell_exec is from lynxlynx, code from my private project):

	try:
		command = ['./notify-send.sh', '--print-id', '--urgency=normal', '--app-name=Nyx', '--force-expire', '--expire-time=145000']
		if task:
			command.extend(['--action=doTomorrow=Do Tomorrow'])
			command.extend(['--action=markTaskDone=Mark Done'])
			command.extend(['--action=deleteTaskLocally=Delete (local)'])
		command.extend([icon_string, title, body])
		result, errors = shell_exec(args=command, timeout=config['notificationLifetimeSeconds'])
	except ShellExecError:
		logging.exception('ShellExecError, possible command timeout?')
		return # Can this crash and lose us normal content?
	except Exception:
		logging.exception('Unhandled exception sending notification!')
		return
	newline_pos = result.stdOut.find('\n')
	# Extracting the first line (task ID) using the position of the first newline
	notificationID = result.stdOut[:newline_pos] if newline_pos != -1 else result.stdOut
	# Everything after the first line - action
	notificationAction = result.stdOut[newline_pos+1:] if newline_pos != -1 else ''
	logging.debug(f"Action from notification ID '{notificationID}' is '{notificationAction}' because it had std_out '{result.stdOut}'. std_err was '{result.stdErr}'")
	if notificationAction == 'default':
		if taskURL != '':
			result, errors = shell_exec(args=['xdg-open', taskURL], timeout=config['notificationLifetimeSeconds'])
	elif notificationAction == 'markTaskDone':
		if task is None:
			logging.critical(f"{notificationAction}: This shouldn't happen…")
		else:
			try:
				markTaskDone(task=task)
			except Exception:
				logging.exception("Failed to mark task done!")
	elif notificationAction == 'deleteTaskLocally':
		if task is None:
			logging.critical(f"{notificationAction}: This shouldn't happen…")
		else:
			deleteTaskLocally(task=task)
	elif notificationAction == 'doTomorrow':
		if task is None:
			logging.critical(f"{notificationAction}: This shouldn't happen…")
		else:
			setDueTomorrow(task=task)
	elif notificationAction == '':
		logging.debug('User closed notificaition')
	else:
		logging.error(f"Unknown action '{notificationAction}'")

	if errors is not None and 'Timeout' in errors:
		try:
			command = ['./notify-send.sh', f'--close={notificationID}']
			result, errors = shell_exec(args=command, timeout=config['notificationLifetimeSeconds'])
		except Exception:
			logging.exception('Unhandled exception when killing notif!')

This works, but is terrible and issue prone, I plan to write a notif library for Python since all the existing ones frankly suck for one reason or another.

@C0rn3j C0rn3j added the enhancement Accepted functionality to be added label Jan 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Accepted functionality to be added
Projects
None yet
Development

No branches or pull requests

3 participants