mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-21 18:14:40 +00:00
fix: register the tray click handler app-wide and probe unversioned AppIndicator names
A per-icon tray handler runs while Tauri holds the per-icon listener table; the inline refresh could build a missing icon that locks the same table. The app-level handler avoids that. The AppIndicator probe now matches the tray backend's backcompat names, which some AppImages need, and keeps the loaded library open.
This commit is contained in:
1 parent
bee9565c35
commit
4da57949ee
4 files changed
+38
-22
No files matched your search
@@ -38,7 +38,7 @@ for the alerts (D9).
|
||||
- The badge is `badge::render_pixmap` (tiny-skia, kept), demultiplied into `Image::new_owned(rgba, 32, 32)`, with `is_dark=true` always (parity).
|
||||
- The tooltip comes from `core::state::tray_tooltip`, truncated to 127 UTF-16 units.
|
||||
- `sync()` diffs the set of icons. `set_icon` and `set_tooltip` run only when the cached `(pct, colour)` or the text changed.
|
||||
- Linux: before building any tray, probe with `libloading` for `libayatana-appindicator3.so.1`, then `libappindicator3.so.1`. If neither loads, log it and skip tray creation, because the tray library panics on a missing library and the release profile aborts on panic. Any other tray creation error is logged and the app continues.
|
||||
- Linux: before building any tray, probe with `libloading` for the names the tray backend loads, in its order: `libayatana-appindicator3.so.1`, `libappindicator3.so.1`, then the unversioned `libayatana-appindicator3.so` and `libappindicator3.so` (its default `backcompat` feature; sometimes the only names in an AppImage). The loaded library stays loaded. If none loads, log it and skip tray creation, because the tray library panics on a missing library and the release profile aborts on panic. Any other tray creation error is logged and the app continues.
|
||||
- **Alerts** (D9):
|
||||
- Windows spike first: a fresh portable exe (CI artifact, no Start-menu shortcut, outside `target/`) on Windows 10 and 11 calls `platform::windows::register_aumid()` and shows one toast. If no toast appears, stop and report to the user before writing more alert code.
|
||||
- `register_aumid()` runs at startup before any toast: write `HKCU\Software\Classes\AppUserModelId\io.github.tiennm99.claudecodeusagebubble` with `DisplayName` "Claude Code Usage Bubble" and `IconUri` pointing at a PNG written to `data_local_dir()/ClaudeCodeUsageBubble/icon.png`, then `SetCurrentProcessExplicitAppUserModelID` with the same id.
|
||||
@@ -68,7 +68,7 @@ for the alerts (D9).
|
||||
- [x] `src/menu.rs`: `core::menu_model` mapped onto `Menu`, `Submenu`, `CheckMenuItem`, `MenuItem` (hints disabled) and separators; autostart check from `autolaunch().is_enabled()`; one `on_menu_event` that runs the command and then rebuilds every tray menu (also for no-op clicks); `show_context_menu(label)` with `SetForegroundWindow` first on Windows
|
||||
- [x] Headless menu test: the full tree (order, labels, ids, check and grey states) walked in `core::menu_model`
|
||||
- [x] `src/tray.rs`: one `TrayIconBuilder::with_id(slug)` per enabled live provider in `LIVE_USAGE` order, `show_menu_on_left_click(false)`, left `Click`/`Up` toggles the widget, badge always dark, tooltip from `tray_tooltip` cut to 127 UTF-16 units, `set_icon`/`set_tooltip` only on a changed `(pct, colour)` or text
|
||||
- [x] Linux: `platform::linux::appindicator_available()` probes `libayatana-appindicator3.so.1` then `libappindicator3.so.1`; without either the tray is skipped and logged; other tray errors are logged
|
||||
- [x] Linux: `platform::linux::appindicator_available()` probes `libayatana-appindicator3.so.1`, `libappindicator3.so.1`, `libayatana-appindicator3.so`, then `libappindicator3.so` (the backend's order); without any the tray is skipped and logged; other tray errors are logged
|
||||
- [x] Autostart plugin with `app_name("ClaudeCodeUsageBubble")`, no args, `MacosLauncher::LaunchAgent` on macOS (Linux and macOS); Windows pinned to a current-user entry with the quoted exe path through `auto-launch` directly (notes below); no migration code needed
|
||||
- [x] `platform::windows::register_aumid()` (HKCU `AppUserModelId` key with `DisplayName` and `IconUri`, PNG under `data_local_dir()/ClaudeCodeUsageBubble/icon.png`, `SetCurrentProcessExplicitAppUserModelID`) run in `setup()` before any toast
|
||||
- [x] `tauri-plugin-notification` shows the `apply_results` and session-expired alerts and the pending "Update applied" alert (no cooldown), after the first tray sync
|
||||
@@ -88,6 +88,7 @@ for the alerts (D9).
|
||||
- The plugin Builder does not expose the Windows enable mode, so the plugin would use `auto-launch`'s default `Dynamic`: `enable()` tries HKLM first, so an elevated tick writes an all-users entry that a normal untick cannot remove (`disable()` ignores the HKLM access-denied error and `is_enabled()` still finds it), and that entry holds the unquoted path. Windows therefore does not register the plugin: `platform::windows::autostart_entry` builds an `auto_launch::AutoLaunch` with `WindowsEnableMode::CurrentUser` and the quoted exe path. Its data is `"<exe>" ` (the legacy quoted path plus the trailing space `auto-launch` always appends); `is_enabled()` is unchanged (value present in HKLM or HKCU, not disabled in `StartupApproved\Run`), so a legacy entry still shows checked. Linux and macOS keep the plugin (D10).
|
||||
- Linux `enable()` writes `~/.config/autostart/ClaudeCodeUsageBubble.desktop`, and the plugin uses `$APPIMAGE` (Tauri's `env().appimage`) as the path when it is set, so no entry of our own is needed. The `Exec` path is not quoted.
|
||||
- `menu::refresh` is `Runtime::refresh_shell(force)`: on the main thread it snapshots state (lock released before any Tauri call), then syncs the trays and, when the menu tree differs from the last one applied (or after any menu click), gives every tray a freshly built menu. Countdown ticks and polls therefore rebuild no menu unless something in it changed. The snapshot is taken inside the main-thread closure because a closure queued from a worker runs after one a menu click runs inline; a snapshot taken on the calling thread could otherwise re-apply a stale tree and recreate a removed tray icon.
|
||||
- The left-click handler is registered once on the Tauri builder (`Builder::on_tray_icon_event`), not per icon: Tauri calls a per-icon handler while holding the per-icon listener table, and the inline main-thread refresh may build a missing icon, whose registration locks that same table (a same-thread deadlock).
|
||||
- Each tray gets its own `Menu` instance; the bubble right-click builds a fresh one and pops it from the main thread after the async command returns.
|
||||
- The bubble windows are still the decorated JSON stand-ins; one is created per enabled provider. A window destroyed by a provider toggle is recreated from its `Destroyed` event if the provider is enabled again, except while the process is exiting. Outside `setup()`, bubbles are built on a short-lived thread (Tauri documents that building a webview window inside a menu or window event handler deadlocks on Windows); a process-wide lock, never taken by the main thread, serialises the toggle and the recreate path so only one builds the window. Widget show/hide uses Tauri `show`/`hide` until the no-activation path exists. Reset position and size changes update and save the settings and push new payloads; moving and resizing windows arrives with the bubble window.
|
||||
- The version item calls the update-check hook, which still only logs until the updater exists.
|
||||
|
||||
+27
-17
@@ -2,28 +2,38 @@
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// The AppIndicator libraries the tray backend loads, in its own order.
|
||||
const APPINDICATOR_LIBRARIES: [&str; 2] =
|
||||
["libayatana-appindicator3.so.1", "libappindicator3.so.1"];
|
||||
/// The AppIndicator libraries the tray backend loads, in its own order. The
|
||||
/// unversioned names come from the backend's `backcompat` feature (on by
|
||||
/// default) and are sometimes the only ones present in an AppImage.
|
||||
const APPINDICATOR_LIBRARIES: [&str; 4] = [
|
||||
"libayatana-appindicator3.so.1",
|
||||
"libappindicator3.so.1",
|
||||
"libayatana-appindicator3.so",
|
||||
"libappindicator3.so",
|
||||
];
|
||||
|
||||
/// Whether an AppIndicator library can be loaded. The tray backend panics
|
||||
/// when neither loads, and the release profile aborts on panic, so the tray
|
||||
/// is built only when this is true. Probed once per process.
|
||||
pub fn appindicator_available() -> bool {
|
||||
static AVAILABLE: OnceLock<bool> = OnceLock::new();
|
||||
*AVAILABLE.get_or_init(|| {
|
||||
for name in APPINDICATOR_LIBRARIES {
|
||||
// SAFETY: these are the libraries the tray backend itself loads;
|
||||
// their initialisers run either way once a tray is built.
|
||||
match unsafe { libloading::Library::new(name) } {
|
||||
Ok(_) => {
|
||||
log::info!("tray: {name} found");
|
||||
return true;
|
||||
// The loaded library is kept for the life of the process rather than
|
||||
// unloaded right before the tray backend loads it again.
|
||||
static LIBRARY: OnceLock<Option<libloading::Library>> = OnceLock::new();
|
||||
LIBRARY
|
||||
.get_or_init(|| {
|
||||
for name in APPINDICATOR_LIBRARIES {
|
||||
// SAFETY: these are the libraries the tray backend itself
|
||||
// loads; their initialisers run either way once a tray is built.
|
||||
match unsafe { libloading::Library::new(name) } {
|
||||
Ok(lib) => {
|
||||
log::info!("tray: {name} found");
|
||||
return Some(lib);
|
||||
}
|
||||
Err(err) => log::debug!("tray: {name} not loadable: {err}"),
|
||||
}
|
||||
Err(err) => log::debug!("tray: {name} not loadable: {err}"),
|
||||
}
|
||||
}
|
||||
log::warn!("tray: no AppIndicator library found; running without a tray");
|
||||
false
|
||||
})
|
||||
log::warn!("tray: no AppIndicator library found; running without a tray");
|
||||
None
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
@@ -634,6 +634,7 @@ pub fn run() {
|
||||
])
|
||||
.on_window_event(on_window_event)
|
||||
.on_menu_event(menu::on_menu_event)
|
||||
.on_tray_icon_event(tray::on_tray_icon_event)
|
||||
.setup(move |app| setup(app, &cli))
|
||||
.build(tauri::generate_context!());
|
||||
|
||||
|
||||
+7
-3
@@ -251,18 +251,22 @@ fn create(app: &AppHandle, view: &TrayView, entries: &[MenuEntry]) -> tauri::Res
|
||||
.tooltip(&view.tooltip)
|
||||
.menu(&menu::build(app, entries)?)
|
||||
.show_menu_on_left_click(false)
|
||||
.on_tray_icon_event(on_tray_icon_event)
|
||||
.build(app)
|
||||
}
|
||||
|
||||
fn on_tray_icon_event(tray: &TrayIcon<Wry>, event: TrayIconEvent) {
|
||||
/// App-level tray click handler, registered on the Tauri builder rather than
|
||||
/// per icon. Tauri calls a per-icon handler while holding the per-icon
|
||||
/// listener table, and the refresh below runs inline on the main thread and
|
||||
/// may build a missing tray icon, which locks that same table to register
|
||||
/// itself: a same-thread deadlock. The app-level table is never touched by
|
||||
/// icon creation.
|
||||
pub fn on_tray_icon_event(app: &AppHandle, event: TrayIconEvent) {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let app = tray.app_handle();
|
||||
if let Some(runtime) = app.try_state::<Arc<Runtime>>() {
|
||||
let runtime = Arc::clone(&runtime);
|
||||
runtime.toggle_widget();
|
||||
|
||||
Reference in new issue
Block a user