How Lepton Implements System Tray Icons and Context Menus in Electron

Lepton implements system tray icons and context menus entirely within the Electron main process using the Tray and Menu APIs, with state management handled through an operationType variable and a setTray() helper function.

Lepton is an open-source snippet manager built on the Electron framework. Its system tray functionality allows users to minimize the application to the tray rather than closing it entirely, keeping the app accessible without cluttering the taskbar. This implementation resides primarily in the main process entry point and relies on Electron's native modules to manage icons, tooltips, and context menus.

Tray Creation and Icon Configuration

The tray initialization logic is encapsulated in the setTray(app, mainWindow) helper function defined in main.js. This function is invoked at three specific points during the application lifecycle: line 150 (during initial setup), line 171 (when configuring window behavior), and line 388 (when handling minimize actions).

The tray icon is loaded from a static asset path using Node.js path utilities:

const iconPath = path.join(__dirname, 'build/icon/icon.png')
miniWindow = new Tray(iconPath)

This code appears at lines 389-412 of main.js. The variable miniWindow holds the Tray instance throughout the application session, remaining active as long as the process runs.

Building the Context Menu

Lepton constructs the tray context menu using Electron's template-based menu system. The menu template is defined directly within the setTray function at lines 393-402:

const trayMenuTemplate = [
  { label: 'Open Window', click: () => { mainWindow.show() } },
  { label: 'Quit', click: () => { operationType = 2; if (process.platform !== 'darwin') app.quit(); mainWindow = null } }
]

The template contains two actions: restoring the main window and terminating the application. The construction and attachment occur at lines 413-420:

const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)
miniWindow.setContextMenu(contextMenu)

This approach creates a native context menu that integrates with the host operating system's tray interface.

Handling User Interactions

Beyond the context menu, Lepton configures additional tray interactions to improve usability. The tooltip is set at line 421 using miniWindow.setToolTip('Lepton'), displaying the application name on hover.

Double-click functionality is implemented through an event listener at lines 421-425:

miniWindow.on('double-click', () => { mainWindow.show() })

This allows users to restore the hidden window quickly without accessing the context menu, providing a familiar interaction pattern for system tray applications.

Integrating with Window Lifecycle

The tray functionality coordinates closely with window lifecycle management through a global state variable called operationType. This variable distinguishes between three operational states:

  • 0 – Normal operation with the window visible
  • 1 – Window minimized to tray (hidden but process running)
  • 2 – Application quit sequence initiated

The mainWindow.on('close', …) handler at lines 36-74 of main.js implements this logic. When a user attempts to close the window while operationType is 0, a dialog presents three options: "Never Mind," "Minimize to tray," and "Quit." Selecting "Minimize to tray" triggers setTray(), hides the window via mainWindow.hide(), prevents the close event with e.preventDefault(), and sets operationType to 1.

This state management ensures the tray icon persists only when the user explicitly chooses to minimize rather than quit, and it prevents accidental application termination.

Key Implementation Files

The system tray functionality spans several files within the repository:

  • main.js – Core Electron entry point containing the setTray() function, window lifecycle handlers, and tray event listeners
  • app/utilities/menu/mainMenu.js – Defines the main application menu template, which operates independently from the tray menu but uses the same Menu API
  • build/icon/icon.png – The icon asset displayed in the system tray
  • package.json – Declares Electron as the runtime dependency enabling the Tray and Menu modules

Summary

  • Lepton creates tray icons via the setTray() helper in main.js using Electron's Tray class and an icon path resolved to build/icon/icon.png
  • Context menus are built from JSON templates using Menu.buildFromTemplate() and attached via setContextMenu() on the tray instance
  • The operationType state variable (0/1/2) coordinates window visibility, distinguishing between normal operation, minimize-to-tray, and quit states
  • User interactions including double-click to restore and tooltip display are handled through native Electron event listeners on the miniWindow tray instance
  • The close event handler at lines 36-74 of main.js integrates tray creation with user confirmation dialogs to prevent accidental closures

Frequently Asked Questions

How does Lepton distinguish between minimizing to tray and quitting the application?

Lepton uses a global operationType variable to track application state. When the user closes the window, a dialog offers three choices (lines 36-74 of main.js). Selecting "Minimize to tray" sets operationType to 1, prevents the window close event, and initializes the tray. Selecting "Quit" sets operationType to 2 and allows the application to terminate normally.

Can the tray icon path be customized in Lepton?

According to the source code in main.js (lines 389-412), the tray icon path is hardcoded to build/icon/icon.png using path.join(__dirname, 'build/icon/icon.png'). Users would need to modify this path in the source and rebuild the application to use a custom icon.

Is the tray functionality cross-platform compatible?

The implementation uses Electron's standard Tray API, which is supported on macOS, Windows, and Linux. However, the quit handler includes a platform-specific check: if (process.platform !== 'darwin') app.quit(), which accounts for differences in macOS application lifecycle expectations where apps typically remain active until explicitly quit from the menu.

How does the tray menu differ from the main application menu?

The tray menu is defined inline within the setTray() function in main.js and contains only "Open Window" and "Quit" actions. The main application menu is defined separately in app/utilities/menu/mainMenu.js and provides comprehensive application navigation. Both use Menu.buildFromTemplate(), but the tray menu is optimized for quick window management while the main menu offers full application functionality.

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 →