Skip to content

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 ​

MethodReturnsPurpose
create(id, tooltip = "", icon:, ...)CryKit::Trays::TrayCreate an icon-only tray
create(id, tooltip = "", icon:, ...) { |menu| ... }TrayCreate a tray with an inline popup menu
create(id:, icon:, menu: MenuClass, ...)TrayAttach a reusable CryKit::Menu definition
register(TrayClass, label: "", hidden: false, auto_open_menu: true)TrayRegister a CryKit::Tray definition
register(TrayClass, OtherClass, ..., auto_open_menu: true)NilRegister 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 / BoolInspect active trays
remove(id)NilRemove a tray if present

Creation options ​

OptionDefaultPurpose
idRequiredUnique, nonempty Symbol; immutable after creation
icon:RequiredApplication asset reference or CryKit::Trays::Icon
tooltip""Hover text
label:""Text beside the icon
hidden:falseDefer display until show
auto_open_menu:trueOpen the menu after left/right click listeners
monochrome:trueLet 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 ​

MemberPurpose
id, menuRead 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_menuOpen 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 ​

crystal
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 optionEffect
quit_on_last_window_closed: falsePermit startup and continued execution without windows
activation_policy: :accessoryOmit 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 ​

crystal
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.

WorkflowIcon behavior
crykit devRead icons from disk; hotreload asset-backed icons even without --watch
crykit buildEmbed icons with other application assets
Invalid hotreload replacementKeep 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 memberPurpose
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.bytesRead 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.

crystal
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.

crystal
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 memberPurpose
iconAsset reference or CryKit::Trays::Icon expression
menu :id, "Tooltip"Tray ID, tooltip, and popup contents
applicationConcrete application injected into the registered definition
menuThis registration's retained menu, available before on_register
on_register / on_unregisterOptional 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 ​

BehaviorDefault
Left-click / right-clickNotify listeners, then open the attached menu
Control-clickTreated as right-click
Tray without a menuNotify listeners only
auto_open_menu: falseNotify listeners; open the menu only when requested
crystal
tray.auto_open_menu = false
tray.on_click(:left) { puts "Left-click action" }
tray.on_click(:right) { tray.open_menu }
CryKit::Trays::Tray::Event memberValue
trayRetained tray that received the click
type:click
buttonCryKit::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.

Built with Crystal. Documented with VitePress.