feat: add app.setActivationToken() on Linux (#54989)

* feat: add app.setActivationToken() on Linux

A Wayland compositor only raises a window when the request carries an XDG
activation token. Electron forwards the tokens it receives at launch and from
its own notifications, but an app that gets one another way, for example from
a notification it sent over D-Bus itself, had no way to hand it over. The new
method sets the token that the next window activation uses.

* fix: document the X11 behavior of app.setActivationToken() and harden its spec

X11 uses the same token as the startup notification ID of the next window that
is created or activated, so the method is not a no-op there. The spec now sets
the token before its window is shown, reads the activation request as it
appears and fails if the child exits instead of skipping.

* docs: say what the token is on X11 in the parameter description
This commit is contained in:
Shelley Vohr authored and GitHub committed 2026-10-22 17:42:23 +00:00
1 parent c80c71c7c6
commit 3b24d8f7f6
6 files changed
+81

No files matched your search

+28
View File
@@ -836,6 +836,34 @@ identity — packaged apps should always set it explicitly.
This API must be called before the `ready` event. The value can also be set using `desktopName` in `package.json`.
### `app.setActivationToken(token)` _Linux_
<!--
```YAML history
added:
- pr-url: https://github.com/electron/electron/pull/54989
```
-->
* `token` string - An [XDG activation](https://wayland.app/protocols/xdg-activation-v1) token, or a
[startup notification](https://specifications.freedesktop.org/startup-notification-spec/latest/) ID on X11.
Sets the token that the next window activation uses, for example
[`win.focus()`](base-window.md#winfocus) or [`win.show()`](base-window.md#winshow).
A Wayland compositor only lets an app raise and focus one of its windows when
the request carries a token that proves the user asked for it. Electron passes
such tokens along by itself when the app is launched or when one of its
[`Notification`](notification.md)s is clicked. Use this method when the app gets
a token some other way, for example from a notification it sent over D-Bus
itself or from another process, and wants the window it activates next to come
to the front. The token is used once. Set it before the window is created or
shown, or once the window is on screen: a token that is set while a new window
is still being shown may not be used.
On X11 the token is set as the `_NET_STARTUP_ID` of the next window that is
created or activated, which is where window managers read it.
### `app.getLocale()`
Returns `string` - The current application locale, fetched using Chromium's `l10n_util` library.
+7
View File
@@ -1038,6 +1038,12 @@ void App::SetDesktopName(const std::string& desktop_name) {
#endif
}
void App::SetActivationToken(const std::string& token) {
#if BUILDFLAG(IS_LINUX)
base::nix::SetActivationToken(token);
#endif
}
std::string App::GetLocale() {
return g_browser_process->GetApplicationLocale();
}
@@ -2175,6 +2181,7 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
.SetMethod("getPath", &App::GetPath)
.SetMethod("setAppLogsPath", &App::SetAppLogsPath)
.SetMethod("setDesktopName", &App::SetDesktopName)
.SetMethod("setActivationToken", &App::SetActivationToken)
.SetMethod("getLocale", &App::GetLocale)
.SetMethod("getPreferredSystemLanguages", &GetPreferredLanguages)
.SetMethod("getSystemLocale", &App::GetSystemLocale)
+1
View File
@@ -211,6 +211,7 @@ class App final : public gin::Wrappable<App>,
const base::FilePath& path);
void SetDesktopName(const std::string& desktop_name);
void SetActivationToken(const std::string& token);
std::string GetLocale();
std::string GetLocaleCountryCode();
std::string GetSystemLocale(gin_helper::ErrorThrower thrower) const;
+32
View File
@@ -2063,6 +2063,38 @@ describe('app module', () => {
});
});
ifdescribe(process.platform === 'linux')('app.setActivationToken(token)', () => {
it('throws when the token is not a string', () => {
expect(() => app.setActivationToken(1 as any)).toThrow(/Error processing argument at index 0/);
});
ifit(isWayland)('activates the next window with the token', async (ctx) => {
const child = cp.spawn(
process.execPath,
[path.join(fixturesPath, 'api', 'activation-token'), '--ozone-platform=wayland'],
{ env: { ...process.env, WAYLAND_DEBUG: '1' }, stdio: ['ignore', 'ignore', 'pipe'] }
);
defer(() => {
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
});
let protocolLog = '';
child.stderr.on('data', (chunk) => {
protocolLog += chunk;
});
const hasActivationProtocol = () => protocolLog.includes('"xdg_activation_v1"');
await waitUntil(() => {
expect(child.exitCode ?? child.signalCode, protocolLog).toBeNull();
return (
/xdg_activation_v1[#@]\d+\.activate\(/.test(protocolLog) ||
(/xdg_surface[#@]\d+\.ack_configure\(/.test(protocolLog) && !hasActivationProtocol())
);
});
if (!hasActivationProtocol()) ctx.skip();
expect(protocolLog).toMatch(/xdg_activation_v1[#@]\d+\.activate\("electron-test-token"/);
});
});
ifdescribe(process.platform === 'linux')('display server selection', () => {
let runtimeDir: string;
beforeEach(() => {
+9
View File
@@ -0,0 +1,9 @@
const { app, BrowserWindow } = require('electron');
// Stays open. The spec reads the window's activation request from the Wayland
// protocol log and then ends this process.
app.whenReady().then(() => {
app.setActivationToken('electron-test-token');
const w = new BrowserWindow({ width: 300, height: 200 });
w.once('closed', () => app.quit());
});
+4
View File
@@ -0,0 +1,4 @@
{
"name": "electron-test-ozone-platform",
"main": "main.js"
}