1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
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
|