diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-07-29 15:02:32 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-29 15:02:32 +0300 |
| commit | 5b0be42e043e25e34ceeecf72e72bdb043164997 (patch) | |
| tree | a70e9e0947bdb80bf04ed76313f6871d2ea1c51b /src/platform | |
| parent | 73d92004f50d3f5fafe64e0f9b7fddbcf4d99965 (diff) | |
fix(windows): restore system notifications and sound playback (#2019)
Diffstat (limited to 'src/platform')
| -rw-r--r-- | src/platform/windows.rs | 124 |
1 files changed, 119 insertions, 5 deletions
diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 2caf116d..a206b0d1 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -42,10 +42,13 @@ use windows_sys::{ KEYEVENTF_KEYUP, }, }, - Shell::{CommandLineToArgvW, ShellExecuteW}, + Shell::{ + CommandLineToArgvW, ShellExecuteW, Shell_NotifyIconW, NIF_ICON, NIF_INFO, NIF_TIP, + NIIF_INFO, NIIF_NOSOUND, NIM_ADD, NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW, + }, WindowsAndMessaging::{ - GetForegroundWindow, GetWindowThreadProcessId, SendMessageTimeoutW, - SMTO_ABORTIFHUNG, WM_IME_CONTROL, + CreateWindowExW, DestroyWindow, GetForegroundWindow, GetWindowThreadProcessId, + LoadIconW, SendMessageTimeoutW, IDI_APPLICATION, SMTO_ABORTIFHUNG, WM_IME_CONTROL, }, }, }, @@ -690,8 +693,110 @@ pub fn read_clipboard_image() -> Option<ClipboardImage> { None } -pub fn show_desktop_notification(_title: &str, _body: Option<&str>) -> std::io::Result<bool> { - Ok(false) +pub fn show_desktop_notification(title: &str, body: Option<&str>) -> std::io::Result<bool> { + let title = title.to_owned(); + let body = body.unwrap_or(&title).to_owned(); + let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1); + std::thread::Builder::new() + .name("herdr-windows-notification".into()) + .spawn(move || show_desktop_notification_on_thread(&title, &body, ready_tx))?; + ready_rx + .recv_timeout(Duration::from_secs(2)) + .map_err(|err| match err { + std::sync::mpsc::RecvTimeoutError::Timeout => std::io::Error::new( + std::io::ErrorKind::TimedOut, + "Windows notification setup timed out", + ), + std::sync::mpsc::RecvTimeoutError::Disconnected => std::io::Error::other( + "Windows notification thread exited before reporting readiness", + ), + })? +} + +fn show_desktop_notification_on_thread( + title: &str, + body: &str, + ready_tx: std::sync::mpsc::SyncSender<std::io::Result<bool>>, +) { + let class_name = wide_null("STATIC"); + let window_name = wide_null("Herdr notifications"); + let hwnd = unsafe { + CreateWindowExW( + 0, + class_name.as_ptr(), + window_name.as_ptr(), + 0, + 0, + 0, + 0, + 0, + null_mut(), + null_mut(), + null_mut(), + std::ptr::null(), + ) + }; + if hwnd.is_null() { + let _ = ready_tx.send(Err(std::io::Error::last_os_error())); + return; + } + + let mut notification = unsafe { std::mem::zeroed::<NOTIFYICONDATAW>() }; + notification.cbSize = size_of::<NOTIFYICONDATAW>() as u32; + notification.hWnd = hwnd; + notification.uID = 1; + notification.hIcon = unsafe { LoadIconW(null_mut(), IDI_APPLICATION) }; + notification.uFlags = NIF_TIP; + if !notification.hIcon.is_null() { + notification.uFlags |= NIF_ICON; + } + copy_wide_truncated(&mut notification.szTip, "Herdr"); + + if unsafe { Shell_NotifyIconW(NIM_ADD, ¬ification) } == 0 { + let _ = ready_tx.send(Err(std::io::Error::other( + "failed to add Herdr notification-area icon", + ))); + unsafe { + DestroyWindow(hwnd); + } + return; + } + + notification.uFlags = NIF_INFO; + notification.dwInfoFlags = NIIF_INFO | NIIF_NOSOUND; + copy_wide_truncated(&mut notification.szInfoTitle, title); + copy_wide_truncated(&mut notification.szInfo, body); + if unsafe { Shell_NotifyIconW(NIM_MODIFY, ¬ification) } == 0 { + unsafe { + Shell_NotifyIconW(NIM_DELETE, ¬ification); + DestroyWindow(hwnd); + } + let _ = ready_tx.send(Err(std::io::Error::other( + "failed to show Herdr desktop notification", + ))); + return; + } + + let _ = ready_tx.send(Ok(true)); + std::thread::sleep(Duration::from_secs(10)); + unsafe { + Shell_NotifyIconW(NIM_DELETE, ¬ification); + DestroyWindow(hwnd); + } +} + +fn copy_wide_truncated<const N: usize>(destination: &mut [u16; N], value: &str) { + destination.fill(0); + let mut offset = 0; + for ch in value.chars() { + let mut units = [0; 2]; + let encoded = ch.encode_utf16(&mut units); + if offset + encoded.len() >= N { + break; + } + destination[offset..offset + encoded.len()].copy_from_slice(encoded); + offset += encoded.len(); + } } fn wide_null(value: &str) -> Vec<u16> { @@ -1127,6 +1232,15 @@ mod tests { }; #[test] + fn windows_notification_text_is_null_terminated_and_unicode_safe() { + let mut destination = [u16::MAX; 6]; + super::copy_wide_truncated(&mut destination, "abc😀def"); + + assert_eq!(String::from_utf16(&destination[..5]).unwrap(), "abc😀"); + assert_eq!(destination[5], 0); + } + + #[test] fn cmd_agent_command_encodes_edge_arguments_without_cmd_expansion() { use base64::Engine as _; |
