From c9ec23d0903e80af5c1838359a62557f1f7203d8 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:50:42 +0800 Subject: [PATCH 1/2] fix(notify): stop polling Notification Center from the UI thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS notifications went through mac-notification-sys with wait_for_click so a click could reveal the pane. That crate notices a click by parking the sending thread and adding, per outstanding notification, a repeating 0.5 s timer on the main run loop that calls deliveredNotifications — a synchronous XPC round trip. A banner nobody clicks stays in Notification Center, so its timer never goes away. Sampled with nine outstanding: a fifth of the UI thread inside that XPC, every window juddering, one more timer per agent turn. Drive NSUserNotificationCenter directly with a delegate of our own: the click arrives through didActivateNotification, the pane rides in the identifier, and nothing runs on the main thread until the user clicks. notify-rust's show is no longer called on macOS, since it is that crate and would replace the delegate; only set_application stays, to name a bare binary. Claude-Session: https://claude.ai/code/session_01VuYUPiDEhQX6aQ4WQZbEGn --- Cargo.lock | 1 - Cargo.toml | 16 +-- src/terminal/remote.rs | 235 +++++++++++++++++++++++++++++------------ 3 files changed, 177 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8164e37..2ba9177d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9841,7 +9841,6 @@ dependencies = [ "ksni", "libc", "log", - "mac-notification-sys", "memchr", "notify 8.2.0", "notify-rust", diff --git a/Cargo.toml b/Cargo.toml index 84a83f5b..cd26f466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,9 +114,11 @@ reqwest_client = { git = "https://github.com/zed-industries/zed", rev = "1d217ee # `capture` output through the same grid. alacritty_terminal.workspace = true -# Desktop notifications driven by OSC 9 / OSC 777 escape sequences. Cross-platform; -# the macOS backend uses the deprecated NSUserNotification (weak — a completion -# toast is fine, revisit mac-notification-sys if it proves unusable). +# Desktop notifications driven by OSC 9 / OSC 777 escape sequences, on Linux and +# Windows. macOS never calls its `show`: that backend (`mac-notification-sys`) +# installs its own delegate on NSUserNotificationCenter and would take the +# click-to-reveal delegate in `terminal::remote::macos_notify` with it. On macOS +# only its `set_application` is used, to name a bare binary to the center. notify-rust = "4" # Filesystem watcher for live config reload: watches `config.json` and reloads @@ -213,11 +215,9 @@ objc2 = "0.6" objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSResponder", "NSAppearance", "NSGraphics", "NSImage"] } # NSData feeds the runtime Dock icon for bare (non-bundled) binaries — see # `set_dock_icon_for_bare_binary` in main.rs. -objc2-foundation = { version = "0.3", features = ["NSData"] } -# Clickable desktop notifications on macOS. `notify-rust` already pulls this -# crate transitively for basic display; we depend on it directly to use -# wait_for_click / NotificationResponse::Click. -mac-notification-sys = "0.6" +# NSUserNotification is the click-to-reveal notification path — see +# `terminal::remote::macos_notify` for why it is driven directly. +objc2-foundation = { version = "0.3", features = ["NSData", "NSString", "NSUserNotification"] } # Dictionary-based Chinese word segmentation for double-click selection # (`terminal::smart_select`), as a *fallback* where the OS has no tokenizer of diff --git a/src/terminal/remote.rs b/src/terminal/remote.rs index b2f67038..a9c28ca5 100644 --- a/src/terminal/remote.rs +++ b/src/terminal/remote.rs @@ -2389,35 +2389,43 @@ pub(crate) fn notify_desktop(title: Option<&str>, body: &str) { /// `u64` here is what lets a caller hand over a `pane_id` — a different number, /// assigned by the daemon — and get a notification that reveals nothing. /// -/// Every other case (no pane, unsupported platform, no room left to wait for a -/// click) falls back to the plain `notify-rust` path below, which is why +/// macOS always goes through `macos_notify`, clickable or not. Elsewhere, every +/// other case (no pane, unsupported platform, no room left to wait for a click) +/// falls back to the plain `notify-rust` path below, which is why /// `try_clickable_notification` reports whether it took the job. pub(crate) fn notify_desktop_for_pane(title: Option<&str>, body: &str, pane: Option) { let summary = sanitize_notification_text(title.unwrap_or("tty7"), NOTIFY_TITLE_MAX); let body = sanitize_notification_text(body, NOTIFY_BODY_MAX); - if let Some(pane) = pane - && try_clickable_notification(&summary, &body, pane.as_u64()) + #[cfg(all(target_os = "macos", not(test)))] { - return; + macos_notify::deliver(summary, body, pane.map(|p| p.as_u64())); } - - std::thread::spawn(move || { - #[cfg(target_os = "macos")] - ensure_notification_app(); - let mut notif = notify_rust::Notification::new(); - notif.summary(&summary).body(&body); - // Without our own AUMID, the Windows backend falls back to - // PowerShell's — icon and name included. Only set ours once the shell - // has indexed a shortcut carrying it: for an AUMID it does not know, - // `show()` reports success and drops the toast, so the ugly fallback - // beats the branded one every time we are not sure. - #[cfg(target_os = "windows")] - if let Some(app_id) = crate::core::aumid::toast_app_id() { - notif.app_id(app_id); + #[cfg(not(all(target_os = "macos", not(test))))] + { + if let Some(pane) = pane + && try_clickable_notification(&summary, &body, pane.as_u64()) + { + return; } - let _ = notif.show(); - }); + + std::thread::spawn(move || { + #[cfg(target_os = "macos")] + ensure_notification_app(); + let mut notif = notify_rust::Notification::new(); + notif.summary(&summary).body(&body); + // Without our own AUMID, the Windows backend falls back to + // PowerShell's — icon and name included. Only set ours once the + // shell has indexed a shortcut carrying it: for an AUMID it does + // not know, `show()` reports success and drops the toast, so the + // ugly fallback beats the branded one every time we are not sure. + #[cfg(target_os = "windows")] + if let Some(app_id) = crate::core::aumid::toast_app_id() { + notif.app_id(app_id); + } + let _ = notif.show(); + }); + } } /// Longest title / body we hand to a notification backend. @@ -2448,58 +2456,128 @@ fn sanitize_notification_text(s: &str, max_chars: usize) -> String { out } -/// Deliver a click-to-reveal notification, reporting whether it was taken. -/// `false` means the caller should fall back to the plain notification path. -#[cfg(all(target_os = "macos", not(test)))] -fn try_clickable_notification(title: &str, body: &str, leaf_id: u64) -> bool { - use std::sync::atomic::{AtomicUsize, Ordering}; +/// What a click on a macOS notification has to carry back: the pane to reveal. +/// +/// It rides in the notification's `identifier`, which is the one field the +/// center hands back verbatim on activation without a dictionary round trip. +/// The sequence number keeps two notifications for the same pane apart — the +/// center treats a repeated identifier as "replace the earlier one". +#[cfg(any(target_os = "macos", test))] +const NOTIFICATION_ID_PREFIX: &str = "tty7-pane-"; - // `mac-notification-sys` blocks the calling thread until the user acts on - // the notification, and its wait has no timeout: a banner nobody touches — - // the common case, since unclicked ones just pile up in Notification - // Center — parks its thread for the rest of the session. Cap how many can - // be outstanding and let the rest through as fire-and-forget, so a chatty - // agent cannot turn a session's notifications into a thread leak. - const MAX_PENDING_CLICKS: usize = 8; - static PENDING: AtomicUsize = AtomicUsize::new(0); - - if PENDING - .fetch_update(Ordering::AcqRel, Ordering::Acquire, |n| { - (n < MAX_PENDING_CLICKS).then_some(n + 1) - }) - .is_err() - { - return false; - } - - let (title, body) = (title.to_string(), body.to_string()); - std::thread::spawn(move || { - show_macos_toast(&title, &body, leaf_id); - PENDING.fetch_sub(1, Ordering::AcqRel); - }); - true +#[cfg(any(target_os = "macos", test))] +fn notification_identifier(leaf_id: u64) -> String { + use std::sync::atomic::AtomicU64; + static SEQ: AtomicU64 = AtomicU64::new(0); + let seq = SEQ.fetch_add(1, Ordering::Relaxed); + format!("{NOTIFICATION_ID_PREFIX}{leaf_id}-{seq}") } +#[cfg(any(target_os = "macos", test))] +fn leaf_id_in_identifier(identifier: &str) -> Option { + let rest = identifier.strip_prefix(NOTIFICATION_ID_PREFIX)?; + let (leaf_id, _seq) = rest.split_once('-')?; + leaf_id.parse().ok() +} + +/// macOS notifications, straight to `NSUserNotificationCenter`. +/// +/// This used to go through `mac-notification-sys` with `wait_for_click`, so a +/// click could reveal the pane. The way that crate notices a click is to park +/// the sending thread and, for every notification outstanding, add a repeating +/// 0.5 s timer to the *main* run loop that calls `deliveredNotifications` — a +/// synchronous XPC round trip — to see whether the banner is still there. A +/// banner nobody clicks stays in Notification Center, so its timer never goes +/// away. Sampled with nine outstanding: a fifth of the UI thread inside that +/// XPC, every window juddering, and each agent turn adding one more timer. +/// +/// Here the click arrives through the center's delegate, which is what the +/// API is for: no parked thread, no timer, nothing on the main thread until +/// the user actually clicks. The pane rides in the notification's identifier. +/// +/// Nothing else may touch the center on this platform. `mac-notification-sys` +/// installs a delegate of its own the first time it sends, the last +/// `setDelegate:` wins, and `notify-rust` is that crate on macOS — so its +/// `show` is never called here, only its `set_application`, which does not +/// install one (that is what names a bare `cargo run` binary to the center). #[cfg(all(target_os = "macos", not(test)))] -fn show_macos_toast(title: &str, body: &str, leaf_id: u64) { - use mac_notification_sys::{Notification, NotificationResponse}; +#[allow( + deprecated, + reason = "UNUserNotificationCenter needs a signed, entitled bundle; NSUserNotification is what a bare binary can use" +)] +mod macos_notify { + use objc2::rc::{Retained, autoreleasepool}; + use objc2::runtime::ProtocolObject; + use objc2::{AnyThread, define_class, msg_send}; + use objc2_foundation::{ + NSObject, NSObjectProtocol, NSString, NSUserNotification, NSUserNotificationCenter, + NSUserNotificationCenterDelegate, + }; - // `send` sets the delivering application on first use and keeps it under a - // `Once`, so whoever notifies first decides the name and icon for the whole - // session. Without this the default wins — `com.apple.Finder` — and every - // later notification, this path or `notify-rust`'s, claims to be Finder. - ensure_notification_app(); + define_class!( + // SAFETY: NSObject has no subclassing requirements; `Delegate` has no + // ivars and no `Drop`. + #[unsafe(super = NSObject)] + #[name = "Tty7NotificationDelegate"] + struct Delegate; - let response = Notification::new() - .title(title) - .message(body) - .wait_for_click(true) - .send(); + // SAFETY: `NSObjectProtocol` has no safety requirements. + unsafe impl NSObjectProtocol for Delegate {} - match response { - Ok(NotificationResponse::Click) => reveal_pane(leaf_id), - Ok(_) => {} - Err(e) => log::warn!("failed to show macOS notification: {e}"), + // SAFETY: `NSUserNotificationCenterDelegate` has no safety requirements. + unsafe impl NSUserNotificationCenterDelegate for Delegate { + /// Runs on the main thread, when the user clicks the banner or the + /// entry in Notification Center. Only a channel push happens here. + #[unsafe(method(userNotificationCenter:didActivateNotification:))] + fn did_activate( + &self, + center: &NSUserNotificationCenter, + notification: &NSUserNotification, + ) { + if let Some(leaf_id) = notification + .identifier() + .and_then(|id| super::leaf_id_in_identifier(&id.to_string())) + { + super::reveal_pane(leaf_id); + } + center.removeDeliveredNotification(notification); + } + } + ); + + fn install_delegate() { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| { + super::ensure_notification_app(); + // SAFETY: `NSObject`'s `init` takes nothing and returns the object. + let delegate: Retained = unsafe { msg_send![Delegate::alloc(), init] }; + let center = NSUserNotificationCenter::defaultUserNotificationCenter(); + // SAFETY: `delegate` is a `Delegate`, which conforms to the protocol. + unsafe { center.setDelegate(Some(ProtocolObject::from_ref(&*delegate))) }; + // The center holds its delegate unretained. Ours lives as long as + // the process, so it is simply never released. + std::mem::forget(delegate); + }); + } + + /// Hands the notification to the center and returns. `deliverNotification:` + /// is an XPC message, so it goes out on a short-lived thread rather than + /// from wherever the OSC sequence was parsed — never the UI thread. + pub(super) fn deliver(title: String, body: String, leaf_id: Option) { + std::thread::spawn(move || { + autoreleasepool(|_| { + install_delegate(); + let notification = NSUserNotification::new(); + notification.setTitle(Some(&NSString::from_str(&title))); + notification.setInformativeText(Some(&NSString::from_str(&body))); + if let Some(leaf_id) = leaf_id { + let id = super::notification_identifier(leaf_id); + notification.setIdentifier(Some(&NSString::from_str(&id))); + } + NSUserNotificationCenter::defaultUserNotificationCenter() + .deliverNotification(¬ification); + }); + }); } } @@ -2679,6 +2757,31 @@ fn xml_escape(s: &str) -> String { mod notification_tests { use super::*; + #[test] + fn a_notification_identifier_carries_its_pane_back_out() { + let id = notification_identifier(42); + assert_eq!(leaf_id_in_identifier(&id), Some(42)); + } + + #[test] + fn two_notifications_for_one_pane_do_not_replace_each_other() { + // The center replaces a delivered notification whose identifier repeats. + assert_ne!(notification_identifier(7), notification_identifier(7)); + } + + #[test] + fn a_foreign_identifier_reveals_nothing() { + for id in [ + "", + "tty7-pane-", + "tty7-pane-x-1", + "tty7-pane-42", + "other-42-1", + ] { + assert_eq!(leaf_id_in_identifier(id), None, "{id:?}"); + } + } + #[test] fn sanitizing_drops_control_bytes_but_keeps_line_breaks() { let raw = "build \x1b[31mfailed\x07\nsee log\ttail"; From 990873f7628601945684611dfdda657e34f10f58 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:08:09 +0800 Subject: [PATCH 2/2] fix(notify): scope notification identifiers to the process Include the pid in the `tty7-pane---` identifier so a banner left over from a previous run (or a concurrent instance) is ignored instead of revealing an unrelated pane. Also drop the dead `com.apple.Terminal` fallback in the delegate installer (mac-notification-sys completes its Once even on failure, so the second `set_application` never ran), collapse the macOS cfg arms so test builds compile the production path, and correct stale comments. Claude-Session: https://claude.ai/code/session_01VuYUPiDEhQX6aQ4WQZbEGn --- Cargo.toml | 6 +-- src/terminal/remote.rs | 90 ++++++++++++++++++++++++++---------------- src/ui/tray/mod.rs | 5 ++- 3 files changed, 61 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cd26f466..d00c8db4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,10 +115,8 @@ reqwest_client = { git = "https://github.com/zed-industries/zed", rev = "1d217ee alacritty_terminal.workspace = true # Desktop notifications driven by OSC 9 / OSC 777 escape sequences, on Linux and -# Windows. macOS never calls its `show`: that backend (`mac-notification-sys`) -# installs its own delegate on NSUserNotificationCenter and would take the -# click-to-reveal delegate in `terminal::remote::macos_notify` with it. On macOS -# only its `set_application` is used, to name a bare binary to the center. +# Windows. On macOS only its `set_application` is used — never `show`; see +# `terminal::remote::macos_notify` for why. notify-rust = "4" # Filesystem watcher for live config reload: watches `config.json` and reloads diff --git a/src/terminal/remote.rs b/src/terminal/remote.rs index a9c28ca5..e546fec5 100644 --- a/src/terminal/remote.rs +++ b/src/terminal/remote.rs @@ -2390,18 +2390,18 @@ pub(crate) fn notify_desktop(title: Option<&str>, body: &str) { /// assigned by the daemon — and get a notification that reveals nothing. /// /// macOS always goes through `macos_notify`, clickable or not. Elsewhere, every -/// other case (no pane, unsupported platform, no room left to wait for a click) -/// falls back to the plain `notify-rust` path below, which is why +/// other case (no pane, unsupported platform, Windows toast queue full) falls +/// back to the plain `notify-rust` path below, which is why /// `try_clickable_notification` reports whether it took the job. pub(crate) fn notify_desktop_for_pane(title: Option<&str>, body: &str, pane: Option) { let summary = sanitize_notification_text(title.unwrap_or("tty7"), NOTIFY_TITLE_MAX); let body = sanitize_notification_text(body, NOTIFY_BODY_MAX); - #[cfg(all(target_os = "macos", not(test)))] + #[cfg(target_os = "macos")] { macos_notify::deliver(summary, body, pane.map(|p| p.as_u64())); } - #[cfg(not(all(target_os = "macos", not(test))))] + #[cfg(not(target_os = "macos"))] { if let Some(pane) = pane && try_clickable_notification(&summary, &body, pane.as_u64()) @@ -2410,8 +2410,6 @@ pub(crate) fn notify_desktop_for_pane(title: Option<&str>, body: &str, pane: Opt } std::thread::spawn(move || { - #[cfg(target_os = "macos")] - ensure_notification_app(); let mut notif = notify_rust::Notification::new(); notif.summary(&summary).body(&body); // Without our own AUMID, the Windows backend falls back to @@ -2460,8 +2458,16 @@ fn sanitize_notification_text(s: &str, max_chars: usize) -> String { /// /// It rides in the notification's `identifier`, which is the one field the /// center hands back verbatim on activation without a dictionary round trip. -/// The sequence number keeps two notifications for the same pane apart — the -/// center treats a repeated identifier as "replace the earlier one". +/// Shape: `tty7-pane---`. +/// +/// The pid is what makes a stale identifier fail closed. Notifications outlive +/// the process that sent them, and the center hands a click on one of those to +/// whatever process now owns the bundle id — a relaunched tty7, or a second +/// instance running alongside. A leaf id is a gpui entity id, which a fresh +/// process hands out again in the same order, so without the pid that click +/// would reveal an unrelated pane. The sequence number keeps two notifications +/// for the same pane apart — the center treats a repeated identifier as +/// "replace the earlier one". #[cfg(any(target_os = "macos", test))] const NOTIFICATION_ID_PREFIX: &str = "tty7-pane-"; @@ -2470,12 +2476,17 @@ fn notification_identifier(leaf_id: u64) -> String { use std::sync::atomic::AtomicU64; static SEQ: AtomicU64 = AtomicU64::new(0); let seq = SEQ.fetch_add(1, Ordering::Relaxed); - format!("{NOTIFICATION_ID_PREFIX}{leaf_id}-{seq}") + let pid = std::process::id(); + format!("{NOTIFICATION_ID_PREFIX}{pid}-{leaf_id}-{seq}") } #[cfg(any(target_os = "macos", test))] fn leaf_id_in_identifier(identifier: &str) -> Option { let rest = identifier.strip_prefix(NOTIFICATION_ID_PREFIX)?; + let (pid, rest) = rest.split_once('-')?; + if pid.parse::().ok()? != std::process::id() { + return None; + } let (leaf_id, _seq) = rest.split_once('-')?; leaf_id.parse().ok() } @@ -2500,7 +2511,7 @@ fn leaf_id_in_identifier(identifier: &str) -> Option { /// `setDelegate:` wins, and `notify-rust` is that crate on macOS — so its /// `show` is never called here, only its `set_application`, which does not /// install one (that is what names a bare `cargo run` binary to the center). -#[cfg(all(target_os = "macos", not(test)))] +#[cfg(target_os = "macos")] #[allow( deprecated, reason = "UNUserNotificationCenter needs a signed, entitled bundle; NSUserNotification is what a bare binary can use" @@ -2527,7 +2538,9 @@ mod macos_notify { // SAFETY: `NSUserNotificationCenterDelegate` has no safety requirements. unsafe impl NSUserNotificationCenterDelegate for Delegate { /// Runs on the main thread, when the user clicks the banner or the - /// entry in Notification Center. Only a channel push happens here. + /// entry in Notification Center. A channel push, then the clicked + /// entry is dropped from the center — a one-way message, unlike the + /// `deliveredNotifications` round trip this module exists to avoid. #[unsafe(method(userNotificationCenter:didActivateNotification:))] fn did_activate( &self, @@ -2548,7 +2561,16 @@ mod macos_notify { fn install_delegate() { static ONCE: std::sync::Once = std::sync::Once::new(); ONCE.call_once(|| { - super::ensure_notification_app(); + // Name the delivering application to the center before it is first + // touched: the center drops requests from a process with no bundle + // identity, which is what a bare `cargo run` binary is. This + // swizzles `-[NSBundle bundleIdentifier]` for the main bundle to + // `com.github.tty7` when LaunchServices knows that id (a bundled + // tty7.app, or a machine that has one installed); when it does not, + // the swizzle's own default, `com.apple.Terminal`, is what the + // center sees. It can only be called once per process, so there is + // no second chance to pass a different name. + let _ = notify_rust::set_application("com.github.tty7"); // SAFETY: `NSObject`'s `init` takes nothing and returns the object. let delegate: Retained = unsafe { msg_send![Delegate::alloc(), init] }; let center = NSUserNotificationCenter::defaultUserNotificationCenter(); @@ -2727,17 +2749,14 @@ fn show_windows_toast( /// Ask the tray dispatch loop to bring `leaf_id` to the front. Runs on whatever /// thread the platform hands the activation to, so it only touches the channel. -#[cfg(all(not(test), any(target_os = "macos", target_os = "windows")))] +#[cfg(any(target_os = "macos", all(target_os = "windows", not(test))))] fn reveal_pane(leaf_id: u64) { if let Some(tx) = crate::ui::tray::sender() { let _ = tx.try_send(crate::ui::tray::TrayAction::RevealPane { leaf_id }); } } -#[cfg(not(any( - all(target_os = "macos", not(test)), - all(target_os = "windows", not(test)) -)))] +#[cfg(not(any(target_os = "macos", all(target_os = "windows", not(test)))))] fn try_clickable_notification(_title: &str, _body: &str, _leaf_id: u64) -> bool { // Linux notifications go through notify-rust; click-to-reveal would need a // D-Bus action listener of its own. @@ -2771,17 +2790,31 @@ mod notification_tests { #[test] fn a_foreign_identifier_reveals_nothing() { + let pid = std::process::id(); for id in [ - "", - "tty7-pane-", - "tty7-pane-x-1", - "tty7-pane-42", - "other-42-1", + String::new(), + "tty7-pane-".into(), + "tty7-pane-x-1".into(), + "tty7-pane-42".into(), + "other-42-1".into(), + format!("tty7-pane-{pid}"), + format!("tty7-pane-{pid}-42"), + format!("tty7-pane-{pid}-x-1"), ] { - assert_eq!(leaf_id_in_identifier(id), None, "{id:?}"); + assert_eq!(leaf_id_in_identifier(&id), None, "{id:?}"); } } + #[test] + fn a_notification_from_another_process_reveals_nothing() { + // Notifications outlive the process that sent them, and gpui hands out + // the same entity ids again in a fresh process. A stale click must not + // land on whatever pane holds that id now. + let other_pid = std::process::id().wrapping_add(1); + let stale = format!("{NOTIFICATION_ID_PREFIX}{other_pid}-42-0"); + assert_eq!(leaf_id_in_identifier(&stale), None); + } + #[test] fn sanitizing_drops_control_bytes_but_keeps_line_breaks() { let raw = "build \x1b[31mfailed\x07\nsee log\ttail"; @@ -2808,17 +2841,6 @@ mod notification_tests { } } -#[cfg(target_os = "macos")] -fn ensure_notification_app() { - use std::sync::Once; - static ONCE: Once = Once::new(); - ONCE.call_once(|| { - if notify_rust::set_application("com.github.tty7").is_err() { - let _ = notify_rust::set_application("com.apple.Terminal"); - } - }); -} - struct OscNotifyScanner { tok: OscTokenizer, } diff --git a/src/ui/tray/mod.rs b/src/ui/tray/mod.rs index 683785d8..b6e896d0 100644 --- a/src/ui/tray/mod.rs +++ b/src/ui/tray/mod.rs @@ -43,8 +43,9 @@ pub(crate) fn icon_is_up() -> bool { } /// A sender for the current tray dispatch loop, if one is running. -// Only the platform notification callbacks call this, and those are compiled -// out of test builds so unit tests never raise a real toast. +// Only the platform notification callbacks call this. The Windows one is +// compiled out of test builds so unit tests never raise a real toast; the macOS +// one is not, but no test sends a notification there either. #[cfg_attr(test, allow(dead_code))] pub(crate) fn sender() -> Option> { SENDER.lock().ok()?.clone()