#!/bin/bash # # breaktimer.sh - reminder di micro-pause per spezzare le sessioni al PC # # Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, see . # # 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