Skip to content

Dunst Notification has a href tag url for web notifications #2030

Description

@justkelvin
Image

This here, can anyone tell me why, started happening after installing the latest version

Activity

  1. Delcado19 commented on Sep 6, 2026

    @Delcado19
    Contributor

    Checked this against the shipped dunst config. This looks like a dunst limitation rather than a HyDE bug: markup = full is set in dunstrc, and per dunst's own documentation that mode only supports <b>, <i>, <s>, <u> — it does not support <a href=""> hyperlinks. The notification body in the screenshot contains an <a href="...">web.whatsapp.com</a> tag sent by the browser (looks like a web-push notification for WhatsApp Web), which Pango can't parse as valid markup under full, so it falls back to showing the raw tag text.

    HyDE ships dunst's own default markup behavior here unmodified, so this isn't something introduced by the latest HyDE release — more likely you just started noticing it once a browser notification happened to include a link. A workaround would be an app-specific match rule (e.g. markup = strip scoped to the browser's notifications) rather than a global fix, since stripping markup globally would also kill bold/italic for other apps.

  2. justkelvin commented on Sep 6, 2026

    @justkelvin
    Author

    Checked this against the shipped dunst config. This looks like a dunst limitation rather than a HyDE bug: markup = full is set in dunstrc, and per dunst's own documentation that mode only supports <b>, <i>, <s>, <u> — it does not support <a href=""> hyperlinks. The notification body in the screenshot contains an <a href="...">web.whatsapp.com</a> tag sent by the browser (looks like a web-push notification for WhatsApp Web), which Pango can't parse as valid markup under full, so it falls back to showing the raw tag text.

    HyDE ships dunst's own default markup behavior here unmodified, so this isn't something introduced by the latest HyDE release — more likely you just started noticing it once a browser notification happened to include a link. A workaround would be an app-specific match rule (e.g. markup = strip scoped to the browser's notifications) rather than a global fix, since stripping markup globally would also kill bold/italic for other apps.

    Not really, it has been working well for the past a year or so I have used HyDE, before I did the latest update like 2 weeks ago I started seen it that way.

  3. Delcado19 commented on Sep 6, 2026

    @Delcado19
    Contributor

    Fair pushback — the markup limitation itself is real (and, as far as I can trace in the shipped dunstrc, hasn't changed), but that alone doesn't explain why your own experience changed a couple weeks ago after a year of it working fine. That timing points at something else shifting, not just "you finally hit a pre-existing limit."

    A few things that could explain it without being a dunst limitation at all:

    • A dunst package update on your system (upstream markup parsing behavior could differ between versions)
    • The sender itself changed — is this actually the browser (web push for WhatsApp Web), or could it be a different app/wrapper that started including a link in the body around the same time?
    • Something in your own dunstrc/wallbash overrides changed markup away from full at some point and then back, or a different dunstrc is now being picked up than before

    If you can share dunst --version and roughly what update you did 2 weeks ago (HyDE update, system update, or both), that would narrow it down a lot.

  4. justkelvin commented on Sep 6, 2026

    @justkelvin
    Author

    Fair pushback — the markup limitation itself is real (and, as far as I can trace in the shipped dunstrc, hasn't changed), but that alone doesn't explain why your own experience changed a couple weeks ago after a year of it working fine. That timing points at something else shifting, not just "you finally hit a pre-existing limit."

    A few things that could explain it without being a dunst limitation at all:

    • A dunst package update on your system (upstream markup parsing behavior could differ between versions)
    • The sender itself changed — is this actually the browser (web push for WhatsApp Web), or could it be a different app/wrapper that started including a link in the body around the same time?
    • Something in your own dunstrc/wallbash overrides changed markup away from full at some point and then back, or a different dunstrc is now being picked up than before

    If you can share dunst --version and roughly what update you did 2 weeks ago (HyDE update, system update, or both), that would narrow it down a lot.

    Image

    something with dust is broken, also notification such as power gets spammed i saw a similar issue which hints to breaking the parsing done dunst.

  5. kRHYME7 commented on Sep 6, 2026

    @kRHYME7
    Contributor

    Swaync got this too. But it depends on what application is notifying. I want to set expectations that not all apps follows proper standards on dbus so some provide weird outputs.
    I think there is solution in swaync where you can use scripts to parse it , never tried it.

    For dunst, I will try to use it for a week and see what client apps are acting up.

    Can you list the clients that output those href tags? Mine is viber and kdeconnect (seldom).

  6. justkelvin commented on Sep 7, 2026

    @justkelvin
    Author

    After a lot of debugging, here is what I found.

    Both SwayNC and Dunst appear to be installed on my system.

    I did a clean reinstall of HyDE on August 25 and only ran:

    ./install.sh

    For some reason, this resulted in both notification daemons being installed. I'm not sure whether this is intentional behavior, but it looks like the installer also pulled in the extra configuration containing SwayNC.

    From the HyDE configuration:

    • core.toml (line 22) includes ../dots/dunst.toml

      • Installs dunst
      • Sets up ~/.config/dunst
    • extra.toml (line 24) includes ../dots/swaync.toml

      • Installs swaync
      • Sets up ~/.config/swaync

    However, HyDE's startup configuration appears to explicitly use Dunst, not SwayNC.

    In variables.lua (line 57):

    hc.start.notifications = "hyde-shell app -u " .. unt .. "-notifications.service -t " .. svc .. " -- dunst"

    So initially, Dunst was running normally.

    What happened

    Dunst appears to have worked for about a day before crashing.

    At 22:54:41 on August 26, /usr/bin/dunst crashed with a segmentation fault (SIGSEGV, exit status 11):

    systemd[925]: hyde-Hyprland-notifications.service: Main process exited, code=dumped, status=11/SEGV
    systemd[925]: hyde-Hyprland-notifications.service: Failed with result 'core-dump'.
    

    After Dunst crashed, the D-Bus interface:

    org.freedesktop.Notifications
    

    became available.

    SwayNC had apparently been trying to start in the background, and once Dunst released the notification interface, SwayNC acquired it:

    systemd[925]: Started Swaync notification daemon.
    

    From that point onward, SwayNC became the active notification daemon, including for notifications from applications such as Chrome.

    I'm still not sure why Dunst did not take over again after subsequent reboots, since I have rebooted the machine multiple times since then.

    For now, I stopped SwayNC and restarted Dunst manually, and notifications are once again displaying the way HyDE appears to intend.

    Original / Dunst

    Dunst notifications

    When SwayNC was running

    SwayNC notifications
  7. Delcado19 commented on Sep 7, 2026

    @Delcado19
    Contributor

    One more data point, on top of justkelvin's dbus/journalctl findings above: I checked the actual shipped service definitions on my own system, and both notification daemons declare the same D-Bus name:

    /usr/share/dbus-1/services/org.knopwob.dunst.service:
      Name=org.freedesktop.Notifications
    
    /usr/share/dbus-1/services/org.erikreider.swaync.service:
      Name=org.freedesktop.Notifications
    

    That's a genuine collision, not app-specific weirdness — whichever of the two currently owns the name is the one that ends up handling every notification, dunst or not. And Scripts/install.sh:459-465 deploys core.toml (dunst) and extra.toml (swaync) unconditionally on a plain ./install.sh, no prompt either way, so both packages and both service files land on every install.

    On why it stays on swaync afterward rather than switching back on reboot: swaync's packaged unit has Restart=on-failure and WantedBy=graphical-session.target, so once it's running it's set up to persist and restart itself. I haven't verified which of the two colliding service files dbus-daemon actually prefers when the name is unclaimed and both are candidates — that part's still open.

    Same root cause either way: two notification daemons shipped and installed together with no exclusivity between them, even though only one (dunst) is what HyDE's own startup config actually launches.

  8. justkelvin commented on Sep 7, 2026

    @justkelvin
    Author

    Here is the battery spam bug @kRHYME7 this will help.


    Why Dunst Spams Notifications at 5% Battery

    The spam is caused by a chain of three interacting bugs between batterynotify.lua and luautils/global/notify.lua:

    1. The 1-Second Countdown Loop (batterynotify.lua:214-228)
      When the battery drops to 5% (conf.battery_critical_threshold), start_critical_countdown() starts a GLib timer that fires every 1 second for 120 seconds (conf.timer). On every single tick, it calls notify_send('Battery Critically Low', ...) to show the remaining time.

    2. Function Signature Mismatch (notify.lua:35-41)
      notify.lua expects a options table:

      function M.send(summary, body, opts)

      However, batterynotify.lua calls it with positional arguments:

      notify_send('Battery Critically Low', message, 'critical', 'xfce4-battery-critical')

      Because argument 3 is the string 'critical' instead of a table:

      • opts.replace_id evaluates to nil, which defaulted to 0.
      • opts.urgency evaluates to nil, which defaulted to 'normal'.
      • The icon argument was completely dropped.
    3. The Unconditional -r 0 Flag (notify.lua:48-51)
      notify.lua built the command:

      notify-send -a "HyDE Power" -t 5000 -r %d ...

      Because replace_id was 0, it executed notify-send ... -r 0. Under the Desktop Notifications Specification and libnotify, replace_id = 0 means "no replacement".
      Instead of updating the existing popup in place, the notification daemon assigned a new ID and spawned a brand-new notification window every second, producing up to 120 stacked popups flooding your display.

    4. Countdown Notification Stuck on Plug-in (batterynotify.lua:253-263)
      When the charger is plugged in, cancel_critical_countdown() cancelled the timer but never closed or dismissed the critical notification window, leaving the frozen popup stuck on screen.


    What Needs to Change

    1. In luautils/global/notify.lua

    • Support both table and positional arguments so calls like notify.send(summary, body, 'critical', icon, timeout, replace_id) work seamlessly alongside table options { urgency = '...', replace_id = ... }.
    • Only pass -r <id> when replace_id > 0. Never pass -r 0.
    • Normalize urgency with :lower() so 'CRITICAL' and 'NORMAL' don't fail.
    • Add a close(replace_id) function using D-Bus (org.freedesktop.Notifications.CloseNotification) so persistent notifications can be dismissed programmatically.

    2. In batterynotify.lua

    • Define a dedicated notification ID for battery alerts (e.g., local BATTERY_NOTIFY_ID = 9991).
    • Pass replace_id = BATTERY_NOTIFY_ID to the critical countdown notification:
      • This updates the single existing popup in place as a live countdown clock without spawning extra windows.
    • Call notify_mod.close(BATTERY_NOTIFY_ID) inside cancel_critical_countdown() so the critical popup disappears as soon as you plug the charger in.
    • Use replacement IDs for Battery Low as well so multiple low-battery warnings replace each other instead of piling up.

    Proposed Diffs

    In luautils/global/notify.lua:

    --- a/Configs/.local/lib/hyde/luautils/global/notify.lua
    +++ b/Configs/.local/lib/hyde/luautils/global/notify.lua
    @@ -32,21 +32,45 @@ local function has_notify_send()
         return notify_send_available
     end
     
    --- Primary send function
    -function M.send(summary, body, opts)
    -    opts = opts or {}
    -    local urgency = opts.urgency or 'normal'
    -    local icon = opts.icon or ''
    -    local timeout = tonumber(opts.timeout) or 5000
    -    local replace_id = tonumber(opts.replace_id) or 0
    +-- Close notification by replace_id via DBus
    +function M.close(replace_id)
    +    local id = tonumber(replace_id)
    +    if not id or id <= 0 then return end
    +    os.execute(string.format(
    +        'gdbus call --session --dest org.freedesktop.Notifications --object-path /org/freedesktop/Notifications --method org.freedesktop.Notifications.CloseNotification %d >/dev/null 2>&1 &',
    +        id
    +    ))
    +end
    +
    +-- Primary send function: supports both table options and positional arguments
    +function M.send(summary, body, opts, icon_arg, timeout_arg, replace_id_arg)
    +    local urgency = 'normal'
    +    local icon = ''
    +    local timeout = 5000
    +    local replace_id = nil
    +    local app_name = 'HyDE Power'
    +
    +    if type(opts) == 'table' then
    +        urgency = opts.urgency or urgency
    +        icon = opts.icon or icon
    +        timeout = tonumber(opts.timeout) or timeout
    +        replace_id = tonumber(opts.replace_id)
    +        app_name = opts.app_name or opts.app or app_name
    +    elseif type(opts) == 'string' or opts == nil then
    +        urgency = opts or urgency
    +        icon = icon_arg or icon
    +        timeout = tonumber(timeout_arg) or timeout
    +        replace_id = tonumber(replace_id_arg)
    +    end
    +    urgency = tostring(urgency):lower()
     
         -- Call notify-send in background with & so we don't block.
         if has_notify_send() then
             local icon_flag = ''
             if icon ~= '' then
                 icon_flag = ' -i ' .. sh_quote(icon)
             end
    -        local cmd = string.format('notify-send -a "HyDE Power" -t %d -r %d -u %s%s %s %s >/dev/null 2>&1 &',
    -            timeout, replace_id, tostring(urgency), icon_flag, sh_quote(summary or ''), sh_quote(body or ''))
    +        local replace_flag = (replace_id and replace_id > 0) and string.format(' -r %d', replace_id) or ''
    +        local cmd = string.format('notify-send -a %s -t %d%s -u %s%s %s %s >/dev/null 2>&1 &',
    +            sh_quote(app_name), timeout, replace_flag, urgency, icon_flag, sh_quote(summary or ''), sh_quote(body or ''))

    In batterynotify.lua:

    --- a/Configs/.local/lib/hyde/batterynotify.lua
    +++ b/Configs/.local/lib/hyde/batterynotify.lua
    @@ -196,6 +196,8 @@ end
     
    +local BATTERY_CRITICAL_NOTIFY_ID = 9991
    +local BATTERY_LOW_NOTIFY_ID = 9992
    +
     -- state ------------------------------------------------------------------
     local verbose = false
     local last_notified_percentage = 0
    @@ -219,8 +221,8 @@ local function start_critical_countdown()
                 local mm = math.floor(crit_remaining / 60); local ss = crit_remaining % 60
                 notify_send('Battery Critically Low',
                     string.format('%d%% is critically low. Device will execute %s in %d:%02d.', pct, conf.execute_critical,
    -                    mm, ss), 'critical', 'xfce4-battery-critical')
    +                    mm, ss), 'critical', 'xfce4-battery-critical', 5000, BATTERY_CRITICAL_NOTIFY_ID)
                 crit_remaining = crit_remaining - 1
    @@ -253,6 +255,7 @@ end
     
     local function cancel_critical_countdown()
    +    notify_mod.close(BATTERY_CRITICAL_NOTIFY_ID)
         if not crit_source then return end
         if GLib and type(crit_source) == 'number' then
             GLib.source_remove(crit_source); crit_source = nil; return

    The Bug
    Image

    When Charger Plugged in (Notice the low battery notification is never cleared):
    Image

    Fix with the above diff applied only one notification appears now:
    Image

    When chrager plugged we just update that same notifcation without popping a new one:
    Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions