Merge pull request #651 from bebehei/dunstctl

Implement a command line control (dunstctl)
This commit is contained in:
Nikos Tsipinakis 2020-05-01 15:38:23 +02:00 committed by GitHub
commit 337ff1edb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 450 additions and 38 deletions

View File

@ -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

View File

@ -517,7 +517,7 @@ Close all notifications.
=back
=head2 Shortcut section
=head2 Shortcut section B<DEPRECATED SEE DUNSTCTL>
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<history_length>) 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)

59
docs/dunstctl.pod Normal file
View File

@ -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<action> 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>
Close the topmost notification currently being displayed.
=item B<close-all>
Close all notifications currently being displayed
=item B<context>
Open the context menu, presenting all available actions and urls for the
currently open notifications.
=item B<history-pop>
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<running>
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<set-running> 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<debug>
Tries to contact dunst and checks for common faults between dunstctl and dunst.
Useful if something isn't working
=item B<help>
Show all available commands with a brief description
=back

101
dunstctl Executable file
View File

@ -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 <command> [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

View File

@ -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,6 +69,22 @@ static const char *introspection_xml =
" <arg name=\"id\" type=\"u\"/>"
" <arg name=\"action_key\" type=\"s\"/>"
" </signal>"
" </interface>"
" <interface name=\""DUNST_IFAC"\">"
" <method name=\"ContextMenuCall\" />"
" <method name=\"NotificationAction\">"
" <arg name=\"number\" type=\"i\"/>"
" </method>"
" <method name=\"NotificationCloseLast\" />"
" <method name=\"NotificationCloseAll\" />"
" <method name=\"NotificationShow\" />"
" <method name=\"Ping\" />"
" <property name=\"running\" type=\"b\" access=\"readwrite\">"
" <annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
" </property>"
" </interface>"
"</node>";
@ -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,9 +137,9 @@ void handle_method_call(GDBusConnection *connection,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
struct dbus_method *m = bsearch(
method_name,
&methods_fdn,
struct dbus_method *m = bsearch(method_name,
methods_fdn,
G_N_ELEMENTS(methods_fdn),
sizeof(struct dbus_method),
cmp_methods);
@ -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)", &notification_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;
registration_id = g_dbus_connection_register_object(connection,
if(!g_dbus_connection_register_object(
connection,
FDN_PATH,
introspection_data->interfaces[0],
&interface_vtable,
&interface_vtable_fdn,
NULL,
NULL,
&err);
&err)) {
DIE("Unable to register dbus connection interface '%s': %s", introspection_data->interfaces[0]->name, err->message);
}
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,6 +660,8 @@ static void dbus_cb_name_acquired(GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
// 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;
}

View File

@ -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

View File

@ -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();
}

View File

@ -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};

View File

@ -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) - <<EOF
bin/dunst
bin/dunstctl
bin/dunstify
dbus/org.knopwob.dunst.service
share/dunst/dunstrc
share/man/man1/dunst.1
share/man/man1/dunstctl.1
systemd/dunst.service
EOF