Ants Projects Hub
← Perch

Changelog

Perch

Every release, newest first — 4 in total.

v2.0.0

2.0.0 — Windows land where rules say

Theme: Windows land where rules say.

Changed

  • Pixel positions in a rule count from the monitor it names (PERC-0084) x = 120 on HDMI-1 now means 120 px in from HDMI-1's usable left edge, as percentages and presets already did. A rule that wrote whole-desktop pixels for a second monitor needs its x and y reduced by that monitor's position.

Fixed

  • The AppImage starts at login when Start at login is on (PERC-0093) The login entry ran perch, which an AppImage does not install, so Perch never started. It now runs the AppImage file itself.

  • The settings window's tables no longer cut their text off (PERC-0087) Each column of the Windows, Rules, Layouts and Profiles tables is now as wide as its text, so names, matches and sizes read in full.

  • Switching tabs no longer snaps a moved window back to its rule (PERC-0085) A title or state change re-ran the matching rule, so a browser window you had dragged jumped back on every tab switch. Perch now re-applies on such a change only when a different rule starts to match.

  • Windows land on the right monitor when a rule names one (PERC-0084) On X11 a rule naming a second monitor added that monitor's offset twice and put the window off-screen. A rule that only names a monitor now moves the window there at the same spot, shrunk to fit if needed, on every backend; before, the config loader refused such a rule even though the settings window let you create one.

  • Running Perch's tests no longer touches the developer's desktop session (PERC-0078) On a Plasma Wayland desktop the backend compliance tests loaded Perch's script into the developer's own KWin, and a few KWin and autostart unit tests used the real session bus. They now stay off it, and CI and local_CI.sh run the suite with the session bus pointed at nothing, so a new one fails. The live X11 tests give openbox a home of its own.

v1.2.0

1.2.0 — settings that apply at once, and a way into settings without the tray

Added

  • perch --settings opens the settings window, even when the tray icon is hidden (PERC-0077) Run perch --settings to open Perch's settings. If Perch is already running, its settings window opens; if not, Perch starts and opens it. This is the way in on desktops that hide tray icons, such as GNOME without the AppIndicator extension. The tray icon now also shows its error state when Perch found no desktop it can manage, and saving your settings no longer clears the tray's warning state.

  • scripts/install-gnome-extension.py installs the bundled GNOME Shell extension (PERC-0062) The dev path docs/06 §Flatpak has described all along, and the only consumer of the BUNDLED_EXTENSION_DIR and EXTENSION_UUID constants, which had none. It copies the extension into $XDG_DATA_HOME, refuses to replace an existing install without --force, and leaves enabling and the session restart to you.

  • state.json has a migration registry, like config.toml (PERC-0059) docs/02 §Versioning and migration covers both files and only config.toml had one. A state document older than the running Perch is migrated in memory and written back at the current version; one that cannot be migrated leaves the store empty and read-only, so the file survives a Perch that cannot read it.

  • Layout entries skipped for a missing monitor are now reported (PERC-0068) docs/09 §Apply semantics has always required it and the reducer only wrote a log line. Activating a layout that names a disconnected output now raises one tray notification listing every entry it skipped.

Changed

  • Perch forgets apps you haven't used for 90 days (PERC-0067) Saved window positions for an app that hasn't appeared in 90 days are dropped when Perch starts, so the saved file doesn't keep apps you've stopped using. If you open one again, it starts in its default spot.

  • An empty string in a match or apply field is rejected (PERC-0057) The two parsers had diverged on it. As a glob or a preset name an empty string matches nothing the user could have meant, so both now refuse it.

  • Inside a layout, the last matching entry wins (PERC-0057) This is what docs/09 has always specified; the engine returned on the first match. Rules are unchanged and still first-match-wins — the asymmetry is deliberate and each document states its own half.

  • AppImage builds now verify the bundle is self-contained (PERC-0056) The build extracts the finished AppImage on a bare ubuntu:22.04 container and fails if any bundled library fails to resolve there. The previous check ran --version, which exits before Qt is touched and so proved nothing about the bundle.

  • The settings dialog now closes after a confirmed import It held the pre-import document parsed when it opened, so the next Apply from any pane wrote that stale copy back over the imported file, silently undoing the import.

