Tray
Electron Tray 호환 API. macOS는 메뉴바 NSStatusItem, Linux는 GTK StatusIcon,
Windows는 system tray Shell_NotifyIconW 경로를 쓴다. Frontend JS + Zig/Rust/Go/Node
백엔드 SDK 모두 노출된다.
플랫폼 지원
섹션 제목: “플랫폼 지원”| API | macOS | Linux | Windows |
|---|---|---|---|
tray.create | ✅ title/tooltip/iconPath | ✅ title/tooltip/iconPath | ✅ tooltip, 기본 icon |
tray.setTitle/setTooltip | ✅ title + tooltip | ✅ title + tooltip | ✅ tooltip update (setTitle도 tooltip으로 매핑) |
tray.setMenu | ✅ item/separator/checkbox/submenu/enabled | ✅ item/separator/checkbox/submenu/enabled | ✅ flat HMENU item/separator, checkbox 표시 |
tray.destroy | ✅ | ✅ hide + unref | ✅ NIM_DELETE |
| Click event | ✅ tray:menu-click for menu items | ✅ activate + menu item click | ✅ left click + menu item click |
Linux는 추가 AppIndicator 의존성을 늘리지 않고 GTK3 GtkStatusIcon 경로를 쓴다.
Windows는 hidden message-only window가 tray callback과 popup menu command를 받는다.
현재 한계
섹션 제목: “현재 한계”| 한계 | 설명 |
|---|---|
| Windows parity | Windows는 아직 iconPath와 중첩 submenu를 완전 구현하지 않는다. 기본 app icon + flat HMENU 경로 유지. |
별도 tray:click 이벤트 | 클릭을 tray:menu-click 채널로 통일한다. Windows/Linux icon activate는 빈 click 값으로 emit한다. macOS 메뉴 설정 시 클릭 → 자동 메뉴 표시라 메뉴 없는 click hook은 별도 작업. |
| radio item | checkbox는 macOS/Linux에서 지원하지만 radio group semantics는 아직 지원하지 않는다. |
| 오른쪽 클릭 구분 | macOS NSStatusItem.button.action은 left/right 통합. Windows right click은 popup menu를 표시한다. |
API
섹션 제목: “API”import { tray } from '@suji/api';
const { trayId } = await tray.create({ title: 'My App', tooltip: 'My App is running', iconPath: '/absolute/path/to/tray.png', // macOS/Linux});
await tray.setMenu(trayId, [ { label: 'Show window', click: 'show-main' }, { type: 'checkbox', label: 'Enable sync', click: 'toggle-sync', checked: true }, { label: 'More', submenu: [ { label: 'Reload', click: 'reload-app' }, { label: 'Disabled action', click: 'disabled-action', enabled: false }, ], }, { type: 'separator' }, { label: 'Quit', click: 'quit-app' },]);
suji.on('tray:menu-click', ({ trayId, click }) => { if (click === 'quit-app') suji.quit();});
await tray.setTitle(trayId, 'Update available');await tray.setTooltip(trayId, 'New version ready');await tray.destroy(trayId);Menu item wire shape:
[ { "type": "item", "label": "Run", "click": "run", "enabled": true }, { "type": "checkbox", "label": "Flag", "click": "flag", "checked": true, "enabled": true }, { "type": "submenu", "label": "More", "enabled": true, "submenu": [{ "label": "Child", "click": "child" }] }, { "type": "separator" }]iconPath는 renderer-controlled file path라 fs.allowedRoots가 설정된 앱에서는 같은
경계로 검사한다. backend SDK 호출은 기존 fs sandbox 정책과 동일하게 우회한다.
Backend SDK
섹션 제목: “Backend SDK”Zig (suji.tray.*)
섹션 제목: “Zig (suji.tray.*)”const r = suji.tray.createWithIcon("My App", "tooltip", "/tmp/tray.png");const items = \\"items":[ \\ {"label":"Settings","click":"open-settings"}, \\ {"type":"checkbox","label":"Sync","click":"sync","checked":true}, \\ {"label":"More","submenu":[{"label":"Reload","click":"reload"}]}, \\ {"type":"separator"}, \\ {"label":"Quit","click":"quit"} \\];_ = suji.tray.setMenuRaw(1, items);_ = suji.tray.destroy(1);Rust (suji::tray::*)
섹션 제목: “Rust (suji::tray::*)”use suji::tray::{self, MenuItem};
let r = tray::create_with_icon("My App", "tooltip", "/tmp/tray.png");let _ = tray::set_menu(1, &[ MenuItem::Item { label: "Settings", click: "open-settings" }, MenuItem::Checkbox { label: "Sync", click: "sync", checked: true, enabled: true }, MenuItem::Submenu { label: "More", enabled: true, submenu: vec![MenuItem::Item { label: "Reload", click: "reload" }], }, MenuItem::Separator,]);let _ = tray::destroy(1);Go (github.com/ohah/suji-go/tray)
섹션 제목: “Go (github.com/ohah/suji-go/tray)”import "github.com/ohah/suji-go/tray"
tray.CreateWithIcon("My App", "tooltip", "/tmp/tray.png")tray.SetMenu(1, []tray.MenuItem{ {Label: "Settings", Click: "open-settings"}, {Checkbox: true, Label: "Sync", Click: "sync", Checked: true}, {Type: "submenu", Label: "More", Submenu: []tray.MenuItem{{Label: "Reload", Click: "reload"}}}, {Separator: true},})tray.Destroy(1)Node (@suji/node)
섹션 제목: “Node (@suji/node)”import { tray } from '@suji/node';
const { trayId } = await tray.create({ title: 'My App', iconPath: '/tmp/tray.png' });await tray.setMenu(trayId, [ { label: 'Settings', click: 'open-settings' }, { type: 'checkbox', label: 'Sync', click: 'sync', checked: true }, { label: 'More', submenu: [{ label: 'Reload', click: 'reload' }] },]);동작 정밀
섹션 제목: “동작 정밀”macOS 경로
섹션 제목: “macOS 경로”create는NSStatusBar.systemStatusBar.statusItemWithLength:로NSStatusItem을 만들고 retain한다.iconPath가 있으면NSImage initWithContentsOfFile:로 이미지를 로드해statusItem.button.image에 설정한다.setMenu는NSMenu를 재귀 빌드한다. checkbox는 클릭 즉시 native checked state를 토글한 뒤tray:menu-click을 emit한다.- 각 clickable
NSMenuItem은 compositetag에 tray id와 checkbox 여부를 담고,representedObject에 click 이름을 보관한다.
Linux 경로
섹션 제목: “Linux 경로”gtk_status_icon_new후iconPath가 있으면gtk_status_icon_set_from_file, 없으면gtk_status_icon_set_from_icon_name("application-x-executable")를 사용한다.setTitle/setTooltip은gtk_status_icon_set_title/gtk_status_icon_set_tooltip_text로 반영한다.setMenu는GtkMenu를 재귀 빌드하고GtkMenuItem/GtkCheckMenuItemactivate signal에서tray:menu-click을 emit한다.- icon activate는 Windows left click과 동일하게
tray:menu-click {"trayId":N,"click":""}를 emit한다. - GTK3 StatusIcon은 deprecated API이며, 향후 AppIndicator/StatusNotifierItem로 교체될 수 있다.
Windows 경로
섹션 제목: “Windows 경로”win_pumphidden message-only window가WM_TRAY/WM_COMMAND를 수신한다.create는Shell_NotifyIconW(NIM_ADD)로 기본 app icon과 tooltip을 등록한다.setMenu는CreatePopupMenu+AppendMenuW로 flat HMENU를 구성하고 right click에서TrackPopupMenu를 호출한다.- left click / double click은
tray:menu-click {"trayId":N,"click":""}를 emit한다. - menu item click은 command id를
(trayId, click)으로 매핑해 같은 이벤트 채널로 emit한다.