System trays
Access macOS menu-bar status items through app.trays. Create trays before app.run or on the running application's UI context.
Manager API
| Method | Returns | Purpose |
|---|---|---|
create(id, tooltip = "", icon:, ...) | CryKit::Trays::Tray | Create an icon-only tray |
create(id, tooltip = "", icon:, ...) { |menu| ... } | Tray | Create a tray with an inline popup menu |
create(id:, icon:, menu: MenuClass, ...) | Tray | Attach a reusable CryKit::Menu definition |
register(TrayClass, label: "", hidden: false, auto_open_menu: true) | Tray | Register a CryKit::Tray definition |
register(TrayClass, OtherClass, ..., auto_open_menu: true) | Nil | Register multiple definitions |
[id] / [id]? | Tray / Tray? | Look up a tray; [] raises if missing |
find_by(tray_id, action_id) | CryKit::Menus::Item? | Find an item, including nested items |
each { |tray| ... }, size, empty? | — / Int32 / Bool | Inspect active trays |
remove(id) | Nil | Remove a tray if present |
Creation options
| Option | Default | Purpose |
|---|---|---|
id | Required | Unique, nonempty Symbol; immutable after creation |
icon: | Required | Application asset reference or CryKit::Trays::Icon |
tooltip | "" | Hover text |
label: | "" | Text beside the icon |
hidden: | false | Defer display until show |
auto_open_menu: | true | Open the menu after left/right click listeners |
monochrome: | true | Let macOS color asset-backed icons for the menu bar |
create(id: :main, tooltip: "My application", icon: app.asset_path("tray.png")) also accepts named arguments. Each tray owns its menu state and listeners.
Tray API
| Member | Purpose |
|---|---|
id, menu | Read identity and optional retained menu |
label, label= | Read or change text beside the icon |
tooltip, tooltip= | Read or change hover text |
icon, icon= | Read or replace the icon with an Icon or asset reference |
show, hide, visible? | Control or read visibility |
auto_open_menu?, auto_open_menu= | Control automatic popup opening |
open_menu | Open the popup for an installed, visible tray with a menu |
remove, removed? | Remove the tray or inspect its removal state |
on_click(button = nil) { |event| ... } | Subscribe to all clicks or :left / :right clicks |
on_event(:click) { |event| ... }, events { |event| ... } | Subscribe through the common event API |
Hiding preserves menu state and listeners. Removal releases subscriptions and allows the ID to be reused; old handles cannot affect a replacement.
A windowless application
require "crykit"
app = CryKit::Application.new(
macos: CryKit::Application::MacOSOptions.new(activation_policy: :accessory),
quit_on_last_window_closed: false)
app.assets = CryKit::Assets.directory("assets")
app.trays.create(:main, "My application", icon: app.asset_path("tray.png")) do |menu|
menu.checkbox(:enabled, "Enabled", checked: true)
menu.separator
menu.text(:quit, "Quit")
menu.on_click { |event| app.quit if event.action == :quit }
end
app.run| Application option | Effect |
|---|---|
quit_on_last_window_closed: false | Permit startup and continued execution without windows |
activation_policy: :accessory | Omit Dock/application-menu presence and automatic startup activation |
Creating a tray does not change the default quit_on_last_window_closed: true. Removing the last tray does not quit. Give resident applications a way to call app.quit. For a bundled agent, set LSUIElement in the Info.plist template. See activation and windowless lifetime and the resident example.
Icons from application assets
app.assets = CryKit::Assets.directory("assets")
tray = app.trays.create(:main, icon: app.asset_path("tray.png"), monochrome: false)
tray.icon = app.asset_path("busy.png")Use app.asset_path("tray.png") inline or asset_path("tray.png") in a CryKit::Tray definition. Both resolve against the application's asset store. Assigning another asset reference preserves the current monochrome setting.
| Workflow | Icon behavior |
|---|---|
crykit dev | Read icons from disk; hotreload asset-backed icons even without --watch |
crykit build | Embed icons with other application assets |
| Invalid hotreload replacement | Keep the previous image and retry on a later valid save |
Hotreload preserves the tray, menu, label, and listeners. Built applications do not need the source icon files.
Icons and retained trays
| Constructor or member | Purpose |
|---|---|
Icon.new(bytes, monochrome: true) | Copy encoded image bytes |
Icon.from_file(path, monochrome: true) | Read a file once; later file edits do not update this icon |
Icon.from_asset(assets, path, monochrome: true) | Read from an application asset store |
icon.bytes | Read a copy of the encoded bytes |
icon.monochrome? | Read whether macOS supplies the icon color |
Icon is CryKit::Trays::Icon. PNG is supported. Images fit proportionally within 18 logical points; a transparent 36×36 image suits a square icon on a Retina display. Use monochrome artwork with transparency for the default monochrome: true, or false to retain image colors.
tray.label = "3"
tray.tooltip = "Three pending tasks"
app.trays.find_by(:main, :quit).try &.enabled=(false)Configuration is retained before startup. Live creation stages native resources before display and unwinds resources on failure. Live updates require the UI context. Other native platforms raise CryKit::Trays::UnsupportedError.
Native menus
Tray popups use the menu builder: text actions, checkboxes, adjacent radio groups, separators, and nested submenus. Root items appear directly in the popup. Structure is fixed after creation; retained item text, enabled, and checked properties remain mutable.
class TrayMenu
include CryKit::Tray
icon asset_path("tray.png"), monochrome: true
menu :main, "My application" do
text :quit, "Quit"
on_click { |event| application.quit if event.action == :quit }
end
end
app.trays.register TrayMenu| Definition member | Purpose |
|---|---|
icon | Asset reference or CryKit::Trays::Icon expression |
menu :id, "Tooltip" | Tray ID, tooltip, and popup contents |
application | Concrete application injected into the registered definition |
menu | This registration's retained menu, available before on_register |
on_register / on_unregister | Optional private hooks for external resources and subscriptions |
Each registration creates a fresh definition, tray, menu, and listeners. register TrayMenu, label: "3", hidden: true, auto_open_menu: false customizes presentation. Construction/registration listeners remain attached until the object is removed, including when a view callback creates it.
Access menu items with tray.menu.try &.[:quit] or app.trays.find_by(:main, :quit). Subscribe with tray.menu.try &.on_click { |event| puts event.action }. Menu events have menu, type, and action; check/radio state changes before listeners run. Tray popups are independent of the application's menu bar and reject native menu roles and commands. Use text :quit, "Quit" with app.quit. Shortcuts apply during popup tracking; global shortcuts, menu replacement, and attached popup windows are not exposed.
Click behavior
| Behavior | Default |
|---|---|
| Left-click / right-click | Notify listeners, then open the attached menu |
| Control-click | Treated as right-click |
| Tray without a menu | Notify listeners only |
auto_open_menu: false | Notify listeners; open the menu only when requested |
tray.auto_open_menu = false
tray.on_click(:left) { puts "Left-click action" }
tray.on_click(:right) { tray.open_menu }CryKit::Trays::Tray::Event member | Value |
|---|---|
tray | Retained tray that received the click |
type | :click |
button | CryKit::Trays::Button::Left or Right; use left? / right? |
click? | Whether this is a click event |
on_click, on_event, and events return independent, idempotent unsubscribe functions. Call the returned function to stop listening. Button filtering only affects that listener. Changing listeners does not change automatic opening. A listener that hides/removes the tray or quits suppresses the default popup.
Subscription lifetime and dispatch
Listeners run in registration order on the UI context before automatic menu opening. A failed listener is logged without stopping later listeners. Removing a listener suppresses it if it has not yet run; new listeners start with the next click. Registration and unsubscription are thread-safe.
Subscriptions registered in a view's on_mount, UI callbacks, or job.notify blocks are removed on unmount. Others remain until explicitly removed or the tray/menu is removed. Hiding preserves listeners. Keep callbacks short; use background jobs for longer work.
Tracking and shutdown
Hiding or removing a tray cancels an open menu. Removal invalidates lookup and callbacks immediately; native resources retire after menu tracking and nested callbacks unwind. Removal from the tray's own callback is supported.
Accepted quit and last-window termination close trays before the native runtime is released. A window's quit veto leaves them usable. Shutdown attempts every tray cleanup even after another stage fails. See application quit approval.