Removed

  • PERCH_LOG_TITLES is gone from the documentation — it never existed in code (PERC-0062) Five documents described it as a privacy opt-in for logging window titles, and no code has ever read it: logging_privacy redacts unconditionally and says so. A control a security standard names and nothing reads is worse than no control, because it invites a reader to believe the redaction is switchable and audited. The documents now say what the code does. Nothing about logging behaviour changed.

Fixed

  • A settings save takes effect at once, with no restart. (PERC-0075) Rules, exclusions, snaps, layouts and profiles edited in the settings window did nothing until Perch restarted. The next window event now obeys them. Windows already placed stay where they are.

  • KDE: Perch now hears when you move or resize a window (PERC-0046) The helper script Perch runs inside KWin used a timer call that doesn't exist there, so every window move or resize notification failed silently. Perch never learned where you had put your windows. Windows whose app name arrives late also went unreported. Both now work, which means Perch remembers your moves on KDE Plasma. The helper script's JavaScript is now checked by a linter on every push.

  • The GNOME and Sway backends no longer claim abilities they lack (PERC-0044) On GNOME and Sway, Perch cannot yet notice windows opening or monitors changing, and on GNOME it cannot register shortcuts. It now says so instead of claiming it can. Behaviour on those desktops is unchanged.

  • Release and check scripts no longer hide their own mistakes (PERC-0065) Behind-the-scenes fixes to the tools that build, check and publish Perch: a mismatched version is caught before a push rather than at release, a manual rebuild of an old release refuses to attach the wrong version, and the AUR and openSUSE submission scripts check what they upload. No change to the app itself.

  • A layout no longer stacks two windows of the same app on one spot (PERC-0066) When a layout has one spot for an app and two of that app's windows are open, Perch now moves only the one you're using, or the first one if you're using neither. The other stays where it is.

  • Starting Perch twice no longer runs two copies that fight over your windows (PERC-0049) If Perch is already running, a second launch now says so and exits, instead of starting a second copy that fights the first over your windows and the saved-positions file. A copy that crashed does not block the next start.

  • KDE: Perch reconnects after KWin restarts, and never acts on a stale command (PERC-0060) If KWin crashes or restarts, Perch now notices, reloads its helper script and reconnects, instead of silently doing nothing while the tray still shows it as connected. A window move that timed out is dropped rather than carried out late. Shortcuts keep working after a brief D-Bus hiccup. The script installer will no longer replace a folder that is not its own, even if PERCH_KWIN_SCRIPT_TARGET points at one by mistake.

  • Autostart and startup: no false "failed" under Flatpak, clean exit on a startup error (PERC-0064) Turning "Start at login" on or off under Flatpak no longer waits five minutes and logs a failure for a change that worked. If startup fails after Perch has connected to your desktop, it now disconnects cleanly instead of leaving its KDE helper loaded. Logging out now always stops Perch properly. A relative path in an XDG folder variable is ignored, as the standard requires.

  • Flatpak on KDE: registering shortcuts through the desktop portal no longer times out (PERC-0048) Perch now listens for the portal's answer before asking, so a quick answer is no longer missed. If the portal still cannot set up shortcuts, Perch falls back to KDE's own shortcut service instead of failing to start its KDE support.

  • Settings window and tray: errors you can see and hear, your desktop theme respected (PERC-0058) The Auto theme no longer replaces your desktop's colours (Breeze, high-contrast) with Perch's own. Invalid match fields now get a red outline and are announced by screen readers. A save that fails says why and opens the page with the problem. Importing a non-text file shows an error instead of doing nothing. "Apply preset" can no longer silently fail to move the window. The Ctrl++ shortcut keeps its + key when registered through the desktop portal. The Rules and Layouts tables now describe the same placement the same way. A missing tray icon is logged.

  • Close the remaining X11 EWMH and ICCCM defects. (PERC-0047) Hotkeys now fire with ScrollLock on, a held hotkey fires once instead of repeating, and "Ctrl++" registers. A desktop without the RandR extension starts instead of failing. Closing a window that offers no close protocol reports that rather than killing the application. Restoring a minimised window works, and a monitor the desktop reports no working area for keeps its full size instead of collapsing to nothing.

  • Module docstrings that described shipped code as still to come (PERC-0062) The X11 and KWin backend docstrings said their commands and hotkey registration had not landed and would raise; both are v1 backends and complete. Same class in the core modules, the app entry point, the rules table model and the KWin hotkey path — all rewritten to describe what the code does.

  • Instructions that named retired tools and a retired drive (PERC-0062) audit_config.yaml and CLAUDE.md drove the drift analyser from a path on the failed /mnt/Storage drive, with a flag the analyser does not accept, so nobody following them could run it. The docs also mandated four Claude Code skills that no longer exist, and two of them stated that local_CI.sh runs one interpreter, which stopped being true when it learned to read ci.yml's matrix.

  • A failing config migration reports the problem instead of a traceback (PERC-0059) A missing migration step escaped as a bare KeyError. It is now a ConfigError naming the file and the version gap, which is what docs/02 §Schema reference promises.

  • A typo'd top-level section is reported instead of ignored (PERC-0059) Unknown keys inside [general] and [exclusions] were rejected, but an unknown table at the top level was not — so [[rule]] for [[rules]] meant every rule in the file went unread with no error. schema_version = true was also accepted, because a bool is an int in Python.

  • A hand edit to config.toml is no longer discarded silently (PERC-0059) The settings dialog parses config.toml when it opens and writes it whole on Apply, so anything edited on disk in between was lost. Perch offers hand-editing as a supported workflow, so Apply now compares the file against what it read and asks before replacing it.

  • Editing a profile in the dialog is validated the way adding one is (PERC-0059) Renaming a profile to a name another already uses, or giving it a topology that is a duplicate or malformed, went through none of the checks add_profile applies. The dialog wrote it and the loader refused the file on the next start, with nothing said at the time.

  • X11: a window is moved to its target desktop before it is placed (PERC-0068) The two v1 backends disagreed — KWin sent the desktop change first, X11 sent it last. A window manager is free to re-place a window when its desktop changes, so the placement has to be the last word. Both messages still go out before a single flush.

  • Excluded and unidentifiable windows are no longer remembered (PERC-0057) A window covered by an exclusion was written to state.json on any user-initiated move, so Perch would restore later exactly what the exclusion asked it to leave alone. A window reporting neither app_id nor wm_class was stored under one shared key, so unrelated windows overwrote each other's geometry.

  • The maximize fallback places the window on the resolved target monitor (PERC-0057) Where a backend rejects native maximize, Perch substitutes the work-area rectangle. It resolved that against the window's own monitor, which is where the window sat before the same action moved it. The log line now names the rule, as docs/02 specifies.

  • maximized = false unmaximizes before a monitor or desktop move (PERC-0057) The pre-move unmaximize only ran when the action also carried a geometry, so on a compositor that ignores writes to a maximized window the move was dropped — the backend the rule exists for.

  • Percent geometry is clamped to the target monitor's work area (PERC-0057) Percentages are not range-checked when the config is parsed, so a negative or oversized one placed the window off-screen — against docs/07's promise that a rule cannot.

  • monitor = "all" is rejected when the config loads (PERC-0057) It validated and then failed on every window it matched. Nothing fans a single action out across every output; the valid forms are an output name, primary, current, or an integer index.

  • A match block combining catch_all with other fields is rejected (PERC-0057) catch_all short-circuits, so the other fields were never consulted and one stray checkbox matched every window. The settings dialog refuses the combination too, rather than writing a config it would then not load.

  • A profile's default_layout is applied when the profile activates (PERC-0057) The field parsed, seeded the documented sample and was editable in the dialog, and nothing read it: docking activated the profile and applied no layout. The config loader now also rejects a default_layout that names no declared layout.

  • Release and CI gate scripts no longer swallow their own failures (PERC-0056) The library harvest, the Flatpak manifest rewrite, the OBS upload and the CI/local lockstep check each had a path where a failure was discarded and the step reported success.

  • AppImage: a program launched from Perch no longer inherits the bundle's libraries (PERC-0056) Opening a link or the config folder from the tray launched the host browser or file manager against the AppImage's bundled AlmaLinux 8 libraries, which could crash it. The entry point now records the host's own library path and Perch restores it for anything it spawns.

  • Shutdown now cancels in-flight actions, and a failed state flush no longer strands the backend Neither step matched docs/01-architecture.md §Teardown order: tasks ran on into teardown, and an OSError from the state.json flush skipped backend.stop() entirely, leaving the KWin script loaded.

  • Background actions now log their failures to perch.log An intent task's exception was never retrieved, so asyncio reported it on the unconfigured root logger — it reached stderr and never the file a bug report attaches.

  • Hyprland queries now refuse when the backend is not connected The guard existed but had an empty body, so a stopped backend still queried the compositor and repopulated the caches stop() had cleared.

  • Perch now reports it when the Hyprland event stream closes A clean EOF left the backend marked healthy while it would never deliver another event.

  • A failed KWin start no longer leaves a script running inside the compositor stop() returned early unless the connection had fully succeeded, so any earlier failure left the bus name held and the injected KWin script loaded with nothing owning it.

  • A backend that fails to start no longer exits Perch with a traceback start() is a documented raiser on three backends — the ordinary trigger is a second copy failing to take the D-Bus name — and it ran unguarded after the tray was already visible. It now degrades to the UI-only mode docs/01-architecture.md §Startup has always described.

  • Restoring a window to a negative position on X11 no longer crashes _NET_MOVERESIZE_WINDOW coordinates are signed, but the wire packing is unsigned, so any window remembered off the left or top edge raised OverflowError outside the backend error taxonomy.

  • Minimising a window on X11 no longer makes Perch forget it permanently ICCCM has the window manager unmap a client to iconify it, and Perch read that as a close — emitting the terminal window_closed for a window still open, after which nothing could restore it.

  • A profile that cannot be deleted no longer corrupts the ones after it The failure was suppressed while the index remap still assumed it had happened, so every later field write landed on the wrong profile.

  • Adding a layout and then renaming it no longer fails the save The rename bookkeeping registered the new name as its own original, so the add step was skipped and a description was written to a table that was never created.

  • A failed save no longer silently discards the other panes' edits The document was rolled back but each pane that had already committed had frozen its baseline, so it reported clean and was never committed again. Panes now stay dirty until the write to disk has landed.

  • Apply then OK no longer deletes a rule or exclusion you did not touch The Rules and Exclusions panes never refreshed their baseline after a successful commit, so a second commit re-applied the first delete against the original index — which by then addressed a surviving entry.

  • A symlinked config.toml is followed instead of being replaced rename(2) acts on the link, so the first save moved the symlink itself to .bak and dropped a regular file in its place — silently detaching a config symlinked into a dotfiles repo.

  • Fractional percent geometries survive an unrelated layout edit They were rounded to whole percents on every re-serialisation, so 12.5% silently became 12% across the file.

  • Editing a layout no longer writes a config.toml Perch cannot read back A window type was serialised as a TOML array; the reader accepts only a comma-separated string and refuses an array outright. Because a layout edit re-serialises every entry, one unrelated change was enough.

  • A config.toml with a non-UTF-8 byte now falls back to the backup It raised UnicodeDecodeError, which was outside the caught set, so it skipped the fallback and surfaced as a traceback.

  • A config.toml from a newer Perch is refused rather than silently replaced by the backup It was treated as corruption, so an older config loaded and the newer one was rotated away on the next save. It now exits non-zero with a pinpoint message, per docs/02-state-format.md §Schema reference.

  • A missing config.toml beside an intact backup now recovers instead of reseeding The atomic write's own crash window leaves exactly that state. Perch seeded defaults over it, and the next save rotated the surviving backup away — losing every rule, layout and profile.

  • A window geometry saved during a state.json write is no longer dropped The dirty flag was cleared after the write rather than with the snapshot, so a change landing mid-write was marked clean without being saved.

  • A malformed record in state.json no longer stops Perch starting A missing field raised KeyError past the handler, so the documented .bak fallback never ran and the exception escaped startup.

  • A state.json from a newer Perch is no longer destroyed by an older one Rolling back a version discarded the newer file, then rotated it into .bak and overwrote both within seconds of the first window event — losing every remembered geometry. The store now latches read-only, as docs/02-state-format.md §Versioning and migration always specified.

