feat(bar): windows systray widget

A new System Tray widget has been added to komorebi-bar, bringing native
Windows system tray functionality directly into the bar.

Special thanks to the Zebar project and its contributors for developing
the systray-util crate library that makes Windows system tray
integration possible.

The widget intercepts system tray icon data by creating a hidden "spy"
window that mimics the Windows taskbar. When applications use the
Shell_NotifyIcon API to manage their tray icons, the widget receives the
same broadcast messages, allowing it to monitor all system tray activity
while forwarding messages to the real taskbar to avoid disruption.

Users can configure which icons to hide using flexible rules. A plain
string matches by exe name (case-insensitive). A structured object can
match on exe, tooltip, and/or GUID fields using AND logic. Each field
supports matching strategies from komorebi's window rules (Equals,
StartsWith, EndsWith, Contains, Regex, and their negated variants),
allowing precise filtering even when multiple icons share the same exe
and GUID. An info button opens a floating panel listing all icons with
their properties and copy buttons, making it easy to identify which
values to use in filter rules.

The widget fully supports mouse interactions including left-click,
right-click, middle-click, and double-click actions on tray icons.
Double-click support uses the LeftDoubleClick action from systray-util
0.2.0, which sends WM_LBUTTONDBLCLK and NIN_SELECT messages. It handles
right-aligned placement correctly by adjusting the rendering order and
toggle button arrow directions to maintain consistent visual appearance
regardless of which panel the widget is placed in.

Some system tray icons register a click callback but never actually
respond to click messages, effectively becoming "zombie" icons from an
interaction standpoint. The widget includes fallback commands for known
problematic icons that override the native click action with a direct
shell command (e.g. opening Windows Security or volume settings).

The implementation uses a background thread with its own tokio runtime
to handle the async systray events, communicating with the UI thread
through crossbeam channels. Icon images are cached efficiently using
hash-based keys that update when icons change. Rapid icon updates are
deduplicated to prevent UI freezing, and image conversion (RgbaImage to
ColorImage) is performed on the background thread to keep the UI
responsive.

The widget automatically detects and removes stale icons whose owning
process has exited, using the Win32 IsWindow API on a configurable
interval. A manual refresh button is also available for immediate
cleanup.

