feat: dim screen on idle before screen-off#25
Open
jibsta210 wants to merge 4 commits into
Open
Conversation
Adds a "dim" stage to cosmic-idle that runs before the existing screen-off stage. After a configurable inactivity period (separate idle notification), the backlight fades smoothly down to a low brightness (default 7%) over ~300ms. Any input restores it. When the next idle stage powers the screen off, we snap-restore the backlight to its pre-dim value *after* the screen goes dark so that when the user later unlocks, the hardware is already at the original brightness rather than stuck at 7%. Why a separate signal path: * dim writes go through logind's Session.SetBrightness, not the cosmic-settings-daemon DisplayBrightness property. SetBrightness writes the kernel sysfs value directly without firing the property-change signal that the OSD listens to, so the brightness slider doesn't pop up every time the screen is about to sleep. (This pairs with the hotkey-only brightness signal patch in cosmic-osd / cosmic-settings-daemon.) * `snap_restore` is called from `fade_done` after the screen has been switched to Off, so the user never sees the snap. Files: * `src/dim.rs` (new): DimState — tracks the pre-dim brightness, drives the fade, talks to logind. * `src/main.rs`: dim_idle_notification + dim_state fields, update_dim_idle handler, snap_restore call in fade_done. * `cosmic-idle-config/src/lib.rs`: dim_idle config keys (timeout, target percent, fade duration). Defaults: 7% target, 300ms fade. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…s HDR state across the timeout cycle
cosmic-idle previously called `output_power.set_mode(Off)` on the wlr
output_power_v1 protocol at the end of its fade-to-black animation.
On cosmic-comp that routed through ThreadCommand::DpmsOff →
DrmCompositor::clear() → atomic commit disabling CRTC + planes +
framebuffer attachment. On Intel xe + Tandem OLED with HDR enabled,
the recovery atomic commit on wake-up failed (kernel atomic_check
rejected the re-enable because the CRTC color pipeline blob refs
became stale), leaving the panel stuck on a black/garbled screen
that required a hard reboot.
Manual `loginctl lock-session` worked fine because it bypasses the
DPMS path entirely — the lockscreen renders on a healthy compositor.
The bug was strictly cosmic-idle's pre-lock screen-off step.
Replace the DPMS-off with a direct backlight write to 0 via
systemd-logind's Session.SetBrightness (same path dim.rs already
uses for the dim-on-idle feature). The panel goes physically dark
because drive current is 0 (LCD: backlight LED off; OLED: pixel
drive current = 0 → zero light emitted), but everything on the
software side stays running:
- CRTC + planes + primary framebuffer stay attached
- HDR signaling state (Colorspace, HDR_OUTPUT_METADATA) untouched
- Hardware color pipeline blobs (DEGAMMA_LUT, CTM, GAMMA_LUT) all
still committed on the CRTC
- cosmic-comp keeps rendering frames (small extra power cost
vs deep DPMS, but acceptable: that's the whole point of
"screen off" not being "suspend")
Wake path:
- update_screen_off_idle(false) reads `saved_panel_brightness`
and writes it back via SetBrightness — instant snap, panel
visibly comes back to its pre-screen-off level.
- dim_state.restore() then animates from there to full original
brightness if dim was active before screen-off (existing path,
unchanged behavior).
- The fade-to-black layer-shell surface is destroyed (existing,
unchanged).
A new field `saved_panel_brightness: Option<u32>` on State covers
the no-dim case — without it, a user who disabled dim would wake
with brightness still at 0 (dim_state.restore would be a no-op
since dim_state has no original_brightness).
Side effects from removing snap_restore: dim_state stays "active"
across the screen-off period, so dim_state.restore on wake animates
brightness from current (0) up to the user's pre-dim original. This
gives a smoother visual recovery than the old snap_restore-then-DPMS
sequence.
dim::read_backlight and dim::set_brightness_via_logind are now
pub(crate) so main.rs can reuse them without code duplication.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously cosmic-idle locked the session 500ms after `fade_done`
fired (i.e. right after the fade-to-black overlay finished animating
in). cosmic-greeter would spawn while the panel was at backlight=0,
PAM auth would start, and Howdy face-auth would fire blind — the
panel was off, so the camera saw nothing usable, and Howdy timed
out (default ~5 seconds) before the user got anywhere near the
laptop. By the time the user came back and woke the screen, Howdy
had already failed, PAM had fallen through to password, and the
lockscreen was sitting in password-only mode. Howdy was effectively
useless on every idle-triggered lock.
New flow:
- fade_done sets `lock_on_wake = true` and writes backlight=0.
NO lock_screen call yet, NO lock-session timer.
- update_screen_off_idle(false) on wake:
1. restore backlight to saved value (panel comes on)
2. if lock_on_wake: call lock_screen() → cosmic-greeter
spawns, PAM auth starts, Howdy fires *now* with the panel
on and the user actually looking at the camera
3. schedule fade_surface destroy in 500ms (after greeter has
had time to put up its session-lock surface)
The fade-to-black overlay is now KEPT ALIVE through the off period
AND the wake-to-lockscreen transition (~100-500ms while
cosmic-greeter spins up). Without that, the wake would briefly
expose the unlocked desktop between backlight restore and lockscreen
appearing — security regression. With it, the user sees only:
opaque black overlay → lockscreen, never a flash of desktop.
The 500ms fade_surface destroy delay is conservative; cosmic-greeter
typically takes ~100-200ms. Extra 300ms of black-overlay-under-
lockscreen is invisible (lock surface is above layer-shell Overlay).
Manual lock paths (loginctl lock-session via shortcut, etc) are
unchanged — they don't go through this code at all.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously fade_done unconditionally saved current brightness into
saved_panel_brightness before writing 0. If dim was active when the
screen-off timeout fired, that saved value was the *dimmed* level
(7% of original by default).
On wake, two restore paths fire and race:
1. dim_idle's resumed event → update_dim_idle(false) →
dim_state.restore → spawns fade_thread that animates panel
from 0 → original brightness (e.g. 0 → 1280)
2. screen_off_idle's resumed event → update_screen_off_idle(false)
→ write saved_panel_brightness (the dimmed value, e.g. 26)
Order of resumed events from the wayland server isn't guaranteed.
If (1) fires and completes before (2), the panel ramps up to full,
then (2) snaps it back down to dim. End state: stuck dim, requires
manual wiggle to undim. (User-reproducible bug.)
Fix: only save in fade_done if dim is NOT already active. When
dim is active, dim_state already has the user's pre-dim original
and dim_state.restore on wake handles the full restore. With the
save skipped:
- dim active path: only dim_state.restore writes brightness
- no-dim path: only saved_panel_brightness write happens
- never both writing to the same panel from different threads
Tested on Tandem OLED + intel_backlight + cosmic-idle's default
30s/120s/7% timings. No more stuck-dim wake.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
Summary
Adds a "dim" idle stage that runs before the existing screen-off stage. After a configurable inactivity period, the backlight fades smoothly down to a low brightness (default 7%) over ~300ms. Any input restores it. When the next idle stage powers the screen off, we snap-restore the backlight to its pre-dim value after the screen has been switched to Off, so when the user later unlocks the hardware is already at its original brightness rather than stuck at 7%.
Motivation
Currently
cosmic-idlehas a binary screen-on / screen-off transition. A pre-dim stage gives the user a visible "you're about to sleep" cue and saves a small amount of energy on long idles. It also matches the typical behaviour of macOS / Windows / GNOME idle handling.The "snap-restore after screen-off" trick is the subtle bit: if you don't restore the brightness while the screen is dark, the user comes back from lock to a backlight stuck at 7% until they wiggle the mouse hard enough to wake the dim restore — which is jarring. Doing the restore while the panel is off makes it invisible.
Implementation
src/dim.rs(new):DimStatestruct that:Session.Brightnessproperty,Session.SetBrightnesscalls on a calloop timer,start_dim,restore(smooth fade back), andsnap_restore(instant, used after screen-off).src/main.rs:dim_idle_notificationanddim_statefields on the main app state.update_dim_idlehandler that subscribes to the new dim-stageIdleNotificationand starts the fade.snap_restore()call insidefade_done()afterset_mode(Off)so the user never sees the snap.cosmic-idle-config/src/lib.rs: three new keys —dim_idle_timeout(seconds),dim_target_percent(u32, default 7),dim_fade_ms(u32, default 300).Why
Session.SetBrightnessrather than the daemon'sDisplayBrightnesspropertySetBrightnesswrites the kernel sysfs value directly without firing the property-change signal that the OSD listens to, so the brightness slider doesn't pop up on every fade frame. (This pairs with pop-os/cosmic-osd#194 / pop-os/cosmic-settings-daemon#144 for the hotkey-only OSD path; without those, this PR alone is still well-behaved with the OSD because logind doesn't trigger it.)Testing
Daily-driven for several weeks. Verified that:
AI Disclosure
Patches were drafted with assistance from Claude (Anthropic). Commits include
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>trailers. I understand each change and will respond to review feedback. I'm aware of the AI-PR policy in the template — happy to rework or close if maintainers prefer.