add previous or next on gnome notification
Opened this issue · 2 comments
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?)
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
.
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:
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.