Skip to content

[ryoku] wm: emit enable/disable DPMS actions instead of state toggle - #278

Closed
Sipper1236 wants to merge 1 commit into
Ryoku-dev:mainfrom
Sipper1236:fix/hyprland-dpms-action
Closed

Sipper1236 wants to merge 1 commit into
Ryoku-dev:mainfrom
Sipper1236:fix/hyprland-dpms-action

Conversation

@Sipper1236

Copy link
Copy Markdown
Contributor

What changed

Fixes output.power in the Hyprland provider to emit the correct Lua dispatcher field.

Root cause: Hyprland's hl.dsp.dpms() reads the toggle field named action (values: enable, disable, toggle). The previous code emitted state, which is silently ignored by the dispatcher — so an explicit on request could leave the display off.

Fix: Map output.power on → action = "enable", output.power off → action = "disable", preserving the optional monitor selector.

Tests

  • Updated TestActEmitsLuaDialect with the corrected literal.
  • Added TestActOutputPowerIsIdempotent — pins the new literal and repeats each request to prove an explicit enable cannot re-disable the display during resume (the bug that caused repeated flashing).

CI

Adds a wm job to .github/workflows/go-unit-tests.yml so the provider contract is now gated in CI. This module was previously untested, which is how the defect shipped.

Upstream status

Checked September 26, 2026 against the local ryoku-arch checkout and the current ryoku-dev/ryoku-arch GitHub sources. The display-power bug is present upstream. The fix should change the provider and its test to emit the correct dispatcher form.

Sources confirmed:

Note

Note: Fixed: display power requests now use the Hyprland action field, so an explicit on no longer leaves the screen off

Hyprland's Lua dispatcher reads the toggle field named action (enable,
disable, toggle); a state key is silently ignored, so an explicit on
request could leave the display off. Map output.power on/off to
enable/disable, preserving the optional monitor selector.

Add TestActOutputPowerIsIdempotent, which pins the new literal and
repeats each request to prove an explicit enable cannot re-disable the
display during resume.

Add a wm job to go-unit-tests so the provider contract is gated in CI;
it was previously untested, which is how the defect shipped.

