Files
komorebi/komorebi-bar/src/widgets/mod.rs
T
Csaba ea35d818a1 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.
2026-03-21 12:35:39 -07:00

184 lines
6.0 KiB
Rust

use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use image::RgbaImage;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::RwLock;
pub mod applications;
pub mod battery;
pub mod cpu;
pub mod date;
pub mod keyboard;
pub mod komorebi;
mod komorebi_layout;
pub mod media;
pub mod memory;
pub mod network;
pub mod storage;
#[cfg(target_os = "windows")]
pub mod systray;
pub mod time;
pub mod update;
pub mod widget;
/// Global cache for icon images and their associated GPU textures.
pub static ICONS_CACHE: IconsCache = IconsCache::new();
/// In-memory cache for icon images and their associated GPU textures.
///
/// Stores raw [`ColorImage`]s and [`TextureHandle`]s keyed by [`ImageIconId`].
/// Texture entries are context-dependent and automatically invalidated when the [`Context`] changes.
#[allow(clippy::type_complexity)]
pub struct IconsCache {
textures: LazyLock<RwLock<(Option<Context>, HashMap<ImageIconId, TextureHandle>)>>,
images: LazyLock<RwLock<HashMap<ImageIconId, Arc<ColorImage>>>>,
}
impl IconsCache {
/// Creates a new empty IconsCache instance.
#[inline]
pub const fn new() -> Self {
Self {
textures: LazyLock::new(|| RwLock::new((None, HashMap::new()))),
images: LazyLock::new(|| RwLock::new(HashMap::new())),
}
}
/// Retrieves or creates a texture handle for the given icon ID and image.
///
/// If a texture for the given ID already exists for the current [`Context`], it is reused.
/// Otherwise, a new texture is created, inserted into the cache, and returned.
/// The cache is reset if the [`Context`] has changed.
#[inline]
pub fn texture(&self, ctx: &Context, id: &ImageIconId, img: &Arc<ColorImage>) -> TextureHandle {
if let Some(texture) = self.get_texture(ctx, id) {
return texture;
}
let texture_handle = ctx.load_texture("icon", img.clone(), TextureOptions::default());
self.insert_texture(ctx, id.clone(), texture_handle.clone());
texture_handle
}
/// Returns the cached texture for the given icon ID if it exists and matches the current [`Context`].
pub fn get_texture(&self, ctx: &Context, id: &ImageIconId) -> Option<TextureHandle> {
let textures_lock = self.textures.read().unwrap();
if textures_lock.0.as_ref() == Some(ctx) {
return textures_lock.1.get(id).cloned();
}
None
}
/// Inserts a texture handle, resetting the cache if the [`Context`] has changed.
pub fn insert_texture(&self, ctx: &Context, id: ImageIconId, texture: TextureHandle) {
let mut textures_lock = self.textures.write().unwrap();
if textures_lock.0.as_ref() != Some(ctx) {
textures_lock.0 = Some(ctx.clone());
textures_lock.1.clear();
}
textures_lock.1.insert(id, texture);
}
/// Returns the cached image for the given icon ID, if available.
pub fn get_image(&self, id: &ImageIconId) -> Option<Arc<ColorImage>> {
self.images.read().unwrap().get(id).cloned()
}
/// Caches a raw [`ColorImage`] associated with the given icon ID.
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
self.images.write().unwrap().insert(id, image);
}
/// Removes the cached image and texture for the given icon ID.
pub fn remove(&self, id: &ImageIconId) {
self.images.write().unwrap().remove(id);
self.textures.write().unwrap().1.remove(id);
}
}
#[inline]
pub(crate) fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
}
/// Represents an image-based icon with a unique ID and pixel data.
#[derive(Clone, Debug)]
pub struct ImageIcon {
/// Unique identifier for the image icon, used for texture caching.
pub id: ImageIconId,
/// Shared pixel data of the icon in `ColorImage` format.
pub image: Arc<ColorImage>,
}
impl ImageIcon {
/// Creates a new [`ImageIcon`] from the given ID and image data.
#[inline]
pub fn new(id: ImageIconId, image: Arc<ColorImage>) -> Self {
Self { id, image }
}
/// Loads an [`ImageIcon`] from [`ICONS_CACHE`] or calls `loader` if not cached.
/// The loaded image is converted to a [`ColorImage`], cached, and returned.
#[inline]
pub fn try_load<F, I>(id: impl Into<ImageIconId>, loader: F) -> Option<Self>
where
F: FnOnce() -> Option<I>,
I: Into<RgbaImage>,
{
let id = id.into();
let image = ICONS_CACHE.get_image(&id).or_else(|| {
let img = loader()?;
let img = Arc::new(rgba_to_color_image(&img.into()));
ICONS_CACHE.insert_image(id.clone(), img.clone());
Some(img)
})?;
Some(ImageIcon::new(id, image))
}
/// Returns a texture handle for the icon, using the given [`Context`].
///
/// If the texture is already cached in [`ICONS_CACHE`], it is reused.
/// Otherwise, a new texture is created from the [`ColorImage`] and cached.
#[inline]
pub fn texture(&self, ctx: &Context) -> TextureHandle {
ICONS_CACHE.texture(ctx, &self.id, &self.image)
}
}
/// Unique identifier for an image-based icon.
///
/// Used to distinguish cached images and textures by either a file path
/// or a Windows window handle.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ImageIconId {
/// Identifier based on a file system path.
Path(Arc<Path>),
/// Windows HWND handle.
Hwnd(isize),
/// System tray icon identifier.
SystrayIcon(String),
}
impl From<&Path> for ImageIconId {
#[inline]
fn from(value: &Path) -> Self {
Self::Path(value.into())
}
}
impl From<isize> for ImageIconId {
#[inline]
fn from(value: isize) -> Self {
Self::Hwnd(value)
}
}