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

> Learn how FreeTodo's TrayManager class handles system tray and notifications in Electron. Explore its implementation for cross-platform menu bar icons and IPC-based alerts.

- Repository: [FreeU-group/lifetrace](https://github.com/freeu-group/lifetrace)
- Tags: how-to-guide
- Published: 2026-03-02

---

**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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/notification.ts)) logs this initialization, while [`electron/main.ts`](https://github.com/freeu-group/lifetrace/blob/main/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

```typescript
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

```typescript
// 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

```typescript
// 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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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`](https://github.com/freeu-group/lifetrace/blob/main/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.