Conversation
A laptop panel that advertises 165 Hz runs at 165 Hz on battery too, and nothing in the shell offers to trade that back for runtime. Setting it by hand does not survive, since the rate lives in monitors.lua and any reload puts the configured one back. This adds a service that picks the rate from the power state: the fastest rate the panel advertises for the resolution it is already on while running on mains, and the slowest one otherwise. Only rates at the current resolution are considered, so the mode cannot change underneath the user, and only internal outputs are touched, matched with the eDP/LVDS/DSI test the monitor helpers in bin/ already use. A panel offering a single rate is left alone. It is off unless shell.json turns it on, because changing what someone's display does on battery is not a thing to start doing on upgrade. `refreshRate.threshold` is the charge below which the slow rate applies even on mains, defaulting to 50. A battery that cannot be read only relaxes that threshold rather than forcing the slow rate, so a laptop with a dead gauge still runs fast while plugged in. The decision is a pure function in RefreshRateModel.js so it is covered by test/shell.d/refresh-rate-test.sh: 23 assertions over the internal-output test, rate extraction, the threshold, the mains requirement, the rounding Hyprland's reported rate needs, and rejecting an output name that would have to be quoted into Lua.
The service applied each spec and dropped the result. Nothing about a refused eval reached the user or the log: the panel simply stayed on the rate it had, which is indistinguishable from the policy deciding not to change it.
sterre-g
force-pushed
the
feat-refresh-rate-power
branch
from
September 1, 2026 17:26
0f6c156 to
26fc18a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A laptop panel that advertises 165 Hz runs at 165 Hz on battery too, and nothing in the shell offers to trade that back for runtime. Setting the rate by hand does not survive either: it lives in
monitors.lua, so the next reload puts the configured one back.This adds a service that picks the rate from the power state. On mains it uses the fastest rate the panel advertises for the resolution it is already on. Otherwise it uses the slowest.
Scope, and what it will not touch
Only rates at the current resolution are considered, so the mode cannot change underneath the user. Only internal outputs are driven, matched with the same
eDP|LVDS|DSItest the monitor helpers inbin/already use, so external displays are never touched. A panel advertising a single rate is left alone.It is off unless asked for. Changing what someone's display does on battery is not a thing to start doing to them on upgrade.
thresholdis the charge below which the slow rate applies even on mains, defaulting to 50. A battery that cannot be read only relaxes that threshold rather than forcing the slow rate, so a laptop with a dead gauge still runs fast while plugged in instead of being pinned low forever.The service follows
UPower.onBatteryfromQuickshell.Services.UPowerfor the mains transition, rather than introducing a second source of power state, and re-checks on a minute timer, because charge crossing the threshold is not something UPower raises an event for.Relationship to #6402
#6402 adds manual refresh-rate control to the Display panel. This is the automatic policy, and they are complementary rather than competing: that PR gives you the picker, this one decides when nobody has picked.
They do interact, and it is worth deciding deliberately rather than discovering it. If both land and this service is enabled, a rate chosen by hand from the panel is overwritten on this service's next tick. The clean resolution is for a manual pick to suppress the automatic policy until it is handed back, which is what I do locally, but that belongs in whichever of the two lands second rather than being guessed at here.
#8595 is the same shape applied to idle timeouts, using separate values on AC and battery. If a general "what changes on battery" grouping is wanted in
shell.jsonrather than a key per feature, those two are the cases to design it against.#8695 caps the screensaver's frame rate at the panel's refresh rate. It reads the rate this service changes, so with both in place the screensaver follows the panel down on battery, which is the intended direction and needs nothing from either side.
Tests
test/shell.d/refresh-rate-test.sh, 23 assertions against the pure decision inRefreshRateModel.js: the internal-output test, rate extraction at the current resolution only, the threshold being inclusive, the mains requirement holding at any charge, an unreadable battery relaxing rather than forcing, the rounding Hyprland's reported rate needs (165.02000against the165.02that was asked for), refusing an output name that would have to be quoted into Lua, and the whole decision over a two-display laptop.Checked that they are load-bearing rather than decorative: removing the internal-output filter fails the two-display assertion, and dropping the mains requirement fails the on-battery one.
Also exercised live rather than only in unit tests: loaded as a plugin against a running shell on a two-display laptop, with the threshold set above the current charge it dropped the panel from 165.02 to 60.01 and left the external on 143.91, and with the threshold below it the panel went back to 165.02.
2026-09-01
Rebased onto current
quattro(b71dcad) with no conflicts.hyprctl evalreached neither the user nor the log, and a panel that stayed on its old rate was indistinguishable from the policy deciding not to change it. It now warns with the exit code and whatever hyprctl printed. This is the same failure mode Fix enabling and disabling a display doing nothing #7036 is about, in a service rather than a panel.Quickshell.Services.UPowerexplicitly as the power-state source, since "follows UPower" did not make clear that this adds no second source of truth.refresh-rate-test.shpasses on the rebase.