Skip to content

feat(macos): add wait_for_action and on_close - #1

Closed
yasumorishima wants to merge 6 commits into
mainfrom
feat/macos-wait-for-action
Closed

yasumorishima wants to merge 6 commits into
mainfrom
feat/macos-wait-for-action

Conversation

@yasumorishima

@yasumorishima yasumorishima commented May 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

Wires the synchronous response flow exposed by mac-notification-sys into notify-rust's NotificationHandle::wait_for_action and NotificationHandle::on_close so that listening for action clicks works on macOS the same way it does on XDG. Refs hoodie#186.

Why this is small in scope

mac-notification-sys 0.6.12 already exposes:

  • NotificationResponse (Click / ActionButton / CloseButton / Reply / None)
  • wait_for_click(true) synchronous flow
  • MainButton::SingleAction / DropdownActions / Response

notify-rust was not wiring any of this up: src/macos.rs::show_notification only forwarded title/body/subtitle/sound/image and discarded the NotificationResponse. This PR is mostly plumbing.

Changes

  • New src/action.rs module — hoists ActionResponse, CloseReason, CloseHandler and ActionResponseHandler out of src/xdg/mod.rs so they can be re-exported at the crate root and shared between platforms.
  • src/macos.rs — adds NotificationHandle::wait_for_action and NotificationHandle::on_close. Maps mac_notification_sys::NotificationResponse::ActionButton(label) back to the identifier originally passed to Notification::action(identifier, label).
  • src/macos.rs::show_notification — interactive notifications (those with any action() configured) are now deferred until wait_for_action / on_close is invoked, since mac_notification_sys::Notification::send is blocking and only returns once the user interacts. Fire-and-forget notifications keep the existing behaviour.
  • src/xdg/{mod,dbus_rs,zbus_rs}.rs — switch from super::{ActionResponse, ...} to crate::{ActionResponse, ...} to use the hoisted definitions.
  • src/lib.rs — re-exports the shared types at the crate root, updates the platform-support table to mark macOS for wait_for_action, on_close, and action, and documents the macOS-specific blocking semantics.
  • examples/mac_actions.rs — macOS counterpart of examples/actions.rs, with both single-action and dropdown variants.

Why defer interactive sends?

mac_notification_sys::Notification::send is synchronous — it delivers the notification and blocks until the user dismisses it or activates an action. The NotificationResponse is only available after that blocking call returns. The XDG flow expects show() to return a handle quickly and the response to be observed later via wait_for_action.

Two ways to reconcile this on macOS:

  1. Send eagerly in show() and store the response on the handle. This makes show() itself blocking on macOS, which silently breaks any caller that expected show() to return promptly.
  2. Defer the send until wait_for_action / on_close. Interactive notifications still go through a single send call, but at the point where the caller is already prepared to block. Fire-and-forget notifications (no action()) keep the eager-send behaviour.

This PR takes option (2). Happy to switch to (1) or to a different design (e.g. an explicit wait() builder method) — the wiring itself is the same either way, and this is the question I'd most like maintainer input on.

