No description
  • Rust 97.9%
  • Nix 2.1%
Find a file
Ben ffd5071055 docs: add AGENTS.md, link CLAUDE.md, fix repository URL
Document the workspace layout, the protocol-logic/IO separation invariant,
build/test/lint flow, protocol gotchas, and the add-a-plugin recipe. Link
CLAUDE.md to AGENTS.md so they stay in sync, and point the workspace
repository URL at git.devries.land to match nix/package.nix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 20:19:07 +02:00
contrib Initial commit: niri-connect v0.1 2026-06-13 16:11:38 +02:00
crates engine: refresh tray on mute / sync-on-connect toggle from D-Bus 2026-06-13 22:07:40 +02:00
nix nix: flake packaging with package, modules, and dev shell 2026-06-13 20:39:46 +02:00
.gitignore nix: flake packaging with package, modules, and dev shell 2026-06-13 20:39:46 +02:00
AGENTS.md docs: add AGENTS.md, link CLAUDE.md, fix repository URL 2026-06-18 20:19:07 +02:00
Cargo.lock tui: add a ratatui terminal UI over the D-Bus API 2026-06-13 21:54:23 +02:00
Cargo.toml docs: add AGENTS.md, link CLAUDE.md, fix repository URL 2026-06-18 20:19:07 +02:00
CLAUDE.md docs: add AGENTS.md, link CLAUDE.md, fix repository URL 2026-06-18 20:19:07 +02:00
flake.lock nix: flake packaging with package, modules, and dev shell 2026-06-13 20:39:46 +02:00
flake.nix nix: flake packaging with package, modules, and dev shell 2026-06-13 20:39:46 +02:00
README.md tui: add a ratatui terminal UI over the D-Bus API 2026-06-13 21:54:23 +02:00

niri-connect

KDE Connect for the niri Wayland compositor — a from-scratch Rust implementation of the KDE Connect protocol, designed for a compositor that has no shell, panel, or tray of its own. It pairs with the stock KDE Connect Android app.

Status: early. The protocol core, TLS transport, pairing, and the v1 plugins (ping, notification, clipboard) work end-to-end; the daemon discovers devices, pairs, and routes packets. See Roadmap.

Why a new implementation?

KDE Connect's daemon is Qt/Plasma-centric, GSConnect is a GNOME Shell extension, and Valent is GTK/GLib. None fit niri. niri-connect is built around what niri actually provides:

  • Clipboard via wlr-data-control/ext-data-control (which niri supports), using the headless wl-clipboard-rs.
  • Notifications via the standard org.freedesktop.Notifications D-Bus interface (mako, swaync, …).
  • A headless daemon + CLI instead of assuming a desktop shell, plus an optional StatusNotifierItem tray that works in any SNI host.

Architecture

A Cargo workspace:

