Created
March 29, 2026 17:20
-
-
Save BohdanTkachenko/e62ec1c95cd4d6430914f3339e8d9594 to your computer and use it in GitHub Desktop.
Manual test app for Electron notification activation token on Wayland
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const { app, BrowserWindow, Notification, ipcMain } = require("electron"); | |
| const dbus = require("dbus-next"); | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| app.commandLine.appendSwitch("ozone-platform", "wayland"); | |
| app.commandLine.appendSwitch("enable-features", "WaylandWindowDecorations,UseOzonePlatform"); | |
| let win; | |
| let notifIface; | |
| async function initDBus() { | |
| const bus = dbus.sessionBus(); | |
| const obj = await bus.getProxyObject("org.freedesktop.Notifications", "/org/freedesktop/Notifications"); | |
| notifIface = obj.getInterface("org.freedesktop.Notifications"); | |
| // Listen for ActivationToken (FDN spec 1.2) -sent BEFORE ActionInvoked | |
| notifIface.on("ActivationToken", (id, token) => { | |
| log(`🔑 ActivationToken: id=${id} token="${token}"`); | |
| // Use our patched API to pass the token to Chromium's Wayland layer | |
| if (app.setActivationToken) { | |
| app.setActivationToken(token); | |
| log(`✅ Token set via app.setActivationToken()`); | |
| } else { | |
| log(`⚠️ app.setActivationToken not available, falling back to env var`); | |
| process.env.XDG_ACTIVATION_TOKEN = token; | |
| } | |
| }); | |
| notifIface.on("ActionInvoked", (id, action) => { | |
| log(`🔔 ActionInvoked: id=${id} action="${action}"`); | |
| if (action === "default") { | |
| if (win) { | |
| win.show(); | |
| win.focus(); | |
| } | |
| } else { | |
| log(`📋 Background action: "${action}" -not focusing window`); | |
| } | |
| }); | |
| notifIface.on("NotificationClosed", (id, reason) => { | |
| const reasons = { 1: "expired", 2: "dismissed", 3: "closed-by-app", 4: "undefined" }; | |
| log(`🔕 Closed: id=${id} reason=${reasons[reason] || reason}`); | |
| }); | |
| } | |
| async function sendNotification(title, body, actions) { | |
| const { Variant } = dbus; | |
| const id = await notifIface.Notify( | |
| "Notif Test", 0, "dialog-information", title, body, | |
| actions, | |
| { "desktop-entry": new Variant("s", "notif-test") }, | |
| -1 | |
| ); | |
| return id; | |
| } | |
| function log(msg) { | |
| console.log(msg); | |
| win?.webContents.send("log", msg); | |
| } | |
| async function fire(label, actions) { | |
| log(`⏳ "${label}" firing in 1s...`); | |
| setTimeout(async () => { | |
| try { | |
| const id = await sendNotification(label, `Actions: [${actions.join(", ")}]`, actions); | |
| log(`✅ "${label}" sent, id=${id}`); | |
| } catch (err) { | |
| log(`❌ "${label}" error: ${err.message}`); | |
| } | |
| }, 1000); | |
| } | |
| function fireNative(label, actions) { | |
| log(`⏳ [Native] "${label}" firing in 1s...`); | |
| setTimeout(() => { | |
| const notif = new Notification({ | |
| title: label, | |
| body: `Electron native notification (actions: ${actions.length})`, | |
| actions, | |
| }); | |
| notif.on("click", () => { | |
| log(`🔔 [Native] click on "${label}"`); | |
| if (win) { | |
| win.show(); | |
| win.focus(); | |
| } | |
| }); | |
| notif.on("action", (_e, idx) => { | |
| log(`📋 [Native] action ${idx} on "${label}"`); | |
| }); | |
| notif.on("close", () => { | |
| log(`🔕 [Native] closed "${label}"`); | |
| }); | |
| notif.show(); | |
| log(`✅ [Native] "${label}" sent`); | |
| }, 1000); | |
| } | |
| function getDesktopFilePath() { | |
| return path.join(process.env.HOME, ".local/share/applications/notif-test.desktop"); | |
| } | |
| function updateDesktopStatus() { | |
| const exists = fs.existsSync(getDesktopFilePath()); | |
| win?.webContents.send("desktop-status", exists); | |
| } | |
| function installDesktopFile() { | |
| const desktopPath = getDesktopFilePath(); | |
| fs.mkdirSync(path.dirname(desktopPath), { recursive: true }); | |
| const content = `[Desktop Entry] | |
| Name=Notif Test | |
| Exec=${process.execPath} --no-sandbox ${path.resolve(__dirname, "main.js")} | |
| Type=Application | |
| StartupNotify=true | |
| StartupWMClass=notif-test | |
| `; | |
| fs.writeFileSync(desktopPath, content); | |
| log(`✅ Desktop file written to ${desktopPath}`); | |
| updateDesktopStatus(); | |
| } | |
| ipcMain.on("action", (_e, action) => { | |
| switch (action) { | |
| case "A": fire("No actions", []); break; | |
| case "B": fire('With "default"', ["default", "Open"]); break; | |
| case "C": fire("Default + custom", ["default", "Open", "mark-read", "Mark Read"]); break; | |
| case "N1": fireNative("Click to focus", []); break; | |
| case "DESKTOP": installDesktopFile(); break; | |
| } | |
| }); | |
| app.whenReady().then(async () => { | |
| win = new BrowserWindow({ | |
| width: 900, height: 500, | |
| webPreferences: { preload: path.join(__dirname, "preload.js") }, | |
| }); | |
| win.loadURL(`data:text/html,${encodeURIComponent(`<!DOCTYPE html> | |
| <html><body style="font-family:monospace;padding:16px;background:#1e1e1e;color:#ccc;margin:0;height:100vh;box-sizing:border-box;overflow:hidden"> | |
| <div style="display:flex;height:100%;gap:16px"> | |
| <div style="flex:0 0 auto;padding-right:16px"> | |
| <h2>FDN + ActivationToken Test</h2> | |
| <p style="color:#888;font-size:12px;margin:4px 0 8px">Direct D-Bus notifications using app.setActivationToken()</p> | |
| <div style="display:flex;gap:8px;flex-wrap:wrap"> | |
| <button onclick="action('A')" style="padding:8px 12px;cursor:pointer">No actions</button> | |
| <button onclick="action('B')" style="padding:8px 12px;cursor:pointer">With "default"</button> | |
| <button onclick="action('C')" style="padding:8px 12px;cursor:pointer">Default + custom</button> | |
| </div> | |
| <h2 style="margin-top:16px">Electron Native Notifications</h2> | |
| <p style="color:#888;font-size:12px;margin:4px 0 8px">Standard Electron Notification API using libnotify</p> | |
| <div style="display:flex;gap:8px;flex-wrap:wrap"> | |
| <button onclick="action('N1')" style="padding:8px 12px;cursor:pointer">Click to focus</button> | |
| </div> | |
| <h2 style="margin-top:16px">Setup</h2> | |
| <div style="display:flex;gap:8px;flex-wrap:wrap"> | |
| <button onclick="action('DESKTOP')" style="padding:8px 12px;cursor:pointer">Install .desktop file</button> | |
| <span id="desktop-status"></span> | |
| </div> | |
| </div> | |
| <pre id="log" style="flex:1;overflow-y:auto;overflow-x:hidden;margin:0;padding:8px;background:#161616;border-left:1px solid #333;font-size:12px;white-space:pre-wrap;word-break:break-all"></pre> | |
| </div> | |
| </body></html>`)}`); | |
| await initDBus(); | |
| log("✅ D-Bus ready, listening for ActivationToken + ActionInvoked"); | |
| updateDesktopStatus(); | |
| console.log("WM_CLASS:", app.getName()); | |
| }); | |
| app.on("window-all-closed", () => app.quit()); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "notif-test", | |
| "version": "1.0.0", | |
| "main": "main.js", | |
| "devDependencies": { | |
| "electron": "^40.0.0" | |
| }, | |
| "dependencies": { | |
| "dbus-next": "^0.10.2" | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const { contextBridge, ipcRenderer } = require("electron"); | |
| contextBridge.exposeInMainWorld("action", (name) => ipcRenderer.send("action", name)); | |
| ipcRenderer.on("log", (_e, msg) => { | |
| const el = document.getElementById("log"); | |
| if (el) { | |
| const ts = new Date().toTimeString().slice(0, 8); | |
| el.textContent += `[${ts}] ${msg}\n`; | |
| el.scrollTop = el.scrollHeight; | |
| } | |
| }); | |
| ipcRenderer.on("desktop-status", (_e, exists) => { | |
| const el = document.getElementById("desktop-status"); | |
| if (el) { | |
| const color = exists ? "#4c4" : "#c44"; | |
| const text = exists ? "installed" : "missing — notifications may not work"; | |
| el.innerHTML = `<span style="color:${color}">${text}</span>`; | |
| } | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment