How FreeTodo Handles System Tray and Notifications in Electron: Implementation Guide

FreeTodo implements system tray integration and native OS notifications through a dedicated TrayManager class that manages cross-platform menu-bar icons and an IPC-based notification bridge that allows renderer processes to trigger native alerts via the main process.

FreeTodo, the task management application from the freeu-group/lifetrace repository, leverages Electron's main process to deliver persistent desktop integration across Windows, macOS, and Linux. The application combines a TrayManager class for system tray lifecycle management with a centralized notification module to surface todo-related events even when the Dynamic Island UI is hidden.

System Tray Management with TrayManager

Architecture and Responsibilities

The TrayManager class, defined in free-todo-frontend/electron/tray-manager.ts, encapsulates all system tray functionality. It handles icon creation, context menu construction, click event handling, and resource cleanup. The class maintains a reference to IslandWindowManager (from free-todo-frontend/electron/island-window-manager.ts) to control the visibility of the Dynamic Island window when users interact with the tray icon.

Icon Initialization and Platform Handling

During instantiation in electron/main.ts (lines 40-45), the tray manager resolves the application icon using platform-aware path logic. For production builds, it loads free_todo_icon_4.png from process.resourcesPath, while development builds use the local public/ directory. The icon is resized to 16 × 16 pixels using nativeImage.resize() to ensure proper rendering across all operating systems.

Context Menu and User Interaction

The buildContextMenu() method constructs a cross-platform menu template featuring actions such as Show/Hide Island, Preferences, and Quit Free Todo. On macOS, the menu triggers on right-click, while left-click events connect to toggleIsland(), which calls IslandWindowManager.toggle() and updates the tray tooltip to reflect the current visibility state. The tooltip dynamically updates between "Free Todo (Visible)" and "Free Todo (Hidden)" based on the island window state.

Application Lifecycle Integration

Cleanup occurs through the destroy() method, invoked during the application's quit sequence. The bootstrap code in electron/main.ts registers app.on('before-quit') and app.on('quit') handlers to ensure the tray icon is properly removed and native resources are released when the application terminates.

Native Notification System Architecture

Notification Module Implementation

The notification system centers on the showSystemNotification() function in free-todo-frontend/electron/notification.ts (lines 41-75). This function constructs an Electron.Notification instance with configurable title, body, and silent parameters. It registers event listeners for click (which triggers windowManager.focus() to bring the main window forward), show, and close to log lifecycle events for diagnostics. If notification creation fails, the error is logged without crashing the application.

IPC Bridge for Renderer Communication

Renderer processes communicate with the notification system through a dedicated IPC channel. The handler in free-todo-frontend/electron/ipc-handlers.ts (lines 25-38) listens for ipcMain.handle('show-notification'), receiving payload objects containing id, title, content, and timestamp fields. This architecture isolates native notification APIs within the main process while exposing a safe contract to the renderer.

Permission Handling and Bootstrap Flow

Electron automatically requests notification permission from the user upon first use. The requestNotificationPermission() function (lines 30-34 in notification.ts) logs this initialization, while electron/main.ts (lines 51-53) invokes this check during the bootstrap sequence to ensure the OS dialog appears when the first notification triggers.

Practical Implementation Examples

Creating a Cross-Platform Tray Icon

import { Tray, nativeImage, Menu, app } from 'electron';
import path from 'node:path';

// Resolve icon for both packaged and development environments
const iconPath = app.isPackaged
  ? path.join(process.resourcesPath, 'standalone', 'public', 'free-todo-logos', 'free_todo_icon_4.png')
  : path.join(__dirname, '..', 'public', 'free-todo-logos', 'free_todo_icon_4.png');

const tray = new Tray(
  nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 })
);

tray.setToolTip('Free Todo – Dynamic Island');
tray.setContextMenu(
  Menu.buildFromTemplate([
    { label: 'Show Island', click: () => windowManager.show() },
    { type: 'separator' },
    { label: 'Quit', role: 'quit' }
  ])
);

Triggering Notifications from the Renderer Process

// In a React component or renderer process
import { ipcRenderer } from 'electron';

export const notifyTodoCreated = (todo) => {
  ipcRenderer.invoke('show-notification', {
    id: `todo-${todo.id}`,
    title: 'New Todo Added',
    content: todo.title,
    timestamp: new Date().toISOString(),
  });
};

Synchronizing Tray Tooltip with Window Visibility

// In TrayManager constructor (lines 31-35 of tray-manager.ts)
setVisibilityChangeCallback((isVisible) => {
  this.tray.setToolTip(`Free Todo ${isVisible ? '(Visible)' : '(Hidden)'}`);
});

Summary

  • TrayManager (free-todo-frontend/electron/tray-manager.ts) centralizes all system tray logic, including icon creation, context menu management, and cleanup handling during application shutdown.
  • IslandWindowManager integration enables the tray icon to toggle the Dynamic Island window visibility and synchronize tooltip states based on UI visibility changes.
  • Native notifications are encapsulated in showSystemNotification() within free-todo-frontend/electron/notification.ts, with renderer access provided through the show-notification IPC channel defined in free-todo-frontend/electron/ipc-handlers.ts.
  • Cross-platform compatibility is achieved through platform-aware icon path resolution (using process.resourcesPath vs. __dirname) and Electron's native notification permissions, which are automatically managed by the OS on first use.
  • Resource cleanup is guaranteed through app.on('before-quit') handlers in electron/main.ts that invoke TrayManager.destroy() to remove tray icons and release native resources.

Frequently Asked Questions

How does FreeTodo handle tray icon cleanup when the application quits?

The application registers quit event listeners in electron/main.ts that invoke TrayManager.destroy(), which removes the tray icon and releases associated native resources before the process terminates. This ensures no orphaned icons remain in the system tray after the app closes.

Can FreeTodo display notifications when the main window is hidden?

Yes. The notification system operates independently of window visibility through the main process. When a user clicks a notification, the click event handler calls windowManager.focus() (implemented in free-todo-frontend/electron/notification.ts, lines 57-61) to restore and bring the main window to the foreground, making the notification system functional even when the Dynamic Island is minimized or hidden.

What image format and size does FreeTodo use for the system tray icon?

FreeTodo uses PNG format (free_todo_icon_4.png) resized to 16 × 16 pixels using Electron's nativeImage.resize() method. The implementation dynamically resolves the icon path based on whether the application is running in a packaged production build (using process.resourcesPath) or development mode.

How does the renderer process request native notifications without direct access to the Notification API?

The renderer uses ipcRenderer.invoke('show-notification', payload) to send notification data to the main process. The ipcMain.handle('show-notification') handler in free-todo-frontend/electron/ipc-handlers.ts (lines 25-38) receives this data and forwards it to showSystemNotification(), maintaining security by keeping native API access within the main process while exposing a controlled interface to the UI.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →