I have severe eye dryness. My doctor told me to follow the 20-20-20 rule every 20 minutes, look at something 20 feet away for 20 seconds. Simple. Except when you're deep in a bug or a feature, 20 minutes evaporates and you've been staring at your monitor for 3 hours straight.
I tried phone reminders. I dismissed them without thinking. I tried sticky notes. I ignored them. I needed something that actually blocked me from working until I did the break.
The key insight: a dismissible reminder is just noise. The popup had to be impossible to close until the 20-second countdown finished.
What it does
- Runs silently in the system tray in the background
- Every 20 minutes fires a loud beeping alarm (winsound, no external files needed)
- Dark blocking popup appears X button is disabled, you cannot close it
- 20-second countdown runs automatically with a progress bar
- The "Continue" button is locked until the countdown finishes, then turns green
- Two buttons: Continue (back to work) or Stop (end the session)
The core trick: disabling the close button
The single most important line in the whole app:
win.protocol("WM_DELETE_WINDOW", lambda: None)
By overriding WM_DELETE_WINDOW with a no-op lambda, clicking the X does absolutely nothing. The window stays open. You have to wait out the countdown.
The countdown + locked button
The "Continue" button starts state="disabled" and only becomes clickable when the timer hits zero:
def tick():
remaining["s"] -= 1
count_var.set(str(max(remaining["s"], 0)))
if remaining["s"] <= 0:
continue_btn.configure(
state="normal",
bg="#003322",
fg="#44ff88",
cursor="hand2"
)
else:
after_id["id"] = win.after(1000, tick)
The button turns green when it unlocks a satisfying visual reward for actually doing the break.
The alarm
No external audio files needed. winsound is part of the Windows standard library:
for _ in range(6):
winsound.Beep(1000, 300)
time.sleep(0.1)
winsound.Beep(1400, 300)
time.sleep(0.1)
Two alternating frequencies, six times. Annoying enough that you cannot ignore it. Runs in a daemon thread so it does not block the UI.
The full popup
def show_popup():
result = {"action": None}
root = tk.Tk()
root.withdraw()
win = tk.Toplevel(root)
win.title("Eye Break!")
win.configure(bg="#0a0a0a")
win.attributes("-topmost", True)
win.protocol("WM_DELETE_WINDOW", lambda: None) # <-- the magic line
# ... countdown, progress bar, locked button ...
win.after(1000, tick)
play_alarm()
root.mainloop()
return result["action"] == "continue"
The popup is always-on-top (-topmost True) so it cannot be buried under other windows.
Setup: auto-start on Windows boot
Step 1 Install dependencies:
pip install pystray pillow
Step 2 Save eye_reminder.py to a permanent location:
C:\Users\you\EyeReminder\eye_reminder.py
Step 3 Create a startup shortcut. Press Win+R, type shell:startup, and create a shortcut there pointing to:
pythonw C:\Users\you\EyeReminder\eye_reminder.py
Using pythonw instead of python means no terminal window appears on startup.
Step 4 Reboot. The app silently starts in your system tray. Right-click the tray icon to quit anytime.
Testing it
Before running with 20-minute intervals, test with 5 seconds:
TEST_MODE = True
INTERVAL_SECONDS = 5 if TEST_MODE else 20 * 60
BREAK_SECONDS = 5 if TEST_MODE else 20
Flip TEST_MODE = False when you are happy with it.
Dependencies
| Package | Why |
|---|---|
tkinter |
Built-in — popup UI |
winsound |
Built-in — alarm beeps |
pystray |
System tray icon |
Pillow |
Required by pystray for the icon image |
Core functionality (popup + alarm) works with zero installs. pystray and Pillow are optional the app works without them, just without the tray icon.
Full source
eye-reminder
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.
Why
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.
Demo
[20 min timer] --> ALARM fires --> Blocking popup appears
|
20-sec countdown
|
"Continue" button unlocks
|
Click Continue --> back to work
Click Stop --> session ends
Features
- 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
Requirements
- Windows
- Python 3.8+
- …
eye-reminder
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.
Why
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.
Demo
[20 min timer] --> ALARM fires --> Blocking popup appears
|
20-sec countdown
|
"Continue" button unlocks
|
Click Continue --> back to work
Click Stop --> session ends
Features
- 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
Requirements
- Windows
- Python 3.8+
- …
My eyes are noticeably less dry after two weeks of using this. Sometimes the best tool is the one you build in an afternoon because nothing else works the way you need it to.
Top comments (0)