Security

  • A badly written window-title rule can no longer freeze Perch (PERC-0069) Window-title rules now have a time limit. A rule that would take too long to check a title, for example one in an imported config, counts as "no match" after 50 ms and is named in the log so you can fix it. Perch now depends on the regex Python package.

  • The GNOME Shell extension checks the geometry it is handed (PERC-0050) set_geometry passed whatever JSON.parse produced straight to Mutter. Coordinates and extents must now be integers in a sane range, the desktop index non-negative, and the monitor a string.

  • Hyprland's instance signature is validated before it becomes a path (PERC-0050) It comes from the environment and is joined under $XDG_RUNTIME_DIR and, on older Hyprland, /tmp. A value carrying a separator walked out of the runtime directory and pointed the socket probe wherever the caller liked.

  • Importing a config validates the bytes it will write (PERC-0050) The import path read the file once for the diff and again to validate it, then wrote the first read. A file that changed in between was written without ever having been checked.

  • The KWin D-Bus service only accepts calls from the script it loaded (PERC-0050) The service sits on the session bus, and its methods feed the reducer and the state store. It now pins the bus name that sends ScriptReady — the script Perch loaded itself — and drops every later call from anywhere else.

  • The config temp file is opened O_NOFOLLOW (PERC-0050) Its name is predictable and its directory reachable, so a symlink planted there redirected the write. The symlinked target is still resolved deliberately — that one is the supported dotfiles workflow.

  • Perch's config and state directories are created owner-only (PERC-0050) They were left at the umask default, so on a shared machine every other account could read which applications you run and where you put their windows. The mode is re-applied to a directory an older install created.

