Skip to content

Conversation

@r0chd
Copy link
Contributor

@r0chd r0chd commented Aug 16, 2025

Implemented system notifications with dbus notification spec, notification is sent right before
password prompt, also implemented --ask flag with notification actions

its currently implemented only for nixos, looking for some input before implementing more for interfaces

image image
  • I have updated the changelog as per my changes
  • I have tested, and self-reviewed my code
  • Style and consistency
    • I ran nix fmt to format my Nix code
    • I ran cargo fmt to format my Rust code
    • I have added appropriate documentation to new code
    • My changes are consistent with the rest of the codebase
  • Correctness
    • I ran cargo clippy and fixed any new linter warnings.
  • If new changes are particularly complex:
    • My code includes comments in particularly complex areas to explain the
      logic
    • I have documented the motive for those changes in the PR body or commit
      description.
  • Tested on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin

Add a 👍 reaction to pull requests you find important.

Summary by CodeRabbit

  • New Features

    • --ask now supports three modes (prompt, notify, both); supplying the flag without a value defaults to prompt.
    • Commands can send desktop notifications for confirmations and can drive confirmation flows via the notification channel.
    • Informational and warning messages are also emitted as desktop notifications alongside terminal output.
  • Chores

    • Added a desktop notification backend and wiring to logging/confirmation flows.

@r0chd r0chd changed the title Implement system notifications System notifications Aug 16, 2025
@NotAShelf
Copy link
Member

I'd rather if this was done with the notify crate and not zbus. Zbus has a history of holding critical fixes for months, and I'd rather avoid Linux-only features since it turns out as future maintenance work one way or another.

Also on a more personal note, this seems to be a bit too much. NixOS icons dependency? zbus? Having to switch between notifications or terminal prompt? Rather drastic imo. We should be able to send a notification using the aforemetioned crate with much less code, including with actions using libnotify.

@r0chd
Copy link
Contributor Author

r0chd commented Aug 17, 2025

notify crate makes sense to me, but it'd still depend on zbus, unless you prefer dbus-rs backend (but that would require dependency on libdbus and pkg-config).

The terminal prompt was just a fallback in case daemon wasnt available or notification was dismissed/expired, I'm not sure how to handle it in a better way.

Also do you think the notification should still use nixos-snowflake icon but just get rid of the dependency or ditch icons all together?

@NotAShelf
Copy link
Member

We could inline a SVG for the icon, probably preferable.

@r0chd r0chd force-pushed the notifications branch 3 times, most recently from dcd37cd to b609190 Compare August 19, 2025 20:06
@NotAShelf NotAShelf changed the title System notifications various: system notifications Aug 23, 2025
src/nixos.rs Outdated
Comment on lines 264 to 291
info!("Apply the config?");

let confirmation = match Notification::new()
.summary(&format!("nh os {variant}"))
.body("NixOS configuration built successfully.")
.action("default", "Apply")
.action("reject", "Reject")
.show()
{
Ok(notification) => {
let mut confirmation = false;
notification.wait_for_action(|action| {
confirmation = match action {
"default" => true,
"reject" => false,
// Notification was closed, fallback to inquire
_ => inquire::Confirm::new("Apply the config?")
.with_default(false)
.prompt()
.unwrap_or_default(),
}
});
confirmation
}
Err(_) => inquire::Confirm::new("Apply the config?")
.with_default(false)
.prompt()?,
};
Copy link
Member

@NotAShelf NotAShelf Aug 23, 2025

Choose a reason for hiding this comment

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

I have given this some thought, and I think I prefer the old approach for the prompt. Namely, I think the ask behaviour should remain as-is unless thee user explicitly opts into notification prompts. Otherwise this will be a very invasive change that I, for one, would not appreciate in my setup. Also note that using info!("Apply the config?"); and inquire::Confirm::new("Apply the config?") will actually prompt the user twice. We had previously used info for the question, and the input prompt would not display a message. This has changed with #382.

Imo the correct approach here would be to simply add a --notify flag that coexists with the prompt. Without having checked myself, I think it would be a nice addition to make it so that if --notify flag is available (or maybe something along the lines of --ask=notif, --ask=prompt and --ask=both in addition to --ask that would imply --ask=prompt or similar for backwards compat) the notification pops up with the confirmation prompt alongside the terminal prompt. Though if you think this is too much, then I'd prefer a simple notification for when

  1. A confirmation prompt is available
  2. A task is done (e.g., a rebuild)

This can also be used to notify the user of an awaiting sudo prompt if the logic can be made a bit more generic, but that would have to wait until #92 is merged. See here for my previous comment on the matter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Flags look good to me, I could wait until #92 as currently notify-rust has a weird behavior with platform specific options where instead of being a noop they're not included in the build hoodie/notify-rust#223 so we're gonna end up with a lot of ugly cfg directives and weird branching, I'm gonna try and merge a PR before doing anything else with this change

Copy link
Member

Choose a reason for hiding this comment

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

#92 has now been merged.

I'm aiming for a new release at worst on Monday, but if you think that won't be too much of a Rush and you can finish the refactor by then I can try to do it Monday afternoon or on Tuesday morning. Otherwise this'll have to be for the 4.3 release.

I've never used notify-rust before (I was thinking of the other notify crate, for some reason) but waiting for the bug you've linked to be fixed might take too long. If possible, consider alternative libraries.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I probably won't be able to find time to finish it before the release

@coderabbitai
Copy link

coderabbitai bot commented Sep 6, 2025

Walkthrough

Adds desktop notification support via notify-rust, introduces a tri-state Ask mode (NotifyAskMode) for confirmation flags (Prompt, Notify, Both), changes several --ask booleans to Option<NotifyAskMode>, adds NotificationSender and nh_info!/nh_warn! macros, and wires notifications into multiple commands and platforms.

Changes

Cohort / File(s) Summary
New Dependency
Cargo.toml
Adds notify-rust = "4.11.7".
Notification Module
src/notify.rs
New NotificationSender (wrapper around notify_rust::Notification) with new(), urgency(), send(), and ask(); respects NH_NOTIFY; platform-gated behavior (Unix/non-macOS actions vs macOS no-op for ask/send).
Logging Macros
src/logging.rs
Adds exported nh_info! and nh_warn! macros that log via tracing and send desktop notifications via NotificationSender; minor write-error handling changes in formatting path.
Module Declarations
src/lib.rs, src/main.rs
Adds pub mod notify; to library and mod notify; to main.
Tri-State CLI Ask
src/interface.rs
Adds pub enum NotifyAskMode { Prompt, Notify } (Notify conditional on unix non-macos); changes several ask fields from bool to Option<NotifyAskMode> with clap attrs (default_missing_value = "prompt", num_args = 0..=1).
Command Confirmation Flows
src/clean.rs, src/home.rs, src/darwin.rs, src/nixos.rs
Replace boolean ask checks with Option<NotifyAskMode> branching: Prompt uses terminal confirms, Notify uses NotificationSender::ask(), Both left unimplemented; many info!nh_info! swaps.
Commands & Utilities Logging
src/commands.rs, src/util.rs, src/util/platform.rs
Replace user-facing info! calls with nh_info!; adjust imports to expose nh_info.
Surface/API Changes
src/lib.rs, src/interface.rs, src/nixos.rs
Public API surface expanded: new notify module, NotificationSender, and NotifyAskMode; several public ask fields change types to Option<NotifyAskMode>.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI
    participant Command
    participant AskMode as NotifyAskMode
    participant Notifier as NotificationSender
    participant Tracing

    User->>CLI: run command (maybe `--ask`)
    CLI->>Command: parse args -> Option<NotifyAskMode>

    alt ask is None
        Command->>Command: proceed without confirmation
    else ask is Some
        Command->>AskMode: match variant
        alt Prompt
            Command->>User: terminal prompt (confirm)
            User-->>Command: accept/reject
        else Notify
            Command->>Notifier: new(summary, body).urgency(Critical)
            Notifier->>Notifier: show notification (actions on unix)
            alt unix && not macos
                User-->>Notifier: accept/reject action
                Notifier-->>Command: returns bool
            else macos or unsupported
                Notifier-->>Command: returns false (no interactive ask)
            end
        else Both
            Note right of Command: path currently unimplemented
        end
    end

    Command->>Tracing: nh_info!/nh_warn! (logs + notification)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "various: system notifications" directly relates to the primary objective and changeset. The PR introduces system notifications throughout the codebase by adding the notify-rust dependency, creating a new NotificationSender module, adding nh_info! and nh_warn! macros that combine logging with notifications, and refactoring confirmation flows to support tri-state ask modes (Prompt, Notify, Both) across multiple files. The title accurately captures this core feature and the use of "various" appropriately indicates changes span multiple components. The title is specific and concrete rather than vague—it names a meaningful feature rather than using generic terms.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1444215 and f1c2b45.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml (1 hunks)
  • src/clean.rs (5 hunks)
  • src/commands.rs (4 hunks)
  • src/darwin.rs (5 hunks)
  • src/home.rs (3 hunks)
  • src/interface.rs (3 hunks)
  • src/lib.rs (1 hunks)
  • src/logging.rs (2 hunks)
  • src/main.rs (1 hunks)
  • src/nixos.rs (12 hunks)
  • src/notify.rs (1 hunks)
  • src/util.rs (2 hunks)
  • src/util/platform.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/darwin.rs
  • src/interface.rs
  • src/util/platform.rs
  • src/commands.rs
  • src/lib.rs
🧰 Additional context used
🧬 Code graph analysis (4)
src/home.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/nixos.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/logging.rs (1)
src/notify.rs (3)
  • new (25-33)
  • urgency (41-44)
  • send (60-78)
src/clean.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: treewide-checks
🔇 Additional comments (15)
src/util.rs (2)

13-18: LGTM: Clean transition to notification-aware logging.

The import changes correctly swap tracing::info for the new nh_info! macro.


339-339: LGTM: Consistent use of notification-aware logging.

Replacing info! with nh_info! aligns with the new notification system.

src/main.rs (1)

13-13: LGTM: Module declaration for notification subsystem.

src/logging.rs (1)

34-43: Acknowledge intentional error handling change.

Terminating these branches with ; stops propagating write errors, which appears intentional for these log formatting paths.

src/home.rs (3)

7-26: LGTM: Import additions support notification-based confirmation flow.


38-40: LGTM: Correctly adapts to Option.


191-191: LGTM: Consistent use of notification-aware logging.

src/clean.rs (3)

17-26: LGTM: Import additions support notification-based confirmation.


254-254: LGTM: Explicit Duration conversion for clarity.

Also applies to: 507-507


523-523: LGTM: Notification-aware logging for path removal.

src/notify.rs (2)

1-44: LGTM: Well-structured builder pattern for notifications.

The new() and urgency() methods provide a clean API with proper documentation.


60-78: LGTM: send() properly gates notifications and propagates errors.

The NH_NOTIFY environment variable check and proper Result return enable graceful degradation.

src/nixos.rs (3)

8-29: LGTM: Import additions support notification-based flows.


44-46: LGTM: Correctly checks for Option.


437-437: LGTM: Consistent migration to notification-aware logging.

The nh_info! replacements maintain user-facing messaging while adding notification capability.

Also applies to: 475-478, 517-517, 549-549, 566-569, 673-676, 680-683, 705-708


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

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/nixos.rs (1)

1-29: Document MSRV or replace is_ok_and/is_some_and usages
No rust-version set in Cargo.toml (and no rust-toolchain or workflow pin found) while these methods require Rust ≥ 1.62. Either add rust-version = "1.62" under [package] in Cargo.toml or refactor these calls to match/if let for older Rust support.

♻️ Duplicate comments (1)
src/nixos.rs (1)

280-294: Eliminate panics and fix Both semantics (don’t auto-confirm).

  • .ask().unwrap() will panic if notifications fail (e.g., no DBus daemon).
  • NotifyAskMode::Both => true silently auto-confirms, which contradicts the earlier review guidance to “show notification alongside the prompt,” not bypass it.

Apply:

