From d15531a76f87e29e32c18ba64445003cafe1734a Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 23 Jun 2026 12:12:29 +0200 Subject: Initial commit: breaktimer break-reminder daemon with Waybar module Co-Authored-By: Claude Opus 4.8 --- breaktimer.sh | 234 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100755 breaktimer.sh (limited to 'breaktimer.sh') diff --git a/breaktimer.sh b/breaktimer.sh new file mode 100755 index 0000000..483f0b4 --- /dev/null +++ b/breaktimer.sh @@ -0,0 +1,234 @@ +#!/bin/bash +# +# breaktimer.sh - reminder di micro-pause per spezzare le sessioni al PC +# +# Macchina a stati: working -> (notifica) -> breaking/longbreak -> working +# La pausa NON erode il tempo di lavoro. Pausa manuale congela il countdown. +# +# Modello tempo: si salva REMAINING (secondi rimanenti della fase corrente), +# decrementato a ogni tick solo quando si lavora/conta. Cosi' la pausa manuale +# congela davvero e il countdown Waybar e' sempre coerente. +# +# Uso: breaktimer.sh start|stop|restart|pause|resume|toggle|status +# breaktimer.sh run (interno) +# +# Dipendenze: dunst (notify-send), pipewire (pw-play / paplay), coreutils. + +# ---------- Configurazione ---------- +MICRO_MIN=30 +BREAK_MIN=3 +LONG_MIN=10 +LONG_EVERY=4 +WORK_START="09:00" +WORK_STOP="18:30" +URGENCY_MICRO="normal" +URGENCY_LONG="critical" + +SOUND_DIR="$HOME/.local/share/sounds/modern-minimal-ui-sounds/stereo" +SOUND_MICRO="" +SOUND_LONG="" +SOUND_BACK="" +SYS_SOUND_MICRO="$SOUND_DIR/message-new-instant.oga" +SYS_SOUND_LONG="$SOUND_DIR/alarm-clock-elapsed.oga" +SYS_SOUND_BACK="$SOUND_DIR/service-login.oga" +# ------------------------------------ + +RUNTIME="${XDG_RUNTIME_DIR:-/tmp}" +PID_FILE="$RUNTIME/breaktimer.pid" +STATE_FILE="$RUNTIME/breaktimer.state" # running|paused +PHASE_FILE="$RUNTIME/breaktimer.phase" # working|breaking|longbreak +REMAIN_FILE="$RUNTIME/breaktimer.remain" # secondi rimanenti della fase +TICK=5 + +now_epoch() { date +%s; } +hm_to_epoch() { date -d "$(date +%F) $1" +%s; } + +in_work_window() { + local n start stop + n=$(now_epoch); start=$(hm_to_epoch "$WORK_START"); stop=$(hm_to_epoch "$WORK_STOP") + [ "$n" -ge "$start" ] && [ "$n" -lt "$stop" ] +} + +is_running() { + [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null +} + +play_sound() { + local file="$1" + { [ -z "$file" ] || [ ! -f "$file" ]; } && return 0 + if command -v pw-play >/dev/null 2>&1; then pw-play "$file" >/dev/null 2>&1 & + elif command -v paplay >/dev/null 2>&1; then paplay "$file" >/dev/null 2>&1 & + fi +} +sound_for_micro() { + if [ -n "$SOUND_MICRO" ] && [ -f "$SOUND_MICRO" ]; then play_sound "$SOUND_MICRO" + else play_sound "$SYS_SOUND_MICRO"; fi +} +sound_for_long() { + if [ -n "$SOUND_LONG" ] && [ -f "$SOUND_LONG" ]; then play_sound "$SOUND_LONG" + else play_sound "$SYS_SOUND_LONG"; fi +} +sound_for_back() { + if [ -n "$SOUND_BACK" ] && [ -f "$SOUND_BACK" ]; then play_sound "$SOUND_BACK" + else play_sound "$SYS_SOUND_BACK"; fi +} + +notify_micro() { + notify-send -u "$URGENCY_MICRO" -a "breaktimer" \ + "🚶 Micro-pausa ($BREAK_MIN min)" \ + "Alzati e muoviti: camminata, squat, mobilita'. Occhi: guarda lontano 20s." + sound_for_micro +} +notify_long() { + notify-send -u "$URGENCY_LONG" -a "breaktimer" \ + "⏸️ Pausa lunga ($LONG_MIN min)" \ + "Stacca davvero. Cammina, bevi, allunga la schiena." + sound_for_long +} +notify_back() { + notify-send -u "low" -a "breaktimer" \ + "▶️ Si riparte" "Pausa finita, blocco di lavoro da $MICRO_MIN min." + sound_for_back +} + +# ---------- Loop principale ---------- +run_loop() { + # singleton: rifiuta se un altro daemon vivo possiede gia' il PID file. + # Evita due loop che si calpestano gli stessi file di stato. + local owner + owner="$(cat "$PID_FILE" 2>/dev/null)" + if [ -n "$owner" ] && [ "$owner" != "$$" ] && kill -0 "$owner" 2>/dev/null; then + echo "breaktimer: gia' in esecuzione (PID $owner), run annullato" >&2 + exit 1 + fi + echo "$$" > "$PID_FILE" + echo "running" > "$STATE_FILE" + echo "working" > "$PHASE_FILE" + local count=0 + local work_sec=$((MICRO_MIN * 60)) + local break_sec=$((BREAK_MIN * 60)) + local long_sec=$((LONG_MIN * 60)) + local remaining=$work_sec + echo "$remaining" > "$REMAIN_FILE" + + # cleanup: NON tocca PID_FILE. Un vecchio daemon (ucciso da restart) puo' + # eseguire il trap in ritardo, dopo che il nuovo ha gia' scritto il suo PID: + # cancellare il file qui ucciderebbe il riferimento al nuovo daemon. + # is_running usa kill -0, quindi un PID file stantio e' innocuo; lo + # sovrascrive il prossimo start. + cleanup() { + echo "stopped" > "$STATE_FILE"; echo "stopped" > "$PHASE_FILE" + rm -f "$REMAIN_FILE" + exit 0 + } + trap cleanup TERM INT + + while true; do + sleep "$TICK" + + # pausa manuale: NON decrementare, lascia remaining congelato + [ "$(cat "$STATE_FILE" 2>/dev/null)" = "paused" ] && continue + + local phase="$(cat "$PHASE_FILE" 2>/dev/null)" + + # fuori finestra oraria, solo in 'working': congela + if [ "$phase" = "working" ] && ! in_work_window; then + continue + fi + + # decrementa il tempo rimanente della fase + remaining=$(( remaining - TICK )) + + if [ "$remaining" -gt 0 ]; then + echo "$remaining" > "$REMAIN_FILE" + continue + fi + + # fase scaduta: transizione + case "$phase" in + working) + count=$((count + 1)) + if [ "$((count % LONG_EVERY))" -eq 0 ]; then + notify_long + echo "longbreak" > "$PHASE_FILE" + remaining=$long_sec + else + notify_micro + echo "breaking" > "$PHASE_FILE" + remaining=$break_sec + fi + ;; + breaking|longbreak) + notify_back + echo "working" > "$PHASE_FILE" + remaining=$work_sec + ;; + *) + echo "working" > "$PHASE_FILE" + remaining=$work_sec + ;; + esac + echo "$remaining" > "$REMAIN_FILE" + done +} + +# ---------- Gestione daemon ---------- +start_daemon() { + if is_running; then + echo "breaktimer: gia' in esecuzione (PID $(cat "$PID_FILE"))" + return 1 + fi + # il loop scrive da solo il proprio PID ($$) dopo il guard singleton: + # setsid fa fork, quindi $! qui non e' il PID reale del loop. + setsid "$0" run >/dev/null 2>&1 < /dev/null & + # attendi che il loop pubblichi il suo PID + for _ in 1 2 3 4 5 6 7 8 9 10; do + is_running && break + sleep 0.2 + done + if is_running; then + echo "breaktimer: avviato in background (PID $(cat "$PID_FILE"))" + else + echo "breaktimer: avvio fallito" >&2 + return 1 + fi +} +stop_daemon() { + if is_running; then + kill -TERM "$(cat "$PID_FILE")" 2>/dev/null + rm -f "$PID_FILE" "$REMAIN_FILE" + echo "stopped" > "$STATE_FILE"; echo "stopped" > "$PHASE_FILE" + echo "breaktimer: fermato" + else + echo "stopped" > "$STATE_FILE"; echo "stopped" > "$PHASE_FILE" + rm -f "$PID_FILE" "$REMAIN_FILE" + echo "breaktimer: non era in esecuzione" + fi +} + +case "$1" in + start) start_daemon ;; + stop) stop_daemon ;; + restart) stop_daemon; sleep 1; start_daemon ;; + run) run_loop ;; + pause) echo "paused" > "$STATE_FILE"; echo "breaktimer: sospeso" ;; + resume) echo "running" > "$STATE_FILE"; echo "breaktimer: ripreso" ;; + toggle) + if [ "$(cat "$STATE_FILE" 2>/dev/null)" = "paused" ]; then + echo "running" > "$STATE_FILE"; echo "breaktimer: ripreso" + else + echo "paused" > "$STATE_FILE"; echo "breaktimer: sospeso" + fi + ;; + status) + if is_running; then + echo "breaktimer: attivo (PID $(cat "$PID_FILE")), stato: $(cat "$STATE_FILE" 2>/dev/null), fase: $(cat "$PHASE_FILE" 2>/dev/null), rimane: $(cat "$REMAIN_FILE" 2>/dev/null)s" + else + echo "breaktimer: non in esecuzione" + fi + ;; + *) + echo "uso: $0 [start|stop|restart|pause|resume|toggle|status]" + exit 1 + ;; +esac -- cgit v1.2.3