v1.1.0

1.1.0 — onboarding, a single-file download, and a Flatpak that works

Added

  • First-run setup wizard, opening on the zero-config message (PERC-0003) src/perch/ui/onboarding.py adds the three-page QWizard specified in docs/08-ui.md. Page 1 states the only thing that matters — you don't need to configure anything, just move your windows and Perch remembers (that page is PERC-0004). Page 2 checks the setup: tray visibility, start-at-login, and which compositor was detected. Page 3 offers an optional route into the config dialog. Badge logic for the two real checks lives in pure check_*() functions, so it is tested without a live desktop; start-at-login is a preference and renders on/off, never a warning.

    Gated on a new [general] onboarding_completed, which defaults false and is absent from the seeded config, so a fresh install and an upgrading config both see the wizard exactly once. It is written on every exit — Finish, Cancel and window-close — so it never reappears unprompted; Finish also persists and applies the autostart choice, while Cancel changes no system setting. Settings → General carries a Run setup wizard again… button, which saves the dialog first and re-seeds the page afterwards so the two cannot fight over Start at login.

    The GNOME-Wayland AppIndicator guidance is now shared between the wizard's tray row and the standalone hint in perch.app, so the two cannot drift.

  • Tray menu: a Donate submenu and a Report an issue entry (PERC-0035) Donate opens as a submenu with one entry per destination in .github/FUNDING.yml; Report an issue opens the GitHub issue tracker. Both go through QDesktopServices.openUrl, which resolves to the OpenURI portal under Flatpak and needs no extra sandbox permission. The destinations are stated in perch.ui.links because .github/ is not shipped in any package, and a test asserts they match FUNDING.yml.

  • README install section for the openSUSE and Fedora RPM repository (PERC-0041) home:milnet:perch on OBS builds green for openSUSE Tumbleweed and Fedora and is now advertised, with copy-paste zypper and dnf commands. Both repositories were checked to be serving the RPM before the link went in.

  • local_CI.sh --docs — a documentation-only push no longer runs the full suite (PERC-0034) New tools/docs_check.py verifies every relative link in the docs set resolves and that no retired or forbidden string has crept outside the documents that record it. It runs as a third ci.yml job and, via --docs, alone — the pre-push hook selects that for a documentation-only push, turning roughly half a minute of gate into well under a second. docs/contributing-dev-setup.md owns what counts as documentation.

  • Self-contained AppImage — a single-file, zero-dependency Linux download Download → chmod +x → run; no Python, PySide6, or system packages for the user to install. Bundles the interpreter, Qt, and the xcb platform libraries (verified self-contained on a bare container). Recipe at packaging/appimage/; built and attached to releases by .github/workflows/release.yml.