-    if let Some(ask) = self.common.ask {
-      let confirmation = match ask {
-        NotifyAskMode::Prompt => {
-          inquire::Confirm::new("Apply the config?")
-            .with_default(false)
-            .prompt()?
-        },
-        NotifyAskMode::Notify => {
-          NotificationSender::new(&format!("nh os {variant}"), "testing")
-            .ask()
-            .unwrap()
-        },
-        NotifyAskMode::Both => true,
-      };
+    if let Some(ask) = self.common.ask {
+      let confirmation = match ask {
+        NotifyAskMode::Prompt => {
+          inquire::Confirm::new("Apply the config?")
+            .with_default(false)
+            .prompt()?
+        },
+        NotifyAskMode::Notify => {
+          match NotificationSender::new(
+                  &format!("nh os {variant}"),
+                  &format!("Apply the configuration on {target_hostname}?"),
+                ).ask()
+          {
+            Ok(b) => b,
+            Err(e) => {
+              warn!("Notification prompt failed ({e}); falling back to terminal prompt");
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
+        // Show a notification but do not block on it; also show the terminal prompt.
+        // This matches the previously requested behavior.
+        NotifyAskMode::Both => {
+          let _ = NotificationSender::new(
+                    &format!("nh os {variant}"),
+                    &format!("Apply the configuration on {target_hostname}?"),
+                  ).send();
+          inquire::Confirm::new("Apply the config?")
+            .with_default(false)
+            .prompt()?
+        },
+      };

Also replace the placeholder body “testing” with a meaningful message.

🧹 Nitpick comments (10)
src/interface.rs (2)

258-263: Document variants and ensure clap help shows choices consistently.

Add short docs for Prompt/Notify/Both and mark the args using value_enum for consistent help output (like DiffType). Low risk, improves UX.

-#[derive(Debug, Clone, ValueEnum, PartialEq)]
+#[derive(Debug, Clone, ValueEnum, PartialEq)]
 pub enum NotifyAskMode {
-  Prompt,
-  Notify,
-  Both,
+  /// Ask in the terminal (stdin prompt)
+  Prompt,
+  /// Ask via a desktop notification action
+  Notify,
+  /// Show both notification and terminal prompt (fallback-safe)
+  Both,
 }

271-274: Make clap show the accepted values and keep behavior unchanged.

ValueEnum is inferred, but adding value_enum mirrors other flags and ensures the choices appear in --help. No behavior change.

-#[arg(long, short, default_missing_value = "prompt", num_args = 0..=1)]
+#[arg(long, short, value_enum, default_missing_value = "prompt", num_args = 0..=1)]
 pub ask: Option<NotifyAskMode>,

Apply to all three: OsRollbackArgs.ask, CommonRebuildArgs.ask, CleanArgs.ask.

Also applies to: 303-306, 427-430

src/commands.rs (2)

441-455: Avoid leaking potentially sensitive CLI args into user-visible notifications.

Embedding raw args in the notification summary risks exposing secrets (tokens, URLs, SSH opts) via the desktop’s notification center. Prefer a static summary; keep details in logs.

-      if let Err(err) = NotificationSender::new(
-        &format!(
-          "nh log {}",
-          self
-            .args
-            .iter()
-            .map(|s| s.to_string_lossy())
-            .collect::<Vec<_>>()
-            .join(" ")
-        ),
-        m,
-      )
+      if let Err(err) = NotificationSender::new("nh log", m)
         .send()
       {
         tracing::debug!(error = ?err, "notification send failed");
       }

441-455: Consider honoring dry-run for notifications as well.

Currently we notify even when self.dry is true. If the intent is “no side effects” on dry runs, gate the send behind !self.dry.

-    if let Some(m) = &self.message {
+    if let Some(m) = &self.message {
+      if !self.dry {
         // notification sending block
+      }
       info!("{m}");
     }
src/home.rs (2)

168-173: Misleading/duplicated --ask warning in Build vs Dry-run

When variant=Build (not dry), this warns “as dry run was requested.” Also, run() already warns for home build. Suggest: warn only for dry-run here; return early for Build without another warn.

Apply:

-    if self.common.dry || matches!(variant, Build) {
-      if self.common.ask.is_some() {
-        warn!("--ask has no effect as dry run was requested");
-      }
-      return Ok(());
-    }
+    if self.common.dry {
+      if self.common.ask.is_some() {
+        warn!("--ask has no effect in dry-run mode");
+      }
+      return Ok(());
+    }
+    if matches!(variant, Build) {
+      return Ok(());
+    }

191-196: Unnecessary unsafe around env::set_var

env::set_var is safe. Remove unsafe.

Apply:

-      unsafe {
-        env::set_var("HOME_MANAGER_BACKUP_EXT", ext);
-      }
+      env::set_var("HOME_MANAGER_BACKUP_EXT", ext);
src/darwin.rs (1)

147-150: Ask semantics differ from Linux tri-state

Darwin still treats ask as Option and only prompts in Switch. If/when you bring notification actions to macOS, consider aligning with NotifyAskMode and adding a notification-backed path similar to Linux.

Would you like me to prep a follow-up PR to introduce a macOS-specific NotifyAskMode shim that falls back to prompt?

src/notify.rs (2)

19-23: Make notification enablement consistent and configurable (and reuse in ask).

send respects NH_NOTIFY, but ask doesn’t. Consider centralizing a permissive boolean parser and using it for both paths so users can disable notifications uniformly.

Apply:

   pub fn send(mut self) -> Result<()> {
-    let enable_notifications = env::var("NH_NOTIFY").is_ok_and(|v| v == "1");
+    let enable_notifications = env::var("NH_NOTIFY")
+      .ok()
+      .map(|v| v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes"))
+      .unwrap_or(false);
     if !enable_notifications {
       return Ok(());
     }

Optionally extract this into a small helper and call it from ask() as an early return (Ok(false)) to allow callers to gracefully fall back to a terminal prompt.


25-28: Avoid overusing Critical urgency; prefer interaction semantics.

Setting Urgency::Critical for plain info notifications can be intrusive. If the intent is “make it stick until seen,” consider .timeout(0) (infinite) or a “resident/transient” hint instead of Critical.

Example:

 #[cfg(all(unix, not(target_os = "macos")))]
-    self.notification.urgency(Urgency::Critical);
+    self.notification
+      .urgency(Urgency::Normal)
+      .timeout(0);
``` <!-- review_comment_end -->

</blockquote></details>
<details>
<summary>src/nixos.rs (1)</summary><blockquote>

`200-215`: **Minor: the “Building …” message is good; consider notifying when activation/elevation is imminent.**

A non-blocking info notification right before the first elevated command can improve UX (“About to prompt for sudo”). This aligns with the PR objective.



Example (before calling an elevated `Command`):

```rust
let _ = NotificationSender::new("nh os", "About to prompt for sudo").send();
``` <!-- review_comment_end -->

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 036c141e2f14fb481f12c4d1498bc5d03d9e1865 and ed84637e355eff2b38c28828e40f16531bfac3b4.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `Cargo.toml` (1 hunks)
* `src/clean.rs` (2 hunks)
* `src/commands.rs` (2 hunks)
* `src/darwin.rs` (2 hunks)
* `src/home.rs` (5 hunks)
* `src/interface.rs` (3 hunks)
* `src/lib.rs` (1 hunks)
* `src/main.rs` (1 hunks)
* `src/nixos.rs` (6 hunks)
* `src/notify.rs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code graph analysis (3)</summary>

<details>
<summary>src/clean.rs (1)</summary><blockquote>

<details>
<summary>src/notify.rs (2)</summary>

* `ask` (32-53)
* `new` (12-17)

</details>

</blockquote></details>
<details>
<summary>src/home.rs (2)</summary><blockquote>

<details>
<summary>src/nixos.rs (1)</summary>

* `fmt` (67-76)

</details>
<details>
<summary>src/notify.rs (2)</summary>

* `ask` (32-53)
* `new` (12-17)

</details>

</blockquote></details>
<details>
<summary>src/nixos.rs (2)</summary><blockquote>

<details>
<summary>src/home.rs (1)</summary>

* `fmt` (52-58)

</details>
<details>
<summary>src/notify.rs (2)</summary>

* `ask` (32-53)
* `new` (12-17)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)</summary>

* GitHub Check: Build NH on Linux
* GitHub Check: Build NH on Darwin
* GitHub Check: treewide-checks
* GitHub Check: Test NH on Linux
* GitHub Check: Test NH on Darwin

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>src/main.rs (1)</summary><blockquote>

`13-13`: **LGTM: module wiring only.**

The `mod notify;` addition is minimal and aligns with the new module graph.

</blockquote></details>
<details>
<summary>src/lib.rs (1)</summary><blockquote>

`14-15`: **LGTM: public surface exposure.**

`pub mod notify;` correctly exposes the module for library consumers.

</blockquote></details>
<details>
<summary>src/commands.rs (1)</summary><blockquote>

`16-20`: **Import looks correct.**

`NotificationSender` import is scoped properly alongside existing items.

</blockquote></details>
<details>
<summary>src/home.rs (2)</summary><blockquote>

`10-19`: **Import of NotifyAskMode looks good**

Tri-state integration in Home flows makes sense.

---

`51-59`: **Nice Display impl for HomeRebuildVariant**

Keeps messaging consistent (“build”/“switch”).

</blockquote></details>
<details>
<summary>src/darwin.rs (1)</summary><blockquote>

`37-39`: **Build-mode warn message LGTM**

Matches behavior that `--ask`/`--dry` don’t affect `darwin build`.

</blockquote></details>
<details>
<summary>src/clean.rs (1)</summary><blockquote>

`22-24`: **New imports OK, but see panic path below**

Using NotificationSender here is fine; avoid `.unwrap()` downstream.

</blockquote></details>
<details>
<summary>src/nixos.rs (3)</summary><blockquote>

`44-47`: **Good guard: `--ask`/`--dry` do nothing for `build`.**

Clear UX and avoids misleading flags.

 <!-- review_comment_end -->

---

`66-77`: **Display impl looks correct and is used consistently in messages.**

No issues.

 <!-- review_comment_end -->

---

`273-279`: **Early return on dry/build paths is correct; warning is helpful.**

No changes needed.

 <!-- review_comment_end -->

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

humantime = "2.2.0"
inquire = { default-features = false, version = "0.7.5", features = [ "crossterm" ] }
nix = { default-features = false, features = [ "fs", "user" ], version = "0.30.1" }
notify-rust = "4.11.7"
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Gate notify-rust per target and pick backend explicitly (avoid accidental zbus coupling).

Given maintainer concerns about zbus and that notifications are Linux-only for now, make the dependency Linux-targeted and select the backend via features. This reduces build surface for darwin/windows and lets us pivot between zbus and dbus cleanly.

Apply one of these diffs:

Option A — keep zbus backend (default-free, explicit):

-[dependencies]
-notify-rust = "4.11.7"
+[dependencies]
+
+[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
+notify-rust = { version = "4.11.7", default-features = false, features = ["zbus"] }

Option B — switch to dbus-rs backend (requires libdbus/pkg-config in env):

-[dependencies]
-notify-rust = "4.11.7"
+[dependencies]
+
+[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
+notify-rust = { version = "4.11.7", default-features = false, features = ["dbus"] }

If unsure which features notify-rust exposes in 4.11.7, please confirm:


🌐 Web query:

What backend feature flags does notify-rust 4.11.7 provide (zbus vs dbus), and which are enabled by default?

💡 Result:

Backend feature flags:

  • z (uses zbus + serde + async) — this is the default backend. [1][3]
  • d (enables dbus, i.e. dbus-rs) — optional; there is also d_vendored to enable dbus/vendored. [1][2]

Which are enabled by default:

  • The crate default feature set is ["z"], so the z/zbus backend is enabled by default. To use dbus-rs you must disable default features (or enable d explicitly / set DBUSRS at runtime per the README). [1][2][3]

Sources: notify-rust Cargo.toml and docs (features list). [1] [2] [3]


Gate and select notify-rust backend under Linux target
notify-rust 4.11.7 defaults to ["z"] (zbus + serde + async); use ["d"] for dbus-rs. Move the dependency under a Linux-only target and disable defaults:

Option A — explicit zbus backend:

-[dependencies]
-notify-rust = "4.11.7"
+[target.'cfg(target_os = "linux")'.dependencies]
+notify-rust = { version = "4.11.7", default-features = false, features = ["z"] }

Option B — explicit dbus-rs backend:

-[dependencies]
-notify-rust = "4.11.7"
+[target.'cfg(target_os = "linux")'.dependencies]
+notify-rust = { version = "4.11.7", default-features = false, features = ["d"] }

(docs.rs, lib.rs)

🤖 Prompt for AI Agents
In Cargo.toml at line 41, the notify-rust dependency is unconditionally included
and uses default features (zbus), but we must gate it to Linux and select the
dbus-rs backend; move the dependency under a Linux-only target table
([target.'cfg(target_os = "linux")'.dependencies]) and replace the plain entry
with one that disables default-features and sets features = ["d"] (i.e.,
notify-rust = { version = "4.11.7", default-features = false, features = ["d"]
}) so it only builds on Linux and uses the dbus-rs backend.

src/commands.rs Outdated
Comment on lines 441 to 528
if let Some(m) = &self.message {
NotificationSender::new(
&format!(
"nh log {}",
self
.args
.iter()
.map(|s| s.to_string_lossy())
.collect::<Vec<_>>()
.join(" ")
),
m,
)
.send();
info!("{m}");
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don't ignore Result from NotificationSender::send(); log and continue.

Ignoring a Result from a #[must_use] type triggers unused_must_use and hides real failures. Log at debug and proceed to keep this best-effort.

-      NotificationSender::new(
+      // Best-effort: log on failure, don't break execution
+      if let Err(err) = NotificationSender::new(
         &format!(
           "nh log {}",
           self
             .args
             .iter()
             .map(|s| s.to_string_lossy())
             .collect::<Vec<_>>()
             .join(" ")
         ),
         m,
-      )
-      .send();
+      )
+      .send()
+      {
+        tracing::debug!(error = ?err, "notification send failed");
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(m) = &self.message {
NotificationSender::new(
&format!(
"nh log {}",
self
.args
.iter()
.map(|s| s.to_string_lossy())
.collect::<Vec<_>>()
.join(" ")
),
m,
)
.send();
info!("{m}");
if let Some(m) = &self.message {
// Best-effort: log on failure, don't break execution
if let Err(err) = NotificationSender::new(
&format!(
"nh log {}",
self
.args
.iter()
.map(|s| s.to_string_lossy())
.collect::<Vec<_>>()
.join(" ")
),
m,
)
.send()
{
tracing::debug!(error = ?err, "notification send failed");
}
info!("{m}");
}
🤖 Prompt for AI Agents
In src/commands.rs around lines 441 to 455, the Result returned by
NotificationSender::send() is currently ignored; capture the Result and handle
the Err case by logging the error at debug (or info) level while continuing
execution (best-effort). Replace the call that discards send()'s return with
code that stores the result and if it is Err(e) calls debug!(...) or
log::debug!(...) with a clear message and the error; do not propagate or
panic—just log and proceed.

src/notify.rs Outdated
Comment on lines 32 to 117
pub fn ask(mut self) -> Result<bool> {
#[cfg(all(unix, not(target_os = "macos")))]
{
self.notification.urgency(Urgency::Critical);
self.notification.action("accept", "Accept");
self.notification.action("reject", "Reject");
}

let handle = self.notification.show().unwrap();

#[cfg(all(unix, not(target_os = "macos")))]
{
let mut confirmation = false;
handle.wait_for_action(|s| {
confirmation = s == "accept";
});
return Ok(confirmation);
}

#[cfg(target_os = "macos")]
Ok(false)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don’t panic on notification delivery; propagate the error and avoid showing on macOS.

show().unwrap() will panic if the server is unavailable (common on minimal/headless systems). Also, the macOS path currently still calls show() before returning Ok(false).

Apply:

   pub fn ask(mut self) -> Result<bool> {
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      self.notification.urgency(Urgency::Critical);
-      self.notification.action("accept", "Accept");
-      self.notification.action("reject", "Reject");
-    }
-
-    let handle = self.notification.show().unwrap();
-
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      let mut confirmation = false;
-      handle.wait_for_action(|s| {
-        confirmation = s == "accept";
-      });
-      return Ok(confirmation);
-    }
-
-    #[cfg(target_os = "macos")]
-    Ok(false)
+    #[cfg(target_os = "macos")]
+    {
+      // Actions aren’t supported via notify-rust on macOS; let callers fall back.
+      return Ok(false);
+    }
+
+    #[cfg(all(unix, not(target_os = "macos")))]
+    {
+      self.notification
+        .urgency(Urgency::Critical)
+        .timeout(0) // require interaction on most servers
+        .action("accept", "Accept")
+        .action("reject", "Reject");
+
+      let handle = self.notification.show()?; // propagate error, don’t panic
+
+      let mut confirmation = false;
+      handle.wait_for_action(|s| {
+        confirmation = s == "accept";
+      });
+      Ok(confirmation)
+    }

Optional: set .appname("nh") and consider .icon("computer") or an inlined SVG later per maintainer preference.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn ask(mut self) -> Result<bool> {
#[cfg(all(unix, not(target_os = "macos")))]
{
self.notification.urgency(Urgency::Critical);
self.notification.action("accept", "Accept");
self.notification.action("reject", "Reject");
}
let handle = self.notification.show().unwrap();
#[cfg(all(unix, not(target_os = "macos")))]
{
let mut confirmation = false;
handle.wait_for_action(|s| {
confirmation = s == "accept";
});
return Ok(confirmation);
}
#[cfg(target_os = "macos")]
Ok(false)
}
pub fn ask(mut self) -> Result<bool> {
#[cfg(target_os = "macos")]
{
// Actions aren’t supported via notify-rust on macOS; let callers fall back.
return Ok(false);
}
#[cfg(all(unix, not(target_os = "macos")))]
{
self.notification
.urgency(Urgency::Critical)
.timeout(0) // require interaction on most servers
.action("accept", "Accept")
.action("reject", "Reject");
let handle = self.notification.show()?; // propagate error, don’t panic
let mut confirmation = false;
handle.wait_for_action(|s| {
confirmation = s == "accept";
});
Ok(confirmation)
}
}
🤖 Prompt for AI Agents
In src/notify.rs around lines 32 to 53, the code currently calls show().unwrap()
(which will panic if the notification server is unavailable) and also invokes
show() even on the macOS branch; change to propagate the notification error
instead of unwrapping and avoid calling show() on macOS. Specifically, make
show() return a Result and use the ? operator (or map_err) to return Err from
ask when notification delivery fails; move the macOS #[cfg(target_os = "macos")]
branch so it returns Ok(false) before any call to show(); keep the unix
action-handling code under the existing unix cfg and use the handle only when
show() succeeded; optionally set .appname("nh") and consider .icon("computer")
on the builder before calling show().

@NotAShelf
Copy link
Member

Actual yapmachine holy hell

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
src/home.rs (1)

176-189: Don’t unwrap; fix strings; implement Both with fallback

  • Avoid .unwrap() to prevent panics and enable fallback.
  • Replace placeholder title/body with accurate, user-facing text.
  • Implement Both = “notify, else prompt”.

Apply:

-    if let Some(ask) = self.common.ask {
-      let confirmation = match ask {
-        NotifyAskMode::Prompt => {
-          inquire::Confirm::new("Apply the config?")
-            .with_default(false)
-            .prompt()?
-        },
-        NotifyAskMode::Notify => {
-          NotificationSender::new("nh os rollback", "testing")
-            .ask()
-            .unwrap()
-        },
-        NotifyAskMode::Both => unimplemented!(),
-      };
+    if let Some(ask) = self.common.ask {
+      let confirmation = match ask {
+        NotifyAskMode::Prompt => {
+          inquire::Confirm::new("Apply the config?")
+            .with_default(false)
+            .prompt()?
+        },
+        NotifyAskMode::Notify => {
+          match NotificationSender::new(
+            &format!("nh home {variant}"),
+            "Apply the config?",
+          )
+          .ask() {
+            Ok(true) => true,
+            Ok(false) => false, // rejected/dismissed
+            Err(e) => {
+              debug!(?e, "Notification failed; falling back to prompt");
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
+        NotifyAskMode::Both => {
+          match NotificationSender::new(
+            &format!("nh home {variant}"),
+            "Apply the config?",
+          )
+          .ask() {
+            Ok(true) => true,
+            _ => {
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
+      };
src/clean.rs (1)

310-337: No unwraps; fix Both semantics; user-facing text OK

  • Replace .ask().unwrap() with proper Result handling and fallback.
  • Implement Both = “notify, else prompt”.
  • Keep the existing, clear prompt string.

Apply:

-    if let Some(ask) = args.ask.as_ref() {
+    if let Some(ask) = args.ask.as_ref() {
       let confirmation = match ask {
         NotifyAskMode::Prompt => {
           Confirm::new("Confirm the cleanup plan?")
             .with_default(false)
             .prompt()?
         },
         NotifyAskMode::Notify => {
           let clean_mode = match self {
             Self::Profile(_) => "profile",
             Self::All(_) => "all",
             Self::User(_) => "user",
           };
-
-          NotificationSender::new(
-            &format!("nh clean {clean_mode}"),
-            "Confirm the cleanup plan?",
-          )
-          .ask()
-          .unwrap()
+          match NotificationSender::new(
+            &format!("nh clean {clean_mode}"),
+            "Confirm the cleanup plan?",
+          )
+          .ask() {
+            Ok(true) => true,
+            Ok(false) => false,
+            Err(e) => {
+              debug!(?e, "Notification failed; falling back to prompt");
+              Confirm::new("Confirm the cleanup plan?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
         },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          let clean_mode = match self {
+            Self::Profile(_) => "profile",
+            Self::All(_) => "all",
+            Self::User(_) => "user",
+          };
+          match NotificationSender::new(
+            &format!("nh clean {clean_mode}"),
+            "Confirm the cleanup plan?",
+          )
+          .ask() {
+            Ok(true) => true,
+            _ => {
+              Confirm::new("Confirm the cleanup plan?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
       };
src/nixos.rs (1)

474-493: Handle notification errors and fix unimplemented Both mode in rollback path.

Same issues as the rebuild confirmation path: using .unwrap() can panic and Both mode is unimplemented.

Apply this fix to handle errors properly and implement the Both mode:

-    if let Some(ask) = self.ask.as_ref() {
-      let confirmation = match ask {
-        NotifyAskMode::Prompt => {
-          inquire::Confirm::new(&format!(
-            "Roll back to generation {}?",
-            target_generation.number
-          ))
-          .with_default(false)
-          .prompt()?
-        },
-        NotifyAskMode::Notify => {
-          NotificationSender::new(
-            "nh os rollback",
-            &format!("Roll back to generation {}?", target_generation.number),
-          )
-          .ask()
-          .unwrap()
-        },
-        NotifyAskMode::Both => unimplemented!(),
-      };
+    if let Some(ask) = self.ask.as_ref() {
+      let confirmation = match ask {
+        NotifyAskMode::Prompt => {
+          inquire::Confirm::new(&format!(
+            "Roll back to generation {}?",
+            target_generation.number
+          ))
+          .with_default(false)
+          .prompt()?
+        },
+        NotifyAskMode::Notify => {
+          match NotificationSender::new(
+            "nh os rollback",
+            &format!("Roll back to generation {}?", target_generation.number),
+          ).ask()
+          {
+            Ok(b) => b,
+            Err(e) => {
+              warn!("Notification prompt failed ({e}); falling back to terminal prompt");
+              inquire::Confirm::new(&format!(
+                "Roll back to generation {}?",
+                target_generation.number
+              ))
+              .with_default(false)
+              .prompt()?
+            }
+          }
+        },
+        NotifyAskMode::Both => {
+          let _ = NotificationSender::new(
+            "nh os rollback",
+            &format!("Roll back to generation {}?", target_generation.number),
+          ).send();
+          inquire::Confirm::new(&format!(
+            "Roll back to generation {}?",
+            target_generation.number
+          ))
+          .with_default(false)
+          .prompt()?
+        },
+      };
🧹 Nitpick comments (2)
src/home.rs (1)

196-201: Remove unnecessary unsafe around env::set_var

env::set_var is safe; drop the unsafe block.

-      unsafe {
-        env::set_var("HOME_MANAGER_BACKUP_EXT", ext);
-      }
+      env::set_var("HOME_MANAGER_BACKUP_EXT", ext);
src/clean.rs (1)

309-337: Skip ask on dry-run for consistency (optional)

Consider skipping notifications/prompts when args.dry is set, mirroring home/darwin.

-    // Clean the paths
-    if let Some(ask) = args.ask.as_ref() {
+    // Clean the paths
+    if !args.dry {
+      if let Some(ask) = args.ask.as_ref() {
         ...
-    }
+      }
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f61db4f and b20137a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml (1 hunks)
  • src/clean.rs (2 hunks)
  • src/commands.rs (2 hunks)
  • src/darwin.rs (4 hunks)
  • src/home.rs (5 hunks)
  • src/interface.rs (3 hunks)
  • src/lib.rs (1 hunks)
  • src/main.rs (1 hunks)
  • src/nixos.rs (6 hunks)
  • src/notify.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/main.rs
  • src/lib.rs
  • src/notify.rs
  • Cargo.toml
  • src/interface.rs
  • src/commands.rs
🧰 Additional context used
🧬 Code graph analysis (4)
src/clean.rs (1)
src/notify.rs (2)
  • ask (32-53)
  • new (12-17)
src/darwin.rs (1)
src/notify.rs (2)
  • ask (32-53)
  • new (12-17)
src/nixos.rs (3)
src/darwin.rs (1)
  • fmt (54-60)
src/home.rs (1)
  • fmt (53-59)
src/notify.rs (2)
  • ask (32-53)
  • new (12-17)
src/home.rs (3)
src/darwin.rs (1)
  • fmt (54-60)
src/nixos.rs (1)
  • fmt (67-76)
src/notify.rs (2)
  • ask (32-53)
  • new (12-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: treewide-checks
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
🔇 Additional comments (7)
src/home.rs (4)

36-39: Build: correct no-op warning for --ask/--dry

Message and gating look good.


52-60: Display impl LGTM

Consistent with darwin/nixos variants.


169-174: Dry-run short-circuit is correct

Avoids prompting in dry mode. Good.


176-189: No other unimplemented!() invocations or NotifyAskMode::Both usages found

src/clean.rs (1)

22-24: Imports LGTM

notify wiring looks consistent.

src/darwin.rs (2)

38-41: Build: correct no-op warning for --ask/--dry

Good parity with home.


53-61: Display impl LGTM

Matches other variants.

src/home.rs Outdated
Comment on lines 176 to 187
if let Some(ask) = self.common.ask {
let confirmation = match ask {
NotifyAskMode::Prompt => {
inquire::Confirm::new("Apply the config?")
.with_default(false)
.prompt()?
},
NotifyAskMode::Notify => {
NotificationSender::new("nh os rollback", "testing")
.ask()
.unwrap()
},
NotifyAskMode::Both => unimplemented!(),
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Propagate notify errors from notify.rs to enable fallback

ask() currently calls .show().unwrap() (see src/notify.rs). Change it to return Result and let callers fall back.

Additional change outside this file (in src/notify.rs):

-    let handle = self.notification.show().unwrap();
+    let handle = self
+      .notification
+      .show()
+      .wrap_err("Failed to show notification")?;

Optionally, on macOS return an error to trigger prompt fallback:

-  #[cfg(target_os = "macos")]
-  Ok(false)
+  #[cfg(target_os = "macos")]
+  Err(eyre!("Notification actions are not supported on macOS"))

And provide a send() helper used on macOS:

// in src/notify.rs
pub fn send(self) -> Result<()> {
  self.notification.show().map(|_| ()).wrap_err("Failed to show notification")
}
🤖 Prompt for AI Agents
In src/home.rs around lines 176-189, the code currently calls
NotificationSender::new(...).ask().unwrap() which panics on notification
failures; change the call to handle and propagate a Result instead (use ? or
match) so callers can fall back to a prompt on error. Update src/notify.rs so
ask()/show() returns Result<()> (do not unwrap internally), add the send()
helper that maps notification.show() into a Result with a wrapped error message,
and ensure platform-specific behavior (on macOS optionally return an Err to
trigger the prompt fallback). Adjust callers in src/home.rs to handle the Result
and fall back to prompting when notification sending fails.

@r0chd r0chd force-pushed the notifications branch 2 times, most recently from 453a5f1 to bb22a66 Compare September 8, 2025 21:11
@r0chd
Copy link
Contributor Author

r0chd commented Sep 8, 2025

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/clean.rs (1)

309-337: Don’t unwrap ask(); implement Both as notify-then-prompt fallback

Unwrap will panic on notification failure; Both is unimplemented. Implement explicit Accept via actions, and fall back to terminal prompt when notification errors or is dismissed.

-    if let Some(ask) = args.ask.as_ref() {
-      let confirmation = match ask {
+    if let Some(ask) = args.ask.as_ref() {
+      let confirmation = match ask {
         NotifyAskMode::Prompt => {
           Confirm::new("Confirm the cleanup plan?")
             .with_default(false)
             .prompt()?
         },
         NotifyAskMode::Notify => {
           let clean_mode = match self {
             Self::Profile(_) => "profile",
             Self::All(_) => "all",
             Self::User(_) => "user",
           };
-
-          NotificationSender::new(
-            &format!("nh clean {clean_mode}"),
-            "Confirm the cleanup plan?",
-          )
-          .ask()
-          .unwrap()
+          match NotificationSender::new(
+            &format!("nh clean {clean_mode}"),
+            "Confirm the cleanup plan?",
+          )
+          .ask() {
+            Ok(true) => true,      // Accepted via notification
+            Ok(false) => false,    // Rejected/dismissed
+            Err(e) => {
+              debug!(?e, "Notification failed; falling back to prompt");
+              Confirm::new("Confirm the cleanup plan?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
         },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          let clean_mode = match self {
+            Self::Profile(_) => "profile",
+            Self::All(_) => "all",
+            Self::User(_) => "user",
+          };
+          match NotificationSender::new(
+            &format!("nh clean {clean_mode}"),
+            "Confirm the cleanup plan?",
+          )
+          .ask() {
+            Ok(true) => true, // Accepted via notification
+            _ => {
+              Confirm::new("Confirm the cleanup plan?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
       };
 
       if !confirmation {
         bail!("User rejected the cleanup plan");
       }
     }

Follow-up in src/notify.rs: replace .show().unwrap() with proper error propagation so callers can fall back.

src/home.rs (1)

176-189: Don’t unwrap ask(); fix strings; implement Both as notify-then-prompt

Avoid panics, use accurate titles, and fall back to prompt on failure/dismiss.

-    if let Some(ask) = self.common.ask {
-      let confirmation = match ask {
+    if let Some(ask) = self.common.ask {
+      let confirmation = match ask {
         NotifyAskMode::Prompt => {
           inquire::Confirm::new("Apply the config?")
             .with_default(false)
             .prompt()?
         },
         NotifyAskMode::Notify => {
-          NotificationSender::new("nh os rollback", "testing")
-            .ask()
-            .unwrap()
+          match NotificationSender::new(
+              &format!("nh home {variant}"),
+              "Apply the config?",
+            ).ask() {
+            Ok(true) => true,
+            Ok(false) => false,
+            Err(e) => {
+              tracing::debug!(?e, "Notification failed; falling back to prompt");
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
         },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          match NotificationSender::new(
+              &format!("nh home {variant}"),
+              "Apply the config?",
+            ).ask() {
+            Ok(true) => true,
+            _ => {
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
       };
src/darwin.rs (1)

158-176: Avoid unwrap; improve prompt text; macOS: notify then prompt

macOS lacks actions; send a notification best-effort, then prompt. Don’t panic on failures.

-          // MacOS doesn't support notification actions
-          NotifyAskMode::Notify | NotifyAskMode::Both => {
-            NotificationSender::new(&format!("nh darwin {variant}"), "testing")
-              .send()
-              .unwrap();
-
-            inquire::Confirm::new("Apply the config?")
-              .with_default(false)
-              .prompt()?
-          },
+          // macOS: no actions; notify then prompt
+          NotifyAskMode::Notify | NotifyAskMode::Both => {
+            if let Err(e) = NotificationSender::new(
+              &format!("nh darwin {variant}"),
+              "Apply the config?",
+            ).send() {
+              debug!(?e, "Notification failed; continuing with prompt");
+            }
+            inquire::Confirm::new("Apply the config?")
+              .with_default(false)
+              .prompt()?
+          },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 453a5f1 and bb22a66.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • Cargo.toml (1 hunks)
  • src/clean.rs (2 hunks)
  • src/commands.rs (1 hunks)
  • src/darwin.rs (4 hunks)
  • src/home.rs (5 hunks)
  • src/interface.rs (3 hunks)
  • src/lib.rs (1 hunks)
  • src/logging.rs (1 hunks)
  • src/main.rs (1 hunks)
  • src/nixos.rs (7 hunks)
  • src/notify.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • Cargo.toml
  • src/nixos.rs
  • src/commands.rs
  • src/interface.rs
  • src/lib.rs
  • src/main.rs
  • src/notify.rs
🧰 Additional context used
🧬 Code graph analysis (4)
src/logging.rs (1)
src/notify.rs (2)
  • urgency (41-44)
  • send (60-73)
src/clean.rs (1)
src/notify.rs (2)
  • ask (94-117)
  • new (25-33)
src/darwin.rs (2)
src/home.rs (1)
  • fmt (53-59)
src/notify.rs (2)
  • ask (94-117)
  • new (25-33)
src/home.rs (3)
src/darwin.rs (1)
  • fmt (54-60)
src/nixos.rs (1)
  • fmt (68-77)
src/notify.rs (2)
  • ask (94-117)
  • new (25-33)
🪛 GitHub Actions: Check formating & lints
src/logging.rs

[error] 133-133: cargo fmt --check detected formatting differences in this file. Run 'cargo fmt' to fix code style issues.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Darwin
🔇 Additional comments (5)
src/logging.rs (1)

92-135: Fix formatting to satisfy cargo fmt

Long single-line expressions likely triggered the formatter failure reported by CI. The above refactor also reflows lines; still, please run cargo fmt locally.

src/home.rs (2)

36-39: Good guard for build mode/dry-run

Warning users that --ask/--dry have no effect for build is appropriate.


52-60: Display impl LGTM

Clear, minimal Display for variant; matches nixos/darwin patterns elsewhere.

src/darwin.rs (2)

38-42: Good UX warnings

Clear message that --ask/--dry don’t affect build mode.


53-61: Display impl LGTM

Matches Home/NixOS variants; concise.

src/logging.rs Outdated
Comment on lines 92 to 101
#[macro_export]
macro_rules! nh_trace {
($($arg:tt)*) => {
use notify_rust::Urgency;
use crate::notify::NotificationSender;
let message = format!($($arg)*);
tracing::trace!($($arg)*);
NotificationSender::new("nh trace", &message).urgency(Urgency::Low).send().unwrap();
};
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don’t panic in logging macros; use $crate path and handle send() errors

Unwrapping here can crash on D-Bus/libnotify failures. Also prefer $crate::... in macros so downstream crates can use them without path breakage.

 #[macro_export]
 macro_rules! nh_trace {
     ($($arg:tt)*) => {
-        use notify_rust::Urgency;
-        use crate::notify::NotificationSender;
-        let message = format!($($arg)*);
-        tracing::trace!($($arg)*);
-        NotificationSender::new("nh trace", &message).urgency(Urgency::Low).send().unwrap();
+        let message = format!($($arg)*);
+        tracing::trace!($($arg)*);
+        if let Err(e) = $crate::notify::NotificationSender::new("nh trace", &message)
+            .urgency(::notify_rust::Urgency::Low)
+            .send()
+        {
+            tracing::debug!(?e, "Failed to send nh trace notification");
+        }
     };
 }
 
 #[macro_export]
 macro_rules! nh_debug {
     ($($arg:tt)*) => {
-        use notify_rust::Urgency;
-        use crate::notify::NotificationSender;
-
-        let message = format!($($arg)*);
-        tracing::debug!($($arg)*);
-        NotificationSender::new("nh debug", &message).urgency(Urgency::Low).send().unwrap();
+        let message = format!($($arg)*);
+        tracing::debug!($($arg)*);
+        if let Err(e) = $crate::notify::NotificationSender::new("nh debug", &message)
+            .urgency(::notify_rust::Urgency::Low)
+            .send()
+        {
+            tracing::debug!(?e, "Failed to send nh debug notification");
+        }
     };
 }
 
 #[macro_export]
 macro_rules! nh_info {
     ($($arg:tt)*) => {
-        use notify_rust::Urgency;
-        use crate::notify::NotificationSender;
-        let message = format!($($arg)*);
-        tracing::info!($($arg)*);
-        NotificationSender::new("nh info", &message).urgency(Urgency::Normal).send().unwrap();
+        let message = format!($($arg)*);
+        tracing::info!($($arg)*);
+        if let Err(e) = $crate::notify::NotificationSender::new("nh info", &message)
+            .urgency(::notify_rust::Urgency::Normal)
+            .send()
+        {
+            tracing::debug!(?e, "Failed to send nh info notification");
+        }
     };
 }
 
 #[macro_export]
 macro_rules! nh_warn {
     ($($arg:tt)*) => {
-        use notify_rust::Urgency;
-        use crate::notify::NotificationSender;
-        let message = format!($($arg)*);
-        tracing::warn!($($arg)*);
-        NotificationSender::new("nh warn", &message).urgency(Urgency::Normal).send().unwrap();
+        let message = format!($($arg)*);
+        tracing::warn!($($arg)*);
+        if let Err(e) = $crate::notify::NotificationSender::new("nh warn", &message)
+            .urgency(::notify_rust::Urgency::Normal)
+            .send()
+        {
+            tracing::debug!(?e, "Failed to send nh warn notification");
+        }
     };
 }

Also applies to: 103-113, 115-124, 126-135

🤖 Prompt for AI Agents
In src/logging.rs around lines 92 to 101 (and similarly for 103-113, 115-124,
126-135), the macros currently use bare paths and unwrap() on
NotificationSender::send(), which can panic and break downstream crates; change
macro references to use $crate::notify::NotificationSender and
$crate::notify::NotificationSender::new (and fully qualified
notify_rust::Urgency via ::notify_rust::Urgency if needed) so the macros resolve
correctly for downstream users, and replace the .send().unwrap() with
non-panicking error handling (e.g. capture the Result and, on Err(e), emit a
tracing::error! or tracing::debug! with the error) so failures to send
notifications do not cause a panic.

src/logging.rs Outdated
Comment on lines 92 to 135
#[macro_export]
macro_rules! nh_trace {
($($arg:tt)*) => {
use notify_rust::Urgency;
use crate::notify::NotificationSender;
let message = format!($($arg)*);
tracing::trace!($($arg)*);
NotificationSender::new("nh trace", &message).urgency(Urgency::Low).send().unwrap();
};
}

#[macro_export]
macro_rules! nh_debug {
($($arg:tt)*) => {
use notify_rust::Urgency;
use crate::notify::NotificationSender;

let message = format!($($arg)*);
tracing::debug!($($arg)*);
NotificationSender::new("nh debug", &message).urgency(Urgency::Low).send().unwrap();
};
}

#[macro_export]
macro_rules! nh_info {
($($arg:tt)*) => {
use notify_rust::Urgency;
use crate::notify::NotificationSender;
let message = format!($($arg)*);
tracing::info!($($arg)*);
NotificationSender::new("nh info", &message).urgency(Urgency::Normal).send().unwrap();
};
}

#[macro_export]
macro_rules! nh_warn {
($($arg:tt)*) => {
use notify_rust::Urgency;
use crate::notify::NotificationSender;
let message = format!($($arg)*);
tracing::warn!($($arg)*);
NotificationSender::new("nh warn", &message).urgency(Urgency::Normal).send().unwrap();
};
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Macro args: format!($($arg)*) will fail on structured tracing fields

These macros will not compile for calls like nh_info!(%val, key=?x) because format! requires a format string literal. Either constrain usage to literal message forms or add macro arms to handle “message-first” vs. “structured fields” cases, and fall back to a static notification message for non-literal forms.

Run to find non-literal uses:


🏁 Script executed:

#!/bin/bash
# Lists nh_* macro invocations that DO NOT start with a string literal
rg -nP --type=rust 'nh_(trace|debug|info|warn)!\(\s*"(?:[^"\\]|\\.)*"' -v src | sed -n '1,200p'

Length of output: 8911


🏁 Script executed:

#!/bin/bash
# Search for any uses of nh_trace!, nh_debug!, nh_info!, or nh_warn! macros
rg -n --type=rust 'nh_(trace|debug|info|warn)!\(' src

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Find all uses of nh_* macros outside their definition file
rg -nP --type=rust 'nh_(?:trace|debug|info|warn)!\s*\(' -g '!src/logging.rs' src

Length of output: 0


Support structured fields or restrict macros to literal-only messages
The nh_* macros call format!($($arg)*), so invocations like nh_info!(%val, key=?x) won’t compile.

  • Either constrain these macros to literal‐only messages (and document/enforce this restriction)
  • Or add additional arms to capture a format string plus structured fields, falling back to a safe notification body when fields are used
    src/logging.rs:92–135
🧰 Tools
🪛 GitHub Actions: Check formating & lints

[error] 133-133: cargo fmt --check detected formatting differences in this file. Run 'cargo fmt' to fix code style issues.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
src/notify.rs (1)

104-117: Propagate errors and avoid calling show() on macOS; add actions/timeout under Unix

Unwrap will panic; also macOS branch still calls show(). Make ask() return Err on macOS to trigger fallback, and use actions + timeout with proper error propagation.

-use color_eyre::eyre::Result;
+use color_eyre::eyre::{Result, eyre};
@@
-  pub fn ask(mut self) -> Result<bool> {
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      self
-        .notification
-        .urgency(self.urgency.unwrap_or(Urgency::Critical));
-      self.notification.action("accept", "Accept");
-      self.notification.action("reject", "Reject");
-    }
-
-    let handle = self.notification.show().unwrap();
-
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      let mut confirmation = false;
-      handle.wait_for_action(|s| {
-        confirmation = s == "accept";
-      });
-      Ok(confirmation)
-    }
-
-    #[cfg(target_os = "macos")]
-    Ok(false)
-  }
+  pub fn ask(mut self) -> Result<bool> {
+    #[cfg(target_os = "macos")]
+    {
+      // Trigger caller fallback (prompt) on unsupported platform.
+      return Err(eyre!("Notification actions are not supported on macOS"));
+    }
+    #[cfg(all(unix, not(target_os = "macos")))]
+    {
+      self
+        .notification
+        .urgency(self.urgency.unwrap_or(Urgency::Critical))
+        .timeout(0) // require interaction on most servers
+        .action("accept", "Accept")
+        .action("reject", "Reject");
+      let handle = self.notification.show()?; // propagate error
+      let mut confirmation = false;
+      handle.wait_for_action(|s| {
+        confirmation = s == "accept";
+      });
+      Ok(confirmation)
+    }
+  }
src/clean.rs (2)

309-332: Skip ask in dry-run for consistency with home/darwin

Guard the entire ask block with if !args.dry to avoid prompting when nothing will be executed.


309-332: Don’t unwrap ask(); implement Both semantics and fallback to prompt

Unwrap will panic if the server is unavailable; Both is unimplemented. Use explicit Accept, otherwise prompt fallback; log and continue on errors.

     if let Some(ask) = args.ask.as_ref() {
       let confirmation = match ask {
         NotifyAskMode::Prompt => {
           Confirm::new("Confirm the cleanup plan?")
             .with_default(false)
             .prompt()?
         },
         NotifyAskMode::Notify => {
           let clean_mode = match self {
             Self::Profile(_) => "profile",
             Self::All(_) => "all",
             Self::User(_) => "user",
           };
-
-          NotificationSender::new(
-            &format!("nh clean {clean_mode}"),
-            "Confirm the cleanup plan?",
-          )
-          .ask()
-          .unwrap()
+          match NotificationSender::new(
+            &format!("nh clean {clean_mode}"),
+            "Confirm the cleanup plan?",
+          ).ask() {
+            Ok(true) => true,    // accepted via notification
+            Ok(false) => false,  // rejected/dismissed
+            Err(e) => {
+              debug!(?e, "Notification failed; falling back to prompt");
+              Confirm::new("Confirm the cleanup plan?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
         },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          let clean_mode = match self {
+            Self::Profile(_) => "profile",
+            Self::All(_) => "all",
+            Self::User(_) => "user",
+          };
+          match NotificationSender::new(
+            &format!("nh clean {clean_mode}"),
+            "Confirm the cleanup plan?",
+          ).ask() {
+            Ok(true) => true, // accepted via notification
+            _ => {
+              Confirm::new("Confirm the cleanup plan?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
       };
src/home.rs (1)

176-189: Fix placeholder strings, remove unwrap, and implement Both with fallback

Use a meaningful summary, avoid panic on notification failure, and wire Both as “try notify, else prompt.”

     if let Some(ask) = self.common.ask {
       let confirmation = match ask {
         NotifyAskMode::Prompt => {
           inquire::Confirm::new("Apply the config?")
             .with_default(false)
             .prompt()?
         },
         NotifyAskMode::Notify => {
-          NotificationSender::new("nh os rollback", "testing")
-            .ask()
-            .unwrap()
+          match NotificationSender::new(
+            &format!("nh home {}", variant),
+            "Apply the config?",
+          ).ask() {
+            Ok(true) => true,
+            Ok(false) => false,
+            Err(e) => {
+              tracing::debug!(?e, "Notification failed; falling back to prompt");
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
         },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          match NotificationSender::new(
+            &format!("nh home {}", variant),
+            "Apply the config?",
+          ).ask() {
+            Ok(true) => true,
+            _ => {
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
       };
🧹 Nitpick comments (2)
src/notify.rs (2)

85-90: Doc tweak to reflect macOS fallback via error

The docs say Ok(false) on unsupported platforms; with the change above we return Err to enable prompt fallback.

-  /// On unsupported platforms (e.g., macOS, Windows), this function always
-  /// returns `Ok(false)` since interactive actions are not supported.
+  /// On unsupported platforms (e.g., macOS), this function returns an error so
+  /// callers can fall back to a terminal prompt.

25-33: Set appname to “nh” for better source attribution in notification centers

Minor polish; helps users identify the app quickly.

   pub fn new(summary: &str, body: &str) -> Self {
     let mut notification = Notification::new();
+    notification.appname("nh");
     notification.summary(summary);
     notification.body(body);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb22a66 and 7c65e7b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • Cargo.toml (1 hunks)
  • src/clean.rs (2 hunks)
  • src/commands.rs (1 hunks)
  • src/darwin.rs (4 hunks)
  • src/home.rs (5 hunks)
  • src/interface.rs (3 hunks)
  • src/lib.rs (1 hunks)
  • src/logging.rs (1 hunks)
  • src/main.rs (1 hunks)
  • src/nixos.rs (7 hunks)
  • src/notify.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/main.rs
  • Cargo.toml
  • src/lib.rs
  • src/logging.rs
  • src/commands.rs
  • src/nixos.rs
  • src/darwin.rs
  • src/interface.rs
🧰 Additional context used
🧬 Code graph analysis (2)
src/home.rs (3)
src/darwin.rs (1)
  • fmt (54-60)
src/nixos.rs (1)
  • fmt (68-77)
src/notify.rs (2)
  • ask (94-117)
  • new (25-33)
src/clean.rs (1)
src/notify.rs (2)
  • ask (94-117)
  • new (25-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
🔇 Additional comments (2)
src/home.rs (2)

52-60: LGTM: Display impl for HomeRebuildVariant

Simple and correct stringification.


169-174: LGTM: Early return on dry/build with warning

Matches the CLI UX you described; avoids prompting when no activation occurs.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/nixos.rs (1)

322-355: Switch variant calls wrong actions (test/boot) and runs two activations.

For Switch you call switch-to-configuration test and later boot. This is incorrect; Switch should call switch once. Refactor to a single match over variant that passes the right subcommand.

Apply:

-    if let Test | Switch = variant {
-      let switch_to_configuration =
-        target_profile.join("bin").join("switch-to-configuration");
-      ...
-      Command::new(switch_to_configuration)
-        .arg("test")
-        .ssh(self.target_host.clone())
-        .message("Activating configuration")
-        .elevate(elevate.then_some(elevation.clone()))
-        .preserve_envs(["NIXOS_INSTALL_BOOTLOADER"])
-        .with_required_env()
-        .run()
-        .wrap_err("Activation (test) failed")?;
-    }
-
-    if let Boot | Switch = variant {
-      let canonical_out_path = out_path
-        .canonicalize()
-        .context("Failed to resolve output path")?;
-      ...
-      Command::new(switch_to_configuration)
-        .arg("boot")
-        .ssh(self.target_host)
-        .elevate(elevate.then_some(elevation))
-        .message("Adding configuration to bootloader")
-        .preserve_envs(["NIXOS_INSTALL_BOOTLOADER"])
-        .with_required_env()
-        .run()
-        .wrap_err("Bootloader activation failed")?;
-    }
+    match variant {
+      Test => {
+        let switch_to_configuration =
+          target_profile.join("bin").join("switch-to-configuration");
+        if !switch_to_configuration.exists() {
+          return Err(eyre!(/* unchanged help text */));
+        }
+        let switch_to_configuration =
+          switch_to_configuration.canonicalize().context("Failed to resolve switch-to-configuration path")?;
+        let switch_to_configuration =
+          switch_to_configuration.to_str().ok_or_else(|| eyre!("switch-to-configuration path contains invalid UTF-8"))?;
+        Command::new(switch_to_configuration)
+          .arg("test")
+          .ssh(self.target_host.clone())
+          .message("Activating configuration")
+          .elevate(elevate.then_some(elevation.clone()))
+          .preserve_envs(["NIXOS_INSTALL_BOOTLOADER"])
+          .with_required_env()
+          .run()
+          .wrap_err("Activation (test) failed")?;
+      }
+      Switch => {
+        let canonical_out_path = out_path.canonicalize().context("Failed to resolve output path")?;
+        Command::new("nix")
+          .elevate(elevate.then_some(elevation.clone()))
+          .args(["build", "--no-link", "--profile", SYSTEM_PROFILE])
+          .arg(&canonical_out_path)
+          .ssh(self.target_host.clone())
+          .with_required_env()
+          .run()
+          .wrap_err("Failed to set system profile")?;
+
+        let switch_to_configuration =
+          out_path.join("bin").join("switch-to-configuration");
+        if !switch_to_configuration.exists() {
+          return Err(eyre!(/* unchanged help text */));
+        }
+        let switch_to_configuration =
+          switch_to_configuration.canonicalize().context("Failed to resolve switch-to-configuration path")?;
+        let switch_to_configuration =
+          switch_to_configuration.to_str().ok_or_else(|| eyre!("switch-to-configuration path contains invalid UTF-8"))?;
+        Command::new(switch_to_configuration)
+          .arg("switch")
+          .ssh(self.target_host.clone())
+          .elevate(elevate.then_some(elevation.clone()))
+          .message("Activating configuration")
+          .preserve_envs(["NIXOS_INSTALL_BOOTLOADER"])
+          .with_required_env()
+          .run()
+          .wrap_err("Activation (switch) failed")?;
+      }
+      Boot => {
+        let canonical_out_path = out_path.canonicalize().context("Failed to resolve output path")?;
+        Command::new("nix")
+          .elevate(elevate.then_some(elevation.clone()))
+          .args(["build", "--no-link", "--profile", SYSTEM_PROFILE])
+          .arg(&canonical_out_path)
+          .ssh(self.target_host.clone())
+          .with_required_env()
+          .run()
+          .wrap_err("Failed to set system profile")?;
+
+        let switch_to_configuration =
+          out_path.join("bin").join("switch-to-configuration");
+        if !switch_to_configuration.exists() {
+          return Err(eyre!(/* unchanged help text */));
+        }
+        let switch_to_configuration =
+          switch_to_configuration.canonicalize().context("Failed to resolve switch-to-configuration path")?;
+        let switch_to_configuration =
+          switch_to_configuration.to_str().ok_or_else(|| eyre!("switch-to-configuration path contains invalid UTF-8"))?;
+        Command::new(switch_to_configuration)
+          .arg("boot")
+          .ssh(self.target_host.clone())
+          .elevate(elevate.then_some(elevation.clone()))
+          .message("Adding configuration to bootloader")
+          .preserve_envs(["NIXOS_INSTALL_BOOTLOADER"])
+          .with_required_env()
+          .run()
+          .wrap_err("Bootloader activation failed")?;
+      }
+      Build | BuildVm => { /* already returned earlier */ }
+    }

Also applies to: 357-403

♻️ Duplicate comments (3)
src/notify.rs (1)

94-117: Don’t unwrap; avoid calling show() on macOS; gate platform paths cleanly.

show().unwrap() will panic if no server is available; also show() is called unconditionally before the macOS Ok(false) branch. Move show() into the Unix branch and propagate errors. Add a no‑Unix fallback returning Ok(false).

Apply:

   pub fn ask(mut self) -> Result<bool> {
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      self
-        .notification
-        .urgency(self.urgency.unwrap_or(Urgency::Critical));
-      self.notification.action("accept", "Accept");
-      self.notification.action("reject", "Reject");
-    }
-
-    let handle = self.notification.show().unwrap();
-
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      let mut confirmation = false;
-      handle.wait_for_action(|s| {
-        confirmation = s == "accept";
-      });
-      Ok(confirmation)
-    }
-
-    #[cfg(target_os = "macos")]
-    Ok(false)
+    #[cfg(target_os = "macos")]
+    {
+      // Actions unsupported via notify-rust on macOS; let callers fall back.
+      return Ok(false);
+    }
+
+    #[cfg(all(unix, not(target_os = "macos")))]
+    {
+      self.notification
+        .urgency(self.urgency.unwrap_or(Urgency::Critical))
+        .timeout(0)
+        .action("accept", "Accept")
+        .action("reject", "Reject");
+
+      let handle = self.notification.show()?; // propagate error
+      let mut confirmation = false;
+      handle.wait_for_action(|s| {
+        confirmation = s == "accept";
+      });
+      return Ok(confirmation);
+    }
+
+    #[cfg(not(unix))]
+    {
+      // Unsupported platform: no interactive actions.
+      Ok(false)
+    }
   }
src/nixos.rs (2)

475-494: Mirror the ask() fix in rollback; remove unwrap and implement Both.

Same unwrap panic and unimplemented Both as rebuild. Fall back to prompt on error; send+prompt for Both.

Apply:

-    if let Some(ask) = self.ask.as_ref() {
-      let confirmation = match ask {
-        NotifyAskMode::Prompt => {
-          inquire::Confirm::new(&format!(
-            "Roll back to generation {}?",
-            target_generation.number
-          ))
-          .with_default(false)
-          .prompt()?
-        },
-        NotifyAskMode::Notify => {
-          NotificationSender::new(
-            "nh os rollback",
-            &format!("Roll back to generation {}?", target_generation.number),
-          )
-          .ask()
-          .unwrap()
-        },
-        NotifyAskMode::Both => unimplemented!(),
-      };
+    if let Some(ask) = self.ask.as_ref() {
+      let question = format!("Roll back to generation {}?", target_generation.number);
+      let confirmation = match ask {
+        NotifyAskMode::Prompt => {
+          inquire::Confirm::new(&question).with_default(false).prompt()?
+        },
+        NotifyAskMode::Notify => {
+          match NotificationSender::new("nh os rollback", &question).ask() {
+            Ok(b) => b,
+            Err(e) => {
+              warn!("Notification prompt failed ({e}); falling back to terminal prompt");
+              inquire::Confirm::new(&question).with_default(false).prompt()?
+            }
+          }
+        },
+        NotifyAskMode::Both => {
+          let _ = NotificationSender::new("nh os rollback", &question).send();
+          inquire::Confirm::new(&question).with_default(false).prompt()?
+        },
+      };

281-297: Avoid panic on notification ask; implement Both semantics with prompt fallback.

.unwrap() can panic and Both is unimplemented!(). Propagate errors and show a terminal prompt on failure; for Both, send a passive notification and still prompt.

Apply:

-    if let Some(ask) = self.common.ask {
-      let confirmation = match ask {
-        NotifyAskMode::Prompt => {
-          inquire::Confirm::new("Apply the config?")
-            .with_default(false)
-            .prompt()?
-        },
-        NotifyAskMode::Notify => {
-          NotificationSender::new(
-            &format!("nh os {variant}"),
-            "Apply the config?",
-          )
-          .ask()
-          .unwrap()
-        },
-        NotifyAskMode::Both => unimplemented!(),
-      };
+    if let Some(ask) = self.common.ask {
+      let question = "Apply the config?";
+      let confirmation = match ask {
+        NotifyAskMode::Prompt => {
+          inquire::Confirm::new(question).with_default(false).prompt()?
+        },
+        NotifyAskMode::Notify => {
+          match NotificationSender::new(&format!("nh os {variant}"), question).ask() {
+            Ok(b) => b,
+            Err(e) => {
+              warn!("Notification prompt failed ({e}); falling back to terminal prompt");
+              inquire::Confirm::new(question).with_default(false).prompt()?
+            }
+          }
+        },
+        NotifyAskMode::Both => {
+          let _ = NotificationSender::new(&format!("nh os {variant}"), question).send();
+          inquire::Confirm::new(question).with_default(false).prompt()?
+        },
+      };
🧹 Nitpick comments (4)
src/notify.rs (4)

85-90: Doc/platform mismatch (Windows).

Comment says “macOS, Windows” return Ok(false), but only macOS is handled. Either implement a non‑Unix fallback (see previous diff) or narrow the doc to macOS.


25-33: Set app name for notification grouping.

Add .appname("nh") once at construction to improve DE integration.

   pub fn new(summary: &str, body: &str) -> Self {
     let mut notification = Notification::new();
+    notification.appname("nh");
     notification.summary(summary);
     notification.body(body);

60-73: Optional: carry urgency across platforms and consider a sane default timeout.

If acceptable, set a short .timeout(...) for non‑interactive sends and optionally an icon. Keep the current NH_NOTIFY gate.

Would you like me to wire a cross‑platform icon (or inline SVG) per maintainer preference?


1-11: Minor: use the imported type in the field.

notification: Notification reads cleaner than notify_rust::Notification since Notification is imported.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c65e7b and a27af9b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • Cargo.toml (1 hunks)
  • src/clean.rs (2 hunks)
  • src/commands.rs (1 hunks)
  • src/darwin.rs (4 hunks)
  • src/home.rs (5 hunks)
  • src/interface.rs (3 hunks)
  • src/lib.rs (1 hunks)
  • src/logging.rs (1 hunks)
  • src/main.rs (1 hunks)
  • src/nixos.rs (7 hunks)
  • src/notify.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/lib.rs
  • Cargo.toml
  • src/commands.rs
  • src/main.rs
  • src/logging.rs
  • src/home.rs
  • src/clean.rs
  • src/darwin.rs
  • src/interface.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/nixos.rs (3)
src/darwin.rs (1)
  • fmt (54-60)
src/home.rs (1)
  • fmt (53-59)
src/notify.rs (2)
  • ask (94-117)
  • new (25-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
🔇 Additional comments (3)
src/notify.rs (1)

60-60: Should ask() observe NH_NOTIFY?

send() is gated by NH_NOTIFY=="1", ask() is not. If the env var is intended as a global kill‑switch, mirror the gate in ask(); if not, document that ask() ignores NH_NOTIFY.

src/nixos.rs (2)

67-78: Display impl reads well.

Variant strings are consistent and used across messages. LGTM.


45-47: Good UX: warn on ineffective flags.

Clear message when --ask/--dry don’t apply to build. LGTM.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/nixos.rs (2)

281-296: NotifyAskMode::Both panics at runtime (unimplemented!)

Calling --ask=both will abort. Implement non-invasive “Both” semantics: show a non-blocking notification and still prompt in the terminal.

Apply:

-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          let _ = NotificationSender::new(
+            &format!("nh os {variant}"),
+            "Apply the config?",
+          ).send();
+          inquire::Confirm::new("Apply the config?")
+            .with_default(false)
+            .prompt()?
+        },

474-492: NotifyAskMode::Both also unimplemented in rollback

Same panic risk as rebuild. Mirror the “Both = notify (non-blocking) + prompt” pattern and provide a specific body.

Apply:

-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          let _ = NotificationSender::new(
+            "nh os rollback",
+            &format!("Roll back to generation {}?", target_generation.number),
+          ).send();
+          inquire::Confirm::new(&format!(
+            "Roll back to generation {}?",
+            target_generation.number
+          ))
+          .with_default(false)
+          .prompt()?
+        },
🧹 Nitpick comments (1)
src/nixos.rs (1)

274-279: Misleading warn message when not a dry run

This branch triggers for Build/BuildVm or dry-run; the message blames only dry-run.

Apply:

-      if self.common.ask.is_some() {
-        warn!("--ask has no effect as dry run was requested");
-      }
+      if self.common.ask.is_some() {
+        warn!("--ask has no effect for `nh os {variant}` or when using --dry");
+      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a27af9b and 125c048.

📒 Files selected for processing (9)
  • src/clean.rs (2 hunks)
  • src/commands.rs (3 hunks)
  • src/darwin.rs (4 hunks)
  • src/generations.rs (2 hunks)
  • src/home.rs (5 hunks)
  • src/interface.rs (3 hunks)
  • src/logging.rs (1 hunks)
  • src/nixos.rs (7 hunks)
  • src/notify.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/commands.rs
  • src/home.rs
  • src/notify.rs
  • src/logging.rs
  • src/darwin.rs
  • src/interface.rs
  • src/clean.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/nixos.rs (3)
src/darwin.rs (1)
  • fmt (54-60)
src/home.rs (1)
  • fmt (53-59)
src/notify.rs (2)
  • ask (95-118)
  • new (25-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: treewide-checks
🔇 Additional comments (4)
src/nixos.rs (3)

67-78: Nice: Display impl for OsRebuildVariant

Clear, human-friendly variant names; useful for logs/UI.


45-47: Good guardrail for build: warns early and avoids noop flags

This matches expected UX for nh os build.


404-405: LGTM: final path debug message

Helpful completion trace with nh_debug!.

src/generations.rs (1)

342-347: Formatting bug: width specifier is invalid; will not compile

{name:<width$} uses a non-numeric positional specifier. Use dynamic width via nested braces or pass width as a named arg.

Apply:

-      format!("{name:<width$}")
+      format!("{name:<{width}}", width = width)

Likely an incorrect or invalid review comment.

@r0chd r0chd force-pushed the notifications branch 3 times, most recently from 29f4862 to 34f38e8 Compare September 17, 2025 20:03
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
src/darwin.rs (1)

158-176: Don’t unwrap notification send; use meaningful body and keep notify-then-prompt

Unwrap can panic if no server is available; also replace “testing” with the actual question. Keep semantics: notify (best-effort) then prompt.

-          // MacOS doesn't support notification actions
-          NotifyAskMode::Notify | NotifyAskMode::Both => {
-            NotificationSender::new(&format!("nh darwin {variant}"), "testing")
-              .send()
-              .unwrap();
-
-            inquire::Confirm::new("Apply the config?")
-              .with_default(false)
-              .prompt()?
-          },
+          // macOS: no actions; notify then prompt
+          NotifyAskMode::Notify | NotifyAskMode::Both => {
+            if let Err(e) = NotificationSender::new(
+              &format!("nh darwin {variant}"),
+              "Apply the config?",
+            ).send() {
+              debug!(?e, "Notification failed; continuing with prompt");
+            }
+            inquire::Confirm::new("Apply the config?")
+              .with_default(false)
+              .prompt()?
+          },
src/nixos.rs (2)

474-492: Same issues on rollback: unimplemented! and panic risk

Mirror the rebuild fix: implement Both and ensure ask path won’t panic when notifications fail.

         NotifyAskMode::Notify => {
           NotificationSender::new(
             "nh os rollback",
             &format!("Roll back to generation {}?", target_generation.number),
           )
-          .ask()
+          .ask()
         },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          let _ = NotificationSender::new(
+            "nh os rollback",
+            &format!("Roll back to generation {}?", target_generation.number),
+          ).send();
+          inquire::Confirm::new(&format!(
+            "Roll back to generation {}?",
+            target_generation.number
+          ))
+          .with_default(false)
+          .prompt()?
+        },

281-296: Unimplemented! in user path and potential panic via ask()

  • NotifyAskMode::Both => unimplemented!() will crash on --ask=both.
  • NotificationSender::ask() currently calls show().unwrap() (see src/notify.rs), which can panic if no server is running.

Implement Both as “notify (best-effort) then prompt” and rely on a non-panicking ask() (see proposed fix in src/notify.rs).

         NotifyAskMode::Notify => {
           NotificationSender::new(
             &format!("nh os {variant}"),
             "Apply the config?",
           )
-          .ask()
+          .ask()
         },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          let _ = NotificationSender::new(
+            &format!("nh os {variant}"),
+            "Apply the config?",
+          ).send();
+          inquire::Confirm::new("Apply the config?")
+            .with_default(false)
+            .prompt()?
+        },
src/notify.rs (1)

100-127: ask(): avoid panic; don’t call show() on macOS; make errors non-fatal

Current show().unwrap() will abort on headless/minimal systems and is executed even on macOS. Restructure under cfgs and handle errors by returning false, letting callers fall back to the terminal prompt.

-  pub fn ask(mut self) -> bool {
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      self
-        .notification
-        .urgency(self.urgency.unwrap_or(Urgency::Critical));
-      self.notification.action("accept", "Accept");
-      self.notification.action("reject", "Reject");
-    }
-
-    let handle = self.notification.show().unwrap();
-
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      let mut confirmation = false;
-      handle.wait_for_action(|s| {
-        confirmation = s == "accept";
-      });
-      confirmation
-    }
-
-    #[cfg(target_os = "macos")]
-    {
-      _ = &mut self;
-      _ = handle;
-      false
-    }
-  }
+  pub fn ask(mut self) -> bool {
+    // macOS: notify-rust does not support actionable notifications; let caller fall back.
+    #[cfg(target_os = "macos")]
+    {
+      let _ = &mut self;
+      return false;
+    }
+
+    // Unix (non-macOS): actionable notification with Accept/Reject
+    #[cfg(all(unix, not(target_os = "macos")))]
+    {
+      self
+        .notification
+        .urgency(self.urgency.unwrap_or(Urgency::Critical));
+      self.notification.action("accept", "Accept");
+      self.notification.action("reject", "Reject");
+
+      let handle = match self.notification.show() {
+        Ok(h) => h,
+        Err(_e) => return false, // server unavailable; let caller fall back
+      };
+
+      let mut confirmation = false;
+      handle.wait_for_action(|s| {
+        confirmation = s == "accept";
+      });
+      return confirmation;
+    }
+
+    // Other targets: unsupported
+    false
+  }
🧹 Nitpick comments (4)
src/interface.rs (3)

259-267: Tri-state ask mode: solid addition; consider Copy

Enum looks good and documented. Derive Copy to avoid needless moves for a tiny POD.

-#[derive(Debug, Clone, ValueEnum, PartialEq)]
+#[derive(Debug, Clone, Copy, ValueEnum, PartialEq)]
 pub enum NotifyAskMode {

276-278: Clap attribute consistency for --ask

You use value_enum here but not on the other ask options. Clap infers ValueEnum from the type, so drop it here for consistency and clearer help output.

-  #[arg(long, short, value_enum, default_missing_value = "prompt", num_args = 0..=1)]
+  #[arg(long, short, default_missing_value = "prompt", num_args = 0..=1)]
   pub ask: Option<NotifyAskMode>,

440-442: Align ask flags across subcommands

Same note as above: ensure the help text shows possible values uniformly across commands; with the change in OsRollbackArgs, all three will behave the same.

src/notify.rs (1)

60-78: send(): ok; consider minor polish

Optional: set app name and (if desired) a cross‑platform icon; keep behind env if you prefer.

-    self.notification.show()?;
+    // Optionally tag the app name for some servers:
+    // self.notification.appname("nh");
+    self.notification.show()?;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29f4862 and 34f38e8.

📒 Files selected for processing (9)
  • src/clean.rs (2 hunks)
  • src/commands.rs (3 hunks)
  • src/darwin.rs (4 hunks)
  • src/generations.rs (2 hunks)
  • src/home.rs (5 hunks)
  • src/interface.rs (3 hunks)
  • src/logging.rs (1 hunks)
  • src/nixos.rs (7 hunks)
  • src/notify.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/generations.rs
  • src/logging.rs
  • src/commands.rs
  • src/home.rs
  • src/clean.rs
🧰 Additional context used
🧬 Code graph analysis (3)
src/darwin.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/nixos.rs (3)
src/darwin.rs (1)
  • fmt (54-60)
src/home.rs (1)
  • fmt (53-59)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/notify.rs (1)
src/commands.rs (3)
  • new (160-171)
  • new (601-609)
  • new (723-732)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: treewide-checks
🔇 Additional comments (7)
src/interface.rs (1)

308-310: Back-compat semantics check for tri‑state --ask

Confirm that bare --ask yields Some(Prompt) (via default_missing_value = "prompt") and absence of the flag yields None (back-compat: no prompt by default). If that’s intended across commands, we’re good.

src/darwin.rs (2)

38-41: Correct: warn that --ask/--dry don’t affect build

Clear UX. No issues.


53-61: Nice: Display impl for variant

Helps keep messages consistent.

src/nixos.rs (4)

45-49: Correct: warn that --ask/--dry don’t affect build

Matches darwin; reduces confusion.


67-78: Good: Display impl for variant

Keeps user-facing strings tidy.


274-279: Dry/build gating message is right

Warning on --ask in dry/build paths is appropriate.


404-405: LGTM: use nh_debug! for final path

Consistent with new logging macros.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/util/platform.rs (1)

275-286: Compile error: using self in a free function; pass the local passthrough.

.passthrough(&self.passthrough) won’t compile (no self here). Use the variable computed at Line 275.

Apply:

-        .passthrough(&self.passthrough)
+        .passthrough(&passthrough)
♻️ Duplicate comments (5)
src/notify.rs (2)

100-127: Don’t unwrap; avoid calling show() on macOS; propagate errors and gate by NH_NOTIFY.

show().unwrap() will panic on systems without a notification server; macOS path still evaluates show() before returning. Reorder cfg blocks, return early on macOS, and use ? to propagate errors. Also consider honoring NH_NOTIFY like send() does.

Apply:

-  pub fn ask(mut self) -> bool {
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      self
-        .notification
-        .urgency(self.urgency.unwrap_or(Urgency::Critical));
-      self.notification.action("accept", "Accept");
-      self.notification.action("reject", "Reject");
-    }
-
-    let handle = self.notification.show().unwrap();
-
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      let mut confirmation = false;
-      handle.wait_for_action(|s| {
-        confirmation = s == "accept";
-      });
-      confirmation
-    }
-
-    #[cfg(target_os = "macos")]
-    {
-      _ = &mut self;
-      _ = handle;
-      false
-    }
-  }
+  pub fn ask(mut self) -> Result<bool> {
+    // Respect global toggle, like `send()`
+    let enable_notifications = env::var("NH_NOTIFY").is_ok_and(|v| v == "1");
+    if !enable_notifications {
+      return Ok(false);
+    }
+
+    #[cfg(target_os = "macos")]
+    {
+      // Actions not supported on macOS via notify-rust backend.
+      return Ok(false);
+    }
+
+    #[cfg(all(unix, not(target_os = "macos")))]
+    {
+      self
+        .notification
+        .urgency(self.urgency.unwrap_or(Urgency::Critical))
+        .timeout(0) // require interaction on most servers
+        .action("accept", "Accept")
+        .action("reject", "Reject");
+
+      let handle = self.notification.show()?; // propagate error
+      let mut confirmation = false;
+      handle.wait_for_action(|s| {
+        confirmation = s == "accept";
+      });
+      return Ok(confirmation);
+    }
+
+    #[allow(unreachable_code)]
+    Ok(false)
+  }

Optional: set .appname("nh") and consider a neutral icon if maintainers approve. Based on learnings.


80-99: Doc/signature mismatch in ask(); adjust API to return Result or fix docs.

Docs say “Returns an error if the notification cannot be shown” and describe Ok(true)/Ok(false), but the function returns bool. This prevents callers from falling back cleanly on transport errors. Recommend pub fn ask(self) -> Result<bool>.

Apply:

-  /// # Errors
-  ///
-  /// Returns an error if the notification cannot be shown.
-  #[must_use]
-  pub fn ask(mut self) -> bool {
+  /// # Errors
+  ///
+  /// Returns an error if the notification cannot be shown.
+  pub fn ask(mut self) -> Result<bool> {
src/clean.rs (2)

337-353: Remove unimplemented!() for Both; implement “notify, else prompt” fallback.

Leaving Both unimplemented will panic at runtime. Implement: try notification; if it returns Err or false, fall back to terminal prompt.

Apply:

-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          match NotificationSender::new(
+            "nh clean",
+            "Do you want to confirm the cleanup plan?",
+          )
+          .ask() {
+            Ok(true) => true,
+            // rejected, dismissed, unsupported, or failed -> prompt
+            _ => {
+              Confirm::new("Confirm the cleanup plan?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },

Note: depends on notify::ask() returning Result as proposed.


345-351: Handle notification transport errors; don’t assume success.

Update the Notify path to handle Result<bool> and fall back on errors.

Apply:

-        NotifyAskMode::Notify => {
-          NotificationSender::new(
-            "nh clean",
-            "Do you want to confirm the cleanup plan?",
-          )
-          .ask()
-        },
+        NotifyAskMode::Notify => match NotificationSender::new(
+          "nh clean",
+          "Do you want to confirm the cleanup plan?",
+        ).ask() {
+          Ok(v) => v,              // true = accepted; false = rejected/dismissed/unsupported
+          Err(_e) => {
+            // Optional: debug!(?e, "Notification failed; falling back to prompt");
+            Confirm::new("Confirm the cleanup plan?")
+              .with_default(false)
+              .prompt()?
+          }
+        },
src/nixos.rs (1)

470-488: Critical: Same panic and unimplemented issues in rollback confirmation.

Identical problems as the rebuild path (lines 293-308):

  1. NotificationSender::ask() panics internally on notification failures
  2. NotifyAskMode::Both => unimplemented!() will crash
  3. No fallback when notifications fail

Apply the same fix pattern as suggested for rebuild:

     if let Some(ask) = &self.ask {
       let confirmation = match ask {
         NotifyAskMode::Prompt => {
           inquire::Confirm::new(&format!(
             "Roll back to generation {}?",
             target_generation.number
           ))
           .with_default(false)
           .prompt()?
         },
         NotifyAskMode::Notify => {
-          NotificationSender::new(
-            "nh os rollback",
-            &format!("Roll back to generation {}?", target_generation.number),
-          )
-          .ask()
+          match NotificationSender::new(
+            "nh os rollback",
+            &format!("Roll back to generation {}?", target_generation.number),
+          ).ask() {
+            Ok(confirmed) => confirmed,
+            Err(e) => {
+              warn!("Notification prompt failed: {e}; falling back to terminal prompt");
+              inquire::Confirm::new(&format!(
+                "Roll back to generation {}?",
+                target_generation.number
+              ))
+              .with_default(false)
+              .prompt()?
+            }
+          }
         },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          let _ = NotificationSender::new(
+            "nh os rollback",
+            &format!("Roll back to generation {}?", target_generation.number),
+          ).send();
+          inquire::Confirm::new(&format!(
+            "Roll back to generation {}?",
+            target_generation.number
+          ))
+          .with_default(false)
+          .prompt()?
+        },
       };
🧹 Nitpick comments (2)
src/notify.rs (1)

9-11: Minor tidy-ups: use imported type and set app name (optional).

  • Field can use imported Notification instead of fully-qualified path.
  • Consider .appname("nh") before show() to improve UX consistency.

Apply:

-  notification: notify_rust::Notification,
+  notification: Notification,

And (near send()):

-    self.notification.show()?;
+    self.notification.appname("nh").show()?;

Also applies to: 66-76

src/clean.rs (1)

360-399: Optional: skip prompting in dry-run for consistency with other flows.

Consider skipping confirmations when args.dry is true, mirroring platform::confirm_action().

-    if let Some(ask) = &args.ask {
+    if !args.dry {
+      if let Some(ask) = &args.ask {
         /* ... existing confirmation logic ... */
-    }
+      }
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34f38e8 and f2b7ffd.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml (1 hunks)
  • src/clean.rs (3 hunks)
  • src/commands.rs (4 hunks)
  • src/darwin.rs (4 hunks)
  • src/home.rs (3 hunks)
  • src/interface.rs (3 hunks)
  • src/lib.rs (1 hunks)
  • src/logging.rs (1 hunks)
  • src/main.rs (1 hunks)
  • src/nixos.rs (12 hunks)
  • src/notify.rs (1 hunks)
  • src/util.rs (2 hunks)
  • src/util/platform.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/main.rs
  • src/logging.rs
  • Cargo.toml
  • src/home.rs
  • src/darwin.rs
  • src/commands.rs
🧰 Additional context used
🧬 Code graph analysis (3)
src/nixos.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/clean.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/notify.rs (1)
src/commands.rs (3)
  • new (164-175)
  • new (619-627)
  • new (741-750)
🪛 GitHub Actions: Check formating & lints
src/util.rs

[error] 1-1: Rust fmt check reported formatting changes in this file. Run 'cargo fmt' to fix.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
🔇 Additional comments (9)
src/util/platform.rs (1)

189-196: LGTM on switching to nh_info!

Switching the prompt preface to nh_info! aligns with the new logging path.

src/lib.rs (1)

14-15: Module exposure looks good.

pub mod notify; is correctly added; no further issues.

src/util.rs (2)

329-333: LGTM on log macro change.

nh_info! improves consistency with the new notification-integrated logging.


1-1: Run cargo fmt to fix formatting issues in this file.

File src/util.rs uses 2-space indentation throughout (lines 1–40+), but the standard Rust formatter expects 4-space indentation. Execute cargo fmt --all locally and push the changes.

src/clean.rs (1)

522-526: LGTM on nh_info! for removals.

User-facing message is appropriate and consistent.

src/nixos.rs (3)

8-8: LGTM: Import changes align with the new notification infrastructure.

The removal of info from tracing and addition of nh_info!, NotifyAskMode, and NotificationSender correctly support the tri-state confirmation system.

Also applies to: 18-18, 26-27


45-45: LGTM: Option handling is correct.

The is_some() checks correctly handle the new Option<NotifyAskMode> type while preserving the original warning logic.

Also applies to: 281-281


425-425: LGTM: nh_info! macro usage is appropriate.

The replacement of tracing::info! with nh_info! correctly surfaces important user-facing messages (progress updates, success confirmations, instructions) while integrating with the notification system.

Also applies to: 463-466, 505-505, 537-537, 562-565, 671-682, 705-708

src/interface.rs (1)

267-275: LGTM: Enum definition is well-structured.

The NotifyAskMode enum with its three variants (Prompt, Notify, Both) is properly defined with appropriate derives and clear documentation. Note that the Both variant implementation remains incomplete in usage sites (see comments on src/nixos.rs).

#[arg(long, short, value_enum, default_missing_value = "prompt", num_args = 0..=1)]
pub ask: Option<NotifyAskMode>,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Critical: Missing value_enum attribute causes CLI parsing failures.

OsRollbackArgs.ask (line 284) includes value_enum in its clap attribute, but CommonRebuildArgs.ask (line 316) and CleanArgs.ask (line 448) do not. Without value_enum, clap cannot parse the enum variants from the command line, so --ask=notify and --ask=both will fail with a parsing error.

Apply this fix:

   /// Ask for confirmation
-  #[arg(long, short, default_missing_value = "prompt", num_args = 0..=1)]
+  #[arg(long, short, value_enum, default_missing_value = "prompt", num_args = 0..=1)]
   pub ask: Option<NotifyAskMode>,

for both CommonRebuildArgs (line 316) and CleanArgs (line 448).

Run this script to verify all ask fields consistently use value_enum:

Also applies to: 316-317, 448-449


🏁 Script executed:

#!/bin/bash
# Find all ask field declarations and their attributes
rg -n -A1 -B1 'pub ask: Option<NotifyAskMode>' src/interface.rs

# Check which ones have value_enum in their clap attribute
rg -n -B2 'pub ask: Option<NotifyAskMode>' src/interface.rs | grep -E '(#\[arg|pub ask)'

Length of output: 740




🏁 Script executed:

#!/bin/bash
# Find NotifyAskMode definition and its traits/derives
rg -n -A10 'enum NotifyAskMode' src/interface.rs

# Check if NotifyAskMode is derived with any trait that enables FromStr
rg -n -B5 'enum NotifyAskMode' src/interface.rs | head -20

Length of output: 440


🏁 Script executed:

#!/bin/bash
# Search for any FromStr implementation for NotifyAskMode
rg -n 'impl.*FromStr.*NotifyAskMode|NotifyAskMode.*FromStr' src/

# Also search for clap derive or value_enum usage patterns
rg -n '#\[derive.*Clap|#\[derive.*ValueEnum' src/interface.rs | head -10

Length of output: 136




🏁 Script executed:

#!/bin/bash
# Search for tests exercising the --ask flag
rg -n '\-\-ask|ask.*Option.*NotifyAskMode' tests/ --type rust 2>/dev/null | head -20

# Also check if there's any test or integration test
fd -e rs tests/ 2>/dev/null | head -10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check clap's ValueEnum behavior - verify that derived ValueEnum works without explicit value_enum attribute
# Search for any documentation or comments about this pattern
rg -n 'ValueEnum|value_enum' src/interface.rs | head -20

Length of output: 438


Add missing value_enum attribute to fix CLI parsing failures.

The verification confirms the issue: NotifyAskMode derives ValueEnum, but the value_enum attribute in the #[arg(...)] macro is still required to enable proper enum parsing. Without it, clap cannot parse enum variants from command-line arguments.

  • Line 284 (OsRollbackArgs): ✓ Correct, has value_enum
  • Line 316 (CommonRebuildArgs): ✗ Missing value_enum
  • Line 448 (CleanArgs): ✗ Missing value_enum

Apply this fix to lines 316 and 448:

-  #[arg(long, short, default_missing_value = "prompt", num_args = 0..=1)]
+  #[arg(long, short, value_enum, default_missing_value = "prompt", num_args = 0..=1)]
   pub ask: Option<NotifyAskMode>,

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/interface.rs around lines 316 and 448, the #[arg(...)] macros for the
fields using NotifyAskMode are missing the value_enum attribute, which prevents
clap from parsing enum variants even though NotifyAskMode derives ValueEnum;
update those two arg attributes to include value_enum (matching the one at line
284) so the enum is parsed correctly from CLI arguments.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (7)
src/clean.rs (1)

338-353: Implement safe ask flow and define Both semantics; avoid panics
Current Notify path depends on ask() panicking on delivery failure, and Both is unimplemented. Switch to a Result-based ask() and implement “notify, else prompt” fallback.

-    if let Some(ask) = &args.ask {
-      let confirmation = match ask {
-        NotifyAskMode::Prompt => {
-          Confirm::new("Confirm the cleanup plan?")
-            .with_default(false)
-            .prompt()?
-        },
-        NotifyAskMode::Notify => {
-          NotificationSender::new(
-            "nh clean",
-            "Do you want to confirm the cleanup plan?",
-          )
-          .ask()
-        },
-        NotifyAskMode::Both => unimplemented!(),
-      };
+    if let Some(ask) = &args.ask {
+      let confirmation = match ask {
+        NotifyAskMode::Prompt => {
+          Confirm::new("Confirm the cleanup plan?")
+            .with_default(false)
+            .prompt()?
+        },
+        NotifyAskMode::Notify => {
+          match NotificationSender::new(
+            "nh clean",
+            "Confirm the cleanup plan?",
+          ).ask() {
+            Ok(true) => true,
+            Ok(false) => false, // rejected/dismissed
+            Err(e) => {
+              debug!(?e, "Notification failed; falling back to prompt");
+              Confirm::new("Confirm the cleanup plan?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
+        NotifyAskMode::Both => {
+          match NotificationSender::new(
+            "nh clean",
+            "Confirm the cleanup plan?",
+          ).ask() {
+            Ok(true) => true,
+            _ => {
+              Confirm::new("Confirm the cleanup plan?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
+      };

Note: requires NotificationSender::ask() -> Result. Based on learnings.

src/logging.rs (1)

98-118: Don’t unwrap in macros; use $crate path and handle errors; support structured fields
Unwrap can crash when no server. Also format!($($arg)*) fails for structured-field calls. Handle errors and add a literal-first arm.

 #[macro_export]
 macro_rules! nh_info {
-    ($($arg:tt)*) => {{
-        use notify_rust::Urgency;
-        use $crate::notify::NotificationSender;
-        let message = format!($($arg)*);
-        tracing::info!($($arg)*);
-        NotificationSender::new("nh info", &message).urgency(Urgency::Normal).send().unwrap();
-    }};
+    // literal message + optional fields
+    ($fmt:literal $(, $fields:tt)*) => {{
+        let message = format!($fmt $(, $fields)*);
+        tracing::info!($fmt $(, $fields)*);
+        if let Err(e) = $crate::notify::NotificationSender::new("nh info", &message)
+            .urgency(::notify_rust::Urgency::Normal)
+            .send()
+        {
+            tracing::debug!(?e, "Failed to send nh info notification");
+        }
+    }};
+    // fallback: just forward to tracing (avoids format! on non-literals)
+    ($($arg:tt)*) => {{
+        tracing::info!($($arg)*);
+    }};
 }
 
 #[macro_export]
 macro_rules! nh_warn {
-    ($($arg:tt)*) => {{
-        use notify_rust::Urgency;
-        use $crate::notify::NotificationSender;
-        let message = format!($($arg)*);
-        tracing::warn!($($arg)*);
-        NotificationSender::new("nh warn", &message).urgency(Urgency::Normal).send().unwrap();
-    }};
+    ($fmt:literal $(, $fields:tt)*) => {{
+        let message = format!($fmt $(, $fields)*);
+        tracing::warn!($fmt $(, $fields)*);
+        if let Err(e) = $crate::notify::NotificationSender::new("nh warn", &message)
+            .urgency(::notify_rust::Urgency::Normal)
+            .send()
+        {
+            tracing::debug!(?e, "Failed to send nh warn notification");
+        }
+    }};
+    ($($arg:tt)*) => {{
+        tracing::warn!($($arg)*);
+    }};
 }
src/home.rs (1)

168-183: Use Result-based ask with fallback; implement Both
Mirror clean.rs: notify ask with explicit Accept; on Err, fall back to prompt; Both = try notify, else prompt.

-    if let Some(ask) = &self.common.ask {
-      let confirmation = match ask {
-        NotifyAskMode::Prompt => {
-          inquire::Confirm::new("Apply the config?")
-            .with_default(false)
-            .prompt()?
-        },
-        NotifyAskMode::Notify => {
-          NotificationSender::new(
-            "nh home switch",
-            "Do you want to apply the Home Manager configuration?",
-          )
-          .ask()
-        },
-        NotifyAskMode::Both => unimplemented!(),
-      };
+    if let Some(ask) = &self.common.ask {
+      let confirmation = match ask {
+        NotifyAskMode::Prompt => {
+          inquire::Confirm::new("Apply the config?")
+            .with_default(false)
+            .prompt()?
+        },
+        NotifyAskMode::Notify => {
+          match NotificationSender::new(
+            "nh home switch",
+            "Apply the Home Manager configuration?",
+          ).ask() {
+            Ok(true) => true,
+            Ok(false) => false,
+            Err(e) => {
+              debug!(?e, "Notification failed; falling back to prompt");
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
+        NotifyAskMode::Both => {
+          match NotificationSender::new(
+            "nh home switch",
+            "Apply the Home Manager configuration?",
+          ).ask() {
+            Ok(true) => true,
+            _ => {
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
+      };

Requires NotificationSender::ask() -> Result. Based on learnings.

src/notify.rs (1)

100-127: ask() must not panic and should signal failure to callers
show().unwrap() will crash when no notification server is present; macOS path still calls show(). Return Result, skip show() on macOS, and support action flow only on Unix.

-  #[must_use]
-  pub fn ask(mut self) -> bool {
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      self
-        .notification
-        .urgency(self.urgency.unwrap_or(Urgency::Critical));
-      self.notification.action("accept", "Accept");
-      self.notification.action("reject", "Reject");
-    }
-
-    let handle = self.notification.show().unwrap();
-
-    #[cfg(all(unix, not(target_os = "macos")))]
-    {
-      let mut confirmation = false;
-      handle.wait_for_action(|s| {
-        confirmation = s == "accept";
-      });
-      confirmation
-    }
-
-    #[cfg(target_os = "macos")]
-    {
-      _ = &mut self;
-      _ = handle;
-      false
-    }
-  }
+  pub fn ask(mut self) -> Result<bool> {
+    // Respect NH_NOTIFY like send(); disabled => let caller fall back.
+    let enable = matches!(env::var("NH_NOTIFY").as_deref(), Ok("1"));
+    if !enable {
+      return Err(color_eyre::eyre::eyre!("Notifications disabled (NH_NOTIFY!=1)"));
+    }
+
+    #[cfg(target_os = "macos")]
+    {
+      // Actions unsupported via notify-rust on macOS; trigger fallback.
+      let _ = &mut self;
+      return Ok(false);
+    }
+
+    #[cfg(all(unix, not(target_os = "macos")))]
+    {
+      self.notification
+        .urgency(self.urgency.unwrap_or(Urgency::Critical))
+        .timeout(0) // require interaction on most servers
+        .action("accept", "Accept")
+        .action("reject", "Reject");
+
+      let handle = self.notification.show()?; // propagate delivery error
+      let mut confirmation = false;
+      handle.wait_for_action(|s| {
+        confirmation = s == "accept";
+      });
+      Ok(confirmation)
+    }
+  }
src/darwin.rs (1)

166-175: Critical: Unwrap can panic and placeholder text remains.

Two blocking issues flagged in previous reviews but still present:

  1. Line 170: .unwrap() will crash if the notification fails (missing daemon, dismissed notification, etc.).
  2. Line 168: Body text "testing" is a placeholder and not user-appropriate.

Per past reviews, these were marked as addressed in commits def8fe0–7c65e7b, but the issues remain in the current code.

Apply this diff:

           // MacOS doesn't support notification actions
           NotifyAskMode::Notify | NotifyAskMode::Both => {
-            NotificationSender::new(&format!("nh darwin {variant}"), "testing")
-              .send()
-              .unwrap();
+            if let Err(e) = NotificationSender::new(
+              &format!("nh darwin {variant}"),
+              "Apply the config?",
+            )
+            .send()
+            {
+              debug!(?e, "Notification failed; continuing with prompt");
+            }

             inquire::Confirm::new("Apply the config?")
               .with_default(false)
               .prompt()?
           },
src/nixos.rs (2)

293-308: Critical: Notification can panic and Both mode is unimplemented.

Three blocking issues previously flagged but still present:

  1. Panic risk (lines 300–305): NotificationSender::ask() calls .show().unwrap() internally (see src/notify.rs line ~110). If the notification daemon is unavailable or the call fails, the application crashes instead of falling back to a terminal prompt.

  2. Unimplemented Both (line 307): NotifyAskMode::Both => unimplemented!() will panic if users pass --ask=both. Per PR discussion, this mode should show both notification and terminal prompt.

  3. No error recovery: The Notify branch assumes ask() always succeeds, leaving no fallback when notifications fail.

Recommended fix:

Change NotificationSender::ask() to return Result<bool> (in src/notify.rs), then update callers:

     if let Some(ask) = &self.common.ask {
       let confirmation = match ask {
         NotifyAskMode::Prompt => {
           inquire::Confirm::new("Apply the config?")
             .with_default(false)
             .prompt()?
         },
         NotifyAskMode::Notify => {
-          NotificationSender::new(
-            "nh os switch",
-            "Do you want to apply the NixOS configuration?",
-          )
-          .ask()
+          match NotificationSender::new(
+            "nh os switch", 
+            "Do you want to apply the NixOS configuration?",
+          ).ask() {
+            Ok(confirmed) => confirmed,
+            Err(e) => {
+              warn!("Notification prompt failed: {e}; falling back to terminal");
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
         },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          let _ = NotificationSender::new(
+            "nh os switch",
+            "Do you want to apply the NixOS configuration?",
+          ).send();
+          inquire::Confirm::new("Apply the config?")
+            .with_default(false)
+            .prompt()?
+        },
       };

470-488: Critical: Rollback mirrors rebuild confirmation issues.

This rollback confirmation path has the same three critical issues as the rebuild confirmation (lines 293–308):

  1. ask() can panic if notification fails (line 485)
  2. Both mode is unimplemented (line 487)
  3. No error recovery or fallback to terminal prompt

Apply the same fix pattern as recommended for the rebuild confirmation above, adjusting the notification body to reference the rollback generation.

🧹 Nitpick comments (5)
src/clean.rs (1)

523-524: Use of nh_info! is fine; ensure macros don’t panic
This site is OK, but nh_info! currently unwraps send(); address in src/logging.rs to avoid crash here if notifications fail.

src/notify.rs (2)

60-66: MSRV-friendly NH_NOTIFY gating
is_ok_and may raise MSRV; prefer matches! to keep builds wide.

-    let enable_notifications = env::var("NH_NOTIFY").is_ok_and(|v| v == "1");
+    let enable_notifications = matches!(env::var("NH_NOTIFY").as_deref(), Ok("1"));

25-33: Set app name (optional)
Setting appname improves UX in some DEs.

-    notification.summary(summary);
+    notification.summary(summary).appname("nh");
src/home.rs (1)

191-195: OK to use nh_info!; ensure macro fix lands
This site is fine once macros avoid unwrap.

src/util.rs (1)

191-195: Remove unnecessary unsafe around env::set_var
set_var is safe; drop the unsafe block.

// before
unsafe {
  env::set_var("HOME_MANAGER_BACKUP_EXT", ext);
}

// after
env::set_var("HOME_MANAGER_BACKUP_EXT", ext);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2b7ffd and fb5bba1.

📒 Files selected for processing (8)
  • src/clean.rs (3 hunks)
  • src/darwin.rs (4 hunks)
  • src/home.rs (3 hunks)
  • src/interface.rs (3 hunks)
  • src/logging.rs (2 hunks)
  • src/nixos.rs (13 hunks)
  • src/notify.rs (1 hunks)
  • src/util.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/interface.rs
🧰 Additional context used
🧬 Code graph analysis (5)
src/darwin.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/logging.rs (1)
src/notify.rs (3)
  • new (25-33)
  • urgency (41-44)
  • send (60-78)
src/nixos.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/clean.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/home.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
🔇 Additional comments (7)
src/logging.rs (1)

34-43: Formatter changes look good
Using write!(..)? keeps error propagation correct.

src/home.rs (1)

38-41: Dry-run/ask warning is appropriate
Message clearly informs users build ignores ask/dry.

src/util.rs (1)

333-333: Switch to nh_info! is fine
Once macros are made non-panicking, this is safe.

src/darwin.rs (2)

1-22: LGTM: Imports are correct.

The added imports (fmt, NotifyAskMode, NotificationSender) properly support the new notification and tri-state ask features.


53-61: LGTM: Clean Display implementation.

The Display trait implementation is correct and enables user-friendly variant names in notifications.

src/nixos.rs (2)

1-30: LGTM: Imports support the new notification features.

The additions of NotifyAskMode, NotificationSender, and nh_info! properly enable the tri-state ask mode and notification-based logging.


425-425: LGTM: Consistent logging macro migration.

Replacing info! with nh_info! throughout the file aligns with the notification-based logging system introduced in this PR. The usage is consistent and the messages are appropriate for user-facing notifications.

Also applies to: 463-466, 505-505, 537-537, 562-565, 671-674, 678-682, 705-708

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (8)
src/logging.rs (2)

98-107: format!($($arg)*) blocks structured fields

Calls like nh_info!(%val, key=?x) won’t compile. Either document “literal‑first only” or add a literal‑only arm plus a fallback arm that only notifies a static message.

Example sketch:

 #[macro_export]
 macro_rules! nh_info {
-    ($($arg:tt)*) => {{
+    ($fmt:literal $(, $arg:tt)*) => {{
         let message = format!($fmt $(, $arg)*);
-        tracing::info!($($arg)*);
+        tracing::info!($fmt $(, $arg)*);
         // send...
     }};
+    ($($fields:tt)+) => {{
+        tracing::info!($($fields)+);
+        // Optional: skip or send a generic notification body here.
+    }}
 }
#!/bin/bash
# List non-literal macro uses; should be empty or intentionally allowed
rg -nP 'nh_(info|warn)!\(\s*"' -v src | sed -n '1,120p'

Also applies to: 109-118


98-107: Don’t panic in logging macros; handle send() errors

.send().unwrap() can crash when notifications fail. Log and continue instead.

Apply:

 #[macro_export]
 macro_rules! nh_info {
     ($($arg:tt)*) => {{
-        use notify_rust::Urgency;
-        use $crate::notify::NotificationSender;
+        use ::notify_rust::Urgency;
         let message = format!($($arg)*);
         tracing::info!($($arg)*);
-        NotificationSender::new("nh info", &message).urgency(Urgency::Normal).send().unwrap();
+        if let Err(e) = $crate::notify::NotificationSender::new("nh info", &message)
+            .urgency(Urgency::Normal)
+            .send()
+        {
+            tracing::debug!(?e, "Failed to send nh info notification");
+        }
     }};
 }
 
 #[macro_export]
 macro_rules! nh_warn {
     ($($arg:tt)*) => {{
-        use notify_rust::Urgency;
-        use $crate::notify::NotificationSender;
+        use ::notify_rust::Urgency;
         let message = format!($($arg)*);
         tracing::warn!($($arg)*);
-        NotificationSender::new("nh warn", &message).urgency(Urgency::Normal).send().unwrap();
+        if let Err(e) = $crate::notify::NotificationSender::new("nh warn", &message)
+            .urgency(Urgency::Normal)
+            .send()
+        {
+            tracing::debug!(?e, "Failed to send nh warn notification");
+        }
     }};
 }

Also applies to: 109-118

src/interface.rs (1)

316-318: Add value_enum to fix enum parsing for --ask

CommonRebuildArgs.ask and CleanArgs.ask are missing value_enum; clap won’t parse --ask=notify|both without it. Align with OsRollbackArgs.

Apply:

-  #[arg(long, short, default_missing_value = "prompt", num_args = 0..=1)]
+  #[arg(long, short, value_enum, default_missing_value = "prompt", num_args = 0..=1)]
   pub ask: Option<NotifyAskMode>,
#!/bin/bash
# Verify all NotifyAskMode ask fields include value_enum
rg -n -C1 'pub ask:\s*Option<NotifyAskMode>' src/interface.rs

Also applies to: 448-450

src/darwin.rs (1)

158-176: macOS: notify then prompt; avoid unwrap and fix text

Use a meaningful body and don’t unwrap send(); log and proceed to prompt.

-          // MacOS doesn't support notification actions
-          NotifyAskMode::Notify | NotifyAskMode::Both => {
-            NotificationSender::new(&format!("nh darwin {variant}"), "testing")
-              .send()
-              .unwrap();
-
-            inquire::Confirm::new("Apply the config?")
-              .with_default(false)
-              .prompt()?
-          },
+          // macOS: no actions; notify then prompt
+          NotifyAskMode::Notify | NotifyAskMode::Both => {
+            if let Err(e) = NotificationSender::new(
+              &format!("nh darwin {variant}"),
+              "Do you want to apply the Darwin configuration?",
+            ).send() {
+              debug!(?e, "Notification failed; continuing with prompt");
+            }
+            inquire::Confirm::new("Apply the config?")
+              .with_default(false)
+              .prompt()?
+          },
src/clean.rs (1)

338-357: Implement Both and handle notify failures (no panic)

Both is unimplemented and Notify assumes infallible ask(). Add notify‑then‑prompt fallback.

-    if let Some(ask) = &args.ask {
-      let confirmation = match ask {
+    if let Some(ask) = &args.ask {
+      let confirmation = match ask {
         NotifyAskMode::Prompt => {
           Confirm::new("Confirm the cleanup plan?")
             .with_default(false)
             .prompt()?
         },
-        NotifyAskMode::Notify => {
-          NotificationSender::new(
-            "nh clean",
-            "Do you want to confirm the cleanup plan?",
-          )
-          .ask()
-        },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Notify => {
+          match NotificationSender::new(
+            "nh clean",
+            "Do you want to confirm the cleanup plan?",
+          ).ask() {
+            Ok(true) => true,
+            Ok(false) => false,
+            Err(e) => {
+              debug!(?e, "Notification failed; falling back to prompt");
+              Confirm::new("Confirm the cleanup plan?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
+        NotifyAskMode::Both => {
+          match NotificationSender::new(
+            "nh clean",
+            "Do you want to confirm the cleanup plan?",
+          ).ask() {
+            Ok(true) => true,
+            _ => Confirm::new("Confirm the cleanup plan?")
+                  .with_default(false)
+                  .prompt()?
+          }
+        },
       };

Optional: skip ask when args.dry for consistency with home/darwin.

src/home.rs (1)

168-183: Implement safe Notify and Both flows; enable fallback

Both panics (unimplemented) and Notify assumes infallible ask(). Make ask notify‑then‑prompt with error handling.

Apply (requires notify.rs ask() -> Result):

-    if let Some(ask) = &self.common.ask {
-      let confirmation = match ask {
+    if let Some(ask) = &self.common.ask {
+      let confirmation = match ask {
         NotifyAskMode::Prompt => {
           inquire::Confirm::new("Apply the config?")
             .with_default(false)
             .prompt()?
         },
-        NotifyAskMode::Notify => {
-          NotificationSender::new(
-            "nh home switch",
-            "Do you want to apply the Home Manager configuration?",
-          )
-          .ask()
-        },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Notify => {
+          match NotificationSender::new(
+            "nh home switch",
+            "Do you want to apply the Home Manager configuration?",
+          ).ask() {
+            Ok(true) => true,
+            Ok(false) => false,
+            Err(e) => {
+              tracing::debug!(?e, "Notification failed; falling back to prompt");
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
+        },
+        NotifyAskMode::Both => {
+          match NotificationSender::new(
+            "nh home switch",
+            "Do you want to apply the Home Manager configuration?",
+          ).ask() {
+            Ok(true) => true,
+            _ => inquire::Confirm::new("Apply the config?")
+                  .with_default(false)
+                  .prompt()?
+          }
+        },
       };

Follow‑up in src/notify.rs: return Result from ask() and propagate show()/wait errors.

src/nixos.rs (2)

470-488: Critical: Apply the same fixes to rollback confirmation.

This rollback confirmation path has identical issues to the rebuild flow (lines 293-308):

  • NotificationSender::ask() can panic internally
  • Both mode will crash with unimplemented!()
  • No fallback when notification delivery fails

Apply the same error-handling pattern documented in the rebuild comment, ensuring the notification body includes the generation number as shown here.


293-308: Critical: Notification path can panic, and Both mode is unimplemented.

Three blocking issues remain despite past review feedback:

  1. Panic risk: NotificationSender::ask() internally calls .show().unwrap() (see src/notify.rs line ~110). When the notification daemon is unavailable or fails, the application will crash instead of gracefully degrading.

  2. Unimplemented Both mode: NotifyAskMode::Both => unimplemented!() will panic if users pass --ask=both. Per the PR discussion, this mode should display both a notification and require terminal confirmation.

  3. No error recovery: The Notify branch assumes ask() always succeeds, leaving users with no way to confirm when notifications fail.

Required fixes:

First, update src/notify.rs to make ask() return Result<bool> and remove internal .unwrap() calls. Then apply this pattern:

     if let Some(ask) = &self.common.ask {
       let confirmation = match ask {
         NotifyAskMode::Prompt => {
           inquire::Confirm::new("Apply the config?")
             .with_default(false)
             .prompt()?
         },
         NotifyAskMode::Notify => {
-          NotificationSender::new(
-            "nh os switch",
-            "Do you want to apply the NixOS configuration?",
-          )
-          .ask()
+          // Attempt notification prompt; fall back to terminal on failure
+          match NotificationSender::new(
+            "nh os switch",
+            "Do you want to apply the NixOS configuration?",
+          ).ask() {
+            Ok(confirmed) => confirmed,
+            Err(e) => {
+              warn!("Notification prompt failed: {e}; falling back to terminal prompt");
+              inquire::Confirm::new("Apply the config?")
+                .with_default(false)
+                .prompt()?
+            }
+          }
         },
-        NotifyAskMode::Both => unimplemented!(),
+        NotifyAskMode::Both => {
+          // Show notification (best-effort) then require terminal confirmation
+          let _ = NotificationSender::new(
+            "nh os switch",
+            "Do you want to apply the NixOS configuration?",
+          ).send();
+          inquire::Confirm::new("Apply the config?")
+            .with_default(false)
+            .prompt()?
+        },
       };

Note: The same pattern must be applied to the rollback flow (lines 470-488) and any other call sites in home.rs and clean.rs mentioned in past reviews.

🧹 Nitpick comments (1)
src/home.rs (1)

190-195: Remove unnecessary unsafe around set_var

std::env::set_var is safe; drop the unsafe block.

-      unsafe {
-        env::set_var("HOME_MANAGER_BACKUP_EXT", ext);
-      }
+      env::set_var("HOME_MANAGER_BACKUP_EXT", ext);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb5bba1 and 1444215.

📒 Files selected for processing (9)
  • Cargo.toml (2 hunks)
  • src/clean.rs (3 hunks)
  • src/darwin.rs (4 hunks)
  • src/home.rs (3 hunks)
  • src/interface.rs (3 hunks)
  • src/logging.rs (2 hunks)
  • src/nixos.rs (13 hunks)
  • src/notify.rs (1 hunks)
  • src/util.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/util.rs
  • Cargo.toml
  • src/notify.rs
🧰 Additional context used
🧬 Code graph analysis (5)
src/nixos.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/home.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/clean.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
src/logging.rs (1)
src/notify.rs (3)
  • new (25-33)
  • urgency (41-44)
  • send (60-78)
src/darwin.rs (1)
src/notify.rs (2)
  • ask (100-127)
  • new (25-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
🔇 Additional comments (7)
src/interface.rs (2)

267-275: Good addition: tri-state ask mode

NotifyAskMode with Prompt/Notify/Both is clear and future‑proof. LGTM.


284-286: Arg parsing: value_enum set correctly here

OsRollbackArgs.ask correctly uses value_enum + default_missing_value. LGTM.

src/darwin.rs (1)

53-61: Nice: Display for variant

Readable messages for “build”/“switch”. LGTM.

src/clean.rs (1)

523-524: LGTM: use nh_info! for deletion logs

Consistent UX with the new notification‑aware logging.

src/nixos.rs (3)

8-8: LGTM: Imports are appropriate for notification support.

The added imports (warn, NotifyAskMode, nh_info, NotificationSender) are correctly structured and necessary for the notification feature.

Also applies to: 18-18, 26-27


45-45: LGTM: Option presence checks are correct.

The is_some() checks appropriately detect when any ask mode is present, correctly warning users when --ask has no effect for dry-run or build-only operations.

Also applies to: 281-281


425-425: LGTM: Status messages appropriately updated.

The nh_info! macro calls provide clear, user-facing status messages for rollback operations, VM instructions, and activation steps. The messaging is consistent and informative.

Also applies to: 463-463, 505-505, 537-537, 562-562, 671-671, 678-678, 705-705

@r0chd r0chd force-pushed the notifications branch 4 times, most recently from 6f53a48 to fa47570 Compare October 18, 2025 14:41
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