Tray and dock
Ende.Tray puts an icon in the system status area
(macOS menu bar extras, Windows system tray, Linux AppIndicator).
Ende.dock is a singleton that controls the
app's dock / taskbar presence: badge, bounce, visibility, and a custom menu.
Menus on both use the Ende.MenuItem type.
Ende.Tray
const icon = await Ende.readFile("./icons/tray.png");
const tray = new Ende.Tray();
tray.setIcon(icon);
tray.setTooltip("My App");
tray.setMenu([
{ item: { label: "Open", id: "open", enabled: true } },
{ item: { label: "Quit", id: "quit", enabled: true } }
]);
tray.addEventListener("menuclick", (e) => {
if (e.detail.id === "open") win.show();
if (e.detail.id === "quit") Ende.exit(0);
});
Lifecycle
The icon stays in the status area until you call tray.destroy() (or the
process exits). Multiple trays can coexist, useful for app indicators that need
separate control surfaces.
tray.destroy();
Tray is also a Disposable, so it works with using:
{
using tray = new Ende.Tray();
// ...
} // automatically destroyed at scope exit
Setting the icon
tray.setIcon(pngBytes); // bytes, not a path
tray.setIconDark(darkPngBytes); // optional dark-mode variant
tray.setIconDark(null); // clear the dark icon
Pass PNG-encoded bytes, not a file path. Read the file yourself:
const png = await Ende.readFile("./icons/tray.png");
tray.setIcon(png);
Provide a separate dark-mode icon via setIconDark if you want different
contrast for dark menu bars (macOS 10.14+, modern Linux). Without one, the same
icon is used in both modes.
For best results, use a template image style (mostly opaque silhouette, transparent elsewhere) at a small size: 22×22 logical pixels for macOS, 16×16 for Windows.
Tooltip
tray.setTooltip("My App: 3 unread");
tray.setTooltip(null); // remove tooltip
Context menu
Right-click on the tray icon opens the menu set by setMenu. The items are the
same Ende.MenuItem shape used by application and
context menus:
tray.setMenu([
{ item: { label: "Open", id: "open", enabled: true } },
"separator",
{
item: {
label: "Settings…",
id: "settings",
accelerator: "CmdOrCtrl+,",
enabled: true
}
},
"separator",
{
item: {
label: "Quit",
id: "quit",
accelerator: "CmdOrCtrl+Q",
enabled: true
}
}
]);
tray.addEventListener("menuclick", (e) => {
switch (e.detail.id) {
case "open":
win.show();
break;
case "settings":
showSettings();
break;
case "quit":
Ende.exit(0);
break;
}
});
tray.setMenu(null); // remove the menu without destroying the tray
Submenus work the same as in the application menu.
Click events
tray.addEventListener("click", () => win.show());
tray.addEventListener("dblclick", () => openSettings());
click fires on a primary-button click. dblclick fires on a double-click. On
platforms where right-click is reserved for the context menu (everywhere), only
left-click produces these events.
Popover panels
For the classic menu-bar-app pattern, click the tray icon to toggle a small
floating window anchored under it, then use attachPanel():
const tray = new Ende.Tray();
tray.setIcon(await Ende.readFile("./icons/tray.png"));
const panel = tray.attachPanel({
url: `http://127.0.0.1:${port}/panel`,
width: 360,
height: 480
});
panel.window.bind("doThing", async () => {/* … */});
The returned Ende.TrayPanel toggles on tray
click, is positioned under the icon, and hides when it loses focus. Pass a
string as shorthand for { url }. TrayPanelOptions also accepts hideOnBlur
(default true) and a position callback to override placement (e.g. for a
bottom-edge taskbar).
panel.show();
panel.hide();
panel.toggle();
console.log(panel.visible);
panel.destroy(); // detach and close the panel window
panel.window; // the underlying BrowserWindow: bind(), executeJs(), etc.
The panel is a convenience built on the primitives. For full control, create a
frameless + noActivate BrowserWindow yourself
and position it with Tray.getBounds():
const bounds = tray.getBounds(); // { x, y, width, height } | null
if (bounds) {
popover.setPosition(bounds.x, bounds.y + bounds.height);
popover.show();
}
getBounds() returns the icon's screen rectangle, or null when the platform
can't report it. On Linux the icon position can't be queried, so an attached
panel shows at its last position rather than anchored to the icon.
Platform support
Tray icons rely on the OS providing a status area. The relevant backends support tray on:
- macOS: status menu items (NSStatusItem).
- Windows: system tray (NotifyIcon).
- Linux: AppIndicator / KStatusNotifierItem. Requires a desktop environment
that surfaces them. Most do, but some minimal i3 setups need extras like
swayncorpolybarconfiguration.
If the backend cannot create a tray icon, the constructor's underlying trayId
is 0 and subsequent calls are no-ops (silently). Check tray.trayId !== 0 if
you need to fall back gracefully.
Ende.dock
Ende.dock is a singleton exposing the app's
dock / taskbar controls. The methods are cross-platform but their effect varies:
macOS-only operations are no-ops on Windows and Linux (they fail gracefully
rather than throwing).
Badge
Ende.dock.setBadge("3"); // short text on the dock / taskbar icon
Ende.dock.setBadge(null); // clear (null or empty string)
Sets a text badge on the dock icon (macOS) or taskbar icon (Windows); on Linux it prefixes the focused window's title. Badges are short, typically a count; the OS truncates long strings.
Bounce
Ende.dock.bounce(); // single bounce / flash
Ende.dock.bounce(true); // bounce continuously until the app is focused
Bounces the dock icon (macOS), flashes the taskbar button (Windows), or sets the
urgency hint on the focused window (Linux). The optional critical argument
(default false) controls whether it bounces once or continuously.
Visibility
Ende.dock.setVisible(false); // remove the app from the dock
Ende.dock.setVisible(true); // restore it
macOS only; controls the app's activation policy. A hidden app does not appear in the dock or the Cmd-Tab switcher, which is what you want for a menu-bar-only app. The application keeps running and can still show windows; users reach it via Spotlight or the tray icon. No-op on Windows and Linux.
Menu
Ende.dock.setMenu([
{ item: { label: "New Window", id: "new", enabled: true } },
{ item: { label: "Quit", id: "quit", enabled: true } }
]);
Ende.dock.setMenu(null); // remove the menu
Ende.dock.addEventListener("menuclick", (e) => {
if (e.detail.id === "quit") Ende.exit(0);
});
macOS only; a custom right-click menu on the dock icon. Clicks are delivered as
menuclick events on Ende.dock.
Reopen event
On macOS, clicking the dock icon while the app has no visible windows fires a
reopen event. The default "show the last hidden window" behavior is swallowed,
so you decide what to do:
Ende.dock.addEventListener("reopen", (e) => {
if (!e.detail.hasVisibleWindows) win.show();
});
Pattern: tray-only background app
To run as a status-bar-only background process (no dock, no main window):
Ende.dock.setVisible(false); // macOS: hide the app from the dock
win.hide(); // hide the implicit startup window
const tray = new Ende.Tray();
tray.setIcon(await Ende.readFile("./icons/tray.png"));
tray.setTooltip("My App");
tray.setMenu([
{ item: { label: "Show window", id: "show", enabled: true } },
{ item: { label: "Quit", id: "quit", enabled: true } }
]);
tray.addEventListener("menuclick", (e) => {
if (e.detail.id === "show") win.show();
if (e.detail.id === "quit") Ende.exit(0);
});
The startup window is created when your binary launches; hiding it keeps it ready to be shown without a startup delay later.