Changed

  • Dropped --filesystem=xdg-config/perch:create from the Flatpak manifest (PERC-0036) It was justified as sharing one config file with a native install, which it never did: the sandbox redirects XDG_CONFIG_HOME, so Perch's config stays inside the sandbox and the host grant went unused. Flatpak Perch keeps its own config, which is the Flathub norm.

  • OBS submission targets the home:milnet:perch subproject and uploads a release tarball (PERC-0041) The subproject matches the convention already used for this account's other projects, and gives Perch its own repository list (Tumbleweed and Fedora, both x86_64). packaging/rpm/_service is deleted: its obs_scm entry was mode="manual", so OBS never ran it and the build died with no .obsinfo file found — and having any buildtime service pulled the obs-service-* packages into the build root, where Fedora could not resolve wget. Two targets failing for two unrelated reasons, from one mechanism nothing needed. packaging/submit/obs.sh now uploads the GitHub release tarball, which is all Source0 ever wanted; it also looked for ~/.oscrc when osc uses ~/.config/osc/oscrc, so it refused to run on a correctly configured machine.

  • Live and planned work moved to ROADMAP.md at the repo root; docs/11-roadmap.md is now history ROADMAP.md is a generated render of the roadmap store, and each item carries a PERC-NNNN id. docs/11-roadmap.md keeps its number and filename (the numbered sequence is the reading order and is stable, per docs/filename-standards.md) but is retitled "Roadmap history" and now says up front what it is: the record of how Perch reached v1.0.0 — the phased plan, each milestone's exit criteria and the evidence they were met, the ground rules, the known risks, and the Phase 2 / 2.5 research logs. It had still described itself as the source of truth for milestone ordering.

  • Tray "Pause restore" is now "Pause Perch" — a full panic switch Paused now suppresses every automatic placement (rules, layouts, and last-seen restore), not just the last-seen auto-restore, so no window is moved while paused. Manual "Snap focused" still applies. Renamed the TogglePauseRestore intent to TogglePause. See docs/08-ui.md §Menu structure.

