From 7409c6fca66dcd89e0bdd4748c50a9522c6d7f11 Mon Sep 17 00:00:00 2001 From: M B <85039141+m-bartlett@users.noreply.github.com> Date: Mon, 19 Sep 2022 09:18:15 -0600 Subject: [PATCH 1/4] add and document optional feature flags during compilation --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++ src/Makefile | 9 ++++++- src/notification.c | 52 ++++++++++++++++++++++++++++------------ 3 files changed, 104 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6206d8d..82405e9 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ A keybindable application to modify volume levels on audio sinks within PulseAud * [Dunstrc config](#dunstrc-config) * [Extra](#extra) * [About](#about) + * [Optional Features](#optional-features) * [Key Binding](#key-binding) * [Custom Icons](#custom-icons) * [PulseAudio Support](#pulseaudio-support) @@ -183,6 +184,64 @@ sections below. ## About +### Optional Features +There are certain features which can be enabled at compile-time. These are features that would would add overhead to the generation and display of the notification if they were specified with a boolean CLI flag, and are generally things that users would either he interested in having all the time or never. The feature inclusion is ultimately decided by the preprocessor, but I've added some logic in the `Makefile` to make it simpler for the user to specify the inclusion of these optional features. + +From the table below, export the **Feature name variable** value as a non-empty value to `make`. Note that specifying a feature variable *after* building will not indicate to `make` that it needs to rebuild; one should use the `-B` flag to force `make` to rebuild. + +For example, to enable the `FORMAT_VOLUME_IN_NOTIFICATION_BODY` feature, I would recommend executing: +`FORMAT_VOLUME_IN_NOTIFICATION_BODY=1 make -B` + +
+ + + + + + + + + + + + + + + + + +
Feature name variableFeature descriptionExample
ENABLE_TRANSIENT_HINTTransient notifications will still timeout even if the user is considered idle. The default dunst config disables idle timeout, so only enable this if you use this dunst feature and would like volume notifications to still disappear.
FORMAT_VOLUME_IN_NOTIFICATION_BODY + +Support formatting the volume percentage integer into the notification body. This literally runs `sprintf` with the volume integer as an argument. For example with a body argument of `~%d~` and the current volume being 25, the resulting notification body would show `~25~`.
**WARNING**: This feature has no sanitation of the user provided body value. If you provide any formatter besides `%d` expect to get a segmentation fault. + +
+ + + + + + + + + + + + + +
Disabled
+ Body NOT formatted with volume +
Enabled
+ Body formatted with volume +
+
+ + ### Key Binding This will obviously depend on your Linux distribution, desktop environment, and preferred means of creating global keyboard shortcuts. diff --git a/src/Makefile b/src/Makefile index 5c20288..74418db 100644 --- a/src/Makefile +++ b/src/Makefile @@ -22,6 +22,13 @@ OBJECTS := $(SOURCES:.c=.o) ICON_SVGS := $(wildcard svg/*.svg) ICON_HEADERS := $(patsubst svg/%.svg, icon/%.h, $(ICON_SVGS)) +FEATURES := +ifneq ($(ENABLE_TRANSIENT_HINT),) + FEATURES += -DENABLE_TRANSIENT_HINT +endif +ifneq ($(FORMAT_VOLUME_IN_NOTIFICATION_BODY),) + FEATURES += -DFORMAT_VOLUME_IN_NOTIFICATION_BODY +endif all: # Multi-threaded make by default $(MAKE) -j $(shell nproc) $(TARGET) @@ -35,7 +42,7 @@ $(TARGET): $(OBJECTS) $(OBJECTS): $(SOURCES) $(HEADERS) $(ICON_HEADERS) %.o: %.c - $(CC) $(CFLAGS) $(LDFLAGS) $(LIB_FLAGS) -c $< -o $@ + $(CC) $(FEATURES) $(CFLAGS) $(LDFLAGS) $(LIB_FLAGS) -c $< -o $@ $(SOURCES): $(MAKEFILE) # If Makefile changes, recompile @touch $(SOURCES) diff --git a/src/notification.c b/src/notification.c index e1ce0c9..da38dc4 100644 --- a/src/notification.c +++ b/src/notification.c @@ -42,35 +42,57 @@ void display_volume_notification(userdata_t *userdata) { volume = (volume % 100) * 100 / PULSEAUDIO_OVERAMPLIFIED_RANGE; } } - size_t body_width = NOTIFICATION_BODY_FORMAT_SIZE + strlen(userdata->notification_body)+1; - char body[body_width]; - sprintf(body, NOTIFICATION_BODY_FORMAT, userdata->notification_body); + #ifdef FORMAT_VOLUME_IN_NOTIFICATION_BODY + + /* Attempt to format the current volume percentage integer into the user's notification body */ + size_t formatted_body_size =strlen(userdata->notification_body) - 2/*len("%d")*/ + 3/*len("100")*/; + char formatted_body[formatted_body_size]; + sprintf(formatted_body, userdata->notification_body, volume); + + size_t body_size = NOTIFICATION_BODY_FORMAT_SIZE + strlen(formatted_body) + 1; + char body[body_size]; + sprintf(body, NOTIFICATION_BODY_FORMAT, formatted_body); + + #else + + size_t body_width = NOTIFICATION_BODY_FORMAT_SIZE + strlen(userdata->notification_body)+1; + char body[body_width]; + sprintf(body, NOTIFICATION_BODY_FORMAT, userdata->notification_body, volume); + + #endif notify_init(summary); NotifyNotification *notification = notify_notification_new(summary, body, NULL); notify_notification_set_category(notification, NOTIFICATION_LITERAL_CATEGORY); notify_notification_set_timeout(notification, userdata->notification_timeout); - bool icon_success = render_notification_icon(notification, icon_body, icon_body_size, - userdata->icon_size, - userdata->icon_primary_color, - userdata->icon_secondary_color); - if (!icon_success) { fprintf(stderr, "Error rendering notification icon\n"); - exit(EXIT_FAILURE); } - /* https://people.gnome.org/~desrt/glib-docs/glib-GVariant.html info on GVariants since notify_notification_set_hint_* methods are deprecated */ - // Set notification synchronous (overwrite existing notification instead of making a fresh one) - notify_notification_set_hint( notification, - NOTIFICATION_LITERAL_HINT_SYNCHRONOUS, - g_variant_new_string(NOTIFICATION_LITERAL_CATEGORY) ); + /* Set notification synchronous (overwrite existing notification instead of making a fresh one) */ + notify_notification_set_hint(notification, + NOTIFICATION_LITERAL_HINT_STACKTAG, + g_variant_new_string(NOTIFICATION_LITERAL_CATEGORY)); - // Add 'value' hint for dunst progress bar to show new volume + /* Add 'value' hint for dunst progress bar to show new volume */ notify_notification_set_hint( notification, NOTIFICATION_LITERAL_HINT_VALUE, g_variant_new_int32(volume) ); + #ifdef ENABLE_TRANSIENT_HINT + /* transient notifications will still timeout even if the user is considered idle */ + notify_notification_set_hint(notification, "transient", g_variant_new_boolean(true)); + #endif + + bool icon_success = render_notification_icon(notification, + icon_body, + icon_body_size, + userdata->icon_size, + userdata->icon_primary_color, + userdata->icon_secondary_color); + + if (!icon_success) { fprintf(stderr, "Error rendering notification icon\n"); exit(EXIT_FAILURE); } + notify_notification_show(notification, NULL); g_object_unref(notification); From f1f2f35132776be4f63c1420d5fe8bff81506769 Mon Sep 17 00:00:00 2001 From: M B <85039141+m-bartlett@users.noreply.github.com> Date: Mon, 19 Sep 2022 09:20:15 -0600 Subject: [PATCH 2/4] improve formatting, comments, entity names, and documentation --- README.md | 2 +- src/main.c | 43 ++++++++++++++++++++++++------------------- src/notification.c | 2 +- src/notification.h | 9 +++++++-- src/pulseaudio.c | 8 ++++---- src/pulseaudio.h | 6 +++--- 6 files changed, 40 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 82405e9..50986e4 100644 --- a/README.md +++ b/README.md @@ -289,7 +289,7 @@ This application supports reading the CSS colors for the SVG icon renderring fro For example the user may test this feature with: ```sh -xrdb -merge <(echo -e "pavol-dunst.primaryColor: #f00 \n pavol-dunst.primaryColor: #0ff") +xrdb -merge <(echo -e "pavol-dunst.primaryColor: #f00\npavol-dunst.secondaryColor: #0ff") ``` diff --git a/src/main.c b/src/main.c index e07e340..3c02629 100644 --- a/src/main.c +++ b/src/main.c @@ -1,9 +1,9 @@ #include #include -#include -#include // EXIT_* -#include // printf -#include // strcpy +#include // "true" +#include // EXIT_OK/EXIT_FAILURE +#include // printf +#include // strcpy #include "userdata.h" #include "notification.h" @@ -21,27 +21,32 @@ static int usage(char *argv[]) { "%s\n" " [-h|--help] - print this usage information and exit\n" " [-m|--mute] [ [1|\"on\"] | [0|\"off\"] | [-1|\"toggle\"] ]\n" - " mute audio if arg is \"1\" or \"on\"\n" - " unmute audio if arg is \"0\" or \"off\"\n" - " toggle audio muted if arg is \"-1\" or \"toggle\"\n" + " mute audio if arg is \"1\" or \"on\"\n" + " unmute audio if arg is \"0\" or \"off\"\n" + " toggle audio muted if arg is \"-1\" or \"toggle\"\n" " [-v|--volume] [+|-]VAL\n" - " if arg starts with \"+\" increase by VAL -> +5 is current volume + 5\n" - " if arg starts with \"-\" decrease by VAL -> -7 is current volume - 7\n" - " set absolute VAL if neither \"+\" or \"-\" are present -> 50 sets volume to 50\n" + " if arg starts with \"+\" increase by VAL -> +5 is current volume + 5\n" + " if arg starts with \"-\" decrease by VAL -> -7 is current volume - 7\n" + " set absolute VAL if neither \"+\" or \"-\" are present -> 50 sets volume to 50\n" " [-t|--timeout] MILLISECONDS - end volume notification after MILLISECONDS milliseconds.\n" " [-b|--body] BODY - set volume notification body to whatever string is provided as BODY.\n" " [-u|--unlock]\n" - " Forcibly unlock (or prevent the locking of) the shared-memory mutex lock that prevents concurrent instances of this process from running. \n" + " Forcibly unlock (or prevent the locking of) the shared-memory mutex lock that\n" + " prevents concurrent instances of this process from running.\n" " [-P|--primary-color] CSS_COLOR - set volume notification icon primary color.\n" - " If this arg is unset it will be read from the Xresources key %s or a default value.\n" + " If this arg is unset it will be read from the Xresources key %s or a default value.\n" " [-S|--secondary-color] CSS_COLOR - set volume notification icon secondary color.\n" - " If this arg is unset it will be read from the Xresources key %s or a default value.\n" + " If this arg is unset it will be read from the Xresources key %s or a default value.\n" " [-I|--icon-size] PIXELS - render volume notification icon size to be PIXELS pixels big.\n" + " -- [+|-]VAL\n" + " if stray positional arg starts with \"+\" increase by VAL -> +5 is current volume + 5\n" + " if stray positional arg starts with \"-\" decrease by VAL -> -7 is current volume - 7\n" + " set absolute VAL if neither \"+\" or \"-\" are present -> 50 sets volume to 50\n" , argv[0], XRESOURCE_KEY_ICON_PRIMARY_COLOR, XRESOURCE_KEY_ICON_SECONDARY_COLOR -); + ); exit(EXIT_FAILURE); } @@ -181,11 +186,11 @@ int main(int argc, char *argv[]) { } char *default_sink_name[256]; - wait_loop(pa_context_get_server_info(context, get_server_info_callback, &default_sink_name)); - wait_loop(pa_context_get_sink_info_by_name( context, - (char *) default_sink_name, - set_volume_callback, - &userdata ) ); + pa_wait_loop(pa_context_get_server_info(context, callback__get_server_info, &default_sink_name)); + pa_wait_loop(pa_context_get_sink_info_by_name(context, + (char *) default_sink_name, + callback__set_volume, + &userdata ));; display_volume_notification(&userdata); process_mutex_unlock(); diff --git a/src/notification.c b/src/notification.c index da38dc4..36e7c3e 100644 --- a/src/notification.c +++ b/src/notification.c @@ -5,7 +5,7 @@ #include "svg.h" const char NOTIFICATION_BODY_FORMAT[] = NOTIFICATION_LITERAL_BODY_FORMAT; -const int NOTIFICATION_BODY_FORMAT_SIZE = strlen(NOTIFICATION_BODY_FORMAT) - 2; +const int NOTIFICATION_BODY_FORMAT_SIZE = strlen(NOTIFICATION_BODY_FORMAT) - 2; const guint8* icon_bodies[] = { (guint8*) silent_svg_raw, (guint8*) low_svg_raw, diff --git a/src/notification.h b/src/notification.h index 6f9c00b..4c22950 100644 --- a/src/notification.h +++ b/src/notification.h @@ -6,7 +6,12 @@ #define NOTIFICATION_LITERAL_BODY_FORMAT "%s" #define NOTIFICATION_LITERAL_CATEGORY "volume" -#define NOTIFICATION_LITERAL_HINT_SYNCHRONOUS "synchronous" +#define NOTIFICATION_LITERAL_HINT_STACKTAG "synchronous" +/* Alternative stacktag values supported by dunst to replace an existing notification: + #define NOTIFICATION_LITERAL_HINT_STACKTAG "private-synchronous", + #define NOTIFICATION_LITERAL_HINT_STACKTAG "x-canonical-private-synchronous", + #define NOTIFICATION_LITERAL_HINT_STACKTAG "x-dunst-stack-tag", +*/ #define NOTIFICATION_LITERAL_HINT_VALUE "value" extern const char NOTIFICATION_BODY_FORMAT[]; @@ -14,4 +19,4 @@ extern const int NOTIFICATION_BODY_FORMAT_SIZE; void display_volume_notification(userdata_t *userdata); -#endif +#endif \ No newline at end of file diff --git a/src/pulseaudio.c b/src/pulseaudio.c index 06f419c..cf26435 100644 --- a/src/pulseaudio.c +++ b/src/pulseaudio.c @@ -36,7 +36,7 @@ int pulseaudio_quit(int new_pa_retval) { } -void wait_loop(pa_operation *op) { +void pa_wait_loop(pa_operation *op) { while (pa_operation_get_state(op) == PA_OPERATION_RUNNING) if (pa_mainloop_iterate(mainloop, 1, &pa_retval) < 0) break; pa_operation_unref(op); @@ -58,7 +58,7 @@ pa_volume_t denormalize(int volume) { } -void set_volume_callback( pa_context *context, +void callback__set_volume( pa_context *context, const pa_sink_info *sink_info, __attribute__((unused)) int eol, void *pulseaudio_userdata ) { @@ -84,7 +84,7 @@ void set_volume_callback( pa_context *context, break; } - // Turn muting off on any volume change, unless muting was specifically turned on or toggled. + // /* Turn muting off on any volume change, unless muting was specifically turned on or toggled. */ // if (!userdata->is_mute_on && !userdata->is_mute_toggle) // pa_context_set_sink_mute_by_index(context, sink_info->index, 0, NULL, NULL); @@ -113,7 +113,7 @@ void set_volume_callback( pa_context *context, pa_context_set_sink_volume_by_index(context, sink_info->index, new_cvolume, NULL, NULL); } -void get_server_info_callback( __attribute__((unused)) pa_context *context, +void callback__get_server_info( __attribute__((unused)) pa_context *context, const pa_server_info *sink_info, void *userdata ) { if (sink_info == NULL) return; diff --git a/src/pulseaudio.h b/src/pulseaudio.h index 4b5f120..4ce106c 100644 --- a/src/pulseaudio.h +++ b/src/pulseaudio.h @@ -18,7 +18,7 @@ int pulseaudio_init_context(pa_context *context, int pa_retval); int pulseaudio_quit(int new_pa_retval); -void wait_loop(pa_operation *op); +void pa_wait_loop(pa_operation *op); int constrain_volume(int volume); @@ -26,12 +26,12 @@ int normalize(pa_volume_t volume); pa_volume_t denormalize(int volume); -void set_volume_callback( pa_context *context, +void callback__set_volume( pa_context *context, const pa_sink_info *sink_info, __attribute__((unused)) int eol, void *userdata ); -void get_server_info_callback( __attribute__((unused)) pa_context *context, +void callback__get_server_info( __attribute__((unused)) pa_context *context, const pa_server_info *sink_info, void *userdata ); From f562e46d145c91e7c8563d971387de1b737bf3b7 Mon Sep 17 00:00:00 2001 From: M B <85039141+m-bartlett@users.noreply.github.com> Date: Mon, 19 Sep 2022 09:47:12 -0600 Subject: [PATCH 3/4] add and document workaround for synchronous notification icon caching --- README.md | 28 +++++++++++++++++++++++++++- src/main.c | 18 +++++++++--------- src/svg.c | 21 ++++++++++++++++++--- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 50986e4..6f59af9 100644 --- a/README.md +++ b/README.md @@ -300,4 +300,30 @@ If the process unexpectedly exits due to an unforeseen error, this single-proces ### Developer Notes -When scaling the rendered SVG to GdkPixBuf—especially in the context of creating an icon for libnotify—it seemed obvious to me that the appropriate function to reference the allocated pixbuf would be `rsvg_handle_get_pixbuf` using the RSVG handle containing the rendered graphic. However this function failed to produce a re-scaled image, i.e. the resulting icon was always whatever the intrinsic document scale was despite passing in differing viewbox values. As a workaround, I found that rendering to the cairo surface directly and then producing the pixbuf from the cairo surface was successful in producing a re-scaled image. In short, using the pixbuf from `gdk_pixbuf_get_from_surface` as the notification icon was sufficient to enable dynamic image scaling. This required linking the gdk main library. +- [List of all hints supported by dunst](https://dunst-project.org/documentation/#NOTIFY-SEND) + +- When scaling the rendered SVG to GdkPixBuf—especially in the context of creating an icon for libnotify—it seemed obvious to me that the appropriate function to reference the allocated pixbuf would be `rsvg_handle_get_pixbuf` using the RSVG handle containing the rendered graphic. However this function failed to produce a re-scaled image, i.e. the resulting icon was always whatever the intrinsic document scale was despite passing in differing viewbox values. As a workaround, I found that rendering to the cairo surface directly and then producing the pixbuf from the cairo surface was successful in producing a re-scaled image. In short, using the pixbuf from `gdk_pixbuf_get_from_surface` as the notification icon was sufficient to enable dynamic image scaling. This requires linking the gdk main library with `$ pkg-config --libs gdk-3.0`. + +- dunst 1.9.0 seems to now cache the icon image for synchronous notifications. This results in the notification icon not changing to reflect the volume magnitude visually if the process is executed again while the previous notification is still displayed despite having the image explicitly set. Recent releases of `pavol-dunst` add a workaround preventing this caching by first displaying a transparent image with the same dimensions and then updating the notification with the real image data with afterward. Caching the blank image should be easy for dunst, but the post-display update allows us to circumvent the notification caching and display the respective symbolic icon showing proportional volume level if `pavol-dunst` is executed in rapid succession + +- An alternative to `notify_notification_set_image_from_pixbuf` is using the `image-data` hint supported by `dunst`. This code was informed from the [test case for the `image-data` hint](https://github.com/dunst-project/dunst/blob/1280c9a9f20f46b24b08ebc99d29a788e5256a43/test/helpers.c#L7-L35): + + ```C + GVariant *hint_data = g_variant_new_from_data(G_VARIANT_TYPE("ay"), + gdk_pixbuf_read_pixels(pixbuf), + gdk_pixbuf_get_byte_length(pixbuf), + TRUE, + (GDestroyNotify) g_object_unref, + g_object_ref(pixbuf)); + GVariant *hint = g_variant_new( + "(iiibii@ay)", + gdk_pixbuf_get_width(pixbuf), + gdk_pixbuf_get_height(pixbuf), + gdk_pixbuf_get_rowstride(pixbuf), + gdk_pixbuf_get_has_alpha(pixbuf), + gdk_pixbuf_get_bits_per_sample(pixbuf), + gdk_pixbuf_get_n_channels(pixbuf), + hint_data); + + notify_notification_set_hint(notification, "image-data", hint); + ``` diff --git a/src/main.c b/src/main.c index 3c02629..8c95c30 100644 --- a/src/main.c +++ b/src/main.c @@ -62,15 +62,15 @@ static bool parse_volume_argument(char *optarg, userdata_t *userdata) { int main(int argc, char *argv[]) { - userdata_t userdata = { .volume = -1, - .new_volume = -1, - .volume_delta = false, - .mute = MUTE_UNKNOWN, - .notification_timeout = DEFAULT_NOTIFICATION_TIMEOUT, - .notification_body = (char*)"", - .icon_primary_color = NULL, - .icon_secondary_color = NULL, - .icon_size = DEFAULT_ICON_SIZE }; + userdata_t userdata = {.volume = -1, + .new_volume = -1, + .volume_delta = false, + .mute = MUTE_UNKNOWN, + .notification_timeout = DEFAULT_NOTIFICATION_TIMEOUT, + .notification_body = (char*)"", + .icon_primary_color = NULL, + .icon_secondary_color = NULL, + .icon_size = DEFAULT_ICON_SIZE}; bool process_mutex = true; diff --git a/src/svg.c b/src/svg.c index 72ec93e..7ecdeb3 100644 --- a/src/svg.c +++ b/src/svg.c @@ -40,14 +40,29 @@ bool render_notification_icon(NotifyNotification* notification, cairo_surface_flush(surface); - // GdkPixbuf* pixbuf = rsvg_handle_get_pixbuf(rsvg_handle); // <- this doesn't scale image + // GdkPixbuf* pixbuf = rsvg_handle_get_pixbuf(rsvg_handle); // <- this doesn't scale the image GdkPixbuf* pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, icon_size, icon_size); + /* : dunst wants to cache the icon image for synchronous notifications. This results in the notification icon + not changing to reflect the volume magnitude visually if the process is executed again while the previous + notification is still displayed. This workaround prevents this caching by first displaying a transparent + image with the same dimensions and then updating the notification with the real image data with afterward. + */ + GdkPixbuf* placeholder = gdk_pixbuf_new(GDK_COLORSPACE_RGB, + /*has_alpha*/ true, + gdk_pixbuf_get_bits_per_sample(pixbuf), + /*width*/ icon_size, + /*height*/ icon_size); + notify_notification_set_image_from_pixbuf(notification, placeholder); + notify_notification_show(notification, NULL); notify_notification_set_image_from_pixbuf(notification, pixbuf); + /* */ - cairo_destroy (cairo); - cairo_surface_destroy (surface); + + cairo_destroy(cairo); + cairo_surface_destroy(surface); g_object_unref(rsvg_handle); + g_object_unref(placeholder); g_object_unref(pixbuf); return true; } \ No newline at end of file From 34fc8c2d2abcea76ed7ba08192f0edbd6200762a Mon Sep 17 00:00:00 2001 From: M B <85039141+m-bartlett@users.noreply.github.com> Date: Tue, 20 Sep 2022 12:05:18 -0600 Subject: [PATCH 4/4] typo fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f59af9..faed8a1 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ sections below. ## About ### Optional Features -There are certain features which can be enabled at compile-time. These are features that would would add overhead to the generation and display of the notification if they were specified with a boolean CLI flag, and are generally things that users would either he interested in having all the time or never. The feature inclusion is ultimately decided by the preprocessor, but I've added some logic in the `Makefile` to make it simpler for the user to specify the inclusion of these optional features. +There are certain features which can be enabled at compile-time. These are features that would would add overhead to the generation and display of the notification if they were specified with a boolean CLI flag, and are generally things that users would either be interested in having all the time or never. The feature inclusion is ultimately decided by the preprocessor, but I've added some logic in the `Makefile` to make it simpler for the user to specify the inclusion of these optional features. From the table below, export the **Feature name variable** value as a non-empty value to `make`. Note that specifying a feature variable *after* building will not indicate to `make` that it needs to rebuild; one should use the `-B` flag to force `make` to rebuild.