# v2.34.0 — Windows and macOS never had a GUI at all

**Released 2026-08-27.** Upgrade:

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

Three fixes, all reported or found by using the shipped product on a Windows machine
rather than by any test. The largest of them had been true since the graphical
features were written, on **every Windows and macOS install YazSes has ever produced**.

## Settings opened nothing, and dictation showed no sign of running

Two symptoms, reported first-hand from a Windows desktop: clicking **Settings…** in the
tray menu did nothing whatsoever, and holding the hotkey typed correct text with no
visible indication that anything had happened — no sonar rings, and a tray icon that
never changed colour.

The first symptom, and half of the second, are one line of code repeated three times:

```python
bool(env.get("DISPLAY") or env.get("WAYLAND_DISPLAY"))
```

That is how `settingsui/launch.py`, `core/daemon.py` and `tray/launch.py` each decided,
independently, whether a graphical session exists. `DISPLAY` and `WAYLAND_DISPLAY` are
**X11 and Wayland** variables. Windows and macOS set neither one and never have — so all
three answered *headless*, always, on those platforms:

- the Settings window refused to open,
- the daemon never spawned the voice-activity overlay,
- the daemon never auto-spawned the tray icon.

Reproduced against the published 2.33.0 binary on a clean Windows host:
`yazses-cli.exe --settings` exits 1 with *"needs a graphical session — no DISPLAY or
WAYLAND_DISPLAY is set"*. PySide6 is present in the bundle; the gate was the only thing
refusing. From the windowed `YazSesApp.exe` that explanation has no console to print
to, which is exactly why the button looked inert rather than broken — and why a defect
this total survived every release.

The predicate now lives in one module, `system/graphical.py`, and takes the platform as
an argument, because the honest answer differs by platform: on Windows and macOS an
interactive process has a desktop by construction and nothing announces it in the
environment, while on Linux and the BSDs those variables are the only evidence
available. A build-failing guard forbids any module deciding it again on its own — but
matched precisely enough to still permit `inject/target.py`, which asks the genuinely
different question *"is this plain X11?"* before shelling out to `xdotool`.

### The tray colour was a second, independent defect

Fixing the gate would have launched the tray and left the icon just as blue, because
the tray could not see a short dictation at all. It polled the daemon **once a second**
at rest, and only dropped to its 0.15 s rate *after* it had already observed a recording
state — a chicken-and-egg, since the fast rate exists to track a burst the slow rate
has to catch first. A one-to-two-second hold could begin and end between two samples and
never be observed.

The overlay polls the same RPC for the same transition at 0.25 s, and says why in a
comment: *quick enough to catch recording start*. The reasoning had been written down in
one of the two processes that needed it. The tray now matches, and its test ties the
rate to how long a burst actually lasts rather than to a number that can quietly be
raised again. The cost is four status queries a second over a local socket while idle;
the [CPU and battery](../how-to/cpu-and-battery.md) page carries the updated figure.

## Every Scoop install silently had no Start Menu entry

`scoop install yazses` printed

```
Creating shortcut for YazSes (yazses.exe) failed:
    Couldn't find C:\scoop\apps\yazses\current\yazses.exe
```

and then reported success. There is no `yazses.exe` in the bundle and there never was:
the Windows build produces `YazSesApp.exe` (windowed — tray and daemon) and
`yazses-cli.exe` (console — the CLI, shimmed onto `PATH` as `yazses`). The manifest's
`bin` entry named the second one correctly, which is why the CLI always worked and this
stayed invisible; the `shortcuts` entry named a file that does not exist — while the
manifest's own notes told the user to launch the tray from the Start Menu.

A new guard reads the executable names out of the PyInstaller spec and fails if any
`bin` or `shortcuts` target is not one the installer actually ships, so renaming a
binary now breaks a test rather than a shortcut.

## The shape of all three

None of these could be caught by a test that runs where the tests run. Two were found by
installing the published build on a bare Windows host and *reading what it printed*
rather than what it returned; the third was reported by someone holding the key on their
own machine. An exit code said success in every case.