Removed

  • Fedora COPR dropped as a distribution channel — OBS builds the Fedora RPM from the same spec (PERC-0041) It would have been a second build service producing one artefact from one spec, with a second set of credentials and a second thing to keep current. Fedora users are unaffected: OBS builds Fedora targets, and the %if 0%{?fedora} guards in the spec stay, because package names differ between the distro families regardless of who builds them. packaging/submit/copr.sh is deleted. The spec also cited a packaging/rpm/COPR.md that never existed.

Fixed

  • X11 backend: a display that goes away during start() no longer crashes Perch An X server can accept the connection and then reset it mid-handshake — a session ending as Perch starts, or a freshly launched Xvfb. python-xlib wraps that in Xlib.error.ConnectionClosedError, which subclasses Exception alone and so escaped start()'s OSError / ConnectionError handler as a raw traceback. It is now named at all three points it can fire (the Display() constructor, the subscription handshake, the first cache-priming round-trips) and raises the documented BackendUnavailable, tearing the half-open display down first, so the core takes its UI-only fallback path instead. Caught by CI run 33145715623.

  • Flatpak autostart: read the portal's Response, not the Request path (PERC-0037) org.freedesktop.portal.Background.RequestBackground returns the object path of an org.freedesktop.portal.Request; the outcome arrives later as that request's Response signal. portal_set_autostart read the returned path as the result dict, so every Flatpak autostart toggle raised AttributeError: 'str' object has no attribute 'get' in a task nobody retrieved — autostart silently never took effect and nothing was logged. It now correlates the Response the way PortalGlobalShortcutsProvider already did, returns whether autostart was granted, and logs a warning on a refusal, a non-zero response code or a timeout.

  • The tray-host probe no longer reports "no host" inside a Flatpak (PERC-0040) It let sdbus pick the default bus, which fails outright in a sandbox where DBUS_SESSION_BUS_ADDRESS is /run/flatpak/bus; every failure is classified as "no host", so the wrong answer was silent. On GNOME Wayland that fired the "install the AppIndicator extension" dialog at users who did not need it.

  • Tray icons no longer come out null on an installed layout (PERC-0039) The bundled-SVG fallback resolved a path relative to the source tree, which only exists in a dev checkout — under Flatpak it pointed at a directory that is not there, and the icon-theme lookup does not cover for it. The XDG data directories are now searched first, which name /app inside a Flatpak and /usr under an RPM; sys.prefix does not, because a Flatpak's interpreter comes from the runtime.

  • Flatpak tray icon now appears — granted the StatusNotifierWatcher talk-name (PERC-0038) The sandbox answered ServiceUnknown for org.kde.StatusNotifierWatcher, so Perch could neither probe for a StatusNotifier host nor register its item. Perch has no window, so this left the Flatpak with no interface at all.

  • KWin script now installs to the host path under Flatpak (PERC-0036) Inside a Flatpak, XDG_DATA_HOME is redirected into the sandbox, so the bundled KWin script was mirrored to a path KWin — which runs on the host — cannot read, leaving the KWin backend unable to drive anything. The target is now resolved from $HOME when sandboxed. PERCH_KWIN_SCRIPT_TARGET still overrides everything.

  • The RPM spec now builds on Fedora — five bugs no local check could catch (PERC-0041) Publishing to OBS for the first time found them all. rpmspec -P passed throughout, because every one of them only fails inside a real build root: openSUSE-only BuildRequires names (appstream-glib, libxml2-tools) left Fedora unresolvable; a %build comment naming %pyproject_wheel unescaped made rpm call the macro with the sentence as arguments; %{_metainfodir} is Fedora-only and undefined on openSUSE; the build selected a python flavor the build root did not ship; and the icon directories were unowned. Fedora now produces perch-1.0.0-7.1.noarch.rpm.

  • The Flathub manifest now builds — it was based on the wrong Qt toolkit and could not be built by anyone (PERC-0042) It based on com.riverbankcomputing.PyQt.BaseApp, the PyQt base app, for an application built on PySide6; it targeted a KDE runtime eight months stale; and its Python dependency includes were commented out and deliberately not committed, to be generated at submission time — so a fresh clone could not build it at all. It now uses io.qt.PySide.BaseApp//6.11 on org.kde.Platform//6.11, with the dependency closure sha256-pinned and committed as python3-deps.yaml, which is what Flathub's network-free builders require. Verified by a real offline build of the submission manifest.

    New alongside it: generate-pip-sources.sh regenerates the closure from pyproject.toml, flatpak-build.sh reproduces Flathub's build locally and smoke-tests the result, and flathub.json restricts the buildbot to the arch the pinned wheels cover. packaging/submit/flathub.sh no longer does the work itself; it also no longer targets the wrong base branch — a new app is PR'd against new-pr, not master.

  • Docs no longer promise an "Include last-seen geometries" export checkbox that does not exist docs/02-state-format.md described an export checkbox for shipping state.json alongside the config, contradicting its own Export bullet and docs/08-ui.md. Export copies config.toml verbatim and has never included state.json. The section now also states a round-trip criterion: what must survive an export/import, what need not, and what travels but stays inert until the target machine has matching monitors. (PERC-0031, PERC-0032)

  • "Reapply rules now" is no longer a silent no-op The tray "Reapply rules now" action was wired to recompute_topology(), whose topology-key early-return meant no window was re-evaluated unless the monitor layout had changed. It now calls a dedicated Reducer.reapply() that re-evaluates every open window regardless of topology, matching the ReapplyRules intent's contract.

  • Tray snap-preset labels are now translatable The built-in snap-preset labels (Left half, Right half, …) were marked for extraction with a context-less QT_TR_NOOP but translated at runtime under the perch.ui.tray context, so a translator's work never reached the tray menu. They now use QT_TRANSLATE_NOOP("perch.ui.tray", …) and translations/perch_en.ts was regenerated (also picking up strings added since M3 that had never been re-extracted).

