Skip to content

Release v1.17.0 - #26

Merged
bethropolis merged 29 commits into
mainfrom
dev/next
Aug 12, 2026
Merged

Release v1.17.0#26
bethropolis merged 29 commits into
mainfrom
dev/next

Conversation

@bethropolis

@bethropolis bethropolis commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Release v1.17.0

Headless KDE Connect daemon for Linux. This brings 28 commits onto main since v1.16.0, mixing new plugins and a batch of clipboard/notifications reliability fixes.

New Features

  • RemoteSystemVolume pluginkcd volume to control the remote device's system volume
  • Notifications: debounce cancel/re-post to stop popup flicker; collapse re-posted popups and default to icon-less bubbles
  • MPRIS: keep now-playing fresh and cache album art (~/.cache/kcd/art/)
  • SFTP: kcd sftp browse subcommand + volumePath parameter for RequestAndMountVolume
  • kcd watch: json-mode auto-reconnect on disconnect
  • Clipboard push-on-connect is now opt-in (default off)
  • Auto-prune stale unpaired devices (configurable threshold)
  • Docker image is now Alpine-based with an entrypoint handling permissions and a default config

Bug Fixes

  • Clipboard: apply inbound copies reliably — wl-copy forks a background manager that inherited our stdout/stderr pipes, so cleanup hung and inbound copies were silently dropped; pushes then re-sent stale/empty clipboard to the phone
  • Clipboard: keep the subprocess deadline alive and surface real tool errors (no more misleading failures)
  • Clipboard: re-probe backend at runtime instead of assuming wayland-1; detect backend when WAYLAND_DISPLAY is unset
  • Clipboard: bind unit to graphical-session.target so the clipboard works on boot
  • Daemon: escalate auto-reconnect backoff across connection flaps (fixes CPU spin from backoff overflow and 2s-floor hammering)
  • CI/Nix: clean GoReleaser artifacts, restore flake.lock from HEAD, resolve release.yml conflicts

…me method

mountWithBody now accepts an optional volumePath parameter. When empty,
existing behaviour is preserved (auto-select first volume from multiPaths).

RequestAndMountVolume follows the same subscribe-request-wait pattern as
RequestAndMount but accepts a specific volume path to mount. If the path
is empty, available volumes are returned without mounting (list mode).

Extracted buildVolumes helper to avoid duplicating the volume-building
logic in Volumes() and the new method.
Adds sftp_browse IPC command with volume resolution logic (index, name,
or path matching). The daemon handler requests fresh credentials, then
either lists volumes or mounts the chosen one via sshfs.

New IPC types:
  - CmdSftpBrowse constant
  - SftpBrowsePayload (deviceId + optional volume)
  - SftpBrowseResponse (optional path + volumes)

New client method:
  - SftpBrowse(deviceID, volume) returns (mountPath, volumes, err)

CLI:
  - kcd sftp browse <device-id>      list volumes (fresh creds)
  - kcd sftp browse <device-id> <v>  mount volume by index/name/path
Adds two new reference documents:
  - docs/IPC_PROTOCOL.md — complete wire protocol reference (28 commands,
    27 event types, packet tables, walkthroughs)
  - docs/CLIENT_GUIDE.md — client developer guide with Python examples,
    feature patterns, and integration templates

Updates docs/CLI.md:
  - Adds sftp browse subcommand docs
  - Adds mpris raw section
  - Fills event type table with 6 previously missing types
  - Fixes share.complete payload schema

Updates packaging/kcd.fish-completion:
  - Adds missing event types to watch --events completions

Updates AGENTS.md:
  - Adds step 8 to plugin checklist (documentation requirement)
The install hook printed a message on every package update which was
annoying for users. Removing the reference skips it entirely.
Adds automatic cleanup of stale devices from the registry. Devices that are
neither paired nor connected are removed when their last-seen timestamp
exceeds the threshold (default: 15m). This prevents old discovery entries
from accumulating in `kcd devices` and `devices.json`.

- LastSeen is now refreshed on every TCP connection (handleNewConnection)
  so active/reconnecting devices stay alive
- Prune runs at daemon startup and before every saveDevices write
- New config key: prune_stale_threshold (Go duration format, '0' to disable)
- Silent cleanup — no events published for pruned devices
Adds result/ to .gitignore and restores flake.lock after Nix build
step to prevent git dirty state error in GoReleaser.
git checkout flake.lock only restores from the index, but if
Nix also stages the file, the index copy is still dirty.
Using git checkout HEAD -- flake.lock properly discards
both staged and working-tree changes.
Keep HEAD -- flake.lock over plain flake.lock to properly
discard staged changes.
Nix build creates flake.lock and a result symlink, which dirties
the git tree and blocks GoReleaser's --clean check. Moving Nix
steps to a separate workflow triggered by workflow_run eliminates
this conflict entirely.
ReconnectBackoff shift overflowed int64 at attempt >= 54, returning 0
instead of capping, causing time.After(0) to fire immediately in a
tight loop (25% CPU on one core).

