Sharing notes from my ongoing learning journey — what I build, break and understand along the way.
TerminalNotes Reminder — A Local, Multi-Language Desktop Reminder App with Images, Holidays, and Autostart for Windows
TerminalNotes Reminder: A Smart, Local Reminder Manager for Windows
I basically built a little “desktop reminder center” called TerminalNotes Reminder. It’s a fully local, SQLite-backed, multi-language, holiday-aware, image-enabled reminder app for Windows.
1. What is this app?
This app is:
- A Windows desktop app
- Built with Tkinter
- It stores one-time and daily recurring reminders
- Pops up a notification window + optional sound when something is due
- Lets me attach multiple images to each reminder
- Shows official holidays on a calendar
- Can start automatically with Windows if I want
All data lives locally, in a data directory next to the script:
- Settings →
data/settings.json - Database →
data/reminders.db - Attachments →
data/attachments/
So nothing goes to the cloud; everything stays on the machine.
2. Structure: config, DB, languages
2.1. Config and paths
At the top I define some basic metadata:
APP_NAME = "TerminalNotes Reminder"
ORG_NAME = "TerminalNotes"
During development I keep everything inside a data folder next to the script:
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
CONFIG_DIR = os.path.join(BASE_DIR, "data")
DB_PATH = os.path.join(CONFIG_DIR, "reminders.db")
CFG_PATH = os.path.join(CONFIG_DIR, "settings.json")
ATTACH_DIR = os.path.join(CONFIG_DIR, "attachments")
On first run, these folders are created automatically.
For debugging I also print the paths to the console.
2.2. Internationalization (i18n)
The app supports three languages: Turkish, German, English.
I keep all UI strings in a LANGS dictionary:
- Button labels (
add,edit,delete, …) - Dialog texts and warnings
- Snooze menu labels
- Settings labels
- etc.
Then I load the current config to figure out which locale is active and use a tiny helper:
def get_lang():
cfg = load_cfg()
return LANGS.get(cfg.get("locale","tr_TR"), LANGS["tr_TR"])
def t(key): return get_lang().get(key, key)
So whenever I write t("add"), it’s automatically translated based on the selected language.
2.3. Settings structure
The configuration file (settings.json) looks roughly like this:
DEFAULT_CFG = {
"scheduler_enabled": True, # whether reminder checks are active
"autostart_enabled": False, # start with Windows?
"locale": "tr_TR", # active UI language
"holiday_country": "TR", # country for holiday lookups
"holiday_subdiv": "", # German state (BY, BE, …)
"sound_enabled": True # play sound on popup?
}
load_cfg_raw()→ creates the file with defaults if it doesn’t existload_cfg()→ loads it and fills missing keys fromDEFAULT_CFGsave_cfg()→ writes updated settings back to disk
3. Database model
I use SQLite (reminders.db) with a single reminders table:
CREATE TABLE IF NOT EXISTS reminders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
message TEXT,
long_text TEXT,
type TEXT NOT NULL, -- "once" or "daily"
hhmm TEXT, -- for daily tasks: "HH:MM"
run_at TEXT, -- for once: exact datetime; for daily: snooze datetime
enabled INTEGER NOT NULL DEFAULT 1,
completed_at TEXT,
created_at TEXT NOT NULL,
image_path TEXT -- single filename or JSON list of filenames
)
In short:
- type
"once"→ fires once at a specific datetime"daily"→ repeats every day at a specific time
- hhmm → the “HH:MM” time for daily reminders
- run_at
- for
once: the actual trigger datetime - for
daily: used as the snoozed datetime (if any)
- for
- image_path
- single attachment →
"file.png" - multiple attachments →
["a.png", "b.jpg"]stored as JSON
- single attachment →
I have small helper functions: db_insert, db_update, db_list, db_insert_once, db_update_once, db_delete_many, etc.
For deletion, I’m careful with attachments:
- Before deleting reminders, I count how often each file is referenced.
- After deletion, I only remove physical files that are no longer used by any record.
This prevents shared attachments from being accidentally deleted.
4. Holidays & calendar integration
I use the holidays library to mark official holidays on the calendar.
From the settings dialog the user can choose:
- Country: TR, DE, or US
- If DE is chosen, a specific state (
BY,BE,HH, …)
load_holidays_for(year):
- Reads
holiday_countryandholiday_subdivfrom config - Creates the appropriate
holidays.Country()object - Returns all holidays for the requested year
These holidays are rendered in the tkcalendar.Calendar inside the add/edit dialog:
- I create calendar events with tag
"holiday"and color them light green. - When a reminder’s date also matches a given day, I add a
"task"event as well, colored differently.
On top of this I built a tooltip system:
- When hovering over or selecting a day, the tooltip shows:
- 🎉 holidays
- 🗒 reminders on that day
So the user can quickly see “what is on this date?” with a simple hover or click.
5. UI layer: Tkinter dialogs
5.1. SetupDialog (Settings window)
This is the small configuration window that appears on first run and later via the Settings button.
It lets me configure:
- UI language: Turkish / Deutsch / English
- Holiday country: Türkiye, Deutschland, United States
- For Germany: the state combo box (Bayern, Berlin, …) becomes visible
- Enable/disable notification sound (
sound_enabled) - Enable/disable the scheduler (
scheduler_enabled) - Start with Windows (
autostart_enabled)
When I click Save, it updates settings.json and applies the autostart setting via the Windows Registry.
5.2. AddEditDialog (Create / edit reminder)
This is the main dialog for creating or editing a reminder. It’s quite feature-rich:
- Title (required)
- Short description
- Long description (scrollable
Text) - Image section
- I can select multiple files at once
- They are shown as thumbnails in a horizontally scrollable strip
- Per image I have Open and Remove buttons
- Newly selected files are copied to
attachments/with a UUID filename
- Date selection
- Uses
tkcalendar.Calendar - Holidays and existing tasks are highlighted
- Uses
- Time selection
- Spinboxes for hour and minute (
HH:MM) with wrap behavior
- Spinboxes for hour and minute (
- “Repeat daily” checkbox
- If checked, the reminder is of type
"daily"and useshhmm(notrun_at)
- If checked, the reminder is of type
- Active? checkbox
On save:
- If the reminder is
onceand the chosen date/time is in the past:- I store it but automatically set it as not enabled and show a warning.
- For
dailyreminders I force them to be enabled initially. - I copy any “new” images into
attachments/and buildimage_pathas either a single filename or a JSON list. - I return the final data via
dlg.result:(title, msg, long_text, run_dt_or_none, enabled, image_rel, typ, hhmm)
The main app then decides how to insert or update this record in the DB.
5.3. Details dialog
When I double-click a reminder or press the “Details” button, I open a detail view:
- Title and short description
- Date and time (for daily reminders this can be the snoozed time)
- Active / inactive flag
- Creation time
- Completed time
- A fixed-height, vertically scrollable
Textwidget for the long description - A horizontal thumbnail strip for images
- Each image has:
- Open (launches with the default image viewer)
- Download… (lets the user choose where to save a copy)
- Each image has:
The window is opened as a modal, topmost dialog centered on the screen.
5.4. Main window (App class)
The main Tk() window:
- Title:
TerminalNotes Reminder - A
Treeviewwith columns:- ID
- Title
- Short description
- Date
- Time
- Active
- Double-clicking an item opens the edit dialog.
- Row highlighting:
- If the reminder is due within 1 hour, I color the row in a reddish tone.
- If it’s due within 24 hours, I color it in a yellowish tone.
- I only color rows if:
- The reminder is active
- The effective datetime is in the future and meaningful
At the bottom I have buttons:
- Add
- Edit
- Delete
- Details
- Settings
- Refresh
For sorting:
- I build a sortable list where each row is tagged with:
- Whether it’s active or inactive
- An “effective datetime” (either actual
run_at, or snoozed time, or today/tomorrow for daily tasks)
- Then I sort by:
- Active first, inactive later
- Soonest effective datetime first
So urgent and active reminders bubble to the top of the list.
6. Reminder scheduler
The core of the app is the scheduler logic: _schedule_check and _check_due.
6.1. The periodic loop
In App.__init__ I start the scheduler with:
self._schedule_check(initial=True)
_schedule_check:
- Reads
scheduler_enabledfrom config - If it’s enabled, calls
_check_due(initial=...) - Then uses
root.after(30000, self._schedule_check)to run again every 30 seconds
So the scheduler is basically a lightweight polling loop in Tkinter’s event loop.
6.2. Finding due reminders (_check_due)
I query all enabled=1 reminders from the database.
For daily reminders:
- If
run_atis set and is in the future → that’s the snoozed time, I wait for that. - Otherwise I compute “today at hh:mm” from the
hhmmfield. - If
completed_atis equal to today → I skip, because it’s already done for today. - I also use a small cooldown map: once a popup is shown, I don’t show the same reminder again for 60 seconds.
When something is due:
- I optionally play a sound (
winsound.MessageBeep) - I show a popup via
_show_popup(rid, title, msg, "daily")
For once reminders:
- I parse
run_atas a datetime. - If
completed_atis already set, I skip. - On the very first check after startup (
initial=True), I skip items whose due time was more than 10 minutes ago to avoid spamming the user with a flood of old missed reminders. - Otherwise, if
now >= run_at, it’s due:- I immediately set
enabled=0in the DB so it only fires once. - I show a popup via
_show_popup.
- I immediately set
6.3. Popup behavior
The popup window has:
- A big green Done button:
- For daily:
- Set
completed_at= today’s date - Clear
run_at(remove snooze)
- Set
- For once:
- Set
enabled=0 - Set
completed_at= current datetime
- Set
- For daily:
- A Details button:
- Opens the details dialog for that reminder
- A Snooze menu button with entries:
- “Not now (5 min)”
- 30 min, 1 h, 3 h, 6 h, 12 h, 1 day
Each option calls_snooze_minutes(rid, minutes, win), which:- Moves
run_atforward by that many minutes - Ensures
enabled=1 - Closes the popup
- Refreshes the list and autostart state
- Moves
The popup is always centered on the monitor, brought to front (-topmost) and focused, so the user can’t miss it.
7. Windows integration: autostart + multi-monitor centering
7.1. Autostart via Registry
_apply_autostart(enable) handles registration in:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
under the name TerminalNotesReminder.
- If
enableis true:- It builds a command like
"pythonw.exe" "script.py"(or falls back tosys.executable) - Writes it into the Registry
- It builds a command like
- If false:
- It removes the value from Registry if it exists
Additionally, _sync_autostart_by_enabled() makes it a bit smarter:
- It counts the number of active reminders in the DB
- If there are no active reminders, it disables autostart automatically
- That way, the app doesn’t auto-start if there’s nothing to remind the user of.
7.2. Centering windows on the correct monitor
center_on_screen(win):
- Uses
ctypes.windll.user32to:- Get the current mouse cursor position
- Find the monitor where the cursor is
- Retrieve the work area of that monitor (excluding taskbar)
- Center the given window within that work area
- If anything fails, it falls back to centering on the primary screen using Tk’s
winfo_screenwidth/height.
I call this for:
- The main window
- The add/edit dialog
- The setup dialog
- The reminder popup
- The details dialog
So everything appears nicely centered on whichever monitor the user is actually using.
8. Attachment handling
The attachment system is designed to support multiple images per reminder safely:
- When the user picks images:
- Each file is added as
{"kind":"new","path":<fullpath>}internally - On save, new ones are copied into
attachments/with a unique UUID name - Existing ones are tracked as
{"kind":"saved","path":<relative_name>}
- Each file is added as
- In the DB:
image_pathis:- Just a filename if there’s a single attachment
- A JSON list if there are multiple
In the details dialog:
- I split this into a list of paths
- For each path:
- Build the full path under
attachments/ - Load a thumbnail with Pillow (
PIL) - Show it with “Open” and “Download…” buttons
- Build the full path under
On delete:
- I compute how many records reference each file before deletion
- I compute again which files are referenced by the records being deleted
- If a file is only referenced by the records I’m deleting (and not by others), I remove it from disk too.
9. In summary
So, what I’ve built is:
- A multi-language, holiday-aware, image-friendly reminder manager for Windows
- Completely local: SQLite + JSON, no external services
- With:
- One-time and daily repeating reminders
- Snoozing in various intervals
- “Done” tracking with completion timestamps
- Popups + optional beep sound
- A calendar that visualizes both holidays and tasks
- Attachment support with thumbnails, open & download actions
- Smart Windows autostart management
- Proper centering on multi-monitor setups
