Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
659 changes: 653 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ hostname = "0.4.1"
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.

owo-colors = "4.2.2"
regex = "1.11.2"
reqwest = { default-features = false, features = [
Expand Down
35 changes: 28 additions & 7 deletions src/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use tracing::{Level, debug, info, instrument, span, warn};
use crate::{
Result,
commands::{Command, ElevationStrategy},
interface,
interface::{self, NotifyAskMode},
notify::NotificationSender,
};

// Nix impl:
Expand Down Expand Up @@ -306,12 +307,32 @@ impl interface::CleanMode {
}

// Clean the paths
if args.ask
&& !Confirm::new("Confirm the cleanup plan?")
.with_default(false)
.prompt()?
{
bail!("User rejected the cleanup plan");
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()
},
NotifyAskMode::Both => unimplemented!(),
};

if !confirmation {
bail!("User rejected the cleanup plan");
}
}

if !args.dry {
Expand Down
14 changes: 9 additions & 5 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ static PASSWORD_CACHE: OnceLock<Mutex<HashMap<String, SecretString>>> =

fn get_cached_password(host: &str) -> Option<SecretString> {
let cache = PASSWORD_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let guard = cache.lock().unwrap_or_else(|e| e.into_inner());
let guard = cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.get(host).cloned()
}

fn cache_password(host: &str, password: SecretString) {
let cache = PASSWORD_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner());
let mut guard = cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.insert(host.to_string(), password);
}

