mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-24 04:15:09 +00:00
feat: add tray icons, the native context menu, notifications and start at login
Each enabled live provider gets a tray icon with the usage badge, a four-line tooltip and the context menu; a left click toggles the widget. Menus are rebuilt from state after every click so native check marks stay right, and a bubble right-click pops the same menu. Alerts go through the notification plugin. On Windows the app id is registered under HKCU with a name and icon first, so a portable exe without a Start-menu shortcut can show toasts. Start at login uses the autostart plugin under the Run value name earlier releases wrote, so an existing entry is picked up as is. On Linux the tray is skipped when no AppIndicator library loads instead of aborting.
This commit is contained in:
1 parent
539f26019b
commit
f2063bda4f
14 files changed
+1250
-209
No files matched your search
@@ -0,0 +1,29 @@
|
||||
//! Linux-only helpers.
|
||||
|
||||
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"];
|
||||
|
||||
/// 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;
|
||||
}
|
||||
Err(err) => log::debug!("tray: {name} not loadable: {err}"),
|
||||
}
|
||||
}
|
||||
log::warn!("tray: no AppIndicator library found; running without a tray");
|
||||
false
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
|
||||
+66
-1
@@ -1,17 +1,23 @@
|
||||
//! Windows-only helpers. This is the only module that talks to Win32 APIs.
|
||||
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use webview2_com::Microsoft::Web::WebView2::Win32::GetAvailableCoreWebView2BrowserVersionString;
|
||||
use windows::core::{HSTRING, PCWSTR, PWSTR};
|
||||
use windows::Win32::Foundation::{CloseHandle, WAIT_OBJECT_0};
|
||||
use windows::Win32::System::Threading::{OpenProcess, WaitForSingleObject, PROCESS_SYNCHRONIZE};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_ICONERROR, MB_OK, MB_SETFOREGROUND};
|
||||
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
MessageBoxW, SetForegroundWindow, MB_ICONERROR, MB_OK, MB_SETFOREGROUND,
|
||||
};
|
||||
|
||||
use crate::core::proxy::{self, SystemProxy};
|
||||
|
||||
const INTERNET_SETTINGS_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings";
|
||||
const PERSONALIZE_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
|
||||
const AUMID_KEY_PREFIX: &str = r"Software\Classes\AppUserModelId\";
|
||||
|
||||
/// The static proxy from `HKCU\…\Internet Settings`, or `None` when
|
||||
/// `ProxyEnable` is not 1 or no usable `ProxyServer` entry exists.
|
||||
@@ -87,3 +93,62 @@ pub fn message_box_error(title: &str, text: &str) {
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Register `aumid` so toasts from this portable exe are shown although no
|
||||
/// Start-menu shortcut carries the id: write
|
||||
/// `HKCU\Software\Classes\AppUserModelId\<aumid>` with `DisplayName` and
|
||||
/// `IconUri` (`icon_png` written to `icon_path`), then make it the process
|
||||
/// id. Each step is attempted; the first error is returned after all ran.
|
||||
pub fn register_aumid(
|
||||
aumid: &str,
|
||||
display_name: &str,
|
||||
icon_png: &[u8],
|
||||
icon_path: &Path,
|
||||
) -> io::Result<()> {
|
||||
let icon = write_if_changed(icon_path, icon_png);
|
||||
if let Err(err) = &icon {
|
||||
log::warn!("AUMID icon {} not written: {err}", icon_path.display());
|
||||
}
|
||||
|
||||
let key = windows_registry::CURRENT_USER
|
||||
.create(format!("{AUMID_KEY_PREFIX}{aumid}"))
|
||||
.and_then(|key| {
|
||||
key.set_string("DisplayName", display_name)?;
|
||||
if icon.is_ok() {
|
||||
key.set_string("IconUri", icon_path.to_string_lossy().as_ref())?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.map_err(io::Error::from);
|
||||
|
||||
// SAFETY: the HSTRING outlives the call.
|
||||
let process = unsafe { SetCurrentProcessExplicitAppUserModelID(&HSTRING::from(aumid)) }
|
||||
.map_err(io::Error::from);
|
||||
|
||||
key.and(process).and(icon)
|
||||
}
|
||||
|
||||
/// Write `bytes` to `path` unless it already holds exactly them.
|
||||
fn write_if_changed(path: &Path, bytes: &[u8]) -> io::Result<()> {
|
||||
if std::fs::read(path).is_ok_and(|current| current == bytes) {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(dir) = path.parent() {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
std::fs::write(path, bytes)
|
||||
}
|
||||
|
||||
/// Bring `window` to the foreground. A popup menu needs its owner in the
|
||||
/// foreground, or it does not dismiss on a click elsewhere.
|
||||
pub fn set_foreground(window: &tauri::WebviewWindow) {
|
||||
match window.hwnd() {
|
||||
// SAFETY: the handle belongs to a live window of this process.
|
||||
Ok(hwnd) => {
|
||||
if !unsafe { SetForegroundWindow(hwnd) }.as_bool() {
|
||||
log::debug!("SetForegroundWindow({}) refused", window.label());
|
||||
}
|
||||
}
|
||||
Err(err) => log::warn!("no HWND for {}: {err}", window.label()),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user