The development of my home brewed reminder system

Introduction

For some time I’ve wanted a reminder system that would display a persistant message on my desktop and that remains on top of other windows until I Dismiss it, and that coould be scheduled to run on whatever schedule I need. When I wasn’t able to find anything that met my requirements, I thought about ways to do what I wanted, and I worked out that I needed two components, something to put my mesage on the desktop, and a way to schedule it. The second part’s alreaady available. In Windows, there’s the Windows Task Scheduler, and in Garuda Linux, there’s systemd with it’a scheduling capabilities, and is much more robust than cron. So I had the scheduling part in hand, but the stumbling block was an app that puts a small window on my desktop with the title and message I want, so I decided to experiment with vibe coding, first using Chat-GPT in VS-Code, then with Ollama paired with a number of coding and chat agents. I was able to vibe-code an app that puts a window on the desktop, and that gets it’s title and the message to be displayed from the command line, but the chat/coding agent I was using could never get the GNU/Linux side to work in WSL, so I gave up on that part, at least for a while. In Windows, I was also able to get PowerShell to generate a beep when I launched the app using a hidden treminal, so I was partially able to accomplish what I wanted.

When I woke up this morning (Saturday, April 19, 2026), it occorred to me that I haven’t asked the AI overview function in Google’s search engine from Firefox yet, and that since Python’s a scripted language with it’s own runtime, it would have the best chance of working in both Windows 11 and Garuda Linux. So I gave it a shot, and within an hour, I had a Python script that put a window on my desktop, obtained it’s title and the message I wanted displayed, and that even sounded a beep when launched. It worked in both Windows and Garuda Linux, at least from the command line, but I couldn’t get it to sound the beep in WSL, so I got all the reminders set up using Windows Task Scheduler, and restarted into Garuda Linux where I kept trying to figure out how to get the beep working. After a bit, I enabled Firefox’s side bar chat dialog using Goodle’s Gemini and continued my efforts. After a few attempts, Gemini suggested we modify my Python script to make it more systemd-friendly, and everything finally works as I want in Garuda Linux, not to mention what I learned about working with systemd.

This is my Python script:

import tkinter as tk
import platform
import os
import subprocess
import argparse
import sys  # Added to handle raw argument checks

def play_beep():
    if platform.system() == "Windows":
        import winsound
        winsound.Beep(1000, 300)
    else:
        try:
            subprocess.run([
                "canberra-gtk-play",
                "--id", "message-new-instant-message"
            ], check=False)
        except FileNotFoundError:
            try:
                sound_file = "/usr/share/sounds/freedesktop/stereo/message-new-instant-message.oga"
                subprocess.run(["paplay", sound_file], check=False)
            except Exception:
                print('\a', end='', flush=True)

def main():
    # If the script is called by systemd via notify-me@Title-Message.service
    # it will typically receive ONE argument that needs splitting.
    if len(sys.argv) == 2 and "-" in sys.argv[1]:
        raw_input = sys.argv[1]
        # Split at the first hyphen only
        title_part, message_part = raw_input.split("-", 1)

    # Convert underscores back to spaces for the GUI
    display_title = title_part.replace("_", " ")
    display_message = message_part.replace("_", " ") + "."
else:
    # Standard CLI behavior (for manual testing)
    parser = argparse.ArgumentParser(description="Cross-platform mini window")
    parser.add_argument("title", help="Text for the window title bar")
    parser.add_argument("message", help="The message body to display")
    args = parser.parse_args()
    display_title = args.title
    display_message = args.message

play_beep()

root = tk.Tk()
root.title(display_title)
root.geometry("350x200") # Slightly larger to ensure text fits
root.attributes('-topmost', True)

# UI Elements
tk.Label(root, text=display_title, font=("Arial", 12, "bold")).pack(pady=(15, 5))
tk.Label(root, text=display_message, font=("Arial", 10), wraplength=300).pack(pady=10)
tk.Button(root, text="Dismiss", command=root.destroy, width=10).pack(pady=10)

root.mainloop()

if name == "main":
    main()

I stored this Python script under my home directory at ~/Show-It/Show-It.py, and I run it as a local task (running in my user space), even though I’m the only user on my Garuda Linux system.

Systemd needs two files for a task, a .service file, and a .timer file. Since I’m scheduling my Say-It.py script to perform several tasks, I only need a single .service file, but I need a .timer file for each task. These files must be stored under /home/$(user)/.config/systemd/user so I decided to create a Show-It directory under user so all the tasks I’ll be scheduling with my Python script will be kept together, but separate from any other apps I may schedule in the future.

