A small, focused replacement for dorkbox/SystemTray
(unmaintained since 2023). Designed for modern JVM desktop apps that already
draw their own UI (Compose Desktop, Skiko, Swing, JavaFX) and need only a
tray icon + right-click menu — not a full UI toolkit.
Why another tray library
dorkbox/SystemTray hardcodes a JNA version check that conflicts with JBR
25's bundled JNA, requires -Djna.nosys=true to work around an AWT loader
clash, and uses runtime bytecode patching (javassist) that fails the
stricter stackmap verifier in modern JVMs. None of that is fixable with a
patch — the architecture predates Project Panama (java.lang.foreign,
JEP 454 finalized in JDK 22) and would need to be rewritten anyway.
libtray is the rewrite. Pure Panama bindings, no JNA, no AWT patching, no transitive GTK/GLib pull-in on Linux. The library is small enough to read end-to-end in one sitting.
Platform backends
| Platform | Backend | Notes |
|---|---|---|
| Linux | org.kde.StatusNotifierItem over D-Bus + com.canonical.dbusmenu for the menu |
Talks to the desktop's tray host directly via libdbus. No GTK / GLib runtime dependency. Works on KDE, GNOME with the SNI extension, Hyprland (via waybar / similar), Cinnamon, Budgie. |
| Windows | Shell_NotifyIcon via shell32 |
Win32 message-pump driven. The classic "balloon notification" surface. |
| macOS | NSStatusBar / NSStatusItem via AppKit + objc_msgSend |
Menu-bar item top-right. Requires the JVM to be running with a Cocoa main thread (most JVM desktop apps already do). |
Each backend lives in its own package so consumers can audit / patch the one that affects them without grokking the others.
Install
Available on Maven Central:
dependencies {
implementation("dev.hivens:libtray:0.1.0")
}Requires JDK 22+ (Project Panama). Caller must pass
--enable-native-access=ALL-UNNAMED (or grant the library's module
specifically) to permit the native calls.
Use
import dev.hivens.libtray.*
val tray = Tray.create(
TrayBuilder(
title = "MyApp",
iconBytes = Files.readAllBytes(Path.of("icon.png")),
tooltip = "MyApp — running",
menu = TrayMenu(listOf(
TrayMenuItem.Standard(id = "show", label = "Show window"),
TrayMenuItem.Separator,
TrayMenuItem.Standard(id = "exit", label = "Exit"),
)),
),
) ?: error("Tray not supported on this platform")
tray.onEvent { event ->
when (event) {
is TrayEvent.Activated -> showMainWindow()
is TrayEvent.MenuItemSelected -> when (event.id) {
"show" -> showMainWindow()
"exit" -> exitProcess(0)
}
else -> Unit
}
}
// On app shutdown
tray.close()Status
Pre-1.0. API may still shift before 1.0. All three backends shipped (Linux SNI, Windows Shell_NotifyIcon, macOS NSStatusItem).
Built and validated against:
- Aura Launcher (
Kitty-Hivens/Aura-Launcher) — primary downstream - Linux: Hyprland, KDE Plasma — verified
- Windows: Shell_NotifyIcon + popup menu — verified on Win10 / Win11
- macOS: NSStatusItem + menu — verified on a macOS VM and a community JavaFX consumer (both JVM and GraalVM native-image)
※ Apache License 2.0 — fork it, ship it, sell it. Patches welcome but not required.