close

DEV Community

Haripriya Veluchamy
Haripriya Veluchamy

Posted on

I built a 20-20-20 eye reminder because my eyes were dying at my desk

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 2 Save eye_reminder.py to a permanent location:

C:\Users\you\EyeReminder\eye_reminder.py
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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+



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)