Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ windows = { version = "0.61", features = [
"Security_Credentials_UI", # UserConsentVerifier + result/availability enums
"Win32_System_WinRT", # IUserConsentVerifierInterop
"Foundation", # async return-type plumbing
"Win32_Security", # SECURITY_ATTRIBUTES (referenced by RegCreateKeyExW)
"Win32_System_Registry", # RegCreateKeyExW/RegSetValueExW: register the toast AUMID in HKCU
"Win32_UI_Shell", # SetCurrentProcessExplicitAppUserModelID
] }
windows-core = "0.61"
# In windows-rs 0.61 IAsyncOperation moved out of windows::Foundation into the
Expand Down
100 changes: 100 additions & 0 deletions src-tauri/src/aumid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Windows-only: register the app's AppUserModelID (AUMID) at runtime so WinRT
//! toast notifications actually render for the installed app (issue #3).
//!
//! On an installed build `tauri-plugin-notification` sets the toast's
//! `System.AppUserModel.ID` to the bundle identifier (`com.karem.whatrust`).
//! Windows only renders a toast whose AUMID is *registered* on the system. The
//! NSIS/MSI installers do tag their Start-Menu shortcut with the AUMID, but
//! that registration is fragile: the Desktop shortcut carries no AUMID, a
//! per-user vs per-machine path mismatch or a regenerated shortcut can drop the
//! property, and a raw-exe run has none at all. When the AUMID is unregistered
//! the WinRT call fails *silently* — and whatRust discards the error (see
//! `notify.rs`), so no notification ever appears.
//!
//! Registering the AUMID under HKCU on every launch makes toast delivery
//! self-sufficient regardless of installer or launch path. Both steps below are
//! per-user (no admin), idempotent, and best-effort: a failure only means
//! toasts may not render, so we log and never panic or block startup.
//!
//! The AUMID is read from the live Tauri config `identifier`, i.e. the exact
//! value the notification plugin passes to `app_id()`, so the two can never
//! drift apart.

use tauri::AppHandle;

/// No-op on every platform except Windows.
#[allow(unused_variables)]
pub fn register(app: &AppHandle) {
#[cfg(windows)]
{
use tauri::Manager;
let config = app.config();
let aumid = &config.identifier;
let display = config.product_name.as_deref().unwrap_or("whatRust");
win::register(aumid, display);
}
}

#[cfg(windows)]
mod win {
use windows::core::PCWSTR;
use windows::Win32::System::Registry::{
RegCloseKey, RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_CURRENT_USER, KEY_WRITE,
REG_OPTION_NON_VOLATILE, REG_SZ,
};
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;

/// UTF-16, NUL-terminated — suitable for `PCWSTR` args and `REG_SZ` data.
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}

pub fn register(aumid: &str, display_name: &str) {
// Step 1 — the registry entry is what makes the Action Center render
// the toast for an unpackaged desktop app.
if let Err(e) = write_registry(aumid, display_name) {
eprintln!("[whatrust] AUMID registry registration failed: {e:?}");
}
// Step 2 — pin this process to the AUMID (taskbar grouping + toast
// attribution). Harmless if it fails; we just log.
let id = wide(aumid);
if let Err(e) = unsafe { SetCurrentProcessExplicitAppUserModelID(PCWSTR(id.as_ptr())) } {
eprintln!("[whatrust] SetCurrentProcessExplicitAppUserModelID failed: {e:?}");
}
}

/// Writes `HKCU\Software\Classes\AppUserModelId\<AUMID>` with a `DisplayName`
/// (REG_SZ). Idempotent — overwritten on every launch, which also self-heals
/// a Start-Menu shortcut that lost its AUMID property.
fn write_registry(aumid: &str, display_name: &str) -> windows::core::Result<()> {
let subkey = wide(&format!("Software\\Classes\\AppUserModelId\\{aumid}"));
let mut hkey = HKEY(std::ptr::null_mut());
unsafe {
RegCreateKeyExW(
HKEY_CURRENT_USER,
PCWSTR(subkey.as_ptr()),
None,
PCWSTR::null(),
REG_OPTION_NON_VOLATILE,
KEY_WRITE,
None,
&mut hkey,
None,
)
.ok()?;
}

// REG_SZ data is the NUL-terminated UTF-16 bytes of the display name.
let name = wide("DisplayName");
let data = wide(display_name);
let bytes = unsafe {
std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(&data[..]))
};
let set = unsafe { RegSetValueExW(hkey, PCWSTR(name.as_ptr()), None, REG_SZ, Some(bytes)) };
// Always close the key, then surface any set error.
unsafe {
let _ = RegCloseKey(hkey);
}
set.ok()
}
}
7 changes: 7 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod settings;
mod tray;
mod commands;
mod notify;
mod aumid;

use tauri::Manager;

Expand Down Expand Up @@ -92,6 +93,12 @@ pub fn run() {
])
.setup(|app| {
let handle = app.handle();

// Windows: register our AppUserModelID so WinRT toast notifications
// actually render for the installed app (no-op elsewhere). Must run
// before any account window can fire a notification. See aumid.rs.
aumid::register(handle);

let s = settings::load(handle);
let args: Vec<String> = std::env::args().collect();
let start_hidden = s.start_minimized || args.iter().any(|a| a == "--minimized");
Expand Down
7 changes: 6 additions & 1 deletion src-tauri/src/notify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@ use tauri::AppHandle;
use tauri_plugin_notification::NotificationExt;

pub fn show(app: &AppHandle, title: &str, body: &str) {
let _ = app.notification().builder().title(title).body(body).show();
// Don't silently swallow failures: on Windows a toast can fail (e.g. an
// unregistered AppUserModelID — see aumid.rs) and the only signal is this
// Result. Logging it makes such failures diagnosable from the console.
if let Err(e) = app.notification().builder().title(title).body(body).show() {
eprintln!("[whatrust] failed to show notification: {e:?}");
}
}
Loading