Fix 1: cap attempt at 31 before shift to avoid overflow entirely.
Fix 2: add reconnecting guard to Device via atomic.Bool to prevent
multiple concurrent reconnect goroutines from piling up.
systemd user services don't inherit desktop env vars, so
os.Getenv("WAYLAND_DISPLAY") was always empty, forcing xclip
even under Wayland.

Replace static isWayland bool with detectBackend() that probes
exec.LookPath("wl-paste") -> exec.LookPath("xclip") at first
use, cached via sync.Once.

Also log Warn when wl-copy/xclip fails instead of silent discard.
New plugin for controlling phone audio volume from the CLI —
the inverse of the existing SystemVolume plugin (phone controls
PC volume).

- Handles incoming kdeconnect.systemvolume (sink list + per-sink
  updates from phone), sends kdeconnect.systemvolume.request
  (volume/mute commands to phone)
- Caches sink list per-device via sync.Map; OnConnect auto-requests
  sink list
- IPC: remote_volume_list, remote_volume_set, remote_volume_mute
- CLI: kcd volume list/set/mute subcommands
- Events: reuses volume.update for both per-sink and full sink-list
  payloads
- Default: enabled (remotesystemvolume = true)
Under systemd user services, neither WAYLAND_DISPLAY nor DISPLAY
is inherited from the login session. detectBackend() only checked
exec.LookPath, found wl-paste, and committed to the Wayland backend
— but wl-paste then defaulted to the wrong socket (wayland-0) and
failed with exit code 1.

Fix: check display environment variables first, then probe
/run/user/1000 for any wayland-* socket before selecting the
Wayland backend. X11 remains the fallback when no socket is found.
Adds [clipboard] config section with push_on_connect option
(default false). When true, the local clipboard is pushed to a
device every time it connects/reconnects via OnConnect. When
false (default), clipboard is only pushed explicitly via
kcd clipboard.
Clipboard stopped working on daemon restarts because the backend was
probed once via sync.Once at first use. When the daemon boots before
the compositor (systemd user default.target), the probe resolved to
unknown and stayed cached for the process lifetime.

- probeBackend: prefer a live WAYLAND_DISPLAY socket, else scan
  $XDG_RUNTIME_DIR for wayland-* sockets, else X11 fallback.
- getBackend: re-probe while unknown, cache only non-unknown results.
- clipboardCmd: inject the probed WAYLAND_DISPLAY into subprocess env
  and bound subprocesses with a 2s timeout so a hung wl-paste cannot
  stall clipboard sync.
- kcd-user.service: After/PartOf/WantedBy graphical-session.target so
  the daemon starts after the compositor exports the display env.
A peer that keeps connecting then dropping (e.g. a dying phone)
previously spawned a fresh auto-reconnect loop at attempt 0 on every
disconnect, pinning the backoff at the 2s floor forever and hammering
the peer.

Persist the attempt counter on the device: a successful dial bumps it,
and the next cycle continues backing off if the connection drops
before reconnectFlapThreshold (15s). A connection that survives that
long counts as stable and resets the counter on drop.
…errors

clipboardCmd cancelled its 2s context immediately, so exec.CommandContext
saw an already-cancelled context — its watchdog could SIGKILL wl-paste
mid-flight, making clipboard pushes flaky. The timeout test passed for
the wrong reason (instant kill, not the timer).

- clipboardCmd no longer owns a deadline; runClipboard bounds execution
  with the 2s timeout and defers cancellation correctly.
- runClipboard captures stderr and wraps it into the error, so failures
  report the real reason (e.g. 'Nothing is copied') instead of a bare
  'exit status N'.
- Push treats an empty/no-selection clipboard as nothing-to-push instead
  of a hard error, so kcd clipboard and --watch don't spam at startup
  before anything has been copied.
- Tests: rework the timeout test to guard against instant-cancel
  regressions and cover stderr surfacing, no-selection detection, and
  empty/success push paths.
wl-copy forks a persistent manager that inherits the stdout/stderr pipes,
so cmd.Output() blocked past the deadline and inbound copies were silently
dropped; pushes then resubmitted stale or empty content to the phone,
producing a bogus 'pasted from your clipboard' toast. Write with runCopy
(fds pointed at the null device) so Run() returns once wl-copy exits.

Also write clips with wl-copy -n and normalize trailing newlines in the
dedup guard so a tooling newline cannot turn the phone's own content into
an outbound echo.
…g reads

runClipboard/runCopy copy WaitDelay from the passed cmd, but a bare
exec.CommandContext defaults it to 0 — which means cmd.Output() blocks on
the stdout pipe until the last fd holder exits. A killed child that spawned
a grandchild retaining the pipe (sh -c 'sleep 30 & wait' — and /bin/sh on
CI runners) kept tests and reads pinned for the full 30s. Cap the delay at
1s so Wait always returns promptly after the 2s deadline.
@bethropolis
bethropolis merged commit 6bd0d7a into main Aug 12, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant