111 lines
4.1 KiB
Bash
Executable File
111 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# timer-run <total_seconds> [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 >/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
|