A shortcuts button can be configured to toggle komorebi-shortcuts by
killing the process if running or starting it otherwise. The refresh,
info, and shortcuts buttons can each be placed in the main visible area
or in the overflow section.
This commit is contained in:
Csaba
2026-03-12 23:58:07 +01:00
committed by LGUG2Z
parent 5f629e1f1a
commit ea35d818a1
14 changed files with 2026 additions and 121 deletions

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "KomobarConfig",
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.40`",
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.41`",
"type": "object",
"properties": {
"center_widgets": {
@@ -353,7 +353,7 @@
},
{
"title": "CubicBezier",
"description": "Custom Cubic Bézier function",
"description": "Custom Cubic Bezier function",
"type": "object",
"properties": {
"CubicBezier": {
@@ -2158,6 +2158,21 @@
}
]
},
"ButtonPosition": {
"description": "Where to place a systray button (refresh, info, shortcuts, etc.)",
"oneOf": [
{
"description": "Show in the main visible area",
"type": "string",
"const": "Visible"
},
{
"description": "Show in the overflow/hidden section",
"type": "string",
"const": "Overflow"
}
]
},
"Catppuccin": {
"description": "Catppuccin palette",
"oneOf": [
@@ -2573,6 +2588,33 @@
}
]
},
"FieldMatch": {
"description": "A field value with an optional matching strategy.\n\nA plain string uses exact (case-insensitive) matching.\nAn object with `value` and `matching_strategy` uses the specified strategy.",
"anyOf": [
{
"description": "Exact case-insensitive match",
"type": "string"
},
{
"description": "Match using a specific strategy",
"type": "object",
"properties": {
"matching_strategy": {
"description": "How to match (Equals, StartsWith, EndsWith, Contains, Regex, etc.)",
"$ref": "#/$defs/MatchingStrategy"
},
"value": {
"description": "The value to match against",
"type": "string"
}
},
"required": [
"value",
"matching_strategy"
]
}
]
},
"FocusFollowsMouseImplementation": {
"description": "Focus follows mouse implementation",
"oneOf": [
@@ -2802,6 +2844,54 @@
"type": "string",
"format": "color-hex"
},
"HiddenIconRule": {
"description": "Rule for matching a systray icon to hide\n\nA plain string matches the exe name (backward compatible).\nAn object with optional `exe`, `tooltip`, and/or `guid` fields\nuses AND logic: all specified fields must match.\nEach field can be a plain string (exact match) or an object with\n`value` and `matching_strategy` for advanced matching.",
"anyOf": [
{
"description": "Match by exe name (case-insensitive, exact)",
"type": "string"
},
{
"description": "Match by one or more properties (all specified fields must match)",
"type": "object",
"properties": {
"exe": {
"description": "Exe name to match",
"anyOf": [
{
"$ref": "#/$defs/FieldMatch"
},
{
"type": "null"
}
]
},
"guid": {
"description": "Icon GUID to match (most stable identifier across restarts)",
"anyOf": [
{
"$ref": "#/$defs/FieldMatch"
},
{
"type": "null"
}
]
},
"tooltip": {
"description": "Tooltip text to match",
"anyOf": [
{
"$ref": "#/$defs/FieldMatch"
},
{
"type": "null"
}
]
}
}
}
]
},
"HidingBehaviour": {
"description": "Window hiding behaviour",
"oneOf": [
@@ -3859,6 +3949,61 @@
}
]
},
"MatchingStrategy": {
"description": "Strategy for matching identifiers",
"oneOf": [
{
"description": "Should not be used, only kept for backward compatibility",
"type": "string",
"const": "Legacy"
},
{
"description": "Equals",
"type": "string",
"const": "Equals"
},
{
"description": "Starts With",
"type": "string",
"const": "StartsWith"
},
{
"description": "Ends With",
"type": "string",
"const": "EndsWith"
},
{
"description": "Contains",
"type": "string",
"const": "Contains"
},
{
"description": "Regex",
"type": "string",
"const": "Regex"
},
{
"description": "Does not end with",
"type": "string",
"const": "DoesNotEndWith"
},
{
"description": "Does not start with",
"type": "string",
"const": "DoesNotStartWith"
},
{
"description": "Does not equal",
"type": "string",
"const": "DoesNotEqual"
},
{
"description": "Does not contain",
"type": "string",
"const": "DoesNotContain"
}
]
},
"MediaConfig": {
"description": "Media widget configuration",
"type": "object",
@@ -4301,6 +4446,21 @@
"Down"
]
},
"OverflowTogglePosition": {
"description": "Position of the overflow toggle button",
"oneOf": [
{
"description": "Toggle button appears on the left side (before visible icons)",
"type": "string",
"const": "Left"
},
{
"description": "Toggle button appears on the right side (after visible icons)",
"type": "string",
"const": "Right"
}
]
},
"PathBuf": {
"description": "A file system path. Environment variables like %VAR%, $Env:VAR, or $VAR are automatically resolved.",
"type": "string"
@@ -8147,6 +8307,82 @@
"filter_state_changes"
]
},
"SystrayConfig": {
"description": "System tray widget configuration",
"type": "object",
"properties": {
"enable": {
"description": "Enable the System Tray widget",
"type": "boolean"
},
"hidden_icons": {
"description": "A list of rules for icons to hide from the system tray\n\nEach entry can be a plain string (matches exe name, case-insensitive)\nor an object with optional `exe`, `tooltip`, and/or `guid` fields\n(all specified fields must match, AND logic, case-insensitive).\n\nRun komorebi-bar with RUST_LOG=info to see the properties of all\nsystray icons in the log output.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/$defs/HiddenIconRule"
}
},
"info_button": {
"description": "Show an info button that opens a floating panel listing all systray icons\nwith their exe, tooltip, and GUID. Set to \"Visible\" to show it in the main\narea, or \"Overflow\" to show it in the hidden/overflow section.",
"anyOf": [
{
"$ref": "#/$defs/ButtonPosition"
},
{
"type": "null"
}
]
},
"overflow_toggle_position": {
"description": "Position of the overflow toggle button (default: Right)",
"anyOf": [
{
"$ref": "#/$defs/OverflowTogglePosition"
},
{
"type": "null"
}
]
},
"refresh_button": {
"description": "Show a refresh button that manually triggers stale icon cleanup.\nSet to \"Visible\" to show it in the main area, or \"Overflow\" to\nshow it in the hidden/overflow section.",
"anyOf": [
{
"$ref": "#/$defs/ButtonPosition"
},
{
"type": "null"
}
]
},
"shortcuts_button": {
"description": "Show a button that toggles komorebi-shortcuts (kills the process if\nrunning, starts it otherwise). Set to \"Visible\" to show it in the main\narea, or \"Overflow\" to show it in the hidden/overflow section.",
"anyOf": [
{
"$ref": "#/$defs/ButtonPosition"
},
{
"type": "null"
}
]
},
"stale_icons_check_interval": {
"description": "Interval in seconds to automatically check for and remove stale icons\nwhose owning process has exited. Clamped between 30 and 600 seconds.\nDefaults to 60. Set to 0 to disable.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0
}
},
"required": [
"enable"
]
},
"TimeConfig": {
"description": "Time widget configuration",
"type": "object",
@@ -8416,6 +8652,20 @@
"Storage"
]
},
{
"title": "Systray",
"description": "System Tray widget configuration (Windows only)",
"type": "object",
"properties": {
"Systray": {
"$ref": "#/$defs/SystrayConfig"
}
},
"additionalProperties": false,
"required": [
"Systray"
]
},
{
"title": "Time",
"description": "Time widget configuration",