#!/usr/bin/env bash # timer-run [label] # Runs entirely in background. No terminal needed after launch. # Sends a dunst notification + audio beep when done. # # Install: ~/.config/scripts/timer-run (companion: timer-pick in same dir) TOTAL="${1:?missing seconds}" LABEL="${2:-}" # ── source the user environment if running without a login shell ─────────────── # When launched from Hyprland keybind → kitty → exec, the process inherits # kitty's env which already has DBUS_SESSION_BUS_ADDRESS, XDG_RUNTIME_DIR etc. # But after setsid detach those are preserved since we don't re-login. # We do need to make sure XDG_RUNTIME_DIR is set for pipewire/pulse socket paths. if [[ -z "${XDG_RUNTIME_DIR:-}" ]]; then export XDG_RUNTIME_DIR="/run/user/$(id -u)" fi # ── detach from terminal immediately ────────────────────────────────────────── # Preserve the full environment across the setsid re-exec (no login shell). if [[ -t 0 || -t 1 || -t 2 ]]; then ( trap '' SIGHUP; exec setsid bash "$0" "$TOTAL" "$LABEL" /dev/null 2>&1 ) & exit 0 fi # ── sleep until done ────────────────────────────────────────────────────────── sleep "$TOTAL" # ── format a human-readable duration ───────────────────────────────────────── fmt_dur() { local s=$1 h=0 m=0 out="" h=$(( s / 3600 )); s=$(( s % 3600 )) m=$(( s / 60 )); s=$(( s % 60 )) (( h > 0 )) && out+="${h}h " (( m > 0 )) && out+="${m}m " (( s > 0 || (h == 0 && m == 0) )) && out+="${s}s" echo "${out% }" } DURATION_STR=$(fmt_dur "$TOTAL") NOTIF_SUMMARY="⏰ Timer done${LABEL:+ — $LABEL}" NOTIF_BODY="Set for ${DURATION_STR}. Finished at $(date '+%H:%M:%S')." # ── dunst notification ──────────────────────────────────────────────────────── # notify-send needs DBUS_SESSION_BUS_ADDRESS. If not in env, find it via the # running dunst process (reliable on single-user Wayland/Hyprland setups). if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then local_uid=$(id -u) # try to grab it from the environment of any process owned by this user for pid in $(pgrep -u "$local_uid" dunst 2>/dev/null); do addr=$(cat /proc/$pid/environ 2>/dev/null \ | tr '\0' '\n' \ | grep '^DBUS_SESSION_BUS_ADDRESS=' \ | cut -d= -f2-) if [[ -n $addr ]]; then export DBUS_SESSION_BUS_ADDRESS="$addr" break fi done fi if command -v notify-send &>/dev/null; then notify-send \ --urgency=critical \ --expire-time=0 \ --icon=alarm-timer \ "$NOTIF_SUMMARY" \ "$NOTIF_BODY" fi # ── audio alert ─────────────────────────────────────────────────────────────── # pw-play / paplay need PIPEWIRE_RUNTIME_DIR or PULSE_RUNTIME_PATH which live # under XDG_RUNTIME_DIR — already ensured above. _beep_pcm() { python3 - <<'PYEOF' import struct, math, sys RATE = 44100 FREQ = 880.0 VOL = 0.65 BEEPS = 3 DUR = 0.55 GAP = 0.18 def sine(freq, secs): n = int(RATE * secs) return [int(VOL * 32767 * math.sin(2 * math.pi * freq * i / RATE)) for i in range(n)] def silence(secs): return [0] * int(RATE * secs) samples = [] for i in range(BEEPS): samples += sine(FREQ, DUR) if i < BEEPS - 1: samples += silence(GAP) sys.stdout.buffer.write(struct.pack(f'<{len(samples)}h', *samples)) PYEOF } if command -v pw-play &>/dev/null; then _beep_pcm | pw-play --rate=44100 --channels=1 --format=s16 - 2>/dev/null elif command -v paplay &>/dev/null; then _beep_pcm | paplay --raw --rate=44100 --channels=1 --format=s16le - 2>/dev/null elif command -v aplay &>/dev/null; then _beep_pcm | aplay -q -r 44100 -c 1 -f S16_LE - 2>/dev/null fi