max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
3 files changed,
+170 insertions,
-1 deletion
| @@ -812,6 +812,45 @@ | |||
| 812 | 812 | done; \ | |
| 813 | 813 | echo "alloy-shot: $(echo "$modes" | wc -l) bound modes, all handled" | |
| 814 | 814 | ||
| 815 | + | # ===================================================================== | |
| 816 | + | # alloy-dim — the lock warning, and the two ways it goes quiet. | |
| 817 | + | # | |
| 818 | + | # swayidle calls this at 270 seconds and again on resume. Neither call is | |
| 819 | + | # visible: if the script is missing or unparseable, swayidle logs to its own | |
| 820 | + | # output and the observable result is the old behaviour, full brightness | |
| 821 | + | # straight to a lock, which is exactly the ambush the stage was added to | |
| 822 | + | # remove. A regression here restores a bug rather than causing a new one, | |
| 823 | + | # which is the kind that survives. | |
| 824 | + | # | |
| 825 | + | # The subcommands are read back out of the shipped sway config and asked of | |
| 826 | + | # the script, the same way alloy-shot's modes are, so a renamed one fails | |
| 827 | + | # here instead of on the idle chain. | |
| 828 | + | # | |
| 829 | + | # The `video` group is checked too, because it is the whole mechanism: the | |
| 830 | + | # script writes /sys/class/backlight/<dev>/brightness, which is writable only | |
| 831 | + | # through the udev rule copied above, and the installer puts the account in | |
| 832 | + | # `video` (crates/alloy/src/install.rs). A missing group makes the script exit | |
| 833 | + | # 0 on every call, having found no writable device — dimming would be absent | |
| 834 | + | # and silent, indistinguishable from it never having been added. | |
| 835 | + | # ===================================================================== | |
| 836 | + | RUN set -eux; \ | |
| 837 | + | test -x /usr/bin/alloy-dim \ | |
| 838 | + | || { echo "alloy-dim is missing or not executable; the lock would arrive with no warning" >&2; exit 1; }; \ | |
| 839 | + | sh -n /usr/bin/alloy-dim \ | |
| 840 | + | || { echo "alloy-dim does not parse; the lock would arrive with no warning" >&2; exit 1; }; \ | |
| 841 | + | verbs=$(sed -n "s/.*alloy-dim \([a-z]*\)'.*/\1/p" /etc/skel/.config/sway/config | sort -u); \ | |
| 842 | + | [ -n "$verbs" ] \ | |
| 843 | + | || { echo "the shipped sway config never calls alloy-dim; the warning stage is not wired up" >&2; exit 1; }; \ | |
| 844 | + | for verb in $verbs; do \ | |
| 845 | + | grep -q "^$verb)" /usr/bin/alloy-dim \ | |
| 846 | + | || { echo "the sway config calls alloy-dim $verb, which the script does not handle" >&2; exit 1; }; \ | |
| 847 | + | done; \ | |
| 848 | + | getent group video >/dev/null \ | |
| 849 | + | || { echo "no video group; alloy-dim would find no writable backlight and dim nothing" >&2; exit 1; }; \ | |
| 850 | + | grep -q 'timeout 270' /etc/skel/.config/sway/config \ | |
| 851 | + | || { echo "the warning stage is gone from the idle chain; alloy-dim would never be called" >&2; exit 1; }; \ | |
| 852 | + | echo "alloy-dim: $(echo "$verbs" | wc -l) verbs wired, video group present" | |
| 853 | + | ||
| 815 | 854 | # ===================================================================== | |
| 816 | 855 | # polkit rules — assert the grant is not inert. | |
| 817 | 856 | # |
| @@ -130,16 +130,51 @@ | |||
| 130 | 130 | # and `alloy sync` carry the same enrollments afterwards. | |
| 131 | 131 | exec $term -e alloy setup --if-first-boot | |
| 132 | 132 | ||
| 133 | - | # Idle: dim to lock at 5 minutes, displays off at 10, and always lock | |
| 133 | + | # Idle: warn at 4.5 minutes, lock at 5, displays off at 10, and always lock | |
| 134 | 134 | # before suspend so a closed lid never resumes into a live session. | |
| 135 | 135 | # -w makes swayidle wait for the lock to come up before sleeping. | |
| 136 | 136 | # Lock styling comes from ~/.config/swaylock/config (it sets daemonize). | |
| 137 | + | # | |
| 138 | + | # The 270-second stage is the warning, and it is why this block has four | |
| 139 | + | # timeouts instead of three. Until it existed the comment here read "dim to | |
| 140 | + | # lock at 5 minutes" and nothing dimmed: full brightness to swaylock with no | |
| 141 | + | # intermediate state, so the lock could only ever arrive as an ambush. Thirty | |
| 142 | + | # seconds of dimmed screen is enough to notice and move the mouse, and the | |
| 143 | + | # `resume` on that stage is what puts the brightness back — including after an | |
| 144 | + | # unlock, since typing a password is input and fires it. | |
| 145 | + | # | |
| 146 | + | # It is also the nearest thing to a grace period on offer: upstream swaylock has | |
| 147 | + | # no --grace, and taking the swaylock-effects fork for one flag is the wrong | |
| 148 | + | # trade for a distro that prefers canonical packages. | |
| 137 | 149 | exec swayidle -w \ | |
| 150 | + | timeout 270 'alloy-dim down' \ | |
| 151 | + | resume 'alloy-dim up' \ | |
| 138 | 152 | timeout 300 'swaylock -f' \ | |
| 139 | 153 | timeout 600 'swaymsg "output * power off"' \ | |
| 140 | 154 | resume 'swaymsg "output * power on"' \ | |
| 141 | 155 | before-sleep 'swaylock -f' | |
| 142 | 156 | ||
| 157 | + | # Do not lock over a video. | |
| 158 | + | # | |
| 159 | + | # sway implements the Wayland idle-inhibit protocol and swayidle honours it, so | |
| 160 | + | # an application that registers an inhibitor while it plays already suppresses | |
| 161 | + | # the block above without help. These rules cover the case where that does not | |
| 162 | + | # happen: mpv only inhibits while its stop-screensaver option is on, and a | |
| 163 | + | # browser that loses the inhibitor mid-session gives no sign of it. Fullscreen | |
| 164 | + | # is the condition rather than mere focus, because a windowed video is usually | |
| 165 | + | # something being glanced at rather than watched. | |
| 166 | + | # | |
| 167 | + | # Deliberately three app_ids and not `[app_id=".*"]`. A blanket rule would make | |
| 168 | + | # any fullscreen window hold the lock off, and on a distro whose primary surface | |
| 169 | + | # is a terminal that means walking away from a fullscreen shell leaves the | |
| 170 | + | # machine unlocked indefinitely. The lock is a security control; the exemption | |
| 171 | + | # belongs to the handful of things that are actually playing something. Add an | |
| 172 | + | # app here when a real one is found to need it, and use `class` rather than | |
| 173 | + | # `app_id` if it turns out to be running through XWayland. | |
| 174 | + | for_window [app_id="mpv"] inhibit_idle fullscreen | |
| 175 | + | for_window [app_id="firefox"] inhibit_idle fullscreen | |
| 176 | + | for_window [app_id="imv"] inhibit_idle fullscreen | |
| 177 | + | ||
| 143 | 178 | # ------------------------------------------------------------------- | |
| 144 | 179 | # Binds — session | |
| 145 | 180 | # ------------------------------------------------------------------- |
| @@ -1,0 +1,95 @@ | |||
| 1 | + | #!/bin/sh | |
| 2 | + | # alloy-dim — warn that the lock is coming, by taking the backlight down. | |
| 3 | + | # | |
| 4 | + | # The sway config claimed "dim to lock at 5 minutes" for as long as it has | |
| 5 | + | # existed and nothing dimmed: the screen went from full brightness straight to | |
| 6 | + | # swaylock at 300 seconds. So the lock always arrived as an ambush, and the only | |
| 7 | + | # way to prevent one was to have been touching the keyboard at the moment it | |
| 8 | + | # fired. | |
| 9 | + | # | |
| 10 | + | # This is the thirty seconds of warning. swayidle calls `down` at 270 and `up` on | |
| 11 | + | # the next input, so moving the mouse cancels a lock that has not happened yet. | |
| 12 | + | # It is also the closest thing to a grace period available: upstream swaylock has | |
| 13 | + | # no --grace (only the swaylock-effects fork does), and adopting a fork for one | |
| 14 | + | # flag is a poor trade for a distro that prefers canonical packages. | |
| 15 | + | # | |
| 16 | + | # Nothing here may fail an idle chain. A non-zero exit from a swayidle timeout | |
| 17 | + | # action is not the end of the world, but a machine with no backlight at all — a | |
| 18 | + | # desktop, or the server profile — must not spend every five minutes reporting | |
| 19 | + | # that it cannot dim a screen it does not have. So absence exits 0. | |
| 20 | + | # | |
| 21 | + | # Why sysfs and not brightnessctl: the image ships no brightness CLI. SwayOSD | |
| 22 | + | # raises brightness by writing /sys/class/backlight/<dev>/brightness directly, | |
| 23 | + | # and the Containerfile puts a udev rule on udev's search path that chgrps that | |
| 24 | + | # file to `video` and adds group write. The installer puts the account in `video` | |
| 25 | + | # for exactly this reason. So a session process can write it, and that is the | |
| 26 | + | # whole mechanism the Fn keys already use. | |
| 27 | + | ||
| 28 | + | set -eu | |
| 29 | + | ||
| 30 | + | # The verb is checked before anything else, and before the backlight is looked | |
| 31 | + | # for, so that a typo is an error on every machine rather than only on the ones | |
| 32 | + | # with a panel. Ordered the other way — which is how this was first written — a | |
| 33 | + | # misspelled verb exits 0 on a desktop, because the no-backlight case returns | |
| 34 | + | # before the argument is ever examined. The Containerfile asserts the config and | |
| 35 | + | # this script agree, so a typo should not reach here; making it loud anyway costs | |
| 36 | + | # three lines and removes a way for that assertion to be wrong. | |
| 37 | + | case "${1:-}" in | |
| 38 | + | down | up) ;; | |
| 39 | + | *) | |
| 40 | + | echo "usage: alloy-dim down|up" >&2 | |
| 41 | + | exit 2 | |
| 42 | + | ;; | |
| 43 | + | esac | |
| 44 | + | ||
| 45 | + | # The first backlight this session can actually write. Iterating rather than | |
| 46 | + | # hardcoding intel_backlight: the name is the driver's, so it differs between | |
| 47 | + | # Intel, AMD and a DDC-driven external panel, and a machine can carry more than | |
| 48 | + | # one. Writability is the test that matters, because an unwritable device means | |
| 49 | + | # the udev rule did not apply and dimming would fail anyway. | |
| 50 | + | device="" | |
| 51 | + | for candidate in /sys/class/backlight/*/; do | |
| 52 | + | [ -w "${candidate}brightness" ] || continue | |
| 53 | + | device="${candidate%/}" | |
| 54 | + | break | |
| 55 | + | done | |
| 56 | + | ||
| 57 | + | # No panel, or no group write on it. Either way there is nothing to dim and | |
| 58 | + | # saying so once per idle period would be noise, not information. | |
| 59 | + | [ -n "$device" ] || exit 0 | |
| 60 | + | ||
| 61 | + | # Where the pre-dim level is remembered. In the runtime directory because it is | |
| 62 | + | # tmpfs cleared at logout: a saved level that outlived the session would restore | |
| 63 | + | # a brightness from yesterday, and there is no cleanup path that would catch it. | |
| 64 | + | # Keyed by device so two panels cannot overwrite each other's saved value. | |
| 65 | + | state="${XDG_RUNTIME_DIR:-/tmp}/alloy-dim.$(basename "$device")" | |
| 66 | + | ||
| 67 | + | case "${1:-}" in | |
| 68 | + | down) | |
| 69 | + | # Already dimmed. Without this guard a second `down` would save the dimmed | |
| 70 | + | # value as the level to restore, and the screen would never come back up — | |
| 71 | + | # which is the one failure here that looks like broken hardware. | |
| 72 | + | [ -e "$state" ] && exit 0 | |
| 73 | + | ||
| 74 | + | current=$(cat "$device/brightness") | |
| 75 | + | printf '%s\n' "$current" >"$state" | |
| 76 | + | ||
| 77 | + | # A fifth of current, floored at 1. Not zero: zero is a dark panel, which is | |
| 78 | + | # indistinguishable from the display power-off that follows at ten minutes, | |
| 79 | + | # and this has to read as a warning rather than as the screen having gone. | |
| 80 | + | # A fraction rather than a fixed value because the starting point is the | |
| 81 | + | # user's own choice and a fixed target would brighten a dim screen. | |
| 82 | + | dimmed=$((current / 5)) | |
| 83 | + | [ "$dimmed" -lt 1 ] && dimmed=1 | |
| 84 | + | printf '%s\n' "$dimmed" >"$device/brightness" | |
| 85 | + | ;; | |
| 86 | + | up) | |
| 87 | + | # Nothing saved means this session did not dim, so there is nothing to put | |
| 88 | + | # back. Restoring unconditionally would invent a brightness on any resume | |
| 89 | + | # that followed something other than a dim. | |
| 90 | + | [ -e "$state" ] || exit 0 | |
| 91 | + | ||
| 92 | + | printf '%s\n' "$(cat "$state")" >"$device/brightness" | |
| 93 | + | rm -f "$state" | |
| 94 | + | ;; | |
| 95 | + | esac |