From 30619b00ce80f9dbde2cf42f1c56c0fbab6116d7 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 23 Sep 2026 23:05:54 +0200 Subject: [PATCH 1/2] hyprsaver --- hypr/.config/hypr/hypridle.conf | 32 ++++++++++++++++ hypr/.config/hypr/hyprland.lua | 4 ++ hypr/.config/hypr/hyprsaver.toml | 40 ++++++++++++++++++++ hypr/.config/hypr/modules/keybinds.lua | 1 + hypr/.config/hypr/modules/shell_commands.lua | 1 + setup/hypr/packages.txt | 2 + 6 files changed, 80 insertions(+) create mode 100644 hypr/.config/hypr/hypridle.conf create mode 100644 hypr/.config/hypr/hyprsaver.toml diff --git a/hypr/.config/hypr/hypridle.conf b/hypr/.config/hypr/hypridle.conf new file mode 100644 index 0000000..1a0fe8a --- /dev/null +++ b/hypr/.config/hypr/hypridle.conf @@ -0,0 +1,32 @@ +# hypridle.conf — idle stages: hyprsaver (5min) -> Noctalia lock (10min) -> DPMS off. +# Managed by dotfiles (stow hypr). +# +# NOTE: this Hyprland (hyprmod Lua runtime, 0.56.x) evaluates `hyprctl dispatch` +# args as Lua — classic `dispatch dpms off` syntax fails. Dispatchers must be +# hl.dsp.* expressions (e.g. hl.dsp.dpms(false)). + +general { + lock_cmd = pidof -q noctalia || noctalia msg session lock + before_sleep_cmd = loginctl lock-session + after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms(true)' +} + +# 5min: GPU screensaver (wlr-layer-shell overlay, any input dismisses it). +listener { + timeout = 300 + on-timeout = hyprsaver + on-resume = hyprsaver --quit +} + +# 10min: lock via Noctalia (SUPER+L runs the same command). +listener { + timeout = 600 + on-timeout = loginctl lock-session +} + +# 10m30s: power off displays. +listener { + timeout = 630 + on-timeout = hyprctl dispatch 'hl.dsp.dpms(false)' + on-resume = hyprctl dispatch 'hl.dsp.dpms(true)' +} diff --git a/hypr/.config/hypr/hyprland.lua b/hypr/.config/hypr/hyprland.lua index c0529f5..f721f55 100644 --- a/hypr/.config/hypr/hyprland.lua +++ b/hypr/.config/hypr/hyprland.lua @@ -32,6 +32,10 @@ hl.on("hyprland.start", function() hl.exec_cmd("noctalia &") end) +hl.on("hyprland.start", function() + hl.exec_cmd("hypridle &") +end) + hl.on("hyprland.start", function() hl.exec_cmd("sh ~/.config/hypr/scripts/portal-host-override.sh") hl.exec_cmd("sh ~/.config/hypr/scripts/ensure-mime-defaults.sh") diff --git a/hypr/.config/hypr/hyprsaver.toml b/hypr/.config/hypr/hyprsaver.toml new file mode 100644 index 0000000..fbadbfa --- /dev/null +++ b/hypr/.config/hypr/hyprsaver.toml @@ -0,0 +1,40 @@ +# hyprsaver.toml — chill playlist: calm shaders + cool palettes, cycling. +# Location: ~/.config/hypr/hyprsaver.toml (stowed from dotfiles). +# Names verified against `hyprsaver --list-shaders` / `--list-palettes`. +# Everything else uses hyprsaver's built-in defaults (see +# /usr/share/hyprsaver/examples/config.toml for the full reference). + +[general] +fps = 30 +shader = "cycle" +palette = "cycle" +shader_cycle_interval = 300 +palette_cycle_interval = 60 +cycle_order = "random" +synced = true +shader_playlist = "chill" +palette_playlist = "chill" + +[playlists.chill] +shaders = [ + "plasma", + "marble", + "snowfall", + "aurora", + "clouds", + "planet", + "starfield", + "fireflies", +] +palettes = [ + "vaporwave", + "frost", + "ocean", + "aurora", +] + +[behavior] +fade_in_ms = 800 +fade_out_ms = 400 +dismiss_on = ["key", "mouse_move", "mouse_click", "touch"] +exclusive_keyboard = true diff --git a/hypr/.config/hypr/modules/keybinds.lua b/hypr/.config/hypr/modules/keybinds.lua index d72d66e..1af8a2c 100644 --- a/hypr/.config/hypr/modules/keybinds.lua +++ b/hypr/.config/hypr/modules/keybinds.lua @@ -226,5 +226,6 @@ hl.bind("SHIFT + PRINT", hl.dsp.exec_cmd("hyprshot -m active --output-folder " . }) hl.bind(var.mainMod .. " + L", hl.dsp.exec_cmd(command.lock), { locked = true, description = "Lock screen" }) +hl.bind(var.mainMod .. " + SHIFT + L", hl.dsp.exec_cmd(command.screensaver), { description = "Start screensaver (any key quits)" }) hl.bind(var.mainMod .. " + K", hl.dsp.exec_cmd(command.keybinds_show), { description = "Show keybind overview" }) hl.bind(var.mainMod .. " + W", hl.dsp.exec_cmd(command.wallpaper_select), { description = "Wallpaper select" }) diff --git a/hypr/.config/hypr/modules/shell_commands.lua b/hypr/.config/hypr/modules/shell_commands.lua index 15f7667..14ea5d6 100644 --- a/hypr/.config/hypr/modules/shell_commands.lua +++ b/hypr/.config/hypr/modules/shell_commands.lua @@ -7,6 +7,7 @@ return { settings = "noctalia msg settings-toggle", control_center = "noctalia msg panel-toggle control-center", lock = "noctalia msg session lock", + screensaver = "hyprsaver", wallpaper_select = "noctalia msg panel-toggle wallpaper", keybinds_show = "sh ~/.config/hypr/scripts/show-keybinds.sh", } diff --git a/setup/hypr/packages.txt b/setup/hypr/packages.txt index b7cc8bc..6ca93cf 100644 --- a/setup/hypr/packages.txt +++ b/setup/hypr/packages.txt @@ -12,6 +12,7 @@ @pacman hyprland +hypridle xdg-desktop-portal-hyprland hyprpolkitagent pantheon-shortcut-overlay @@ -23,3 +24,4 @@ noctalia @aur hyprmod +hyprsaver From aed6d46846850dd23bb7755779293011939ff2de Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 23 Sep 2026 23:31:12 +0200 Subject: [PATCH 2/2] tte screensaver --- hypr/.config/hypr/hypridle.conf | 8 +-- hypr/.config/hypr/modules/screensaver.lua | 13 +++++ hypr/.config/hypr/modules/shell_commands.lua | 2 +- .../hypr/modules/windows_and_workspaces.lua | 10 ++++ hypr/.config/hypr/screensaver.txt | 2 + .../hypr/scripts/launch-screensaver.sh | 51 +++++++++++++++++++ .../.config/hypr/scripts/screensaver-start.sh | 24 +++++++++ hypr/.config/hypr/scripts/screensaver-stop.sh | 36 +++++++++++++ hypr/.config/hypr/scripts/screensaver-tte.sh | 41 +++++++++++++++ setup/hypr/packages.txt | 1 + 10 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 hypr/.config/hypr/modules/screensaver.lua create mode 100644 hypr/.config/hypr/screensaver.txt create mode 100755 hypr/.config/hypr/scripts/launch-screensaver.sh create mode 100755 hypr/.config/hypr/scripts/screensaver-start.sh create mode 100755 hypr/.config/hypr/scripts/screensaver-stop.sh create mode 100755 hypr/.config/hypr/scripts/screensaver-tte.sh diff --git a/hypr/.config/hypr/hypridle.conf b/hypr/.config/hypr/hypridle.conf index 1a0fe8a..28182d7 100644 --- a/hypr/.config/hypr/hypridle.conf +++ b/hypr/.config/hypr/hypridle.conf @@ -1,4 +1,4 @@ -# hypridle.conf — idle stages: hyprsaver (5min) -> Noctalia lock (10min) -> DPMS off. +# hypridle.conf — idle stages: screensaver (300s, backend in modules/screensaver.lua) -> Noctalia lock (10min) -> DPMS off. # Managed by dotfiles (stow hypr). # # NOTE: this Hyprland (hyprmod Lua runtime, 0.56.x) evaluates `hyprctl dispatch` @@ -11,11 +11,11 @@ general { after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms(true)' } -# 5min: GPU screensaver (wlr-layer-shell overlay, any input dismisses it). +# Screensaver via backend wrappers (switch backend in modules/screensaver.lua). listener { timeout = 300 - on-timeout = hyprsaver - on-resume = hyprsaver --quit + on-timeout = ~/.config/hypr/scripts/screensaver-start.sh + on-resume = ~/.config/hypr/scripts/screensaver-stop.sh } # 10min: lock via Noctalia (SUPER+L runs the same command). diff --git a/hypr/.config/hypr/modules/screensaver.lua b/hypr/.config/hypr/modules/screensaver.lua new file mode 100644 index 0000000..f43eb88 --- /dev/null +++ b/hypr/.config/hypr/modules/screensaver.lua @@ -0,0 +1,13 @@ +-- Screensaver commands for idle (hypridle on-timeout/on-resume) and manual +-- (SUPER+SHIFT+L) use. scripts/screensaver-start.sh and +-- scripts/screensaver-stop.sh exec the .start / .stop command below. +-- +-- Active backend: tte (fullscreen kitty + terminal-text-effects). +return { + start = "~/.config/hypr/scripts/launch-screensaver.sh", + stop = "~/.config/hypr/scripts/screensaver-stop.sh", + -- Alternative backend: hyprsaver (GPU fractal shaders, wlr-layer-shell). + -- Swap the commented pair in to switch (no other file changes needed). + -- start = "hyprsaver", + -- stop = "hyprsaver --quit", +} diff --git a/hypr/.config/hypr/modules/shell_commands.lua b/hypr/.config/hypr/modules/shell_commands.lua index 14ea5d6..bc54b95 100644 --- a/hypr/.config/hypr/modules/shell_commands.lua +++ b/hypr/.config/hypr/modules/shell_commands.lua @@ -7,7 +7,7 @@ return { settings = "noctalia msg settings-toggle", control_center = "noctalia msg panel-toggle control-center", lock = "noctalia msg session lock", - screensaver = "hyprsaver", + screensaver = "sh ~/.config/hypr/scripts/screensaver-start.sh force", wallpaper_select = "noctalia msg panel-toggle wallpaper", keybinds_show = "sh ~/.config/hypr/scripts/show-keybinds.sh", } diff --git a/hypr/.config/hypr/modules/windows_and_workspaces.lua b/hypr/.config/hypr/modules/windows_and_workspaces.lua index 183e2e7..905f14c 100644 --- a/hypr/.config/hypr/modules/windows_and_workspaces.lua +++ b/hypr/.config/hypr/modules/windows_and_workspaces.lua @@ -93,6 +93,16 @@ hl.window_rule({ border_size = 0, }) +-- Manual tte screensaver preview (SUPER+SHIFT+L): fullscreen kitty. +hl.window_rule({ + name = "screensaver-fullscreen", + match = { + class = "org.screensaver", + }, + fullscreen = true, + float = true, +}) + -- windowrule { -- name = no-rounding-steam-games diff --git a/hypr/.config/hypr/screensaver.txt b/hypr/.config/hypr/screensaver.txt new file mode 100644 index 0000000..4fd5c0c --- /dev/null +++ b/hypr/.config/hypr/screensaver.txt @@ -0,0 +1,2 @@ +hyprland +// idle looks good on you diff --git a/hypr/.config/hypr/scripts/launch-screensaver.sh b/hypr/.config/hypr/scripts/launch-screensaver.sh new file mode 100755 index 0000000..8d7b9f5 --- /dev/null +++ b/hypr/.config/hypr/scripts/launch-screensaver.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# launch-screensaver.sh — idle-triggered tte screensaver (hypridle on-timeout). +# Spawns one fullscreen kitty running screensaver-tte.sh per monitor. +# Usage: launch-screensaver.sh [force] +# force ignore the screensaver-off toggle file. +# Toggle off: touch ~/.local/state/hypr/screensaver-off +# +# Lessons baked in: no focus-restore after spawn (it raced the new window and +# caused flash-then-nothing), no pkill -f patterns anywhere (teardown is done +# by screensaver-stop.sh via exact client PIDs). + +set -euo pipefail + +SCREENSAVER_CLASS="org.screensaver" +TOGGLE_FILE="$HOME/.local/state/hypr/screensaver-off" + +command -v tte >/dev/null 2>&1 || exit 1 +command -v kitty >/dev/null 2>&1 || exit 1 + +# Already running (precise client check). +if hyprctl clients -j 2>/dev/null | jq -e '.[] | select(.class=="org.screensaver")' >/dev/null 2>&1; then + exit 0 +fi + +# Opt-out toggle (unless forced, e.g. manual invocation). +if [[ -f "$TOGGLE_FILE" ]] && [[ "${1:-}" != "force" ]]; then + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# hyprctl prints plain-text errors (not JSON) when HYPRLAND_INSTANCE_SIGNATURE +# is stale (e.g. shell/tmux predates the current compositor). Fail clearly +# instead of dumping a jq parse error. +monitors_json="$(hyprctl monitors -j 2>/dev/null)" || { + echo "launch-screensaver: hyprctl failed - is HYPRLAND_INSTANCE_SIGNATURE stale? (try a fresh terminal)" >&2 + exit 1 +} +echo "$monitors_json" | jq -e . >/dev/null 2>&1 || { + echo "launch-screensaver: hyprctl did not return JSON - stale HYPRLAND_INSTANCE_SIGNATURE?" >&2 + exit 1 +} + +# NOTE: hyprmod Lua runtime (Hyprland 0.56.x) evaluates `hyprctl dispatch` args +# as Lua — classic `dispatch focusmonitor eDP-1` fails. Dispatchers must be +# hl.dsp.* expressions. Focus each monitor before exec so the saver kitty +# spawns there; deliberately no focus-restore afterwards (spawn race). +for m in $(echo "$monitors_json" | jq -r '.[] | .name'); do + hyprctl dispatch "hl.dsp.focus({monitor=\"$m\"})" >/dev/null 2>&1 + hyprctl dispatch "hl.dsp.exec_cmd('kitty --class=\"$SCREENSAVER_CLASS\" --override font_size=18 --override window_padding_width=0 --override background_opacity=1 --override mouse_hide_wait=1.0 -e \"$SCRIPT_DIR/screensaver-tte.sh\"')" >/dev/null 2>&1 +done diff --git a/hypr/.config/hypr/scripts/screensaver-start.sh b/hypr/.config/hypr/scripts/screensaver-start.sh new file mode 100755 index 0000000..8b4f824 --- /dev/null +++ b/hypr/.config/hypr/scripts/screensaver-start.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# screensaver-start.sh — exec the start command from modules/screensaver.lua. +# Usage: screensaver-start.sh [force] (forwarded to .sh targets, e.g. the tte +# launcher's toggle bypass; dropped for bare commands like `hyprsaver`). + +set -euo pipefail + +CMD="$(lua -e 'print(dofile(os.getenv("HOME").."/.config/hypr/modules/screensaver.lua").start)' 2>/dev/null)" || { + echo "screensaver-start: cannot read modules/screensaver.lua" >&2 + exit 1 +} +CMD="${CMD/#\~/$HOME}" +[[ -n "$CMD" ]] || { + echo "screensaver-start: empty start command" >&2 + exit 1 +} + +if [[ "$CMD" == *.sh ]]; then + exec "$CMD" "$@" +else + # Bare command, may carry flags (e.g. "hyprsaver --quit"). + # shellcheck disable=SC2086 + exec $CMD +fi diff --git a/hypr/.config/hypr/scripts/screensaver-stop.sh b/hypr/.config/hypr/scripts/screensaver-stop.sh new file mode 100755 index 0000000..56af40a --- /dev/null +++ b/hypr/.config/hypr/scripts/screensaver-stop.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# screensaver-stop.sh — exec the stop command from modules/screensaver.lua. +# When it points back here (tte backend), run the built-in teardown below: +# kill saver windows by Hyprland client PID (class org.screensaver) plus +# stray tte processes (exact comm match). Never `pkill -f `: a +# full-cmdline pattern also matches bystander shells merely mentioning it. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ME="$(realpath "$SCRIPT_DIR/screensaver-stop.sh")" + +WANT="$(lua -e 'print(dofile(os.getenv("HOME").."/.config/hypr/modules/screensaver.lua").stop)' 2>/dev/null)" || { + echo "screensaver-stop: cannot read modules/screensaver.lua" >&2 + exit 1 +} +WANT="${WANT/#\~/$HOME}" + +# Alternative backend configured (e.g. "hyprsaver --quit"): run it instead. +if [[ "$(realpath -m "$WANT")" != "$ME" ]]; then + # shellcheck disable=SC2086 + exec $WANT +fi + +# --- tte teardown --- +pids="$(hyprctl clients -j 2>/dev/null | jq -r '.[] | select(.class=="org.screensaver") | .pid' 2>/dev/null)" || pids="" +if [[ -n "${pids:-}" ]]; then + # shellcheck disable=SC2086 + kill $pids 2>/dev/null || true +fi + +# tte runs under comm "tte" (verified); -x matches the comm only, so this +# cannot hit bystander shells. +pkill -x tte 2>/dev/null || true + +exit 0 diff --git a/hypr/.config/hypr/scripts/screensaver-tte.sh b/hypr/.config/hypr/scripts/screensaver-tte.sh new file mode 100755 index 0000000..2b30a60 --- /dev/null +++ b/hypr/.config/hypr/scripts/screensaver-tte.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# screensaver-tte.sh — runs inside each fullscreen tte kitty. +# Loops random tte effects until any keypress, then exits (kitty closes). +# Child teardown uses the exact PID ($!) — never pkill -f patterns, which +# would also match bystander shells mentioning the class name. + +set -euo pipefail + +SCREENSAVER_TEXT="$HOME/.config/hypr/screensaver.txt" + +trap 'exit 0' SIGINT SIGTERM SIGHUP SIGQUIT + +printf '\033]11;rgb:00/00/00\007' # black background + +input="$SCREENSAVER_TEXT" +[[ -f "$input" ]] || input="/etc/hostname" + +while true; do + tte -i "$input" \ + --frame-rate 60 --canvas-width 0 --canvas-height 0 \ + --anchor-canvas c --anchor-text c \ + --random-effect --exclude-effects dev_worm \ + --no-eol --no-restore-cursor & + TPID=$! + # Wait for tte to appear (Python startup) before polling it. + for _ in $(seq 1 10); do + kill -0 "$TPID" 2>/dev/null || break + pgrep -x tte >/dev/null 2>&1 && break + sleep 0.1 + done + while kill -0 "$TPID" 2>/dev/null; do + if read -r -n1 -t 1; then + kill "$TPID" 2>/dev/null || true + wait "$TPID" 2>/dev/null || true + exit 0 + fi + done + wait "$TPID" 2>/dev/null || true + # Throttle respawn if tte exits instantly (e.g. a sub-second effect). + sleep 0.2 +done diff --git a/setup/hypr/packages.txt b/setup/hypr/packages.txt index 6ca93cf..6a758fc 100644 --- a/setup/hypr/packages.txt +++ b/setup/hypr/packages.txt @@ -25,3 +25,4 @@ noctalia @aur hyprmod hyprsaver +python-terminaltexteffects