···11+use std::{
22+ fs,
33+ path::Path,
44+ process::{Command, Stdio},
55+};
66+77+use inquire::{validator::Validation, Confirm, Editor, MultiSelect, Select, Text};
88+99+use crate::prelude::*;
1010+1111+fn list_roles(roles_folder: &Path) -> Result<Vec<String>> {
1212+ Ok(fs::read_dir(roles_folder)
1313+ .context("Failed to read roles")?
1414+ .into_iter()
1515+ .filter_map(|r| {
1616+ r.ok()
1717+ .map(|d| d.file_name().to_string_lossy().trim_end_matches(".nix").to_string())
1818+ .filter(|n| !n.contains('+'))
1919+ })
2020+ .collect())
2121+}
2222+2323+const NO_HARDWARE_MOD: &str = "No NixOS Hardware Module";
2424+2525+fn list_hardware_modules() -> Result<Vec<String>> {
2626+ let mut cmd = Command::new("nix");
2727+ cmd.arg("eval")
2828+ .arg("github:nixos/nixos-hardware#nixosModules")
2929+ .arg("--raw")
3030+ .arg("--apply")
3131+ .arg("s: builtins.toJSON (builtins.attrNames s)")
3232+ .stdout(Stdio::piped())
3333+ .stderr(Stdio::null());
3434+3535+ let output = cmd.output().context("Failed to fetch systems")?.stdout;
3636+ let mut modules: Vec<String> = serde_json::from_slice(&output).context("Failed to parse systems")?;
3737+3838+ modules.insert(0, NO_HARDWARE_MOD.into());
3939+4040+ Ok(modules)
4141+}
4242+4343+fn list_targets() -> Result<Vec<String>> {
4444+ let mut cmd = Command::new("nix");
4545+ cmd.arg("eval")
4646+ .arg("nixpkgs#lib.systems.flakeExposed")
4747+ .arg("--raw")
4848+ .arg("--apply")
4949+ .arg("builtins.toJSON")
5050+ .stdout(Stdio::piped())
5151+ .stderr(Stdio::null());
5252+5353+ let output = cmd.output().context("Failed to fetch systems")?.stdout;
5454+ let modules = serde_json::from_slice(&output).context("Failed to parse systems")?;
5555+ Ok(modules)
5656+}
5757+5858+fn get_nixpkgs_version() -> Result<String> {
5959+ let mut cmd = Command::new("nix");
6060+ cmd.arg("eval")
6161+ .arg("nixpkgs#lib.version")
6262+ .arg("--raw")
6363+ .stdout(Stdio::piped())
6464+ .stderr(Stdio::null());
6565+6666+6767+ let output = cmd.output().context("Failed to fetch systems")?.stdout;
6868+ let output = String::from_utf8_lossy(&output);
6969+7070+ let otp = output.trim().split('.').take(2).collect::<Vec<_>>();
7171+7272+ Ok(otp.join("."))
7373+}
7474+7575+7676+fn gen_hardware_config(root: Option<&Path>) -> Result<String> {
7777+ let mut cmd = Command::new("nixos-generate-config");
7878+ cmd.arg("--show-hardware-config").stdout(Stdio::piped());
7979+8080+ if let Some(root) = root {
8181+ cmd.arg("--root").arg(root);
8282+ }
8383+8484+ let output = cmd.output().context("Failed to fetch systems")?.stdout;
8585+ let output = String::from_utf8_lossy(&output);
8686+8787+ let otp = output.trim().split('\n').skip(3).collect::<Vec<_>>();
8888+ Ok(format!("({})", otp.join("\n")))
8989+}
9090+9191+struct System {
9292+ name: String,
9393+ description: String,
9494+ edition: String,
9595+ target: String,
9696+ include_base_mods: bool,
9797+ roles: Vec<String>,
9898+ hardware_config: String,
9999+ hardware_mod: String,
100100+}
101101+102102+fn dialog(flake_root: &Path) -> Result<System> {
103103+ // Name
104104+105105+ let fl_root_val = flake_root.to_owned();
106106+107107+ let system_filter = move |name: &str| {
108108+ Ok(if !name.is_empty() && name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
109109+ if fl_root_val.join("systems").join(format!("{name}.nix")).is_file() {
110110+ Validation::Invalid("System config already exists!".into())
111111+ } else {
112112+ Validation::Valid
113113+ }
114114+ } else {
115115+ Validation::Invalid("System configs should use kebab-case".into())
116116+ })
117117+ };
118118+119119+ let system_name = Text::new("Enter a name for the new system")
120120+ .with_validator(system_filter)
121121+ .prompt()
122122+ .context("Failed to get system name")?;
123123+124124+ // Target
125125+126126+ let targets = list_targets().context("Failed to get targets")?;
127127+ let system_target = Select::new("Select what arch/OS this system will target", targets)
128128+ .prompt()
129129+ .context("Failed to prompt for system target")?;
130130+131131+ // Description
132132+133133+ let description = Text::new("Enter system description")
134134+ .prompt_skippable()
135135+ .context("Failed to prompt for description")?
136136+ .unwrap_or_else(|| "Generic".to_string());
137137+138138+ // Base Mods
139139+140140+ let include_base_mods = Confirm::new("Should this system include base modules?")
141141+ .with_default(true)
142142+ .prompt()
143143+ .context("Failed to prompt")?;
144144+145145+ // Generate Hardware Config?
146146+147147+ let gen_hw_config = Confirm::new("Would you like to auto-generate a hardware configuration for this system with `nixos-generate-config`?").prompt().context("Failed to prompt")?;
148148+ let hw_config = if gen_hw_config {
149149+ gen_hardware_config(None).context("Failed to generate hw config")?
150150+ } else {
151151+ String::new()
152152+ };
153153+154154+ // NixOS Hardware
155155+156156+ let hardware_mod = if let Ok(modules) = list_hardware_modules() {
157157+ let hardware_mod = Select::new("(Optional) Select a NixOS Hardware Module for the System", modules).with_starting_cursor(0).prompt().context("Failed to prompt for hw modules")?;
158158+ if hardware_mod == NO_HARDWARE_MOD {
159159+ String::new()
160160+ } else {
161161+ hardware_mod
162162+ }
163163+ } else {
164164+ println!("Failed to fetch modules from NixOS hardware, continuing...");
165165+ String::new()
166166+ };
167167+168168+ // Asking For System Roles
169169+170170+ let roles_path = flake_root.join("roles");
171171+ let roles = list_roles(&roles_path).context("Failed to list roles")?;
172172+ let selected_roles = MultiSelect::new("Select the roles for this new system", roles)
173173+ .prompt()
174174+ .context("Failed to promp user for roles")?;
175175+176176+ Ok(System {
177177+ name: system_name,
178178+ description: description,
179179+ target: system_target,
180180+ roles: selected_roles.into_iter().map(|r| format!("\"{r}\"")).collect(),
181181+ edition: get_nixpkgs_version().context("Failed to get latest")?,
182182+ include_base_mods,
183183+ hardware_config: hw_config,
184184+ hardware_mod,
185185+ })
186186+}
187187+188188+impl System {
189189+190190+ const FILE_TEMPLATE: &str = include_str!("sys_template.nix");
191191+192192+ fn generate_file(&self) -> String {
193193+ Self::FILE_TEMPLATE
194194+ .replace("__TARGET__", &self.target)
195195+ .replace("__DESCRIPTION__", &self.description)
196196+ .replace("__EDITION__", &self.edition)
197197+ .replace("__INCL_BASE_MODS__", &self.include_base_mods.to_string())
198198+ .replace("__ROLES__", &self.roles.join(" "))
199199+ .replace("__HARDWARE_MOD__", &self.hardware_mod)
200200+ .replace("__HARDWARE_CONFIG__", &self.hardware_config)
201201+ }
202202+203203+}
204204+205205+pub fn add_system_dialog(flake_root: &Path) -> Result {
206206+ let sys = dialog(flake_root)?;
207207+208208+ let file = sys.generate_file();
209209+210210+ let edited_file = Editor::new("Review the new system file with (e), confirm with (enter)").with_predefined_text(&file).with_file_extension("nix").prompt().context("Failed to get edits")?;
211211+212212+ let path = flake_root.join("systems").join(format!("{}.nix", sys.name));
213213+214214+ println!("Saving New File to {}", path.display());
215215+216216+ fs::write(path, &edited_file).context("Failed to write new file")
217217+}
+22
create-sys/src/main.rs
···11+use clap::clap_derive::Parser;
22+33+mod add;
44+55+mod prelude {
66+ use anyhow::Error as AError;
77+ use std::result::Result as SResult;
88+99+ pub type Result<T = (), E = AError> = SResult<T, E>;
1010+ pub use anyhow::Context;
1111+}
1212+1313+use prelude::*;
1414+1515+#[derive(Parser)]
1616+struct Cli {}
1717+1818+fn main() -> Result {
1919+ let dir = std::env::current_dir()?;
2020+ let dir = dir.parent().context("")?;
2121+ add::add_system_dialog(dir)
2222+}