UPDATE: I no longer use these notifications, but still use custom scripts with systemd timers.

After a while, the notifications become like a spam and you don’t care anymore, especially when you are hyper-focused on coding. Instead, I made it a habit to take breaks regularly and move around, in addition to taking 10min naps whenever I feel too tired.

The nix code pieces here should still work if you want to add it to your system though.


I recently noticed that after lengthy coding sessions, my vision becomes blurry and my eyes become so dry that I almost want to put them under a faucet for relief. This motivated me to care more about by my eye health, and the easiest solution I could came up with was to add notifications with timers to follow the 20/20/20 rule, which can be accomplished very easily with systemd and libnotify.

I am using NixOS as my daily driver, and it is very simple to configure systemd services on it:

nix
 1systemd.user = {
 2    services = {
 3        eye-strain-notify = {
 4          Install.WantedBy = ["default.target"];
 5          Unit.Description = "Send notification to take 20sec break for preventing eye strain.";
 6          Service.ExecStart = "${pkgs.libnotify}/bin/notify-send 'Break' 'Eye strain break 20 secs!' -u critical";
 7        };
 8    };
 9    timers = {
10        eye-strain-notify = {
11          Install.WantedBy = ["timers.target"];
12          Unit.Description = "Trigger eye strain notification on 20 and 40 min mark of every hour.";
13          Timer.OnCalendar = "*-*-* *:20,40:00";
14        };
15    };
16};

To summarize, we create a systemd service which sends a notification to the system with libnotify library using notify-send command. To trigger this service periodically, we also create a systemd timer which is run 20 and 40 minute marks of every hour.

Why not trigger it on every hour on top of 20 and 40 minute marks, you ask? It’s because I decided to also take care of my physical health so that I don’t turn into Gollum in front of my computer. For that reason, I also added a different hourly notification for taking a break and stretching my body.

nix
 1systemd.user = {
 2    services = {
 3        eye-strain-notify = {
 4          Install.WantedBy = ["default.target"];
 5          Unit.Description = "Send notification to take 20sec break for preventing eye strain.";
 6          Service.ExecStart = "${pkgs.libnotify}/bin/notify-send 'Break' 'Eye strain break 20 secs!' -u critical";
 7        };
 8        stretch-notify = {
 9          Install.WantedBy = ["default.target"];
10          Unit.Description = "Send notification to take a break and stretch.";
11          Service.ExecStart = "${pkgs.libnotify}/bin/notify-send 'Break' 'Stretch!' -u critical";
12        };
13    };
14    timers = {
15        eye-strain-notify = {
16          Install.WantedBy = ["timers.target"];
17          Unit.Description = "Trigger eye strain notification on 20 and 40 min mark of every hour.";
18          Timer.OnCalendar = "*-*-* *:20,40:00";
19        };
20        stretch-notify = {
21          Install.WantedBy = ["timers.target"];
22          Unit.Description = "Trigger strecth notification every hour.";
23          Timer.OnCalendar = "*-*-* *:00:00";
24        };
25    };
26};

One additional important note here is that I defined this sytemd services and timers on my home configuration instead of sytem configuration. This way, the services are created under my user home, on ~/.config/systemd/user/ path to be specific. This is not really critical as I am the only user on my system, but in case you want to create systemd services and timers on a system with multiple users, this distinction might be important. I still followed this best practice for completeness sake.

After applying these changes, you will see that the services and timers files are created under ~/.config/systemd/user/:

bash
1$ ls ~/.config/systemd/user/
2eye-strain-notify.service
3eye-strain-notify.timer
4stretch-notify.service
5stretch-notify.timer

We can also check the service status with the following commands.

bash
 1$ systemctl status --user eye-strain-notify.service
 2○ eye-strain-notify.service - Send notification to take 20sec break for preventing eye strain.
 3Loaded: loaded (/home/user/.config/systemd/user/eye-strain-notify.service; enabled; preset: enabled)
 4     Active: inactive (dead) since Sun 2023-07-30 11:40:53 CEST; 15min ago
 5   Duration: 30ms
 6TriggeredBy: ● eye-strain-notify.timer
 7...
 8
 9$ systemctl status --user eye-strain-notify.timer
10● eye-strain-notify.timer - Trigger eye strain notification on 20 and 40 min mark of every hour.
11     Loaded: loaded (/home/user/.config/systemd/user/eye-strain-notify.timer; enabled; preset: enabled)
12     Active: active (waiting) since Sat 2023-07-30 10:17:59 CEST; 1 day 1h ago
13    Trigger: Sun 2023-07-30 12:20:00 CEST; 21min left
14   Triggers: ● eye-strain-notify.service
15...
16   
17$ systemctl status --user stretch-notify.service
18...
19
20$ systemctl status --user stretch-notify.timer
21...

One other command for getting more info about the timers is:

bash
1$ systemctl --user list-timers --all
2NEXT                         LEFT       LAST PASSED UNIT                    ACTIVATES
3Sun 2023-07-30 12:20:00 CEST 16min left -    -      eye-strain-notify.timer eye-strain-notify.service
4Sun 2023-07-30 13:00:00 CEST 56min left -    -      stretch-notify.timer    stretch-notify.service

As an alternative to systemd, cronjobs can be also be used. In fact, it was the first method I tried. However, on nix wiki, using systemd timers were advised. Considering that systemd is already installed on the system, there is no reason to use cron additionally. It also plays nicer with user vs. system level services since cronjobs can’t be defined with home-manager, which is better since I am also using wayland with sway.

Another option is to use a dedicated application with more features, but I couldn’t find something that works well in linux, especially on wayland. I also don’t need most of the features that come with such applications, and systemd triggered notifications are more than enough for my use case.

That’s all. Thank for reading, and I hope you take good care of your physical and eye health as well.