A lightweight Python app that enforces the 20-20-20 rule for eye health on Windows.
Every 20 minutes, a blocking popup appears with a loud alarm. You cannot dismiss it until a 20-second countdown finishes. No cheating.
The 20-20-20 rule says: every 20 minutes, look at something 20 feet away for 20 seconds. Every reminder app I tried was too easy to dismiss. This one isn't.
[20 min timer] --> ALARM fires --> Blocking popup appears
|
20-sec countdown
|
"Continue" button unlocks
|
Click Continue --> back to work
Click Stop --> session ends
- Runs silently in the system tray
- Loud beeping alarm on every trigger (no audio files needed)
- Popup is always-on-top and the X button is disabled
- "Continue" button locked until full 20-second countdown completes
- System tray icon with right-click Quit option
- Built-in test mode (5-sec interval) for quick verification
- Windows
- Python 3.8+
pystrayandPillow(for system tray icon)
Core functionality (popup + alarm) works with zero installs. Tray icon needs the two packages below.
pip install pystray pillowpython eye_reminder.pyThe app starts immediately. Popup fires after 20 minutes.
To verify it works before committing to 20-minute waits, set TEST_MODE = True at the top of the file:
TEST_MODE = True # popup fires in 5 seconds, countdown is 5 secondsFlip back to False for normal use.
- Press
Win+R, typeshell:startup, press Enter - Create a shortcut in that folder pointing to:
Using
pythonw C:\path\to\eye_reminder.pypythonwhides the terminal window.
Download write_shortcut.py from this repo and run:
# Step 1: generates the PowerShell setup script
python write_shortcut.py
# Step 2: creates the startup shortcut
powershell -ExecutionPolicy Bypass -File "C:\Users\<you>\EyeReminder\make_shortcut.ps1"eye-reminder/
├── eye_reminder.py # main app
├── write_shortcut.py # helper: creates Windows startup shortcut
└── README.md
Blocking the close button
win.protocol("WM_DELETE_WINDOW", lambda: None)Overrides the X button with a no-op. The window cannot be closed until you click a button.
Locked Continue button
continue_btn = tk.Button(..., state="disabled")
def tick():
remaining["s"] -= 1
if remaining["s"] <= 0:
continue_btn.configure(state="normal", fg="#44ff88")
else:
win.after(1000, tick)Button stays grey and unclickable until the countdown hits zero.
Alarm (no audio files)
import winsound
for _ in range(6):
winsound.Beep(1000, 300)
time.sleep(0.1)
winsound.Beep(1400, 300)
time.sleep(0.1)Uses the Windows built-in winsound module. No mp3, no wav, no external deps.
At the top of eye_reminder.py:
| Variable | Default | Description |
|---|---|---|
TEST_MODE |
False |
Set True for 5-sec test intervals |
INTERVAL_SECONDS |
20 * 60 |
Time between popups (seconds) |
BREAK_SECONDS |
20 |
Countdown duration (seconds) |
| Package | Required | Purpose |
|---|---|---|
tkinter |
Yes (built-in) | Popup UI |
winsound |
Yes (built-in) | Alarm beeps |
threading |
Yes (built-in) | Background timer |
pystray |
Optional | System tray icon |
Pillow |
Optional | Tray icon image |
MIT