Skip to content

Instantly share code, notes, and snippets.

@BohdanTkachenko
Created March 29, 2026 17:20
Show Gist options
  • Select an option

  • Save BohdanTkachenko/e62ec1c95cd4d6430914f3339e8d9594 to your computer and use it in GitHub Desktop.

Select an option

Save BohdanTkachenko/e62ec1c95cd4d6430914f3339e8d9594 to your computer and use it in GitHub Desktop.
Manual test app for Electron notification activation token on Wayland
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());
{
"name": "notif-test",
"version": "1.0.0",
"main": "main.js",
"devDependencies": {
"electron": "^40.0.0"
},
"dependencies": {
"dbus-next": "^0.10.2"
}
}
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