diff --git a/Makefile b/Makefile index 57b04a7..7fe273d 100644 --- a/Makefile +++ b/Makefile @@ -106,9 +106,15 @@ test/test: ${OBJ} ${TEST_OBJ} ${CC} -o ${@} ${TEST_OBJ} $(filter-out ${TEST_OBJ:test/%=src/%},${OBJ}) ${CFLAGS} ${LDFLAGS} .PHONY: doc doc-doxygen -doc: docs/dunst.1 +doc: docs/dunst.1 docs/dunstctl.1 + +# Can't dedup this as we need to explicitly provide the name and title text to +# pod2man :( docs/dunst.1: docs/dunst.pod ${POD2MAN} --name=dunst -c "Dunst Reference" --section=1 --release=${VERSION} $< > $@ +docs/dunstctl.1: docs/dunstctl.pod + ${POD2MAN} --name=dunstctl -c "dunstctl reference" --section=1 --release=${VERSION} $< > $@ + doc-doxygen: ${DOXYGEN} docs/internal/Doxyfile @@ -137,6 +143,7 @@ clean-dunstify: clean-doc: rm -f docs/dunst.1 + rm -f docs/dunstctl.1 rm -fr docs/internal/html rm -fr docs/internal/coverage @@ -151,15 +158,19 @@ clean-coverage-run: ${FIND} . -type f -name '*.gcov' -delete ${FIND} . -type f -name '*.gcda' -delete -.PHONY: install install-dunst install-doc \ +.PHONY: install install-dunst install-dunstctl install-doc \ install-service install-service-dbus install-service-systemd \ - uninstall \ + uninstall uninstall-dunstctl \ uninstall-service uninstall-service-dbus uninstall-service-systemd -install: install-dunst install-doc install-service install-dunstify +install: install-dunst install-dunstctl install-doc install-service install-dunstify install-dunst: dunst doc install -Dm755 dunst ${DESTDIR}${BINDIR}/dunst install -Dm644 docs/dunst.1 ${DESTDIR}${MANPREFIX}/man1/dunst.1 + install -Dm644 docs/dunstctl.1 ${DESTDIR}${MANPREFIX}/man1/dunstctl.1 + +install-dunstctl: dunstctl + install -Dm755 dunstctl ${DESTDIR}${BINDIR}/dunstctl install-doc: install -Dm644 dunstrc ${DESTDIR}${DATADIR}/dunst/dunstrc @@ -176,12 +187,16 @@ endif install-dunstify: dunstify install -Dm755 dunstify ${DESTDIR}${BINDIR}/dunstify -uninstall: uninstall-service +uninstall: uninstall-service uninstall-dunstctl rm -f ${DESTDIR}${BINDIR}/dunst rm -f ${DESTDIR}${BINDIR}/dunstify rm -f ${DESTDIR}${MANPREFIX}/man1/dunst.1 + rm -f ${DESTDIR}${MANPREFIX}/man1/dunstctl.1 rm -rf ${DESTDIR}${DATADIR}/dunst +uninstall-dunstctl: + rm -f ${DESTDIR}${BINDIR}/dunstctl + uninstall-service: uninstall-service-dbus uninstall-service-dbus: rm -f ${DESTDIR}${SERVICEDIR_DBUS}/org.knopwob.dunst.service diff --git a/docs/dunst.pod b/docs/dunst.pod index f94a7dd..b97e647 100644 --- a/docs/dunst.pod +++ b/docs/dunst.pod @@ -517,7 +517,7 @@ Close all notifications. =back -=head2 Shortcut section +=head2 Shortcut section B Keyboard shortcuts are defined in the following format: "Modifier+key" where the modifier is one of ctrl,mod1,mod2,mod3,mod4 and key is any keyboard key. @@ -602,6 +602,13 @@ See TIME FORMAT for valid times. =back +=head1 DUNSTCTL + +Dunst now contains a command line control command that can be used to interact +with it. It supports all functions previously done only via keyboard shortcuts +but also has a lot of extra functionality. So see more see the dunstctl man +page. + =head1 HISTORY Dunst saves a number of notifications (specified by B) in memory. @@ -865,9 +872,8 @@ Example time: "1000ms" "10m" =head1 MISCELLANEOUS -Dunst can be paused by sending a notification with a summary of -"DUNST_COMMAND_PAUSE", resumed with a summary of "DUNST_COMMAND_RESUME" and -toggled with a summary of "DUNST_COMMAND_TOGGLE". +Dunst can be paused via the `dunstctl set-running false` command. To unpause dunst use +`dunstctl set-status true` and to unpause `dunstctl set-status false`. Alternatively you can send SIGUSR1 and SIGUSR2 to pause and unpause respectively. For Example: @@ -908,4 +914,4 @@ If you feel that copyrights are violated, please send me an email. =head1 SEE ALSO -dwm(1), dmenu(1), twmn(1), notify-send(1) +dunstctl(1), dwm(1), dmenu(1), twmn(1), notify-send(1) diff --git a/docs/dunstctl.pod b/docs/dunstctl.pod new file mode 100644 index 0000000..c537349 --- /dev/null +++ b/docs/dunstctl.pod @@ -0,0 +1,59 @@ +=head1 NAME + +dunstctl - Command line control utility for dunst, a customizable and lightweight notification-daemon + +=head1 SYNOPSIS + +dunstctl COMMAND [PARAMETER] + +=head1 COMMANDS + +=over 4 + +=item B notification_position + +Performs the default action or, if not available, opens the context menu of the +notification at the given position (starting count at the top, first +notification being 0). + +=item B + +Close the topmost notification currently being displayed. + +=item B + +Close all notifications currently being displayed + +=item B + +Open the context menu, presenting all available actions and urls for the +currently open notifications. + +=item B + +Redisplay the notification that was most recently closed. This can be called +multiple times to show older notifications, up to the history limit configured +in dunst. + +=item B + +Check if dunst is currently running or paused. If dunst is paused notifications +will be kept but not shown until it is unpaused. + +=item B true/false + +Set the paused status of dunst. If true, dunst is running normally, if false, +dunst is paused. See the running command and the dunst man page for more +information. + +=item B + +Tries to contact dunst and checks for common faults between dunstctl and dunst. +Useful if something isn't working + +=item B + +Show all available commands with a brief description + +=back + diff --git a/dunstctl b/dunstctl new file mode 100755 index 0000000..347c77c --- /dev/null +++ b/dunstctl @@ -0,0 +1,101 @@ +#!/bin/sh + +set -eu + +DBUS_NAME="org.freedesktop.Notifications" +DBUS_PATH="/org/freedesktop/Notifications" +DBUS_IFAC_DUNST="org.dunstproject.cmd0" +DBUS_IFAC_PROP="org.freedesktop.DBus.Properties" +DBUS_IFAC_FDN="org.freedesktop.Notifications" + +die(){ printf "%s\n" "${1}" >&2; exit 1; } + +show_help() { + cat <<-EOH + Usage: dunstctl [parameters]" + Commands: + action Perform the default action, or open the + context menu of the notification at the + given position + close Close the last notification + close-all Close the all notifications + context Open context menu + history-pop Pop one notification from history + running Check if dunst is running or paused + set-running [true|false] Set the pause status + debug Print debugging information + help Show this help + EOH +} +dbus_send_checked() { + dbus-send "$@" \ + || die "Failed to communicate with dunst, is it running? Or maybe the version is outdated. You can try 'dunstctl debug' as a next debugging step." +} + +method_call() { + dbus_send_checked --print-reply=literal --dest="${DBUS_NAME}" "${DBUS_PATH}" "$@" +} + +property_get() { + dbus_send_checked --print-reply=literal --dest="${DBUS_NAME}" "${DBUS_PATH}" "${DBUS_IFAC_PROP}.Get" "string:${DBUS_IFAC_DUNST}" "string:${1}" +} + +property_set() { + dbus_send_checked --print-reply=literal --dest="${DBUS_NAME}" "${DBUS_PATH}" "${DBUS_IFAC_PROP}.Set" "string:${DBUS_IFAC_DUNST}" "string:${1}" "${2}" +} + +command -v dbus-send >/dev/null 2>/dev/null || \ + die "Command dbus-send not found" + + +case "${1:-}" in + "action") + method_call "${DBUS_IFAC_DUNST}.NotificationAction" "int32:${2:-0}" >/dev/null + ;; + "close") + method_call "${DBUS_IFAC_DUNST}.NotificationCloseLast" >/dev/null + ;; + "close-all") + method_call "${DBUS_IFAC_DUNST}.NotificationCloseAll" >/dev/null + ;; + "context") + method_call "${DBUS_IFAC_DUNST}.ContextMenuCall" >/dev/null + ;; + "history-pop") + method_call "${DBUS_IFAC_DUNST}.NotificationShow" >/dev/null + ;; + "running") + property_get running | ( read -r _ _ paused; printf "%s\n" "${paused}"; ) + ;; + "set-running") + [ "${2:-}" ] \ + || die "No status parameter specified. Please give either 'true' or 'false' as running parameter." + [ "${2}" = "true" ] || [ "${2}" = "false" ] \ + || die "Please give either 'true' or 'false' as running parameter." + property_set running variant:boolean:"${2}" + ;; + "help"|"--help"|"-h") + show_help + ;; + "debug") + dbus-send --print-reply=literal --dest="${DBUS_NAME}" "${DBUS_PATH}" "${DBUS_IFAC_FDN}.GetServerInformation" >/dev/null 2>/dev/null \ + || die "Dunst is not running." + + dbus-send --print-reply=literal --dest="${DBUS_NAME}" "${DBUS_PATH}" "${DBUS_IFAC_FDN}.GetServerInformation" \ + | ( + read -r name _ version _ + [ "${name}" = "dunst" ] + printf "dunst version: %s\n" "${version}" + ) \ + || die "Another notification manager is running. It's not dunst" + + dbus-send --print-reply=literal --dest="${DBUS_NAME}" "${DBUS_PATH}" "${DBUS_IFAC_DUNST}.Ping" >/dev/null 2>/dev/null \ + || die "Dunst controlling interface not available. Is the version too old?" + ;; + "") + die "dunstctl: No command specified. Please consult the usage." + ;; + *) + die "dunstctl: unrecognized command '${1:-}'. Please consult the usage." + ;; +esac diff --git a/src/dbus.c b/src/dbus.c index 5bb6210..e3427b8 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -8,6 +8,7 @@ #include "dunst.h" #include "log.h" +#include "menu.h" #include "notification.h" #include "queues.h" #include "settings.h" @@ -17,6 +18,12 @@ #define FDN_IFAC "org.freedesktop.Notifications" #define FDN_NAME "org.freedesktop.Notifications" +#define DUNST_PATH "/org/freedesktop/Notifications" +#define DUNST_IFAC "org.dunstproject.cmd0" +#define DUNST_NAME "org.freedesktop.Notifications" + +#define PROPERTIES_IFAC "org.freedesktop.DBus.Properties" + GDBusConnection *dbus_conn; static GDBusNodeInfo *introspection_data = NULL; @@ -62,7 +69,23 @@ static const char *introspection_xml = " " " " " " - " " + " " + " " + + " " + " " + " " + " " + " " + " " + " " + " " + + " " + " " + " " + + " " ""; static const char *stack_tag_hints[] = { @@ -98,7 +121,6 @@ DBUS_METHOD(Notify); DBUS_METHOD(CloseNotification); DBUS_METHOD(GetCapabilities); DBUS_METHOD(GetServerInformation); - static struct dbus_method methods_fdn[] = { {"CloseNotification", dbus_cb_CloseNotification}, {"GetCapabilities", dbus_cb_GetCapabilities}, @@ -106,7 +128,7 @@ static struct dbus_method methods_fdn[] = { {"Notify", dbus_cb_Notify}, }; -void handle_method_call(GDBusConnection *connection, +void dbus_cb_fdn_methods(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, @@ -115,12 +137,12 @@ void handle_method_call(GDBusConnection *connection, GDBusMethodInvocation *invocation, gpointer user_data) { - struct dbus_method *m = bsearch( - method_name, - &methods_fdn, - G_N_ELEMENTS(methods_fdn), - sizeof(struct dbus_method), - cmp_methods); + + struct dbus_method *m = bsearch(method_name, + methods_fdn, + G_N_ELEMENTS(methods_fdn), + sizeof(struct dbus_method), + cmp_methods); if (m) { m->method(connection, sender, parameters, invocation); @@ -131,6 +153,144 @@ void handle_method_call(GDBusConnection *connection, } } +DBUS_METHOD(dunst_ContextMenuCall); +DBUS_METHOD(dunst_NotificationAction); +DBUS_METHOD(dunst_NotificationCloseAll); +DBUS_METHOD(dunst_NotificationCloseLast); +DBUS_METHOD(dunst_NotificationShow); +DBUS_METHOD(dunst_Ping); +static struct dbus_method methods_dunst[] = { + {"ContextMenuCall", dbus_cb_dunst_ContextMenuCall}, + {"NotificationAction", dbus_cb_dunst_NotificationAction}, + {"NotificationCloseAll", dbus_cb_dunst_NotificationCloseAll}, + {"NotificationCloseLast", dbus_cb_dunst_NotificationCloseLast}, + {"NotificationShow", dbus_cb_dunst_NotificationShow}, + {"Ping", dbus_cb_dunst_Ping}, +}; + +void dbus_cb_dunst_methods(GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + + struct dbus_method *m = bsearch(method_name, + methods_dunst, + G_N_ELEMENTS(methods_dunst), + sizeof(struct dbus_method), + cmp_methods); + + if (m) { + m->method(connection, sender, parameters, invocation); + } else { + LOG_M("Unknown method name: '%s' (sender: '%s').", + method_name, + sender); + } +} + +static void dbus_cb_dunst_ContextMenuCall(GDBusConnection *connection, + const gchar *sender, + GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + LOG_D("CMD: Calling context menu"); + context_menu(); + + g_dbus_method_invocation_return_value(invocation, NULL); + g_dbus_connection_flush(connection, NULL, NULL, NULL); +} + +static void dbus_cb_dunst_NotificationAction(GDBusConnection *connection, + const gchar *sender, + GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + int notification_nr = 0; + g_variant_get(parameters, "(i)", ¬ification_nr); + + LOG_D("CMD: Calling action for notification %d", notification_nr); + + if (notification_nr < 0 || queues_length_waiting() < notification_nr) { + g_dbus_method_invocation_return_error(invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Couldn't activate action for notification in position %d, %d notifications currently open", + notification_nr, queues_length_waiting()); + return; + } + + const GList *list = g_list_nth_data(queues_get_displayed(), notification_nr); + + if (list && list->data) { + struct notification *n = list->data; + LOG_D("CMD: Calling action for notification %s", n->summary); + notification_do_action(n); + } + + g_dbus_method_invocation_return_value(invocation, NULL); + g_dbus_connection_flush(connection, NULL, NULL, NULL); +} + +static void dbus_cb_dunst_NotificationCloseAll(GDBusConnection *connection, + const gchar *sender, + GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + LOG_D("CMD: Pushing all to history"); + queues_history_push_all(); + wake_up(); + + g_dbus_method_invocation_return_value(invocation, NULL); + g_dbus_connection_flush(connection, NULL, NULL, NULL); +} + +static void dbus_cb_dunst_NotificationCloseLast(GDBusConnection *connection, + const gchar *sender, + GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + LOG_D("CMD: Closing last notification"); + const GList *list = queues_get_displayed(); + if (list && list->data) { + struct notification *n = list->data; + queues_notification_close_id(n->id, REASON_USER); + wake_up(); + } + + g_dbus_method_invocation_return_value(invocation, NULL); + g_dbus_connection_flush(connection, NULL, NULL, NULL); +} + +static void dbus_cb_dunst_NotificationShow(GDBusConnection *connection, + const gchar *sender, + GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + LOG_D("CMD: Showing last notification from history"); + queues_history_pop(); + wake_up(); + + g_dbus_method_invocation_return_value(invocation, NULL); + g_dbus_connection_flush(connection, NULL, NULL, NULL); +} + +/* Just a simple Ping command to give the ability to dunstctl to test for the existence of this interface + * Any other way requires parsing the XML of the Introspection or other foo. Just calling the Ping on an old dunst version will fail. */ +static void dbus_cb_dunst_Ping(GDBusConnection *connection, + const gchar *sender, + GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + g_dbus_method_invocation_return_value(invocation, NULL); + g_dbus_connection_flush(connection, NULL, NULL, NULL); +} + + static void dbus_cb_GetCapabilities( GDBusConnection *connection, const gchar *sender, @@ -347,11 +507,9 @@ static void dbus_cb_GetServerInformation( GVariant *parameters, GDBusMethodInvocation *invocation) { - GVariant *value; - - value = g_variant_new("(ssss)", "dunst", "knopwob", VERSION, "1.2"); - g_dbus_method_invocation_return_value(invocation, value); + GVariant *answer = g_variant_new("(ssss)", "dunst", "knopwob", VERSION, "1.2"); + g_dbus_method_invocation_return_value(invocation, answer); g_dbus_connection_flush(connection, NULL, NULL, NULL); } @@ -420,28 +578,81 @@ void signal_action_invoked(const struct notification *n, const char *identifier) } } -static const GDBusInterfaceVTable interface_vtable = { - handle_method_call +GVariant *dbus_cb_dunst_Properties_Get(GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data) +{ + struct dunst_status status = dunst_status_get(); + + if (STR_EQ(property_name, "running")) { + return g_variant_new_boolean(status.running); + } else { + LOG_W("Unknown property!\n"); + *error = g_error_new(G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "Unknown property"); + return NULL; + } +} + +gboolean dbus_cb_dunst_Properties_Set(GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GVariant *value, + GError **error, + gpointer user_data) +{ + if (STR_EQ(property_name, "running")) { + dunst_status(S_RUNNING, g_variant_get_boolean(value)); + wake_up(); + return true; + } + + *error = g_error_new(G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "Unknown property"); + + return false; +} + + +static const GDBusInterfaceVTable interface_vtable_fdn = { + dbus_cb_fdn_methods +}; + +static const GDBusInterfaceVTable interface_vtable_dunst = { + dbus_cb_dunst_methods, + dbus_cb_dunst_Properties_Get, + dbus_cb_dunst_Properties_Set, }; static void dbus_cb_bus_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) { - guint registration_id; - GError *err = NULL; + if(!g_dbus_connection_register_object( + connection, + FDN_PATH, + introspection_data->interfaces[0], + &interface_vtable_fdn, + NULL, + NULL, + &err)) { + DIE("Unable to register dbus connection interface '%s': %s", introspection_data->interfaces[0]->name, err->message); + } - registration_id = g_dbus_connection_register_object(connection, - FDN_PATH, - introspection_data->interfaces[0], - &interface_vtable, - NULL, - NULL, - &err); - - if (registration_id == 0) { - DIE("Unable to register dbus connection: %s", err->message); + if(!g_dbus_connection_register_object( + connection, + FDN_PATH, + introspection_data->interfaces[1], + &interface_vtable_dunst, + NULL, + NULL, + &err)) { + DIE("Unable to register dbus connection interface '%s': %s", introspection_data->interfaces[1]->name, err->message); } } @@ -449,7 +660,9 @@ static void dbus_cb_name_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) { - dbus_conn = connection; + // If we're not able to get org.fd.N bus, we've still got a problem + if (STR_EQ(name, FDN_NAME)) + dbus_conn = connection; } /** diff --git a/src/dbus.h b/src/dbus.h index 4a72479..b74d95c 100644 --- a/src/dbus.h +++ b/src/dbus.h @@ -3,6 +3,7 @@ #ifndef DUNST_DBUS_H #define DUNST_DBUS_H +#include "dunst.h" #include "notification.h" /// The reasons according to the notification spec diff --git a/test/dbus.c b/test/dbus.c index 659731a..5db9a7b 100644 --- a/test/dbus.c +++ b/test/dbus.c @@ -799,6 +799,12 @@ TEST assert_methodlists_sorted(void) methods_fdn[i+1].method_name)); } + for (size_t i = 0; i+1 < G_N_ELEMENTS(methods_dunst); i++) { + ASSERT(0 > strcmp( + methods_dunst[i].method_name, + methods_dunst[i+1].method_name)); + } + PASS(); } diff --git a/test/dunst.c b/test/dunst.c index 827f98d..476c518 100644 --- a/test/dunst.c +++ b/test/dunst.c @@ -1,6 +1,15 @@ +#define dbus_signal_status_changed(status) signal_sent_stub(status) #include "../src/dunst.c" #include "greatest.h" +static bool signal_sent = false; + +void signal_sent_stub(struct dunst_status status) +{ + signal_sent = true; + return; +} + TEST test_dunst_status(void) { status = (struct dunst_status) {false, false, false}; diff --git a/test/test-install.sh b/test/test-install.sh index 50e7336..ab0d950 100755 --- a/test/test-install.sh +++ b/test/test-install.sh @@ -9,10 +9,12 @@ make -C "${BASE}" SYSTEMD=1 SERVICEDIR_SYSTEMD="${PREFIX}/systemd" SERVICEDIR_DB diff -u <(find "${PREFIX}" -type f -printf "%P\n" | sort) - <