From 6efdc98b4e5f483cd5de6f2187afead029517a96 Mon Sep 17 00:00:00 2001 From: midischwarz12 Date: Sat, 9 Aug 2025 19:32:12 -0500 Subject: [PATCH] feat: add {os,home,darwin} edit --- CHANGELOG.md | 1 + src/darwin.rs | 88 +++++++++++++++++++++++++++++++++++++++- src/home.rs | 95 ++++++++++++++++++++++++++++++++++++++++++- src/interface.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++- src/nixos.rs | 87 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 370 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2527ad24..f1610ddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ functionality, under the "Removed" section. - It's roughly %4 faster according to testing, but IO is still a limiting factor and results may differ. - Added more context to some minor debug messages across platform commands. +- Added `nh {os,home,darwin} edit` subcommand. ### Fixed diff --git a/src/darwin.rs b/src/darwin.rs index dbc1c309..46a9f2c3 100644 --- a/src/darwin.rs +++ b/src/darwin.rs @@ -8,7 +8,9 @@ use crate::Result; use crate::commands; use crate::commands::Command; use crate::installable::Installable; -use crate::interface::{DarwinArgs, DarwinRebuildArgs, DarwinReplArgs, DarwinSubcommand, DiffType}; +use crate::interface::{ + DarwinArgs, DarwinEditArgs, DarwinRebuildArgs, DarwinReplArgs, DarwinSubcommand, DiffType, +}; use crate::nixos::toplevel_for; use crate::update::update; use crate::util::{get_hostname, print_dix_diff}; @@ -33,6 +35,7 @@ impl DarwinArgs { args.rebuild(&Build) } DarwinSubcommand::Repl(args) => args.run(), + DarwinSubcommand::Edit(args) => args.run(), } } } @@ -231,3 +234,86 @@ impl DarwinReplArgs { Ok(()) } } + +impl DarwinEditArgs { + fn run(self) -> Result<()> { + let editor = if let Ok(var) = env::var("EDITOR") { + var + } else { + bail!("Unset environment variable `EDITOR`.") + }; + + // Use NH_DARWIN_FLAKE if available, otherwise use the provided installable + let target_installable = if let Ok(darwin_flake) = env::var("NH_DARWIN_FLAKE") { + debug!("Using NH_DARWIN_FLAKE: {}", darwin_flake); + + let mut elems = darwin_flake.splitn(2, '#'); + let reference = match elems.next() { + Some(r) => r.to_owned(), + None => return Err(eyre!("NH_DARWIN_FLAKE missing reference part")), + }; + let attribute = elems + .next() + .map(crate::installable::parse_attribute) + .unwrap_or_default(); + + Installable::Flake { + reference, + attribute, + } + } else { + self.installable + }; + + match target_installable { + Installable::File { path, .. } => { + let str_path = path + .to_str() + .ok_or_else(|| eyre!("Edit path is not valid UTF-8"))?; + + Command::new(editor) + .arg(str_path) + .with_required_env() + .show_output(true) + .run()? + } + Installable::Flake { reference, .. } => { + match &reference[0..5] { + "path:" => { + // Removes `path:` prefix + let path = &reference[5..]; + + Command::new(editor) + .arg(path) + .with_required_env() + .show_output(true) + .run()? + } + _ => { + Command::new("nix") + .arg("flake") + .arg("prefetch") + .arg("-o") + .arg("result") + .arg(reference) + .with_required_env() + .show_output(true) + .run()?; + + Command::new(editor) + .arg("result") + .with_required_env() + .show_output(true) + .run()?; + + std::fs::remove_file("result")? + } + }; + } + Installable::Store { .. } => bail!("Nix doesn't support nix store installables."), + Installable::Expression { .. } => bail!("Nix doesn't support nix expression."), + }; + + Ok(()) + } +} diff --git a/src/home.rs b/src/home.rs index 8aaac3a8..502d0d6f 100644 --- a/src/home.rs +++ b/src/home.rs @@ -9,7 +9,9 @@ use tracing::{debug, info, warn}; use crate::commands; use crate::commands::Command; use crate::installable::Installable; -use crate::interface::{self, DiffType, HomeRebuildArgs, HomeReplArgs, HomeSubcommand}; +use crate::interface::{ + self, DiffType, HomeEditArgs, HomeRebuildArgs, HomeReplArgs, HomeSubcommand, +}; use crate::update::update; use crate::util::{get_hostname, print_dix_diff}; @@ -30,6 +32,7 @@ impl interface::HomeArgs { args.rebuild(&Build) } HomeSubcommand::Repl(args) => args.run(), + HomeSubcommand::Edit(args) => args.run(), } } } @@ -398,3 +401,93 @@ impl HomeReplArgs { Ok(()) } } + +impl HomeEditArgs { + fn run(self) -> Result<()> { + let editor = if let Ok(var) = env::var("EDITOR") { + var + } else { + bail!("Unset environment variable `EDITOR`.") + }; + + // Use NH_HOME_FLAKE if available, otherwise use the provided installable + let installable = if let Ok(home_flake) = env::var("NH_HOME_FLAKE") { + debug!("Using NH_HOME_FLAKE: {home_flake}"); + + let mut elems = home_flake.splitn(2, '#'); + let reference = match elems.next() { + Some(r) => r.to_owned(), + None => return Err(eyre!("NH_HOME_FLAKE missing reference part")), + }; + let attribute = elems + .next() + .map(crate::installable::parse_attribute) + .unwrap_or_default(); + + Installable::Flake { + reference, + attribute, + } + } else { + self.installable + }; + + let toplevel = toplevel_for( + installable, + false, + &self.extra_args, + self.configuration.clone(), + )?; + + match toplevel { + Installable::File { path, .. } => { + let str_path = path + .to_str() + .ok_or_else(|| eyre!("Edit path is not valid UTF-8"))?; + + Command::new(editor) + .arg(str_path) + .with_required_env() + .show_output(true) + .run()? + } + Installable::Flake { reference, .. } => { + match &reference[0..5] { + "path:" => { + // Removes `path:` prefix + let path = &reference[5..]; + + Command::new(editor) + .arg(path) + .with_required_env() + .show_output(true) + .run()? + } + _ => { + Command::new("nix") + .arg("flake") + .arg("prefetch") + .arg("-o") + .arg("result") + .arg(reference) + .with_required_env() + .show_output(true) + .run()?; + + Command::new(editor) + .arg("result") + .with_required_env() + .show_output(true) + .run()?; + + std::fs::remove_file("result")? + } + }; + } + Installable::Store { .. } => bail!("Nix doesn't support nix store installables."), + Installable::Expression { .. } => bail!("Nix doesn't support nix expression."), + }; + + Ok(()) + } +} diff --git a/src/interface.rs b/src/interface.rs index bb929648..32c726f0 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -121,6 +121,13 @@ impl OsArgs { let is_flake = args.uses_flakes(); Box::new(OsReplFeatures { is_flake }) } + OsSubcommand::Edit(args) => { + if args.uses_flakes() { + Box::new(FlakeFeatures) + } else { + Box::new(LegacyFeatures) + } + } OsSubcommand::Switch(args) | OsSubcommand::Boot(args) | OsSubcommand::Test(args) @@ -160,6 +167,9 @@ pub enum OsSubcommand { /// Load system in a repl Repl(OsReplArgs), + /// Edit a NixOS configuration + Edit(OsEditArgs), + /// List available generations from profile path Info(OsGenerationsArgs), @@ -316,7 +326,29 @@ impl OsReplArgs { #[must_use] pub fn uses_flakes(&self) -> bool { // Check environment variables first - if env::var("NH_OS_FLAKE").is_ok() { + if env::var("NH_OS_FLAKE").is_ok_and(|v| !v.is_empty()) { + return true; + } + + // Check installable type + matches!(self.installable, Installable::Flake { .. }) + } +} + +#[derive(Debug, Args)] +pub struct OsEditArgs { + #[command(flatten)] + pub installable: Installable, + + /// When using a flake installable, select this hostname from nixosConfigurations + #[arg(long, short = 'H', global = true)] + pub hostname: Option, +} + +impl OsEditArgs { + pub fn uses_flakes(&self) -> bool { + // Check environment variables first + if env::var("NH_OS_FLAKE").is_ok_and(|v| !v.is_empty()) { return true; } @@ -447,6 +479,13 @@ impl HomeArgs { let is_flake = args.uses_flakes(); Box::new(HomeReplFeatures { is_flake }) } + HomeSubcommand::Edit(args) => { + if args.uses_flakes() { + Box::new(FlakeFeatures) + } else { + Box::new(LegacyFeatures) + } + } HomeSubcommand::Switch(args) | HomeSubcommand::Build(args) => { if args.uses_flakes() { Box::new(FlakeFeatures) @@ -468,6 +507,9 @@ pub enum HomeSubcommand { /// Load a home-manager configuration in a Nix REPL Repl(HomeReplArgs), + + /// Edit a home-manager configuration + Edit(HomeEditArgs), } #[derive(Debug, Args)] @@ -543,6 +585,34 @@ impl HomeReplArgs { } } +#[derive(Debug, Args)] +pub struct HomeEditArgs { + #[command(flatten)] + pub installable: Installable, + + /// Name of the flake homeConfigurations attribute, like username@hostname + /// + /// If unspecified, will try @ and + #[arg(long, short)] + pub configuration: Option, + + /// Extra arguments passed to nix repl + #[arg(last = true)] + pub extra_args: Vec, +} + +impl HomeEditArgs { + pub fn uses_flakes(&self) -> bool { + // Check environment variables first + if env::var("NH_HOME_FLAKE").is_ok_and(|v| !v.is_empty()) { + return true; + } + + // Check installable type + matches!(self.installable, Installable::Flake { .. }) + } +} + #[derive(Debug, Parser)] /// Generate shell completion files into stdout pub struct CompletionArgs { @@ -567,6 +637,13 @@ impl DarwinArgs { let is_flake = args.uses_flakes(); Box::new(DarwinReplFeatures { is_flake }) } + DarwinSubcommand::Edit(args) => { + if args.uses_flakes() { + Box::new(FlakeFeatures) + } else { + Box::new(LegacyFeatures) + } + } DarwinSubcommand::Switch(args) | DarwinSubcommand::Build(args) => { if args.uses_flakes() { Box::new(FlakeFeatures) @@ -586,6 +663,8 @@ pub enum DarwinSubcommand { Build(DarwinRebuildArgs), /// Load a nix-darwin configuration in a Nix REPL Repl(DarwinReplArgs), + /// Edit a nix-darwin configuration + Edit(DarwinEditArgs), } #[derive(Debug, Args)] @@ -645,6 +724,28 @@ impl DarwinReplArgs { } } +#[derive(Debug, Args)] +pub struct DarwinEditArgs { + #[command(flatten)] + pub installable: Installable, + + /// When using a flake installable, select this hostname from darwinConfigurations + #[arg(long, short = 'H', global = true)] + pub hostname: Option, +} + +impl DarwinEditArgs { + pub fn uses_flakes(&self) -> bool { + // Check environment variables first + if env::var("NH_DARWIN_FLAKE").is_ok_and(|v| !v.is_empty()) { + return true; + } + + // Check installable type + matches!(self.installable, Installable::Flake { .. }) + } +} + #[derive(Debug, Args)] pub struct UpdateArgs { #[arg(short = 'u', long = "update", conflicts_with = "update_input")] diff --git a/src/nixos.rs b/src/nixos.rs index a85d201e..82274376 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -12,7 +12,8 @@ use crate::generations; use crate::installable::Installable; use crate::interface::OsSubcommand::{self}; use crate::interface::{ - self, DiffType, OsBuildVmArgs, OsGenerationsArgs, OsRebuildArgs, OsReplArgs, OsRollbackArgs, + self, DiffType, OsBuildVmArgs, OsEditArgs, OsGenerationsArgs, OsRebuildArgs, OsReplArgs, + OsRollbackArgs, }; use crate::update::update; use crate::util::ensure_ssh_key_login; @@ -38,6 +39,7 @@ impl interface::OsArgs { } OsSubcommand::BuildVm(args) => args.build_vm(), OsSubcommand::Repl(args) => args.run(), + OsSubcommand::Edit(args) => args.run(), OsSubcommand::Info(args) => args.info(), OsSubcommand::Rollback(args) => args.rollback(), } @@ -720,6 +722,89 @@ impl OsReplArgs { } } +impl OsEditArgs { + fn run(self) -> Result<()> { + let editor = if let Ok(var) = env::var("EDITOR") { + var + } else { + bail!("Unset environment variable `EDITOR`.") + }; + + // Use NH_OS_FLAKE if available, otherwise use the provided installable + let target_installable = if let Ok(os_flake) = env::var("NH_OS_FLAKE") { + debug!("Using NH_OS_FLAKE: {}", os_flake); + + let mut elems = os_flake.splitn(2, '#'); + let reference = match elems.next() { + Some(r) => r.to_owned(), + None => return Err(eyre!("NH_OS_FLAKE missing reference part")), + }; + let attribute = elems + .next() + .map(crate::installable::parse_attribute) + .unwrap_or_default(); + + Installable::Flake { + reference, + attribute, + } + } else { + self.installable + }; + + match target_installable { + Installable::File { path, .. } => { + let str_path = path + .to_str() + .ok_or_else(|| eyre!("Edit path is not valid UTF-8"))?; + + Command::new(editor) + .arg(str_path) + .with_required_env() + .show_output(true) + .run()? + } + Installable::Flake { reference, .. } => { + match &reference[0..5] { + "path:" => { + // Removes `path:` prefix + let path = &reference[5..]; + + Command::new(editor) + .arg(path) + .with_required_env() + .show_output(true) + .run()? + } + _ => { + Command::new("nix") + .arg("flake") + .arg("prefetch") + .arg("-o") + .arg("result") + .arg(reference) + .with_required_env() + .show_output(true) + .run()?; + + Command::new(editor) + .arg("result") + .with_required_env() + .show_output(true) + .run()?; + + std::fs::remove_file("result")? + } + }; + } + Installable::Store { .. } => bail!("Nix doesn't support nix store installables."), + Installable::Expression { .. } => bail!("Nix doesn't support nix expression."), + }; + + Ok(()) + } +} + impl OsGenerationsArgs { fn info(&self) -> Result<()> { let profile = match self.profile {