Custom Notifications with 'gdbus call' + 'wired-notify'
June 9, 2022
-- guides
- linux
I wanted to write this short guide on the functionality of gdbus call as I couldn’t find any resources online regarding it besides its own documentation. I’ll also quickly cover my wired-notify config, since it’s a really neat notification daemon that I don’t think is nearly popular enough :)
Here’s the reason I went through this trouble, first off: a tiny configurable volume HUD that live-updates in place as you change the device volume.
Although gdbus call has many use-cases, the one I’ll focus on here is sending custom notifications, as notify-send doesn’t expose many of the properties I needed to set. notify-send, along with other notification frameworks I looked at, didn’t support updating existing notifications using identifiers. gdbus call does allow this granular control, along with exposing some other cool properties.
Syntax
gdbus call
--session
--dest org.freedesktop.Notifications
--object-path /org/freedesktop/Notifications
--method org.freedesktop.Notifications.Notify
--
"identifier"
"1"
""
"Notification title"
"Notification description"
"[]"
"{}"
"4000" Running this will return (uint32 1,), the ‘1’ representing the notification’s unique identifier. This is useful if you want to parse that identifier and re-use it later; one way to do so would be using sed (e.g., sed 's/[^ ]* //; s/,.//' would strip everything but the identifier).
A few other tips:
--sessionrepresents the user’ssystemd --userinstance--dest,--object-path, and--methodin this case are all used to point tofreedesktop.notifications.notify, and the following parameters are specified in freedesktop’s documentation- The ‘1’ represents the identifier, and the replaces ID
- The following blank string is the notification icon
- The following two strings are for the Summary (notification title) and Body (multi-line description)
- The empty array is for actions, for ex, sending a callback to the application when clicked
- The empty dictionary is for hints; these are used to send extra data such as (in my use case) the current volume level, an image, extra text, etc
- The ‘4000’ represents the expiration timeout in ms. If set to -1, it falls back to the notification server’s default
Body text
The body text can, in most cases, contain limited markup:
<b>...</b>for bold,<i>...</i>for italics,<u>...</u>for underlines,<a href="...">...</a>for links, and<img src="..." alt="...">for images
Providing hints
Hints are provided as a dictionary, using the following format:
{'Name': <'type':'value'>}
So, for example, sending a hint for the integer value of my volume notification looks like:
{'value': <58>}
And for providing a file path for an image preview:
{'image-path': <'file:///dir/image.jpg'}
Note that when providing the dictionary in the command, it should be encased by double-quotes.
That’s all there is to it! I hope this comes in handy for someone at some point :)
More configs
Finally, if you’re curious, here’s my wired-notify config, as well as the simple script I call on volume update to send a notification to it:
(
max_notifications: 4,
timeout: 10000,
poll_interval: 16,
history_length: 20,
replacing_enabled: true,
replacing_resets_timeout: true,
min_window_width: 300,
debug: false,
debug_color: Color(r: 0.0, g: 1.0, b: 0.0, a: 1.0),
debug_color_alt: Color(r: 1.0, g: 0.0, b: 0.0, a: 1.0),
layout_blocks: [
(
name: "root",
parent: "",
hook: Hook(parent_anchor: TM, self_anchor: TM),
offset: Vec2(x: 0.0, y: 40.0),
render_anti_criteria: [Progress],
params: NotificationBlock((
monitor: 0,
border_width: 0,
border_rounding: 8,
background_color: Color(hex: "#99000000"),
border_color: Color(hex: "#99000000"),
border_color_low: Color(hex: "#99000000"),
border_color_critical: Color(hex: "#99000000"),
border_color_paused: Color(hex: "#99000000"),
gap: Vec2(x: 0.0, y: 12.0),
notification_hook: Hook(parent_anchor: BL, self_anchor: TL),
)),
),
(
name: "image",
parent: "root",
hook: Hook(parent_anchor: TL, self_anchor: TL),
offset: Vec2(x: 0.0, y: 0.0),
params: ImageBlock((
image_type: Hint,
padding: Padding(left: 6.0, right: 2.0, top: 6.0, bottom: 6.0),
rounding: 6.0,
scale_width: 64,
scale_height: 64,
filter_mode: Lanczos3,
)),
),
(
name: "summary",
parent: "image",
hook: Hook(parent_anchor: MR, self_anchor: BL),
offset: Vec2(x: 0.0, y: 0.0),
params: TextBlock((
text: "%s",
font: "Arial Bold 11",
ellipsize: End,
color: Color(hex: "#F3F3F3"),
color_hovered: Color(hex: "#DEDEDE"),
padding: Padding(left: 8.0, right: 8.0, top: 6.0, bottom: 2.0),
dimensions: (width: (min: 280, max: 280), height: (min: 0, max: 0)),
)),
),
(
name: "body",
parent: "summary",
hook: Hook(parent_anchor: BL, self_anchor: TL),
offset: Vec2(x: 0.0, y: -3.0),
params: ScrollingTextBlock((
text: "%b",
font: "Arial 11",
color: Color(hex: "#F3F3F3"),
color_hovered: Color(hex: "#DEDEDE"),
padding: Padding(left: 8.0, right: 8.0, top: 2.0, bottom: 6.0),
width: (min: 150, max: 250),
scroll_speed: 0.1,
lhs_dist: 35.0,
rhs_dist: 35.0,
scroll_t: 1.0,
)),
),
(
name: "rootProgress",
parent: "",
hook: Hook(parent_anchor: MM, self_anchor: MM),
offset: Vec2(x: 0.0, y: 0.0),
render_criteria: [Progress],
params: NotificationBlock((
monitor: 0,
border_width: 0,
border_rounding: 8,
background_color: Color(hex: "#99000000"),
border_color: Color(hex: "#99000000"),
gap: Vec2(x: 0.0, y: 12.0),
notification_hook: Hook(parent_anchor: BL, self_anchor: TL),
)),
),
(
name: "progressBlock",
parent: "rootProgress",
hook: Hook(parent_anchor: TM, self_anchor: TM),
offset: Vec2(x: 0.0, y: 0.0),
params: ProgressBlock((
padding: Padding(left: 12.0, right: 12.0, top: 12.0, bottom: 12.0),
border_width: 0.0,
border_rounding: 10.0,
fill_rounding: 10.0,
border_color: Color(hex: "#F3F3F3"),
background_color: Color(hex: "#99000000"),
fill_color: Color(hex: "#F3F3F3"),
width: 276.0,
height: 20.0,
border_color_hovered: Color(hex: "#F3F3F3"),
background_color_hovered: Color(hex: "#99000000"),
fill_color_hovered: Color(hex: "#F3F3F3"),
)),
),
(
name: "progressSummary",
parent: "progressBlock",
hook: Hook(parent_anchor: TM, self_anchor: BM),
offset: Vec2(x: 0.0, y: 0.0),
params: TextBlock((
text: "%s",
ellipsize: NoEllipsize,
font: "Arial Bold 11",
color: Color(hex: "#F3F3F3"),
color_hovered: Color(hex: "#DEDEDE"),
padding: Padding(left: 0.0, right: 0.0, top: 6.0, bottom: 12.0),
dimensions: (width: (min: 0, max: 300), height: (min: 0, max: 0)),
)),
),
(
name: "progressBody",
parent: "progressBlock",
hook: Hook(parent_anchor: BM, self_anchor: TM),
offset: Vec2(x: 0.0, y: 0.0),
params: TextBlock((
text: "%b",
font: "Arial 11",
ellipsize: NoEllipsize,
color: Color(hex: "#F3F3F3"),
color_hovered: Color(hex: "#DEDEDE"),
padding: Padding(left: 0.0, right: 0.0, top: 12.0, bottom: 6.0),
dimensions: (width: (min: 0, max: 300), height: (min: 0, max: 0)),
)),
),
],
) #!/usr/bin/env perl
## The sink should be the default
my $sink = `pactl get-default-sink`;
## Run pactl and store output
open(my $fh, '-|', 'pactl list sinks');
## Set the record separator to consecutive newlines (same as -000)
$/="\n\n";
while (<$fh>) {
if (/Name:.$sink/) {
## Extract the current volume
/Volume:.*?(d+)%/;
my $volume=$1;
## Send the notification, using gdbus so we can specify the
## ID to update on repeat
exec "gdbus call --session --dest org.freedesktop.Notifications --object-path /org/freedesktop/Notifications --method org.freedesktop.Notifications.Notify 'identifier' '999' '' 'Volume' '$volume%' '[]' '{"value": <$volume>}' '4000'";
}
}