<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ABDOURAHMAN MOHAMED</title>
    <description>The latest articles on DEV Community by ABDOURAHMAN MOHAMED (@a1909).</description>
    <link>https://dev.to/a1909</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3966082%2Fb88112a5-db2d-47ad-8db9-a51a4fd0c61d.PNG</url>
      <title>DEV Community: ABDOURAHMAN MOHAMED</title>
      <link>https://dev.to/a1909</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/a1909"/>
    <language>en</language>
    <item>
      <title>How I fixed a silent hang in the XDG Desktop Portal and turned it into an npm package</title>
      <dc:creator>ABDOURAHMAN MOHAMED</dc:creator>
      <pubDate>Wed, 03 Jun 2026 08:35:29 +0000</pubDate>
      <link>https://dev.to/a1909/how-i-fixed-a-silent-hang-in-the-xdg-desktop-portal-and-turned-it-into-an-npm-package-602</link>
      <guid>https://dev.to/a1909/how-i-fixed-a-silent-hang-in-the-xdg-desktop-portal-and-turned-it-into-an-npm-package-602</guid>
      <description>&lt;p&gt;I was building Parallel — an Electron app for local network screen sharing on Linux. No server, no account, just WebRTC and mDNS between two machines on the same WiFi.&lt;/p&gt;

&lt;p&gt;Everything worked until I tried to add remote input control on Wayland.&lt;/p&gt;

&lt;p&gt;The problem with Wayland&lt;/p&gt;

&lt;p&gt;The X11 display protocol allowed apps to inject input directly. Wayland was designed to prevent this — no app can touch the input system without going through the XDG Desktop Portal RemoteDesktop interface, which requires the user to explicitly grant permission.&lt;/p&gt;

&lt;p&gt;So I implemented the portal workflow. Three calls: CreateSession, SelectDevices, Start. After the user clicks Allow on the dialog, you can inject input freely.&lt;/p&gt;

&lt;p&gt;The code looked correct. But on GNOME it would silently hang.&lt;/p&gt;

&lt;p&gt;The race condition&lt;/p&gt;

&lt;p&gt;Each portal call responds asynchronously via a D-Bus signal on a specific path. The naive pattern is:&lt;/p&gt;

&lt;p&gt;make the call → subscribe to the response → wait&lt;br&gt;
The problem: CreateSession requires no user interaction, so GNOME approves it almost instantly. The Response signal fires before your subscriber has time to register. It's already gone. Your code waits forever.&lt;/p&gt;

&lt;p&gt;The fix&lt;/p&gt;

&lt;p&gt;The response path is deterministic:&lt;/p&gt;

&lt;p&gt;/org/freedesktop/portal/desktop/request/{sender}/{token}&lt;br&gt;
Where sender is your D-Bus connection name and token is a string you generate yourself. Since you control both, you can calculate the exact path before making the call:&lt;/p&gt;

&lt;p&gt;const token = makeToken('prefix')&lt;br&gt;
const path  = predictHandlePath(bus, token)&lt;/p&gt;

&lt;p&gt;// Subscribe FIRST&lt;br&gt;
const response = waitForResponse(bus, path)&lt;/p&gt;

&lt;p&gt;// Then make the call&lt;br&gt;
await remoteDesktop.CreateSession({ handle_token: new Variant('s', token) })&lt;/p&gt;

&lt;p&gt;// Now safely await&lt;br&gt;
await response&lt;br&gt;
This applies to every call in the sequence — CreateSession, SelectDevices, SelectSources, and Start.&lt;/p&gt;

&lt;p&gt;The package&lt;/p&gt;

&lt;p&gt;Once it worked I extracted it into wayland-input so nobody else has to go through this.&lt;/p&gt;

&lt;p&gt;npm install wayland-input &lt;br&gt;
(&lt;a href="https://www.npmjs.com/package/wayland-input" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/wayland-input&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;import { createInputInjector } from 'wayland-input'&lt;/p&gt;

&lt;p&gt;const input = await createInputInjector({ portalTimeoutMs: 30000 })&lt;/p&gt;

&lt;p&gt;// Call when sharing starts — this is when the permission dialog appears&lt;br&gt;
input.preinit()&lt;/p&gt;

&lt;p&gt;await input.inject({ type: 'mousemove', x: 500, y: 300 })&lt;br&gt;
await input.inject({ type: 'keydown', key: 'Enter' })&lt;/p&gt;

&lt;p&gt;await input.close()&lt;br&gt;
Handles the full portal session lifecycle, coordinate scaling for remote desktop scenarios, configurable timeouts, and automatic xdotool fallback on X11.&lt;/p&gt;

&lt;p&gt;GitHub: github.com/Abdourahman1909/wayland-input&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1lt28n98jj3lbn030tn7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1lt28n98jj3lbn030tn7.png" alt=" " width="800" height="719"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>linux</category>
      <category>npm</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