Note: Fixed: display power requests now use the Hyprland action field, so
an explicit on no longer leaves the screen off
neur0map added a commit that referenced this pull request Sep 26, 2026
The DPMS dispatcher's field is `action`, and an explicit on or off must emit
the same literal every time it is called, never a toggle: the resume path can
re-issue an enable while the lock surface is still appearing, and a toggling
field would flip the panel back off and flash the screen after wake (#264,
#258, #277). The provider already maps on/off to the action field; this adds
the regression test that holds that invariant, adapted from @Sipper1236's
PR #278, whose code fix the seam already carried.

Note: Fixed: a test now pins that re-asserting display power cannot toggle it off
@neur0map

Copy link
Copy Markdown
Collaborator

Thanks for this, and for the idempotency test especially. The seam already maps output.power onto the DPMS action field on the dev line, so the code half was in place; I've ported your test (credited to you in the commit) onto unstable-dev to pin the no-toggle invariant, and it ships to main with the next fast-forward. Closing as superseded rather than merging into main directly, since main is kept strictly fast-forwarded from the dev branch. Your fix is landing either way, cheers.

@neur0map neur0map closed this Sep 26, 2026
itsKontra added a commit to itsKontra/ryoku-fedora that referenced this pull request Sep 27, 2026
* [system] bound the qylock guard waits and cap the cutover retry loop

The launch guard and the generation guard take the same exclusive launch
lock, and the generation guard holds it for its whole life on purpose.
Requesting a launch guard while a generation guard was up therefore waited
for a lock that could never be granted, and ensure_qylock_launch_guard
polled it with no timeout: ryoku update sat at shell-quiesce forever with
the shell unit deactivating (Ryoku-dev#273). The generation guard now satisfies the
launch boundary itself (while it is up, launches are already excluded), and
both ensure loops fail loud after 30 s naming the stale holder instead of
polling forever. The launch guard's keep-alive also closes the lock fds now,
the same leak the generation guard already fixed.

wait_qylock_cutover retried the whole cutover in an unbounded loop and every
attempt stops and re-reaches ryoku-session.target, so one persistent failure
restarted the desktop every few seconds until units hit start-limit (Ryoku-dev#275).
It now makes three attempts with backoff, leaves the running session
untouched when it gives up, and says which command retries by hand; the
transient unit keeps systemd's own start limit as a backstop instead of
StartLimitIntervalSec=0.

Note: Fixed: ryoku update no longer hangs at shell-quiesce on the qylock guards, and a failing lockscreen cutover no longer restart-loop the desktop

* [system] clamshell reclaims a leaked lock, fails loud

A daemon from before the fd-leak fix orphaned its udevadm monitor with the
lock fd open; that orphan pins the flock across an upgrade, and the new
daemon answered the contention with a silent exit 0, indistinguishable from
a healthy second instance, so Restart=on-failure never retried and nothing
reached the journal (Ryoku-dev#274). The daemon now reclaims non-daemon holders in
rounds (killing a leaked holder does not close the fd in the children that
inherited it), treats only the recorded daemon or a ryoku-clamshell process
as a legitimate "already running", and exits 1 with the fuser command when
the lock is held by something it cannot classify.

session-start at login ran from the compositor's exec where its stderr
vanishes; a silent early exit left a blank desktop and a held sleep
inhibitor with no evidence anywhere. The autostart lines on both desktops
now call session-start-logged, which appends stderr and the exit code to
~/.local/state/ryoku/session-start.log.

Note: Fixed: the clamshell daemon recovers a lock leaked by an older version instead of staying silently dead, and login session-start failures now leave a log

* [system] the power-cutover post hook survives its own first install

The hook pair stages the pre-upgrade helper at /run/ryoku-power-cutover in
PreTransaction and execs it in PostTransaction. On the transaction that first
installs the pair the staging hook is not on disk yet, so /run never appears
and the PostTransaction execv fails with "No such file or directory", leaving
pacman reporting a failed hook on exactly the upgrade that introduces the
guarded cutover (Ryoku-dev#272). The post hook now execs the installed binary, which
prefers the staged copy when it exists (same semantics as before, and a
staged copy of this code cannot exec itself) and falls back to itself when
nothing was staged.

Note: Fixed: the upgrade that first installs the guarded power cutover no longer reports a failed pacman hook

* [ryoku] cli: rollback across the packaging split, keep the db on failure

A release that predates the compositor split serves no ryoku-desktop-hyprland
or ryoku-desktop-niri, and the installed meta's exact-version pins on six
packages make the whole downgrade transaction unsatisfiable, so every rollback
across the split failed and burned a snapper snapshot (Ryoku-dev#271). A channel move
or rollback now drops installed split metas the target repo does not serve
before the transaction; the downgrade of ryoku-desktop re-ties the graph.

The failure cleanup also deleted the [ryoku] sync database after ANY failed
transaction, which turned one clear dependency error into a "target not
found" storm on the next run and destroyed the evidence. The db is dropped
only when the failure actually names the database or its signature.

Note: Fixed: ryoku rollback to a release from before the compositor split works again, and a failed update no longer deletes the package database that explains it

* [installation] script installer survives an oh-my-zsh-git box

ryoku-oh-my-zsh provides and replaces both upstream zsh frameworks, but the
desktop transaction still aborted on a box carrying oh-my-zsh-git: a plain
removal under --noconfirm refuses while an installed plugin package depends
on the framework, and pacman then reports the dependency conflict as a
failed transaction preparation. The conflict step drops the upstream
framework with -Rdd before the desktop install; its plugins re-resolve
against ryoku-oh-my-zsh's provides in the same step, and the recorded undo
restores the framework.

Note: Fixed: converting an Arch box that runs oh-my-zsh or oh-my-zsh-git no longer fails at the desktop package step

* release: bump unstable to 0.74.7-beta.20

* [ryoku] i18n: regenerate translations

* [ryoku] a palette change reaches the cursor without a re-pick

Recoloring the Material cursor for a new palette rewrote the image files, but
a running compositor keeps the loaded cursor theme cached in memory, so the
new colours only appeared after re-picking the theme or logging back in. The
seam gains cursor.reassert: Hyprland re-runs setcursor with the store's
resolved theme (DYNAMIC included), niri regenerates its config tree, which it
watches. The daemon calls it right after a successful recolor, best-effort,
so a provider without the action simply answers with an error nobody sees.

Note: Fixed: the pointer recolours with the wallpaper again, without re-picking the cursor theme

* [ryoku] cli: name the backlight Ryoku writes to when a box has several

A laptop can expose more than one /sys/class/backlight node, and exactly one
of them is a phantom on that hardware; a brightness control that "only has
off and full" is what writing percentages to the wrong node looks like from
the user's seat. With more than one device present the doctor finding now
names the node every Ryoku brightness writer targets (media keys, OSD daemon
and bar slider all call the same helper) plus each candidate's current and
maximum level, so the next report from such a box pinpoints the phantom in
one line instead of a guess.

Note: New: ryoku doctor reports which backlight device Ryoku drives, and every candidate's level, on multi-backlight laptops

* [ryoku] ryogami names why a wallhaven download was refused

Note: Fixed: a refused wallhaven download now says why in the log instead of doing nothing

* [ryoku] sync the shell daemon's vendored seam copy

The cursor.reassert action landed in the seam; the daemon builds against its
vendored copy, which must match or the pre-push vendor gate fails.

* release: bump unstable to 0.74.8-beta.20

* [ryoku] cli: name the reverse-PRIME first-commit hazard in doctor

On a hybrid laptop whose connected panel is driven by the iGPU while the
render pin puts NVIDIA leftmost in AQ_DRM_DEVICES, some kernel/driver
combinations fail the session's very first cross-GPU commit once ("amdgpu:
Not enough memory for command submission"), leaving a live compositor with a
black panel until reboot (Ryoku-dev#270). Clearing the pin makes it disappear, but the
pin is deliberate Ryoku policy on every multi-GPU machine, so doctor reports
the combination instead of touching it: panel driver from sysfs, pin's first
driver resolved through ryoku-gpu order, and the two commands that move the
pin either way. Detection is pure over its inputs and fixture-tested.

Note: New: ryoku doctor flags a NVIDIA-first render pin over an iGPU-driven panel, the combination behind a black panel on first commit

* release: bump unstable to 0.74.9-beta.20

* [ryoku] i18n: regenerate translations

* [tooling] the install gate asserts the hook contract that actually ships

The container-install gate pinned the post-transaction hook's Exec to the
staged /run path, which is exactly the design that fails on the transaction
first installing the hook pair (Ryoku-dev#272): the staging hook is not on disk yet, so
nothing ever writes /run/ryoku-power-cutover and the execv fails. The gate now
asserts the shipped contract instead: the hook execs the installed binary, and
that binary prefers the staged copy (RYOKU_CUTOVER_STAGED guards the exec), so
both the first-install fallback and the preserved-executor semantics stay
proven on every publish.

Note: Fixed: the publish gate no longer rejects the first-install-safe power-cutover hook

* release: bump unstable to 0.75.0-beta.20

* [ryoku] ryogami: matugen app-template toggles write via the live verb

The toggle row called `ryoku-hub hypr matugen set`, a command path that does
not exist: the hub printed usage and exited 0, and the write was detached, so
the failure was silent and every App Templates toggle looked dead while
matugen.json never changed. The row now uses the same merge writer as the rest
of the picker and the Hub (`ryoku-hub desktop matugen set`), so the watched
FileView reloads and the switch follows the click (issue Ryoku-dev#276).

Note: Fixed: the App Templates toggles in Wallpaper Picker > Settings > Matugen respond to clicks again

* release: bump unstable to 0.75.1-beta.20

* [ryoku] rashin: pass the hermes installer only flags that build knows

Setup hardcoded --non-interactive --skip-browser --skip-computer-use, but the
official installer exits 1 on any option it does not recognize, so once
upstream dropped --skip-computer-use every "Set up Hermes agent" died with
"unknown option: --skip-computer-use" (issue Ryoku-dev#279). Setup now downloads the
script and passes --non-interactive (the guard against the hidden tty prompt)
plus each optional flag only when the downloaded script names it, so upstream
growing or dropping an option can no longer break the one-click flow.

Note: Fixed: setting up the Hermes agent from the Hub works again on current installer builds

* [tooling] publish: populate the base keyring before the container -Sy

The container-install gate does `pacman -Sy --noconfirm --needed git` as its
first step in a fresh base image. A fresh container ships an unpopulated pacman
keyring, so that very first sync can fail on the image's OWN repository
signature ("error: cachyos: signature from ... is invalid") before any Ryoku
package is in the transaction, wedging the whole release on a mutable base image
the build did not change. The step now initializes and populates the base
keyring (archlinux, plus cachyos on the CachyOS matrix leg) before syncing, and
refreshes the CachyOS signing key from the upstream keyserver once on a retry,
the same recovery the installer documents for a rotated key. SigLevel is never
lowered, so a genuinely unsigned Ryoku package still fails the gate.

Note: Fixed: the release publish no longer aborts when a base container's pacman keyring is unpopulated

* release: bump unstable to 0.75.2-beta.20

* [ryoku] wm: pin that output.power on/off is idempotent

The DPMS dispatcher's field is `action`, and an explicit on or off must emit
the same literal every time it is called, never a toggle: the resume path can
re-issue an enable while the lock surface is still appearing, and a toggling
field would flip the panel back off and flash the screen after wake (Ryoku-dev#264,
Ryoku-dev#258, Ryoku-dev#277). The provider already maps on/off to the action field; this adds
the regression test that holds that invariant, adapted from @Sipper1236's
PR Ryoku-dev#278, whose code fix the seam already carried.

Note: Fixed: a test now pins that re-asserting display power cannot toggle it off

* release: bump unstable to 0.75.3-beta.20

* [test] container-install: name the lid drop-in's real owner on failure

The gate died with "ryoku-desktop does not claim
/etc/systemd/logind.conf.d/10-ryoku-lid.conf" while pacman -Qo found an owner,
and the message could not say who: a sibling package claiming the path, a
scriptlet replacing the file outside the database, and a missing install line
all looked identical. The failure now carries the owner pacman reports, so the
next run is decisive instead of a mystery.

* release: bump unstable to 0.75.4-beta.20

* [test] container-install: drain the package list before matching

The claim check piped `pacman -Ql ryoku-desktop` into `grep -q` under
pipefail. grep exits at the first match, which SIGPIPEs pacman mid-write on a
package this large, the pipeline reports failure, and the gate dies with "does
not claim" on a package that does: the same run's `pacman -Qo` named
ryoku-desktop as the owner, and a packaged-box probe against the published
channel showed the file on disk, owned, and listed. The list is now captured
whole before matching, so the assertion cannot lose a race with its own
producer.

* release: bump unstable to 0.75.5-beta.20

* [system] Harden the merged session start, clamshell and rollback

Review fixes on top of the upstream merge.

session-start-logged ran start_login_session on the left of ||, which
turns errexit off inside it, so a failed step (a guard timeout) carried
on and the log recorded exit 0. It now runs the step as its own process.

The qylock cutover waiter retries three times itself, but its unit also
restarted on failure and each pass outlasts the default start limit, so
a persistent failure looped. The unit no longer restarts.

The clamshell lock reclaim listed its own forks, which hold fd 9 under
the ryoku-clamshell comm, as a live daemon, so a leaked holder was never
reclaimed. The holder scan now runs with fd 9 closed, and "already
running" needs a recorded PID that is still ryoku-clamshell.

A rollback across the compositor split treated a failed or empty repo
listing as "serves nothing" and removed the compositor metapackages. It
now keeps them when the listing cannot be read.

The reverse-PRIME doctor note points at Hybrid graphics mode, which
keeps the pin cleared, instead of ryoku-gpu disable, which the next
doctor run undid.

* [ryoku] shell: read a sun window that spans midnight as daytime

isDaytime projects sunrise and sunset onto today's clock. When the
sunset clock is earlier than the sunrise clock, as with a forecast in
another timezone than the box, nothing read as daytime and the sun
theme stayed dark all day. TestResolveModeSunFollowsTheWindow hit the
same case whenever CI ran within an hour of midnight.

Note: Fixed: The follow-the-sun theme no longer stays dark when the forecast's day crosses midnight on the box's clock

---------

Co-authored-by: carlos <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ryoku-i18n <noreply@ryoku>
Co-authored-by: itsKontra <[email protected]>
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.

2 participants