aboutsummaryrefslogtreecommitdiff
path: root/src/platform
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-07-29 15:02:32 +0300
committerGitHub <noreply@github.com>2026-07-29 15:02:32 +0300
commit5b0be42e043e25e34ceeecf72e72bdb043164997 (patch)
treea70e9e0947bdb80bf04ed76313f6871d2ea1c51b /src/platform
parent73d92004f50d3f5fafe64e0f9b7fddbcf4d99965 (diff)
fix(windows): restore system notifications and sound playback (#2019)
Diffstat (limited to 'src/platform')
-rw-r--r--src/platform/windows.rs124
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, &notification) } == 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, &notification) } == 0 {
+ unsafe {
+ Shell_NotifyIconW(NIM_DELETE, &notification);
+ 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, &notification);
+ 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 _;