//! System tray icon (AGENTS.md §12, §13). //! //! V1 surface: a single hidden helper window + dedicated message-pump //! thread; the helper owns a Shell_NotifyIconW registration. //! //! Phase 7 C1 additions on top of the Phase 6 C3 scaffolding: //! - Richer context menu: `Open`, grayed-out "New messages: N" info //! row, `Pause notifications` (checked when paused), `Settings`, //! `Restart connection`, separator, `Quit`. (AGENTS.md §12.) //! - `TrayState` with `Arc` paused + `Arc` //! new_messages. `lib.rs` mutates them; `show_context_menu` reads //! them when building the menu. //! - New callbacks: `on_pause_toggle`, `on_settings`, `on_restart`. //! - Registered window message `WM_ACTIVATE_INSTANCE`. The WndProc //! treats it as "another instance launched, please surface the main //! window". Used by `single_instance::activate_existing_instance` //! when a second launch detects the singleton mutex is taken. #![cfg(windows)] use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::thread::JoinHandle; use windows_sys::Win32::Foundation::{ GetLastError, HWND, LPARAM, LRESULT, POINT, WPARAM, }; use windows_sys::Win32::System::Threading::GetCurrentThreadId; use windows_sys::Win32::UI::Shell::{ Shell_NotifyIconW, NIF_ICON, NIF_INFO, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW, }; use windows_sys::Win32::UI::WindowsAndMessaging::{ AppendMenuW, CreatePopupMenu, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetCursorPos, GetMessageW, GetWindowLongPtrW, PostMessageW, RegisterClassExW, RegisterWindowMessageW, SendMessageW, SetForegroundWindow, SetWindowLongPtrW, TrackPopupMenuEx, TranslateMessage, CS_HREDRAW, CW_USEDEFAULT, GWLP_USERDATA, MF_CHECKED, MF_GRAYED, MF_SEPARATOR, MF_STRING, MSG, TPM_BOTTOMALIGN, TPM_LEFTALIGN, TPM_RETURNCMD, WM_COMMAND, WM_DESTROY, WM_LBUTTONDBLCLK, WM_RBUTTONUP, WM_USER, WNDCLASSEXW, WS_EX_TOOLWINDOW, }; use windows_sys::core::GUID; const IDM_OPEN: u16 = 1; const IDM_NEW_MESSAGES: u16 = 2; const IDM_PAUSE: u16 = 3; const IDM_SETTINGS: u16 = 4; const IDM_RESTART: u16 = 5; const IDM_QUIT: u16 = 6; const WM_TRAYICON: u32 = WM_USER + 1; // Phase 7 C2: posted from any thread to ask the tray's WndProc to // raise a toast via NIM_MODIFY. Payload is a heap-allocated // `ToastPayload`; the WndProc reconstructs the Box and drops it. const WM_SHOW_TOAST: u32 = WM_USER + 2; // Windows 10+ toast severity flags (Win32 NIIF_* constants — // windows-sys 0.59 doesn't re-export the names, so we hard-code). // `pub` so `lib.rs` can pick a severity based on Gotify priority // without re-hard-coding the bit patterns. pub const NIIF_NONE: u32 = 0x0000_0000; pub const NIIF_INFO: u32 = 0x0000_0001; pub const NIIF_WARNING: u32 = 0x0000_0002; pub const NIIF_ERROR: u32 = 0x0000_0003; pub const NIIF_NOSOUND: u32 = 0x0000_0010; const TRAY_CLASS_NAME: &str = "WindowsGotifyClientRust.TrayHidden\0"; const TRAY_TIP: &str = "Windows Gotify Client\0"; // Wide-char constant for RegisterWindowMessageW. `RegisterWindowMessageW` // is idempotent across processes — both the live and the launching // instance get the same u32 ID back. const ACTIVATE_INSTANCE_MSG_NAME: &str = "WindowsGotifyClientRust.ActivateInstance\0"; /// Shared, runtime-mutable state surfaced through the tray menu. Lives /// behind `Arc` so callers (`lib.rs` WS drain + the menu builder) can /// share without a lock for the small atomic reads/writes here. pub struct TrayState { pub paused: AtomicBool, pub new_messages: AtomicU32, /// Phase 7 C2: cached "show desktop notifications" master toggle /// so the WS drain's toast gating can read it lock-free without /// a config reload per message. `lib.rs` writes here from the /// SettingsPage toggle callback. pub desktop_notifications: AtomicBool, /// Phase 7 C2: cached "notify on High priority" toggle. pub notify_high: AtomicBool, /// Phase 7 C2: cached "notify on Max priority" toggle. pub notify_max: AtomicBool, /// Phase 7 C2: "Restart connection" request flag. The tray's /// `on_restart` callback sets it from the tray thread; the UI /// drain timer (which runs on the UI thread) swaps it back to /// `false` and triggers the actual refetch. Decouples the tray /// callback from the UI-bound `Rc>` so the closure /// can stay Send + Sync. pub restart_requested: AtomicBool, } impl TrayState { fn new() -> Self { Self { paused: AtomicBool::new(false), new_messages: AtomicU32::new(0), desktop_notifications: AtomicBool::new(true), notify_high: AtomicBool::new(true), notify_max: AtomicBool::new(true), restart_requested: AtomicBool::new(false), } } } /// Callbacks the tray dispatches to the main runtime. pub struct TrayCallbacks { pub on_open: Box, pub on_pause_toggle: Box, pub on_settings: Box, pub on_restart: Box, pub on_quit: Box, } pub struct TrayIcon { hwnd: HWND, thread: Option>, quit_flag: std::sync::Arc, /// Incremented each time the user picks a menu item or a registered /// message arrives so the caller can correlate traces across /// threads. Not load-bearing. tick: std::sync::Arc, state: Arc, } impl TrayIcon { /// Build the helper window, register the tray icon, and spawn the /// message-pump thread. Fails fast on any setup error. pub fn new(callbacks: TrayCallbacks) -> Result { let hinstance = unsafe { GetCurrentThreadId() } as *mut _; // RegisterClassExW — registration is process-wide, so the // class name is namespaced ("...TrayHidden") to avoid colliding // with anything else. CS_HREDRAW is the smallest constant that // satisfies the win32 docs requirement of "any class style". let class_name_w: Vec = TRAY_CLASS_NAME.encode_utf16().collect(); let wc = WNDCLASSEXW { cbSize: std::mem::size_of::() as u32, style: CS_HREDRAW, lpfnWndProc: Some(wnd_proc), cbClsExtra: 0, cbWndExtra: 0, hInstance: hinstance, hIcon: std::ptr::null_mut(), hCursor: std::ptr::null_mut(), hbrBackground: std::ptr::null_mut(), lpszMenuName: std::ptr::null(), lpszClassName: class_name_w.as_ptr(), hIconSm: std::ptr::null_mut(), }; let atom = unsafe { RegisterClassExW(&wc) }; if atom == 0 { return Err(format!("RegisterClassExW failed: err={}", unsafe { GetLastError() })); } // CreateWindowExW with HWND_MESSAGE-style (we leave the hWndParent // argument null and pick an explicit WS_EX_TOOLWINDOW + zero // size to keep the helper out of the taskbar / Alt-Tab list). // Window size 0×0 + CW_USEDEFAULT for the position. let hwnd = unsafe { CreateWindowExW( WS_EX_TOOLWINDOW, class_name_w.as_ptr(), std::ptr::null(), 0, // no WS_OVERLAPPEDWINDOW, etc. CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, std::ptr::null_mut(), std::ptr::null_mut(), hinstance, std::ptr::null(), ) }; if hwnd.is_null() { return Err(format!("CreateWindowExW failed: err={}", unsafe { GetLastError() })); } // Resolve the registered window message id. Both this process // (for the WndProc switch) and a fresh second-launching process // (to send the message via single_instance::activate_existing) // call RegisterWindowMessageW with the same string; the OS // returns the same u32 either way. let activate_msg_w: Vec = ACTIVATE_INSTANCE_MSG_NAME.encode_utf16().collect(); let activate_msg_id = unsafe { RegisterWindowMessageW(activate_msg_w.as_ptr()) }; // Attach a `TrayShared` pointer to the window so the WndProc // can find the callbacks + state without going through TLS or // statics. let tick = std::sync::Arc::new(AtomicUsize::new(0)); let state = Arc::new(TrayState::new()); let shared = Box::new(TrayShared { callbacks, activate_msg_id, tick: tick.clone(), state: state.clone(), }); let shared_ptr = Box::into_raw(shared) as isize; unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, shared_ptr); } // Spawn the message-pump thread. It owns a clone of // `shared_ptr` and a quit flag. let quit_flag = std::sync::Arc::new(AtomicBool::new(false)); let quit_for_thread = quit_flag.clone(); // HWND is a raw pointer (not Send) — coerce to a usize for // the cross-thread handoff. The thread ignores it anyway // (WndProc looks up the shared pointer from the window's // own user-data slot). let hwnd_for_thread = hwnd as usize; let thread = std::thread::Builder::new() .name("gotify-tray".into()) .spawn(move || { run_message_loop(hwnd_for_thread, shared_ptr, quit_for_thread); }) .map_err(|e| format!("tray thread spawn: {e}"))?; // Register the icon. NULL hIcon → Windows draws a default // 16×16 placeholder; the C2 polish pass will swap in a real // `.ico`. let tip_w: Vec = TRAY_TIP.encode_utf16().collect(); let mut nid = NOTIFYICONDATAW { cbSize: std::mem::size_of::() as u32, hWnd: hwnd, uID: 1, uFlags: NIF_ICON | NIF_TIP | NIF_MESSAGE, uCallbackMessage: WM_TRAYICON, hIcon: std::ptr::null_mut(), szTip: [0u16; 128], szInfo: [0u16; 256], Anonymous: unsafe { std::mem::zeroed() }, szInfoTitle: [0u16; 64], dwState: 0, dwStateMask: 0, dwInfoFlags: 0, guidItem: GUID { data1: 0, data2: 0, data3: 0, data4: [0; 8] }, hBalloonIcon: std::ptr::null_mut(), }; // Copy the wide tip into the fixed-size buffer. for (i, ch) in tip_w.iter().take(127).enumerate() { nid.szTip[i] = *ch; } unsafe { let ok = Shell_NotifyIconW(NIM_ADD, &nid); if ok == 0 { tracing::warn!("tray: Shell_NotifyIconW NIM_ADD returned 0"); } } // We also have to set the version explicitly so the union // field doesn't keep the old layout. Some Windows builds // require this *before* NIM_ADD in older docs, but NIM_ADD // + NIM_VERSION-then-MODIFY is the modern order. // (NIM_VERSION = 4 in the Win32 header; windows-sys 0.59 // didn't re-export the name, so we hard-code the constant.) let mut version_nid = nid; version_nid.Anonymous.uVersion = 4u32; unsafe { Shell_NotifyIconW(windows_sys::Win32::UI::Shell::NIM_SETVERSION, &version_nid); } Ok(Self { hwnd, thread: Some(thread), quit_flag, tick: std::sync::Arc::new(AtomicUsize::new(0)), state, }) } /// Shared state for the menu (paused toggle + new-messages /// counter). The WS drain closure in `lib.rs` bumps the counter; /// the menu builder reads both. pub fn state(&self) -> Arc { self.state.clone() } pub fn tick(&self) -> usize { self.tick.load(Ordering::Relaxed) } /// Public hook used by `single_instance::activate_existing_instance` /// to forward the registered message to this tray's WndProc without /// re-importing the Win32 / message-id internals here. pub fn request_activate(&self) { if self.hwnd.is_null() { return; } // SAFETY: SendMessageW with our registered message id (a // value that exists because the tray was constructed with the // same id). 0/0 are unused wparam/lparam slots for the // activation message. unsafe { SendMessageW(self.hwnd, self.activate_msg_id_for_send(), 0, 0); } } /// Phase 7 C2: show a desktop toast for an incoming Gotify message. /// `title` becomes `szInfoTitle`, `body` becomes `szInfo`, /// `icon_flags` is one of the NIIF_* constants. Safe to call from /// any thread — we heap-allocate the payload and PostMessageW it /// to the tray's helper window so the actual `Shell_NotifyIconW` /// call (which must happen on the registration thread) runs in the /// WndProc. pub fn show_toast(&self, title: &str, body: &str, icon_flags: u32) { if self.hwnd.is_null() { return; } let payload = Box::new(ToastPayload { title: title.to_string(), body: body.to_string(), icon_flags, }); let ptr = Box::into_raw(payload) as isize; // SAFETY: PostMessageW queues the message; the WndProc owns // the only reference to the heap-allocated payload and will // reconstruct + drop it. If the WndProc never runs (e.g. the // tray was just dropped and the helper HWND is destroyed), // we leak the Box — that's a hard-to-trigger edge case we // accept for V1. unsafe { PostMessageW(self.hwnd, WM_SHOW_TOAST, 0, ptr as LPARAM); } } fn activate_msg_id_for_send(&self) -> u32 { // Re-resolve the registered message id in the calling // process. The system-wide table returns the same u32 that // TrayIcon::new() registered on the live instance. We avoid // stashing the id inside TrayIcon because single_instance // lives in a different module; the indirection keeps the // send path testable in isolation. let wide: Vec = ACTIVATE_INSTANCE_MSG_NAME.encode_utf16().collect(); unsafe { RegisterWindowMessageW(wide.as_ptr()) } } } impl Drop for TrayIcon { fn drop(&mut self) { // Remove the icon first so the user doesn't see a stale tray // entry pointing at a window that's about to be destroyed. let nid = NOTIFYICONDATAW { cbSize: std::mem::size_of::() as u32, hWnd: self.hwnd, uID: 1, uFlags: 0, uCallbackMessage: 0, hIcon: std::ptr::null_mut(), szTip: [0u16; 128], szInfo: [0u16; 256], Anonymous: unsafe { std::mem::zeroed() }, szInfoTitle: [0u16; 64], dwState: 0, dwStateMask: 0, dwInfoFlags: 0, guidItem: GUID { data1: 0, data2: 0, data3: 0, data4: [0; 8] }, hBalloonIcon: std::ptr::null_mut(), }; unsafe { let _ = Shell_NotifyIconW(NIM_DELETE, &nid); } // Tell the message loop to exit, then post WM_DESTROY so the // window's HWND is gone before the thread joins. self.quit_flag.store(true, Ordering::SeqCst); // Posting via the helper window's DestroyWindow is enough: it // queues WM_DESTROY on the same thread, which causes the // message loop to fall through. unsafe { DestroyWindow(self.hwnd); } if let Some(handle) = self.thread.take() { let _ = handle.join(); } } } /// State shared between the WndProc (UI thread) and the message-pump /// thread. The pointer lives in the helper window's GWLP_USERDATA slot. struct TrayShared { callbacks: TrayCallbacks, activate_msg_id: u32, tick: std::sync::Arc, state: Arc, } /// Heap-allocated payload for the WM_SHOW_TOAST posted message. Lives /// only long enough to be reconstructed by the WndProc and dropped. struct ToastPayload { title: String, body: String, icon_flags: u32, } // Helper: convert a Rust `&str` into a UTF-16 buffer for menu item // text. The caller appends the null terminator implicitly via // `chain(once(0))`. fn wide(s: &str) -> Vec { s.encode_utf16().chain(std::iter::once(0)).collect() } fn run_message_loop(_hwnd: usize, shared_ptr: isize, quit_flag: std::sync::Arc) { // SAFETY: shared_ptr came from `Box::into_raw` in `new()`. We // reconstruct the box at the end of the loop, regardless of how // we exit (clean shutdown, DestroyWindow, etc.). let _shared = unsafe { Box::from_raw(shared_ptr as *mut TrayShared) }; let mut msg: MSG = unsafe { std::mem::zeroed() }; loop { // SAFETY: GetMessageW blocks until a message arrives. It // returns 0 on WM_QUIT and -1 on error; we treat both as // exit. let rc = unsafe { GetMessageW(&mut msg, std::ptr::null_mut(), 0, 0) }; if rc == 0 || rc == -1 { break; } // SAFETY: standard message-pump pair; both are no-ops for // windowless message dispatch. unsafe { TranslateMessage(&msg); DispatchMessageW(&msg); } if quit_flag.load(Ordering::SeqCst) { break; } } } unsafe extern "system" fn wnd_proc( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { // Recover the shared pointer (cast back to the same `isize` width // that SetWindowLongPtrW stored). let shared_ptr = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as isize; if shared_ptr == 0 { return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; } let shared = unsafe { &*(shared_ptr as *const TrayShared) }; // Registered message has to be checked first because it is the // `RegisterWindowMessageW` return value, not a compile-time // constant. if msg == shared.activate_msg_id { // Phase 7 C1: a second instance just asked us to surface the // main window. Reset the "New messages" counter before // dispatching on_open, mirroring the tray double-click path. shared.state.new_messages.store(0, Ordering::Relaxed); shared.tick.fetch_add(1, Ordering::Relaxed); (shared.callbacks.on_open)(); return 0; } if msg == WM_SHOW_TOAST { // Phase 7 C2: show a desktop toast via NIM_MODIFY. Reconstruct // the heap payload from LPARAM and drop it before returning. let ptr = lparam as isize; if ptr != 0 { // SAFETY: `ptr` came from `Box::into_raw` in `show_toast` // and we own the only reference (the WndProc is // single-threaded for this message). Reconstructing the // Box drops the String buffers when this scope ends. let payload = unsafe { Box::from_raw(ptr as *mut ToastPayload) }; show_toast(hwnd, &payload); } return 0; } match msg { WM_TRAYICON => { let event = lparam as u32; match event { WM_LBUTTONDBLCLK => { shared.state.new_messages.store(0, Ordering::Relaxed); shared.tick.fetch_add(1, Ordering::Relaxed); (shared.callbacks.on_open)(); } WM_RBUTTONUP => { show_context_menu(hwnd, shared); } _ => {} } 0 } WM_COMMAND => { let id = (wparam & 0xFFFF) as u16; match id { IDM_OPEN => { shared.state.new_messages.store(0, Ordering::Relaxed); shared.tick.fetch_add(1, Ordering::Relaxed); (shared.callbacks.on_open)(); } IDM_PAUSE => { shared.tick.fetch_add(1, Ordering::Relaxed); let now = !shared.state.paused.load(Ordering::Relaxed); shared.state.paused.store(now, Ordering::Relaxed); (shared.callbacks.on_pause_toggle)(now); } IDM_SETTINGS => { shared.tick.fetch_add(1, Ordering::Relaxed); (shared.callbacks.on_settings)(); } IDM_RESTART => { shared.tick.fetch_add(1, Ordering::Relaxed); (shared.callbacks.on_restart)(); } IDM_QUIT => { shared.tick.fetch_add(1, Ordering::Relaxed); (shared.callbacks.on_quit)(); } _ => {} } 0 } WM_DESTROY => 0, _ => unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }, } } fn show_context_menu(hwnd: HWND, shared: &TrayShared) { // SAFETY: CreatePopupMenu / AppendMenuW / TrackPopupMenuEx are all // standard user-side APIs; the menu is destroyed via the implicit // lifetime of the TrackPopupMenuEx call (it auto-cleans on return). unsafe { let menu = CreatePopupMenu(); if menu.is_null() { tracing::warn!("tray: CreatePopupMenu failed"); return; } AppendMenuW( menu, MF_STRING, IDM_OPEN as usize, wide("Open").as_ptr(), ); // "New messages: N" — info row, grayed so it can't be picked. // Menu strings are UTF-16 null-terminated; we build them per // show so the count is live. let n = shared.state.new_messages.load(Ordering::Relaxed); let info_label = if n == 0 { "New messages: 0".to_string() } else { format!("New messages: {n}") }; AppendMenuW( menu, MF_STRING | MF_GRAYED, IDM_NEW_MESSAGES as usize, wide(&info_label).as_ptr(), ); AppendMenuW(menu, MF_SEPARATOR, 0, std::ptr::null()); let pause_flag = if shared.state.paused.load(Ordering::Relaxed) { MF_STRING | MF_CHECKED } else { MF_STRING }; AppendMenuW( menu, pause_flag, IDM_PAUSE as usize, wide("Pause notifications").as_ptr(), ); AppendMenuW( menu, MF_STRING, IDM_SETTINGS as usize, wide("Settings...").as_ptr(), ); AppendMenuW( menu, MF_STRING, IDM_RESTART as usize, wide("Restart connection").as_ptr(), ); AppendMenuW(menu, MF_SEPARATOR, 0, std::ptr::null()); AppendMenuW( menu, MF_STRING, IDM_QUIT as usize, wide("Quit").as_ptr(), ); // TrackPopupMenuEx requires the window to be the foreground // window or the menu dismisses immediately. SetForegroundWindow(hwnd); let mut pt: POINT = std::mem::zeroed(); GetCursorPos(&mut pt); let cmd = TrackPopupMenuEx( menu, TPM_BOTTOMALIGN | TPM_LEFTALIGN | TPM_RETURNCMD, pt.x, pt.y, hwnd, std::ptr::null(), ); // `cmd == 0` means the user dismissed without picking // anything; fall through cleanly. if cmd > 0 { shared.tick.fetch_add(1, Ordering::Relaxed); match cmd as u16 { IDM_OPEN => { shared.state.new_messages.store(0, Ordering::Relaxed); (shared.callbacks.on_open)() } IDM_PAUSE => { let now = !shared.state.paused.load(Ordering::Relaxed); shared.state.paused.store(now, Ordering::Relaxed); (shared.callbacks.on_pause_toggle)(now); } IDM_SETTINGS => (shared.callbacks.on_settings)(), IDM_RESTART => (shared.callbacks.on_restart)(), IDM_QUIT => (shared.callbacks.on_quit)(), _ => {} } } } } // Keep the Mutex import alive for the future "menu rebuild on // state-change" pass (so callbacks can re-show the menu). Phase 7 C1 // doesn't need it but the import documents the intended surface. #[allow(dead_code)] fn _mutex_keepalive(_m: &Mutex<()>) {} /// Phase 7 C2: render a single Gotify message as a desktop toast /// via `Shell_NotifyIconW` with `NIF_INFO`. On Windows 10+ this is /// routed through Action Center and shows up exactly like a modern /// toast; on older Windows it falls back to a classic balloon. The /// WndProc owns the call so it runs on the same thread that called /// `NIM_ADD` — a hard Win32 requirement that the heap-allocated /// payload dance in `show_toast` is structured around. fn show_toast(hwnd: HWND, payload: &ToastPayload) { let mut nid = NOTIFYICONDATAW { cbSize: std::mem::size_of::() as u32, hWnd: hwnd, uID: 1, // Re-declare every flag every time — NIM_MODIFY only honors // the fields whose bits are set in `uFlags`. We set // NIF_TIP|NIF_INFO so the OS picks up the existing tip AND // the new info text. NIF_ICON is left alone: we don't ship // a custom icon yet (TODO Phase 7 C5) so reusing whatever // was registered at NIM_ADD keeps the visual consistent. uFlags: NIF_INFO | NIF_TIP, uCallbackMessage: 0, hIcon: std::ptr::null_mut(), szTip: [0u16; 128], szInfo: [0u16; 256], Anonymous: unsafe { std::mem::zeroed() }, szInfoTitle: [0u16; 64], dwState: 0, dwStateMask: 0, dwInfoFlags: payload.icon_flags, guidItem: GUID { data1: 0, data2: 0, data3: 0, data4: [0; 8] }, hBalloonIcon: std::ptr::null_mut(), }; // Truncate aggressively: szInfoTitle is 64 wide chars (incl. the // null terminator), szInfo is 256. copy_wide_trunc(&payload.title, &mut nid.szInfoTitle); copy_wide_trunc(&payload.body, &mut nid.szInfo); unsafe { let ok = Shell_NotifyIconW(NIM_MODIFY, &nid); if ok == 0 { tracing::warn!( title = %payload.title, "tray: Shell_NotifyIconW NIM_MODIFY returned 0 (toast failed)" ); } } } /// UTF-16 encode `s` into `dst`, truncating to `dst.len() - 1` /// characters and null-terminating so Win32 treats it as a C-style /// wide string. fn copy_wide_trunc(s: &str, dst: &mut [u16]) { debug_assert!(!dst.is_empty()); let max = dst.len() - 1; // leave room for the null terminator for (i, ch) in s.encode_utf16().take(max).enumerate() { dst[i] = ch; } // Fill the rest with zeros so the buffer is "null-terminated" // for Win32 readers that look at the trailing bytes (some shell // versions do, surprisingly). let written = s.encode_utf16().count().min(max); for slot in &mut dst[written..] { *slot = 0; } }