npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wayland-input

v1.0.1

Published

Wayland-native input injection via XDG Desktop Portal RemoteDesktop — mouse, keyboard, and scroll with automatic X11 fallback

Readme

wayland-input

Wayland-native input injection via XDG Desktop Portal RemoteDesktop — mouse, keyboard, and scroll with automatic X11 fallback.

Handles the full XDG Desktop Portal RemoteDesktop session lifecycle, including a race condition fix that prevents silent hangs during session initialisation on compositors that auto-approve portal requests (e.g. GNOME).

Install

npm install wayland-input

Requirements

  • Linux (Wayland or X11)
  • For Wayland: GNOME 41+ or KDE Plasma 6.1+ (any compositor that implements the XDG Desktop Portal RemoteDesktop)
  • For X11 fallback: xdotool installed (sudo apt install xdotool)

Compositor compatibility

| Compositor | Input injection | Notes | |---|---|---| | GNOME (41+) | ✅ Full support | Via XDG Desktop Portal | | KDE Plasma (6.1+) | ✅ Full support | Via XDG Desktop Portal | | Hyprland | 🔄 In progress | Portal backend being developed | | Sway | ❌ Not supported | No portal backend | | Niri | ❌ Not supported | No portal backend | | Cosmic | ❌ Not supported | No portal backend | | X11 (any desktop) | ✅ Full support | Via xdotool fallback | | XWayland apps | ✅ Full support | Automatically uses X11 fallback |

Usage

TypeScript

import { createInputInjector } from 'wayland-input'

// Create the injector at app startup — no dialog appears yet
const input = await createInputInjector({ portalTimeoutMs: 30000 })

// Call preinit() when your sharing session begins — this is when
// the Wayland permission dialog appears. Call it early so the session
// is ready before the first inject() call arrives.
input.preinit()

// Mouse
await input.inject({ type: 'mousemove', x: 500, y: 300 })
await input.inject({ type: 'click',     button: 1, x: 500, y: 300 })
await input.inject({ type: 'mousedown', button: 1 })
await input.inject({ type: 'mouseup',   button: 1 })

// Scroll
await input.inject({ type: 'scroll', deltaX: 0, deltaY: 100 })

// Keyboard
await input.inject({ type: 'keydown', key: 'Enter' })
await input.inject({ type: 'keyup',   key: 'Enter' })

// Always close when done
await input.close()

JavaScript (CommonJS)

const { createInputInjector } = require('wayland-input')

const input = await createInputInjector({ portalTimeoutMs: 30000 })
input.preinit()

await input.inject({ type: 'mousemove', x: 500, y: 300 })
await input.inject({ type: 'click', button: 1, x: 500, y: 300 })
await input.inject({ type: 'keydown', key: 'Enter' })

await input.close()

How it works

On Wayland

The library requests input control through the XDG Desktop Portal RemoteDesktop interface (org.freedesktop.portal.RemoteDesktop). On first use, the system shows a permission dialog asking the user to allow input control. Once approved, the library can inject:

  • Mouse movement — absolute (when stream dimensions are known) or relative
  • Mouse buttons — left, middle, right click via evdev button codes
  • Scroll — both axes via NotifyPointerAxis
  • Keyboard — full key support via X11 keysyms via NotifyKeyboardKeysym

The race condition fix

GNOME auto-approves the CreateSession call instantly — the Response signal arrives before a naive listener has time to subscribe, causing the initialisation to hang forever. This library solves this by predicting the exact D-Bus response path before making the call and subscribing first:

// Wrong (misses instant responses):
makeCall() → subscribeToResponse() → wait forever

// Correct (this library):
predictPath() → subscribeToResponse() → makeCall() → response caught

This is a non-obvious fix — the XDG Desktop Portal specification does not document this timing behaviour, so implementations that follow the spec naively will hang silently.

On X11

Falls back to xdotool automatically. No portal, no permission dialog, works out of the box.

API

createInputInjector(options?): Promise<WaylandInput>

Creates an input injector. Automatically detects Wayland or X11. No permission dialog appears at this point.

Options:

{
  portalTimeoutMs?: number  // default: 120000 (2 minutes)
}

portalTimeoutMs controls how long to wait for the user to click Allow in the Wayland permission dialog before timing out. Set it lower (e.g. 30000) to fail fast if the user doesn't respond.

input.preinit(): void

Starts the Wayland portal session in the background. The permission dialog appears at this point. Call this when your sharing session begins — not at app startup — so the dialog appears at the right moment for the user.

On X11 this is a no-op.

input.inject(event: InputEvent): Promise<void>

Injects an input event. The InputEvent type:

type InputEvent = {
  type: 'mousemove' | 'click' | 'mousedown' | 'mouseup' | 'keydown' | 'keyup' | 'scroll'

  // Mouse position (absolute)
  x?: number
  y?: number

  // Mouse movement (relative)
  dx?: number
  dy?: number

  // Mouse button (1=left, 2=middle, 3=right)
  button?: number

  // Keyboard key (browser KeyboardEvent.key format e.g. 'Enter', 'a', 'ArrowUp')
  key?: string

  // Scroll amount
  deltaX?: number
  deltaY?: number

  // Source video dimensions (for coordinate scaling in remote desktop scenarios)
  videoWidth?: number
  videoHeight?: number
}

input.close(): Promise<void>

Closes the portal session and releases all resources. Always call this when done — GNOME blocks new sessions if the previous one was not explicitly closed.

Supported keys

All printable characters plus:

Backspace Tab Enter Escape Delete Home End ArrowLeft ArrowRight ArrowUp ArrowDown PageUp PageDown Insert Shift Control Alt Meta CapsLock F1F12

Deep dive

For a full technical explanation of how the portal protocol works, the race condition, and the two-phase listener pattern — see TECHNICAL.md.

License

MIT © ABDOURAHMAN MOHAMED