- Rust 97.9%
- Nix 2.1%
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> |
||
|---|---|---|
| contrib | ||
| crates | ||
| nix | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
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 headlesswl-clipboard-rs. - Notifications via the standard
org.freedesktop.NotificationsD-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 1714–1764. 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 findrings 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.