This is my .service file:

[Unit]
# Note: %i is a specifier for the instance name.
# When creating a template service, the part of the filename after the @ is substituted here at runtime.
Description=Notification Service for %i

[Service]
Type=simple

# Wayland requirements for Garuda KDE
Environment=WAYLAND_DISPLAY=wayland-0

# You may have to edit this to match your UID (in a terminal run id -u)
Environment=XDG_RUNTIME_DIR=/run/user/1000

# We pass the %i variable (the instance name) directly to your script and you should replace $(user) with your user name
ExecStart=/usr/bin/python /home/$(user)/Show-It/Show-It.py "%i"

[Install]
WantedBy=default.target

Because I’ve already named my Python script Show-It.py, and I’ve stored it under my home directory at ~/(user)/Say-It/Say-It.py, and I’ve also stored my .service file in /home/(user)/systemd/user/Say-It, I’ve also saved my .timer files in the same directory (/home/$(user)/systemd/user/Say-It). Below, I’ll add my template for my .timer files.
Make sure to follow the commented instructions.

My .timer file template

 [Unit]
 # Briefly describe the purpose of this task, replacing $(descripyion)
 Description=$(descripyion)
 
 [Timer]
 # Replace $(time) with the planned time as in 10:00 AM
 # Schedule: Every day at $(time)
 # Replace the ? characters with the 24-hour time e,g,: 10:00:00 = 10:00 AM or 13:00:00 = 1:00 PM, etc.
 OnCalendar=--* ??:??:??
 
 # Formatting: Title_Part-Message_Part and replace $(Title-Message) with your real title-message combination
 Unit=Show-It@$(Title-Message).service
 
 [Install]
 WantedBy=timers.target

Last, but not least comes my templated .timer file creation guide:

The “New Timer” Checklist

Unique Filename: Save each new timer as name-of-task.timer in ~/.config/systemd/user/.

The Unit Line: Ensure Unit= points to your template: Show-It@Title_Here-Message_Here.service.

No Forbidden Characters: Avoid !, ', ", ?, or * inside that Unit= line. Use underscores for spaces.

Activate: Always run systemctl --user daemon-reload and systemctl --user enable --now <filename>.timer.

Three steps to create a new task for an existing .service

Step 1. Create the timer file from this template:

[Unit]
# Briefly describe the purpose of this task, replacing $(descripyion) with your description
Description=$(descripyion)

[Timer]
# Note: In all cases, schedule the time of the task by replacing ??:??:?? with Hour:Minute:Second
# Note: For a one time task, also set the date by replacing --* with Year-Month-Day
OnCalendar=--* ??:??:??

# Formatting: Title_Part-Message_Part and replace $(Title-Message) your title-message
Unit=Show-It@$(Title-Message).service

[Install]
WantedBy=timers.target

Step 2. Name the new timer file:

# use a concise name that describes the purpose of the reminder
# example: Day-Meds-Reminder may be a name for a reminder to take day meds:

Step 3. Enable and test the task:

# reload systemd
systemctl --user daemon-reload

# Enable the task by replacing $(filename.timer) with the real name of the .timer file
systemctl --user enable --now $(filename.timer).timer

# Test your task by replacing $(Title-Message) with your real Title-Message
# as in systemctl --user start Show-It@Test-This_is_a_test_message.servicesystemctl --user start Show-It@$(Title-Message).service

Edit: I added a Step 4 to fix a failure to launch as scheduled issue.

Step 4. Add the following to the config.fish file or .bashrc:

if not test -e /var/lib/systemd/linger/ernie
    loginctl enable-linger ernie
end

This snippet will test whether linger is enabled at sysstem start to ensure any .timer files that are created launch as scheduled

This is about everything I used to create a personal reminder system that’s not oonly free, but very well integrated into my dostribution’s operating ssystem. While I’m certain that I’ll receive a few questions on this as well as an abundance of objections to the use of systemd. After doing significant research, I’m convinced that systeemd is more robust, and more flexible than cron (my initial choice), but getting my script to beep went beyond the scope of cron’s functionality while with systemd and a slight modification of my Python script, I was able to get an audible sound wheen the window launches (sonething I’m not sure I could have obtained in cron).

If I know the answer to any questions, I’ll post what I know. If I don’t know the answer, I’ll put in some time to research the question in an effort to obtain a good answer, during which time I’ll be learning new things too! I hope readers like my effort,

Ernie

9 Likes