Expand Down Expand Up @@ -449,7 +453,7 @@ impl Command {
Some(cached_password)
} else {
let password =
inquire::Password::new(&format!("[sudo] password for {}:", host))
inquire::Password::new(&format!("[sudo] password for {host}:"))
.without_confirmation()
.prompt()
.context("Failed to read sudo password")?;
Expand Down Expand Up @@ -492,11 +496,11 @@ impl Command {
for (key, action) in &self.env_vars {
match action {
EnvAction::Set(value) => {
elev_cmd = elev_cmd.arg(format!("{}={}", key, value));
elev_cmd = elev_cmd.arg(format!("{key}={value}"));
},
EnvAction::Preserve => {
if let Ok(value) = std::env::var(key) {
elev_cmd = elev_cmd.arg(format!("{}={}", key, value));
elev_cmd = elev_cmd.arg(format!("{key}={value}"));
}
},
_ => {},
Expand Down
47 changes: 37 additions & 10 deletions src/darwin.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
use std::{env, path::PathBuf};
use std::{env, fmt, path::PathBuf};

use color_eyre::eyre::{Context, bail, eyre};
use tracing::{debug, warn};

use crate::{
Result,
commands,
commands::{Command, ElevationStrategy},
commands::{self, Command, ElevationStrategy},
installable::Installable,
interface::{
DarwinArgs,
DarwinRebuildArgs,
DarwinReplArgs,
DarwinSubcommand,
DiffType,
NotifyAskMode,
},
nixos::toplevel_for,
notify::NotificationSender,
update::update,
util::{get_hostname, print_dix_diff},
};
Expand All @@ -34,7 +35,7 @@ impl DarwinArgs {
match self.subcommand {
DarwinSubcommand::Switch(args) => args.rebuild(&Switch, elevation),
DarwinSubcommand::Build(args) => {
if args.common.ask || args.common.dry {
if args.common.ask.is_some() || args.common.dry {
warn!("`--ask` and `--dry` have no effect for `nh darwin build`");
}
args.rebuild(&Build, elevation)
Expand All @@ -49,6 +50,16 @@ enum DarwinRebuildVariant {
Build,
}

impl fmt::Display for DarwinRebuildVariant {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
DarwinRebuildVariant::Build => "build",
DarwinRebuildVariant::Switch => "switch",
};
write!(f, "{s}")
}
}

impl DarwinRebuildArgs {
fn rebuild(
self,
Expand Down Expand Up @@ -144,13 +155,29 @@ impl DarwinRebuildArgs {
let _ = print_dix_diff(&PathBuf::from(CURRENT_PROFILE), &target_profile);
}

if self.common.ask && !self.common.dry && !matches!(variant, Build) {
let confirmation = inquire::Confirm::new("Apply the config?")
.with_default(false)
.prompt()?;
if !self.common.dry && !matches!(variant, Build) {
if let Some(ask) = self.common.ask {
let confirmation = match ask {
NotifyAskMode::Prompt => {
inquire::Confirm::new("Apply the config?")
.with_default(false)
.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()?
},
};

if !confirmation {
bail!("User rejected the new config");
if !confirmation {
bail!("User rejected the new config");
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/generations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,11 @@ pub fn print_info(
.iter()
.map(|f| {
let (name, width) = f.column_info(widths);
format!("{:<width$}", name)
format!("{name:<width$}")
})
.collect::<Vec<String>>()
.join(" ");
println!("{}", header);
println!("{header}");

// Print generations in descending order
for generation in generations.iter().rev() {
Expand Down Expand Up @@ -382,11 +382,11 @@ pub fn print_info(
Field::Spec => specialisations.clone(),
Field::Size => generation.closure_size.clone(),
};
format!("{:width$}", cell_content)
format!("{cell_content:width$}")
})
.collect::<Vec<String>>()
.join(" ");
println!("{}", row);
println!("{row}");
}

Ok(())
Expand Down
45 changes: 35 additions & 10 deletions src/home.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{env, ffi::OsString, path::PathBuf};
use std::{env, ffi::OsString, fmt, path::PathBuf};

use color_eyre::{
Result,
Expand All @@ -7,10 +7,17 @@ use color_eyre::{
use tracing::{debug, info, warn};

use crate::{
commands,
commands::Command,
commands::{self, Command},
installable::Installable,
interface::{self, DiffType, HomeRebuildArgs, HomeReplArgs, HomeSubcommand},
interface::{
self,
DiffType,
HomeRebuildArgs,
HomeReplArgs,
HomeSubcommand,
NotifyAskMode,
},
notify::NotificationSender,
update::update,
util::{get_hostname, print_dix_diff},
};
Expand All @@ -26,7 +33,7 @@ impl interface::HomeArgs {
match self.subcommand {
HomeSubcommand::Switch(args) => args.rebuild(&Switch),
HomeSubcommand::Build(args) => {
if args.common.ask || args.common.dry {
if args.common.ask.is_some() || args.common.dry {
warn!("`--ask` and `--dry` have no effect for `nh home build`");
}
args.rebuild(&Build)
Expand All @@ -42,6 +49,16 @@ enum HomeRebuildVariant {
Switch,
}

impl fmt::Display for HomeRebuildVariant {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
HomeRebuildVariant::Build => "build",
HomeRebuildVariant::Switch => "switch",
};
write!(f, "{s}")
}
}

impl HomeRebuildArgs {
fn rebuild(self, variant: &HomeRebuildVariant) -> Result<()> {
use HomeRebuildVariant::Build;
Expand Down Expand Up @@ -150,16 +167,24 @@ impl HomeRebuildArgs {
}

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

if self.common.ask {
let confirmation = inquire::Confirm::new("Apply the config?")
.with_default(false)
.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()
},
NotifyAskMode::Both => unimplemented!(),
};

if !confirmation {
bail!("User rejected the new config");
Expand Down
22 changes: 16 additions & 6 deletions src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,15 +256,25 @@ pub enum DiffType {
Never,
}

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

#[derive(Debug, Args)]
pub struct OsRollbackArgs {
/// Only print actions, without performing them
#[arg(long, short = 'n')]
pub dry: bool,

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

/// Explicitly select some specialisation
#[arg(long, short)]
Expand Down Expand Up @@ -295,8 +305,8 @@ pub struct CommonRebuildArgs {
pub dry: bool,

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

#[command(flatten)]
pub installable: Installable,
Expand Down Expand Up @@ -427,8 +437,8 @@ pub struct CleanArgs {
pub dry: bool,

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

/// Don't run nix store --gc
#[arg(long = "no-gc", alias = "nogc")]
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod interface;
pub mod json;
pub mod logging;
pub mod nixos;
pub mod notify;
pub mod search;
pub mod update;
pub mod util;
Expand Down
Loading
Loading