What I am NOT changing

  • Windows still has no listener support — that needs a separate decision (the open issue mentions tauri-winrt-notification vs the new win32_notif). Out of scope here.
  • The existing wait_for_action(|action: &str|) signature is preserved as-is, including the "__closed" magic string. Milestone 5 (Feature/milestone 5 hoodie/notify-rust#266) plans to migrate this to FnOnce(ActionResponse); this PR doesn't move the API yet so it stays compatible with current code on both XDG and macOS.

Limitations / things to verify

  • I do not have a Mac available, so cargo test --no-run and clippy on macos-latest are the actual confidence signal here. The XDG side has been verified locally with cargo check --no-default-features --features z and cargo clippy --no-default-features --features z -- -D warnings (clean).
  • macOS does not differentiate close reasons in the underlying NotificationResponse, so on_close always reports CloseReason::Dismissed. Documented at the call site.
  • A Click (body click) on a notification with at least one configured action is currently mapped to the first action's identifier. Open to changing this to "__default" or to always emitting "__closed" if you'd prefer.

Test plan

  • cargo check --features z (Linux) — clean
  • cargo clippy --features z -- -D warnings (Linux) — clean
  • cargo check (macOS, via CI) — pending
  • cargo test --no-run (macOS, via CI) — pending
  • cargo clippy -- -D warnings (macOS, via CI) — pending
  • cargo run --example mac_actions on a real Mac — out of my reach, would appreciate a sanity check

Draft for now — opening so CodeRabbit / CodeQL can have a look and so the macOS CI matrix runs.

Summary by CodeRabbit

  • New Features

    • macOS support for interactive notification actions with blocking wait-for-action and on-close handling.
    • Cross-platform action/close response APIs to handle action clicks and close reasons.
  • Documentation

    • Updated docs with macOS-specific behavior for actions and delivery.
    • Added a macOS example demonstrating single and multiple action buttons and waiting for actions.

Wire up the synchronous response flow exposed by mac-notification-sys
so that NotificationHandle::wait_for_action and
NotificationHandle::on_close behave on macOS the same way they do on
XDG. Notifications carrying any action() are now delivered when the
user awaits a response, mirroring the blocking semantics of the
underlying crate; fire-and-forget notifications keep the existing
behaviour.

Move the shared types (ActionResponse, CloseReason, CloseHandler,
ActionResponseHandler) from src/xdg/mod.rs into a new
src/action.rs so they can be re-exported at the crate root and shared
between platforms.

Adds examples/mac_actions.rs and updates the platform-support table.

Refs: hoodie#186
@coderabbitai

coderabbitai Bot commented May 1, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a new cross-platform action module and types, re-exports them at crate root, updates macOS backend to defer delivery for notifications with actions and provide blocking wait_for_action/on_close, adjusts XDG imports/docs, and adds a macOS example demonstrating action handling.

Changes

Notification Action Handler Support

Layer / File(s) Summary
Data Shape / Types
src/action.rs
Adds CloseReason, ActionResponse<'a>, ActionResponseHandler, CloseHandler<T>, From impls, and blanket impls for closures.
Crate Export & Docs
src/lib.rs
Adds mod action;, re-exports ActionResponse, ActionResponseHandler, CloseHandler, CloseReason; updates platform-support tables and adds macOS specifics docs.
macOS Core Implementation
src/macos.rs
NotificationHandle adds pending: bool; show_notification defers sending for action-bearing notifications; adds wait_for_action and on_close which call show_blocking() to perform blocking delivery and map NotificationResponse to action identifiers or CloseReason; adds identifier helper methods.
XDG Import & Module Notes
src/xdg/mod.rs, src/xdg/dbus_rs.rs, src/xdg/zbus_rs.rs
Moves action-related imports to crate root, updates docs to state action types live in crate::action, and reorganizes top-level use statements (no logic changes).
Example
examples/mac_actions.rs
Adds macOS example that sets bundle id, shows one-action and multi-action notifications and uses wait_for_action; adds non-macOS stub directing to XDG example.

Sequence Diagram

sequenceDiagram
    actor User
    participant App as Application Code
    participant Handle as NotificationHandle (pending)
    participant MacSys as mac_notification_sys
    participant OS as macOS Notification Center

    App->>Handle: create notification with actions & call show()
    Handle->>App: return pending handle
    App->>Handle: call wait_for_action(callback)
    Handle->>MacSys: show_blocking(wait_for_click=true)
    MacSys->>OS: display notification
    User->>OS: click action or close
    OS->>MacSys: deliver NotificationResponse
    MacSys-->>Handle: return response
    Handle->>App: invoke callback with ActionResponse::Custom or ActionResponse::Closed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I tucked my actions in a shell,

Pending softly, waiting well.
A click, a close, a tiny bell,
I hop, I cheer — the handlers tell.
Carrot clap for async spell.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(macos): add wait_for_action and on_close' is concise and clearly summarizes the main change—adding macOS support for action and close handling on NotificationHandle.
Description check ✅ Passed The PR description is comprehensive, including a clear summary, rationale, detailed change list, design decisions with trade-off justification, known limitations, and a test plan, exceeding the template requirements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/macos-wait-for-action

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@yasumorishima
yasumorishima marked this pull request as ready for review May 1, 2026 22:15
@yasumorishima

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yasumorishima

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/macos.rs`:
- Around line 79-82: The current match arm for
Ok(NotificationResponse::ActionButton(label)) uses
identifier_for_label(&label).map_or_else(String::new, str::to_owned) which
returns an empty string on lookup miss; change it to fall back to the clicked
label instead. Update the map_or_else call in src/macos.rs (the
NotificationResponse::ActionButton arm and the identifier_for_label call) so
that the closure returns the original label (converted/owned) when
identifier_for_label returns None, rather than String::new().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 55200545-11db-4bcc-897e-4817062d4838

📥 Commits

Reviewing files that changed from the base of the PR and between 490ea8f and 3a34f2f.

📒 Files selected for processing (7)
  • examples/mac_actions.rs
  • src/action.rs
  • src/lib.rs
  • src/macos.rs
  • src/xdg/dbus_rs.rs
  • src/xdg/mod.rs
  • src/xdg/zbus_rs.rs

Comment thread src/macos.rs Outdated
Per CodeRabbit suggestion: returning an empty string on a missed lookup hides what the user actually clicked. Falling back to the label itself lets callers handle the unknown case via the closures default arm.
@yasumorishima

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

yasumorishima and others added 2 commits May 3, 2026 00:00
Replace the dead-code dummy function with #[allow(unused_imports)]
on the use statement. ActionResponse appears only in a rustdoc link
in macos.rs, so the import is needed for link resolution but never
referenced from code.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/macos.rs`:
- Around line 181-198: The else branch in show_notification currently defers
delivery for notifications with actions (returning NotificationHandle::pending),
making .show() a no-op; instead build the mac_notification_sys::Notification
with actions (use its .action(...) API when iterating notification.actions),
call .send() so the notification is displayed immediately, and then return
NotificationHandle::new(notification.clone()) so existing fire-and-forget call
sites still see the notification; remove or repurpose
NotificationHandle::pending only for cases where you truly want deferred
delivery and keep NotificationHandle::new as the default for sent notifications.
- Around line 110-114: The on_close implementation currently synthesizes
CloseReason::Dismissed for non-pending handles; instead, only invoke the
CloseHandler when the macOS notification actually closes (i.e., after observing
the OS close event or after show_blocking returns a real close result). Update
on_close (and the show_blocking usage) so that: if self.pending is true, call
show_blocking and only call handler.call(...) with the observed close reason
returned by show_blocking/OS callback; if self.pending is false, do NOT
synthesize CloseReason::Dismissed—either register the handler to be invoked by
the real OS close callback or return/reject the unsupported case (do not call
handler.call early). Ensure you reference on_close, show_blocking, CloseHandler,
and CloseReason::Dismissed when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 949b2c36-2956-4888-acdb-a9bae181dd16

📥 Commits

Reviewing files that changed from the base of the PR and between 069497e and baff341.

📒 Files selected for processing (2)
  • examples/mac_actions.rs
  • src/macos.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/mac_actions.rs

Comment thread src/macos.rs
Comment on lines +110 to +114
pub fn on_close<A>(self, handler: impl CloseHandler<A>) {
if self.pending {
let _ = self.show_blocking();
}
handler.call(CloseReason::Dismissed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

on_close() fires before any real close event for non-pending handles.

When pending is false, this reports CloseReason::Dismissed immediately even though a normal show() notification may still be on screen, and a scheduled notification has not closed yet at all. That breaks the CloseHandler contract and can trigger cleanup/state transitions too early. Please avoid calling the handler unless macOS has actually observed the notification closing, or explicitly reject unsupported cases instead of synthesizing a close event.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/macos.rs` around lines 110 - 114, The on_close implementation currently
synthesizes CloseReason::Dismissed for non-pending handles; instead, only invoke
the CloseHandler when the macOS notification actually closes (i.e., after
observing the OS close event or after show_blocking returns a real close
result). Update on_close (and the show_blocking usage) so that: if self.pending
is true, call show_blocking and only call handler.call(...) with the observed
close reason returned by show_blocking/OS callback; if self.pending is false, do
NOT synthesize CloseReason::Dismissed—either register the handler to be invoked
by the real OS close callback or return/reject the unsupported case (do not call
handler.call early). Ensure you reference on_close, show_blocking, CloseHandler,
and CloseReason::Dismissed when making the change.

Comment thread src/macos.rs
Comment on lines 181 to +198
pub(crate) fn show_notification(notification: &Notification) -> Result<NotificationHandle> {
let mut n = mac_notification_sys::Notification::default();
n.title(notification.summary.as_str())
.message(&notification.body)
.maybe_subtitle(notification.subtitle.as_deref())
.maybe_sound(notification.sound_name.as_deref());
if notification.actions.is_empty() {
let mut n = mac_notification_sys::Notification::default();
n.title(notification.summary.as_str())
.message(&notification.body)
.maybe_subtitle(notification.subtitle.as_deref())
.maybe_sound(notification.sound_name.as_deref());

if let Some(ref image_path) = notification.path_to_image {
n.content_image(image_path);
}
if let Some(ref image_path) = notification.path_to_image {
n.content_image(image_path);
}

n.send()?;
n.send()?;

Ok(NotificationHandle::new(notification.clone()))
Ok(NotificationHandle::new(notification.clone()))
} else {
Ok(NotificationHandle::pending(notification.clone()))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't turn show() into a no-op for notifications with actions.

This branch defers delivery entirely, so .action(...).show() no longer displays anything on macOS unless the caller later invokes wait_for_action() or on_close(). That is a behavioral regression for existing fire-and-forget call sites that ignore the returned handle, and it breaks the core expectation that show() actually shows the notification.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/macos.rs` around lines 181 - 198, The else branch in show_notification
currently defers delivery for notifications with actions (returning
NotificationHandle::pending), making .show() a no-op; instead build the
mac_notification_sys::Notification with actions (use its .action(...) API when
iterating notification.actions), call .send() so the notification is displayed
immediately, and then return NotificationHandle::new(notification.clone()) so
existing fire-and-forget call sites still see the notification; remove or
repurpose NotificationHandle::pending only for cases where you truly want
deferred delivery and keep NotificationHandle::new as the default for sent
notifications.

@yasumorishima

Copy link
Copy Markdown
Owner Author

Closing this preflight PR. It exists only to run CI and CodeRabbit on the fork before opening the upstream PR, and that job is done. The branch is kept; closing these keeps "disappeared from my open PR list" a reliable signal that an upstream PR was merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants