feat: add Hyprland screensaver helper

This commit is contained in:
2026-04-15 10:32:48 -07:00
committed by Kat Huang
parent 82de11eb3c
commit f86ae23055
4 changed files with 228 additions and 2 deletions

View File

@@ -4,8 +4,14 @@ general {
after_sleep_cmd = hyprctl dispatch dpms on after_sleep_cmd = hyprctl dispatch dpms on
} }
listener {
timeout = 600
on-timeout = hypr-screensaver start
on-resume = hypr-screensaver stop
}
listener { listener {
timeout = 900 timeout = 900
on-timeout = hyprctl dispatch dpms off on-timeout = hypr-screensaver stop && hyprctl dispatch dpms off
on-resume = hyprctl dispatch dpms on on-resume = hyprctl dispatch dpms on
} }

View File

@@ -5,7 +5,7 @@
module Main (main) where module Main (main) where
import Control.Concurrent (threadDelay) import Control.Concurrent (threadDelay)
import Control.Monad (when) import Control.Monad (void, when)
import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Reader (asks) import Control.Monad.Trans.Reader (asks)
import Data.Char (toLower) import Data.Char (toLower)
@@ -26,6 +26,7 @@ import qualified StatusNotifier.Tray as SNITray
import System.Environment (lookupEnv) import System.Environment (lookupEnv)
import System.Environment.XDG.BaseDir (getUserConfigFile) import System.Environment.XDG.BaseDir (getUserConfigFile)
import System.Log.Logger (Priority (WARNING), rootLoggerName, setLevel, updateGlobalLogger) import System.Log.Logger (Priority (WARNING), rootLoggerName, setLevel, updateGlobalLogger)
import System.Process (spawnCommand)
import System.Taffybar (startTaffybar) import System.Taffybar (startTaffybar)
import System.Taffybar.Context import System.Taffybar.Context
( Backend (BackendWayland, BackendX11), ( Backend (BackendWayland, BackendX11),
@@ -491,6 +492,34 @@ simplifiedScreenLockWidget =
{ ScreenLock.screenLockIcon = T.pack "\xF023" <> " Lock" { ScreenLock.screenLockIcon = T.pack "\xF023" <> " Lock"
} }
simplifiedScreensaverWidget :: TaffyIO Gtk.Widget
simplifiedScreensaverWidget =
liftIO $ do
label <- Gtk.labelNew (Just (T.pack "\xF108" <> " Saver"))
ebox <- Gtk.eventBoxNew
Gtk.containerAdd ebox label
_ <- widgetSetClassGI ebox "screensaver"
Gtk.widgetSetTooltipText ebox (Just "Left click: toggle screensaver\nRight click: stop screensaver")
void $ Gtk.onWidgetButtonPressEvent ebox $ \event -> do
eventType <- Gdk.getEventButtonType event
button <- Gdk.getEventButtonButton event
if eventType /= Gdk.EventTypeButtonPress
then return False
else case button of
1 -> do
void $ spawnCommand "hypr-screensaver toggle >/dev/null 2>&1"
return True
3 -> do
void $ spawnCommand "hypr-screensaver stop >/dev/null 2>&1"
return True
_ -> return False
Gtk.widgetShowAll ebox
Gtk.toWidget ebox
screensaverWidget :: TaffyIO Gtk.Widget
screensaverWidget =
decorateWithClassAndBoxM "screensaver" simplifiedScreensaverWidget
simplifiedWlsunsetWidget :: TaffyIO Gtk.Widget simplifiedWlsunsetWidget :: TaffyIO Gtk.Widget
simplifiedWlsunsetWidget = simplifiedWlsunsetWidget =
-- Inner widget: no extra pill wrapping (the combiner provides that). -- Inner widget: no extra pill wrapping (the combiner provides that).
@@ -584,6 +613,7 @@ endWidgetsForHost hostName =
ramSwapWidget, ramSwapWidget,
diskUsageWidget, diskUsageWidget,
networkWidget, networkWidget,
screensaverWidget,
sunLockWidget, sunLockWidget,
mprisWidget mprisWidget
] ]
@@ -594,6 +624,7 @@ endWidgetsForHost hostName =
audioBacklightWidget, audioBacklightWidget,
cpuWidget, cpuWidget,
ramSwapWidget, ramSwapWidget,
screensaverWidget,
sunLockWidget, sunLockWidget,
mprisWidget mprisWidget
] ]

188
dotfiles/lib/bin/hypr-screensaver Executable file
View File

@@ -0,0 +1,188 @@
#!/usr/bin/env bash
set -euo pipefail
script_path="$(readlink -f "${BASH_SOURCE[0]}")"
state_dir="${XDG_RUNTIME_DIR:-/tmp}/hypr-screensaver"
mkdir -p "$state_dir"
title_prefix="hypr-screensaver:"
usage() {
cat <<'EOF'
Usage: hypr-screensaver <start|stop|toggle|status|session>
Commands:
start Launch the screensaver on every Hyprland monitor.
stop Stop any running screensaver windows.
toggle Start if stopped, otherwise stop.
status Exit 0 if any screensaver window is running, otherwise exit 1.
session Run the configured screensaver payload for one monitor.
The default payload is an mpv-rendered lavfi animation. You can override the
source with HYPR_SCREENSAVER_SOURCE, for example:
HYPR_SCREENSAVER_SOURCE='/path/to/video.mp4'
HYPR_SCREENSAVER_SOURCE='av://lavfi:mandelbrot=s=2560x1440:r=60'
EOF
}
monitors_json() {
hyprctl -j monitors
}
monitor_names() {
monitors_json | jq -r '.[].name'
}
monitor_specs() {
monitors_json | jq -c '.[] | { name, width, height }'
}
focused_monitor() {
monitors_json | jq -r '.[] | select(.focused) | .name'
}
screensaver_window_pids() {
hyprctl -j clients 2>/dev/null | jq -r --arg prefix "$title_prefix" '
.[]
| select((.title // "") | startswith($prefix))
| .pid
' | sort -u
}
is_running() {
local pid
for pid in $(screensaver_window_pids); do
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
done
shopt -s nullglob
local pid_file
for pid_file in "$state_dir"/*.pid; do
pid="$(<"$pid_file")"
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
done
return 1
}
default_source() {
local width="$1"
local height="$2"
printf 'av://lavfi:life=s=%sx%s:r=60:mold=10:ratio=0.065:death_color=#101414:life_color=#7dd3fc:mold_color=#1e3a5f,format=yuv420p' \
"$width" "$height"
}
start() {
local current_monitor spec monitor width height pid
if is_running; then
exit 0
fi
current_monitor="$(focused_monitor || true)"
while IFS= read -r spec; do
monitor="$(jq -r '.name' <<<"$spec")"
width="$(jq -r '.width' <<<"$spec")"
height="$(jq -r '.height' <<<"$spec")"
[ -n "$monitor" ] || continue
HYPR_SCREENSAVER_MONITOR="$monitor" \
HYPR_SCREENSAVER_WIDTH="$width" \
HYPR_SCREENSAVER_HEIGHT="$height" \
"$script_path" session >/dev/null 2>&1 &
pid=$!
printf '%s\n' "$pid" > "$state_dir/${monitor}.pid"
sleep 0.15
done < <(monitor_specs)
if [ -n "$current_monitor" ]; then
hyprctl dispatch focusmonitor "$current_monitor" >/dev/null 2>&1 || true
fi
}
stop() {
local pid pid_file
for pid in $(screensaver_window_pids); do
kill "$pid" >/dev/null 2>&1 || true
done
shopt -s nullglob
for pid_file in "$state_dir"/*.pid; do
pid="$(<"$pid_file")"
kill "$pid" >/dev/null 2>&1 || true
rm -f "$pid_file"
done
}
session() {
local monitor="${HYPR_SCREENSAVER_MONITOR:?missing HYPR_SCREENSAVER_MONITOR}"
local width="${HYPR_SCREENSAVER_WIDTH:-1920}"
local height="${HYPR_SCREENSAVER_HEIGHT:-1080}"
local source="${HYPR_SCREENSAVER_SOURCE:-$(default_source "$width" "$height")}"
local -a mpv_args=(
--no-config
--really-quiet
--fullscreen
--fs-screen-name="$monitor"
--screen-name="$monitor"
--force-window=immediate
--border=no
--title-bar=no
--ontop
--keep-open=yes
--loop-file=inf
--audio=no
--osc=no
--osd-level=0
--input-default-bindings=no
--wayland-app-id=hypr-screensaver
--title="${title_prefix}${monitor}"
--image-display-duration=inf
"$source"
)
if command -v mpv >/dev/null 2>&1; then
exec mpv "${mpv_args[@]}"
fi
exec nix shell nixpkgs#mpv --command mpv "${mpv_args[@]}"
}
status() {
is_running
}
case "${1:-}" in
start)
start
;;
stop)
stop
;;
toggle)
if is_running; then
stop
else
start
fi
;;
status)
status
;;
session)
session
;;
""|-h|--help|help)
usage
;;
*)
usage >&2
exit 2
;;
esac

View File

@@ -126,6 +126,7 @@ makeEnable config "myModules.hyprland" true {
slurp # Region selection slurp # Region selection
swappy # Screenshot annotation swappy # Screenshot annotation
nwg-displays # GUI monitor arrangement nwg-displays # GUI monitor arrangement
mpv # Graphical screensaver payload
# hy3 plugin from flake (properly built against matching Hyprland) # hy3 plugin from flake (properly built against matching Hyprland)
inputs.hy3.packages.${pkgs.stdenv.hostPlatform.system}.hy3 inputs.hy3.packages.${pkgs.stdenv.hostPlatform.system}.hy3