Crate Role
niri-connect-core Protocol: packets, identity, device registry, pairing state machine, plugin trait. No I/O.
niri-connect-net UDP discovery, the TLS transport (KDE Connect's inverted client/server roles), trust-on-first-use certificate pinning.
niri-connect-plugins Ping, notification, clipboard, mousepad, mpris, share, battery, findmyphone — pure logic behind injected backend traits.
niri-connect-wayland Clipboard (wl-clipboard-rs) and remote-input (wlr-virtual-pointer + virtual-keyboard) backends.
niri-connect-fdo Notification sink and MPRIS media control (zbus).
niri-connect-daemon niri-connectd: ties it together.
niri-connect-cli niri-connect: control the daemon over D-Bus.
niri-connect-tui niri-connect-tui: a ratatui terminal UI over the same D-Bus API.

How pairing works (the protocol's subtle part)

Devices broadcast a UDP identity packet on port 1716. The device that receives a broadcast opens the TCP connection and becomes the TLS server; the broadcaster that accepts it becomes the TLS client — the roles are inverted from the usual TCP convention. Both present a self-signed certificate (mutual TLS); trust is established by pinning the peer's certificate during pairing, and re-verified on every later connection.

Build & run

cargo build --release
./target/release/niri-connectd        # run the daemon (foreground; RUST_LOG=info for logs)

# In another shell — the CLI controls the daemon over D-Bus:
./target/release/niri-connect status              # identity + live device list
./target/release/niri-connect devices             # machine-readable device list
./target/release/niri-connect pair <device-id>    # request pairing
./target/release/niri-connect accept <device-id>  # accept an incoming request
./target/release/niri-connect unpair <device-id>
./target/release/niri-connect ping <device-id>
./target/release/niri-connect send <device-id> <file>    # send a file to the phone
./target/release/niri-connect find <device-id>           # ring the phone to find it

Terminal UI

niri-connect-tui is a full-screen TUI over the same D-Bus API — a live device list with per-device actions and the mute / sync-on-connect toggles:

./target/release/niri-connect-tui

Keys: j/k move · p pair · a accept · d unpair · space ping · f ring · s send file · m mute · y sync-on-connect · r refresh · q quit.

State and config live under XDG dirs:

  • ~/.config/niri-connect/config.json
  • ~/.local/state/niri-connect/ (device id, TLS cert/key, pinned peer certs)

config.json

All fields are optional:

{
  "device_name": "my-laptop",
  "auto_accept_pairing": false,
  "clipboard_poll_secs": 1,
  "tray": true,
  "sync_notifications_on_connect": true
}

sync_notifications_on_connect (default true) asks the phone to re-send its current notifications on connect so standing ones appear immediately; set it false to only get notifications that arrive after connecting.

Tray

The daemon publishes a StatusNotifierItem — the freedesktop tray standard — so it appears in any SNI host: waybar, KDE Plasma, the GNOME AppIndicator extension, AGS, noctalia, Sway/i3 bars with an SNI module, and so on. It is not tied to any one bar. The menu lists discovered devices with per-device actions (pair / accept / unpair / ping / ring / send file…), shows each phone's battery %, and has a "Mute phone notifications" toggle. "Send file…" opens a native file picker via the desktop portal. It turns "needs attention" when a pairing request is waiting. Set tray = false for a fully headless setup.

For waybar specifically, the item shows in the standard tray module — no niri-connect-specific config needed:

// ~/.config/waybar/config
{ "modules-right": ["tray"], "tray": { "spacing": 8 } }

Pairing: when a device requests pairing, the daemon raises a desktop notification showing the verification key and waits. Confirm it with:

niri-connect accept <device-id>

Set auto_accept_pairing = true for unattended setups (accepts every request without asking — use with care). You can also initiate from this side with niri-connect pair <device-id>.

Nix / NixOS

The repo is a flake. Build or run directly:

nix build git+https://git.devries.land/ben/niri-connect
nix run   git+https://git.devries.land/ben/niri-connect -- --help   # niri-connectd

Flake inputs and modules

{
  inputs.niri-connect.url = "git+https://git.devries.land/ben/niri-connect";

  # NixOS: enables the user service and opens ports 1714-1764.
  # nixosConfigurations.<host>.modules = [ niri-connect.nixosModules.default {
  #   services.niri-connect.enable = true;
  # } ];

  # home-manager: installs the package + a graphical-session user service.
  # home-manager users:
  #   imports = [ niri-connect.homeManagerModules.default ];
  #   services.niri-connect.enable = true;
}

There's also overlays.default, packages.default, and a devShells.default (cargo, clippy, rustfmt, rust-analyzer). The package wraps niri-connectd so it finds libwayland (dlopened) and xdg-open at runtime; the NixOS module opens the KDE Connect firewall range for you.

Running under niri

Add to ~/.config/niri/config.kdl:

spawn-at-startup "niri-connectd"

You'll also want a notification daemon (e.g. mako) running so phone notifications appear:

spawn-at-startup "mako"

systemd user service (alternative)

contrib/niri-connectd.service~/.config/systemd/user/:

systemctl --user enable --now niri-connectd

Firewall

KDE Connect uses UDP and TCP on ports 17141764. If you run a firewall, open that range, e.g. with firewalld:

firewall-cmd --permanent --add-port=1714-1764/udp
firewall-cmd --permanent --add-port=1714-1764/tcp
firewall-cmd --reload

niri-connect can coexist with an existing kdeconnectd on the same host (it shares the discovery port via SO_REUSEPORT and falls back to an ephemeral TCP port), but for normal use you'd run only one.

Roadmap

  • Protocol core, identity, pairing state machine
  • UDP discovery, mutual TLS, certificate pinning
  • Plugins: ping, notification, clipboard
  • Runnable daemon (discovery, connections, pairing, routing)
  • D-Bus control surface + niri-connect pair/accept/unpair/ping
  • StatusNotifierItem tray (universal — any SNI host)
  • Remote input — phone as trackpad + keyboard (wlr-virtual-pointer + virtual-keyboard); keyboard is US-layout ASCII for now
  • MPRIS media control — list players, now-playing, play/pause/next/prev/stop, volume, seek
  • File share — receive files (→ Downloads), text (→ clipboard), URLs (→ open), and send files to the phone (niri-connect send)
  • Battery — report ours to the phone, alert on the phone's low battery
  • Find-my-phone — niri-connect find rings the phone; ring the desktop on the phone's request
  • Polish: persisted device names, event-driven tray, battery push-on-change
  • SFTP browse; non-US keyboard layouts (arbitrary non-ASCII input)
  • mDNS discovery (protocol v8)

License

GPL-2.0-or-later, matching the KDE Connect ecosystem.