v1.0.0

Perch v1.0.0

First stable release of Perch — a persistent, compositor-aware window geometry manager for Linux desktops.

Sits in the system tray, remembers where each window belongs, and restores position / size / monitor / virtual desktop when a window reopens. Ships a rules engine, named layouts, per-monitor profiles, snap presets, and global hotkeys.

Highlights

  • Tray + PySide6 config dialog (General / Windows / Rules / Layouts / Profiles / Hotkeys / Exclusions / Import-Export) with dark-theme support, keyboard navigation, and accessible labels.
  • Windows pane — live table of tracked windows with per-row Save-as-last-seen / Forget-last-seen / Apply-preset buttons.
  • Tray "Snap focused window" submenu — maximise, half-halves, quarters, Centre (keep size), Centre (60 %).
  • Rules engine — match by app_id / wm_class / title (regex) / PID / type, apply geometry / snap / monitor / desktop / maximized. 100 % line-covered evaluator.
  • Full backends for X11 (any EWMH WM) and KWin / Plasma Wayland (bundled JS script + long-poll IPC). Community stubs for Mutter, Sway, Hyprland.
  • Global hotkeys via the xdg-desktop-portal GlobalShortcuts interface, with KGlobalAccel and XGrabKey as platform fallbacks.
  • Autostart via XDG .desktop files (non-Flatpak) or the Background portal (Flatpak).
  • Packaging for Flathub, openSUSE OBS, Fedora COPR, AUR (perch stable + perch-git), and the KDE Store.

What's new since the design-complete snapshot

Everything. See CHANGELOG.md for the full per-milestone history, including the post-smoke-test fixes (M9.f.1…M9.f.15) that wired every dialog pane to real functionality, fixed the KWin script's Qt is not defined bug, and shipped the intent-dispatch audit that gates CI against future shipped-stub regressions.

Install

Packaging artefacts are authored in packaging/ — Flathub, OBS, COPR, AUR, KDE Store. Flathub submission, OBS project, and AUR push follow from this tag.

From source:

git clone https://github.com/milnet01/perch.git
cd perch && git checkout v1.0.0
pip install --user --break-system-packages -e ".[dev]"
perch

Requires Python 3.12+ and PySide6 ≥ 6.8.

License

GPL-3.0-or-later.