Skip to content

Instantly share code, notes, and snippets.

@martin5211
Created July 24, 2026 16:27
Show Gist options
  • Select an option

  • Save martin5211/eb7783f079c34d5a5d97ee4980136d1c to your computer and use it in GitHub Desktop.

Select an option

Save martin5211/eb7783f079c34d5a5d97ee4980136d1c to your computer and use it in GitHub Desktop.
Notifications for Waydroid in WSL

Create the following file ~/.local/bin/wsl-toast-daemon:

#!/usr/bin/env python3
# wsl-toast-daemon: implements org.freedesktop.Notifications on the session bus
# and forwards each notification as a Windows toast (via powershell.exe WinRT).
# Waydroid's session auto-detects this service at start and forwards Android
# notifications to it. Must run in a WSL session with Windows interop alive.
import os
import subprocess
import threading
import time

import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib

PS = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"

# PowerShell's registered AppUserModelID — toasts from unregistered ids are dropped
PS_SCRIPT = r"""
$t=[System.Security.SecurityElement]::Escape($env:TOAST_TITLE)
$b=[System.Security.SecurityElement]::Escape($env:TOAST_BODY)
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType=WindowsRuntime] | Out-Null
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml("<toast><visual><binding template='ToastGeneric'><text>$t</text><text>$b</text></binding></visual></toast>")
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe').Show($toast)
"""


def show_toast(title, body):
    env = dict(os.environ)
    env["TOAST_TITLE"] = title[:100] or "Waydroid"
    env["TOAST_BODY"] = body[:400]
    env["WSLENV"] = (env.get("WSLENV", "") + ":TOAST_TITLE/w:TOAST_BODY/w").lstrip(":")
    subprocess.run(
        [PS, "-NoProfile", "-NonInteractive", "-Command", PS_SCRIPT],
        env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30,
    )


class NotificationService(dbus.service.Object):
    IFACE = "org.freedesktop.Notifications"

    def __init__(self, bus):
        name = dbus.service.BusName(self.IFACE, bus)
        super().__init__(name, "/org/freedesktop/Notifications")
        self.counter = 0
        self.last_toast = {}

    @dbus.service.method(IFACE, in_signature="susssasa{sv}i", out_signature="u")
    def Notify(self, app_name, replaces_id, app_icon, summary, body, actions, hints, timeout):
        self.counter += 1
        app, title, text = str(app_name), str(summary), str(body)
        decision = "toast"
        if not title and not text:
            # Empty = either a group-summary echo (arrives right after a real
            # toast — suppress) or a custom-layout notification that carries no
            # extractable text
            if time.monotonic() - self.last_toast.get(app, 0) < 6:
                decision = "skip-echo"
            else:
                title, text = app, "New notification"
                decision = "toast-generic"
        with open("/tmp/toast-daemon.log", "a") as f:
            f.write(f"app={app_name!r} summary={summary!r} body={body!r} -> {decision}\n")
        if decision == "skip-echo":
            return self.counter
        self.last_toast[app] = time.monotonic()
        threading.Thread(
            target=show_toast, args=(title or app, text), daemon=True
        ).start()
        return self.counter

    @dbus.service.method(IFACE, in_signature="u")
    def CloseNotification(self, notification_id):
        pass

    @dbus.service.method(IFACE, out_signature="as")
    def GetCapabilities(self):
        return ["body"]

    @dbus.service.method(IFACE, out_signature="ssss")
    def GetServerInformation(self):
        return ("wsl-toast-daemon", pwd.getpwuid(os.getuid()).pw_name, "1.0", "1.2")

    @dbus.service.signal(IFACE, signature="uu")
    def NotificationClosed(self, notification_id, reason):
        pass

    @dbus.service.signal(IFACE, signature="us")
    def ActionInvoked(self, notification_id, action_key):
        pass


if __name__ == "__main__":
    DBusGMainLoop(set_as_default=True)
    NotificationService(dbus.SessionBus())
    GLib.MainLoop().run()

Make it executable chmod +x ~/.local/bin/wsl-toast-daemon.

Create this other executable script launcher in /.local/bin/waydroid-up, and make sure it is in the PATH:

#!/bin/sh
# Daily Waydroid launcher: Windows toast notifications + session.
# Run from an interactive terminal and keep it open (Windows interop lives here).
pgrep -f wsl-toast-daemon >/dev/null || "$HOME/.local/bin/wsl-toast-daemon" &
exec waydroid session start

From now instead of using waydroid session start use waydroid-up which will launch the wsl-toast-daemon automatically on each session.

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