# v2.39.0 — the sharing icon you never agreed to

**Released 2026-09-24.** Upgrade:

```sh
sudo snap refresh yazses     # snap
pipx upgrade yazses          # or: uv tool upgrade yazses
scoop update yazses          # Windows
```

If you dictate on Wayland, this is the release that stops your desktop showing a
screen-sharing indicator the whole time YazSes is running.

## Why it was there at all

On Wayland, typing into another window needs either `ydotoold` or the
`xdg-desktop-portal` RemoteDesktop API. The portal works, and it makes your desktop
display a sharing indicator while its session is open — which is a strange thing to
see on an application whose entire promise is that nothing leaves your machine.

YazSes was supposed to prefer `ydotoold` and fall back to the portal. It could not.
Three independent defects, all verified on a real Ubuntu 24.04 machine:

1. **The daemon was never installed.** `yazses setup` installed the `ydotool` client;
   `ydotoold` is a *separate package* on Debian and Ubuntu. So setup wrote a systemd
   unit pointing at a binary nothing had installed.
2. **No udev rule existed anywhere in the project.** `/dev/uinput` ships
   `0600 root:root`, so belonging to the `input` group — which setup does arrange,
   and which the unit's own comment credited for device access — grants nothing.
   Installing the daemon *without* the rule produces a crash loop: observed at **726
   restarts**, each one binding its socket, printing "listening", then aborting on
   `failed to open uinput device`.
3. **The socket was looked for in the wrong place.** Ubuntu ships ydotool **0.1.8**,
   which predates `--socket-path` and ignores it silently: the unit passes
   `--socket-path=%t/.ydotool_socket` and 0.1.8 listens on `/tmp/.ydotool_socket`
   regardless. Readiness probed only `$XDG_RUNTIME_DIR`, so it answered "not ready"
   on a machine where ydotoold was installed, enabled and running.

So the portal was not a fallback. It was the default, on the most common Linux
desktop, and nobody had agreed to it.

## The portal is now offered, not taken

`auto` no longer selects the portal on its own. Without consent, dictation falls
through to the clipboard — which delivers your words — and YazSes offers the choice
instead, in the terminal, on the desktop, and in Settings → **Desktop portal**.

**Nothing that works today is regressed.** Three things already count as consent and
need no migration: naming `backend = "portal"`, an existing restore token on disk
(you answered the dialog before), and the strictly confined snap, for which the portal
is the only way to type on Wayland at all. A token is written on every successful
portal start, so if the portal has ever worked for you, you are already covered and
will not be asked again.

An explicit `portal_consent = "deny"` is obeyed **even in the snap**. Quietly doing
the thing a user forbade, on the grounds that we know better, is the same paternalism
as taking the portal unasked.

## The indicator clears between dictations

Even when the portal *is* in use, its session no longer stays open for the daemon's
whole life. It is released after `[injection] portal_idle_release_s` seconds of idle
(default 60) and re-opened on demand, replaying the restore token so no dialog
returns. With no token it stays open regardless — raising that dialog mid-sentence is
worse than the icon.

## `yazses setup` can now deliver the good path

It installs `ydotoold` **and** `/etc/udev/rules.d/60-yazses-uinput.rules`, reloads and
triggers udev, and shows both in `--dry-run`.

**Log out and back in afterwards.** The group and the udev rule only reach a new
session; without that the device stays unreadable and the portal is still used. The
previous release's advice omitted this, which is why it did not work.

## One backend table instead of five copies

`inject/registry.py` is now the single source of truth for which backends exist, their
order, what each costs, and whether it needs consent. `get_injector`, `yazses doctor`,
the config validator and the Settings combo all render the same result, so they cannot
disagree — the drift that let the validator reject a `backend = "ydotool"` that the
documentation advertised and the selector honoured. Adding a backend, or a platform,
is now one row.

New command:

```console
$ yazses inject-backend
Session: wayland    configured: 'auto'
Using:   clipboard
         instant, but a no-op in terminals and it overwrites your clipboard

  ydotool    ydotoold is not running (no socket)
             Run `yazses setup`, then log out and back in.
  portal     needs-consent
  wtype      gnome does not implement the virtual-keyboard protocol wtype needs

  portal is available but needs your permission
  Allow it:  yazses inject-backend --allow-portal
```

Nothing in the CLI could write `[injection] backend` before, so a headless or SSH user
had no supported way to change it.

## Also fixed

- **wtype could be chosen where it silently types nothing.** It needs
  `virtual-keyboard-manager-v1`, which GNOME's Mutter and KDE's KWin deliberately do
  not implement. Ranking it below the portal was sufficient only while the portal was
  automatic; once the portal waits for consent, an unconsented GNOME user fell
  straight onto a backend that drops every burst. It is now reported *unavailable*
  there, with the reason.
- **A socket file is not a working daemon.** Readiness also checks that our own
  ydotoold can open the device — and only for a user-owned socket, since one owned by
  root belongs to a system unit whose privileges we neither have nor need.
- **A documented config value was silently reverted.** `backend = "ydotool"` was
  documented and honoured but missing from the validator's enum, so the loader
  replaced it with `auto`. The enum is now derived from the table.
- **`contrib/` had already drifted** from what setup writes, including the false claim
  about `input`-group membership — in a file a distro packager might install. A
  byte-equality test now covers the unit and the new udev rule.

## Verification

Gates at release: **ruff clean**, **mypy clean across 526 source files**, **15,493
tests passed / 300 skipped**, green on Linux (3.11–3.14, x86-64 and arm64), macOS,
Windows and FreeBSD.

Every fix was mutation-tested — reverted to confirm its test goes red rather than
passing vacuously. Several tests were also found to be reading the developer's own
desktop (the portal token, `/tmp/.ydotool_socket`, `XDG_CURRENT_DESKTOP`) and passing
for the wrong reason; those are now isolated.
