From 62dc2e6499c773c8d0c82dcacd73f1a31bf2c3c2 Mon Sep 17 00:00:00 2001 From: Nikkuss Date: Fri, 5 Jun 2026 23:14:20 +0400 Subject: [PATCH] home-manager --- nix/flakeModule.nix | 68 +++++++++++++++++++++++++++++++++++++++++++-- src/app.rs | 4 +-- src/manifest.rs | 4 +++ src/ui.rs | 68 +++++++++++++++++++++++++++++---------------- 4 files changed, 115 insertions(+), 29 deletions(-) diff --git a/nix/flakeModule.nix b/nix/flakeModule.nix index 2e0bf0f..431280a 100644 --- a/nix/flakeModule.nix +++ b/nix/flakeModule.nix @@ -1,7 +1,6 @@ { config, lib, - flake-parts-lib, inputs, den, ... @@ -17,6 +16,14 @@ in default = [ ]; description = "A list of master keys for encrypting secrets."; }; + homeIdentities = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "laptop-home" + "wawa-wawa-home" + ]; + }; secretsDir = mkOption { type = types.str; default = null; @@ -39,6 +46,12 @@ in description = "A function that takes the NixOS configuration and returns a NixOS module to apply to the host based on its network configuration."; }; }; + homeManagerModule = { + default = mkOption { + type = types.functionTo types.attrs; + default = identity: { }; + }; + }; }; config = let @@ -74,6 +87,9 @@ in ++ (lib.optionals value.global all_keys) ++ cfg.masterKeys ); + # Descriptive flag: is this secret consumed by a home-manager user? + # True when it is global or targets any identity in `homeIdentities`. + home = value.global || lib.any (h: lib.elem h cfg.homeIdentities) value.hosts; in { inherit (value) @@ -83,7 +99,7 @@ in global ; keys = value.keys or [ ]; - inherit sopskeys; + inherit sopskeys home; } ) secrets; @@ -130,12 +146,55 @@ in ) secret_map ) ); + + # Home-manager analog of sops_secrets_map. Takes a key identity (e.g. + # `laptop-home`) and deliberately omits `neededForUsers`, which the + # sops-nix home-manager module does not support (it only matters for + # decrypting before system users exist, a NixOS-only concern). + home_secrets_map = + identity: + lib.mkMerge ( + lib.concatLists ( + lib.mapAttrsToList ( + name: value: + let + hasHost = (lib.elem identity value.hosts) || value.global; + isYamlOrJson = value.format == "yaml" || value.format == "json"; + in + ( + [ + ( + if hasHost && !(isYamlOrJson && value.keys != [ ]) then + { + ${name} = { + inherit (value) format; + sopsFile = inputs.self + "/${cfg.secretsDir}/${name}"; + }; + } + else + { } + ) + ] + ++ lib.optionals hasHost ( + lib.map (v: { + "${name}-${v}" = { + inherit (value) format; + sopsFile = inputs.self + "/${cfg.secretsDir}/${name}"; + key = v; + }; + }) value.keys + ) + ) + ) secret_map + ) + ); in { flake.secretsManifest = { secretsDir = cfg.secretsDir; masterKeys = cfg.masterKeys; hosts = lib.mapAttrs (_: keys: { inherit keys; }) per_host_keys; + homeIdentities = cfg.homeIdentities; secrets = secret_map; secrets_map = sops_secrets_map; }; @@ -151,6 +210,11 @@ in config.sops.secrets = sops_secrets_map config.networking.hostName; }; }; + secrets.homeManagerModule = { + default = identity: { + config.sops.secrets = home_secrets_map identity; + }; + }; perSystem = { pkgs, self', ... }: let diff --git a/src/app.rs b/src/app.rs index 66ec476..89092dd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,12 +1,10 @@ use color_eyre::Report; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use ratatui::{prelude::CrosstermBackend, widgets::ListState, DefaultTerminal, Terminal}; -use ratatui_form::{Email, Form}; +use ratatui::{prelude::CrosstermBackend, widgets::ListState, Terminal}; use std::{io, path::Path}; use crate::{ event::{AppEvent, Event, EventHandler}, - form::FormState, manifest::{load_manifest, Manifest}, }; diff --git a/src/manifest.rs b/src/manifest.rs index b9ebc1e..e2df43d 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -11,6 +11,8 @@ pub struct Manifest { #[serde(rename = "masterKeys")] pub master_keys: Vec, pub hosts: BTreeMap, + #[serde(rename = "homeIdentities", default)] + pub home_identities: Vec, pub secrets: BTreeMap, } @@ -28,6 +30,8 @@ pub struct SecretInfo { #[serde(rename = "neededForUsers")] pub needed_for_users: bool, pub keys: Vec, + #[serde(default)] + pub home: bool, } #[derive(Debug, Clone, Deserialize, PartialEq)] diff --git a/src/ui.rs b/src/ui.rs index 19826c0..6bfddc0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -91,25 +91,29 @@ fn render_secret_detail(state: &App, area: Rect, buf: &mut Buffer) { Span::styled(scope, Style::default().fg(Color::White)), ])); if !secret.global { - // Hosts - let hosts_str = if secret.hosts.is_empty() { - if secret.global { - manifest - .hosts - .keys() - .cloned() - .collect::>() - .join(", ") - } else { - "none".to_string() - } + // Hosts / identities. Home-manager identities are coloured distinctly + // so it is obvious which targets are user (home) rather than system. + let mut host_spans: Vec = vec![Span::styled( + " Hosts: ", + Style::default().fg(Color::DarkGray), + )]; + if secret.hosts.is_empty() { + host_spans.push(Span::styled("none", Style::default().fg(Color::White))); } else { - secret.hosts.join(", ") - }; - lines.push(Line::from(vec![ - Span::styled(" Hosts: ", Style::default().fg(Color::DarkGray)), - Span::styled(hosts_str, Style::default().fg(Color::White)), - ])); + for (i, identity) in secret.hosts.iter().enumerate() { + if i > 0 { + host_spans.push(Span::styled(", ", Style::default().fg(Color::DarkGray))); + } + let is_home = manifest.home_identities.contains(identity); + let style = if is_home { + Style::default().fg(Color::Magenta) + } else { + Style::default().fg(Color::White) + }; + host_spans.push(Span::styled(identity.clone(), style)); + } + } + lines.push(Line::from(host_spans)); } // Recipients @@ -128,6 +132,14 @@ fn render_secret_detail(state: &App, area: Rect, buf: &mut Buffer) { Span::styled("yes", Style::default().fg(Color::Yellow)), ])); } + + // Home-manager: shown when this secret is consumed by a home-manager user. + if secret.home { + lines.push(Line::from(vec![ + Span::styled(" Home-mgr: ", Style::default().fg(Color::DarkGray)), + Span::styled("yes", Style::default().fg(Color::Magenta)), + ])); + } while lines.len() < 6 { lines.push(Line::from("")); } @@ -151,14 +163,22 @@ fn render_secret_detail(state: &App, area: Rect, buf: &mut Buffer) { let label = if manifest.master_keys.contains(key) { Span::styled(" (master)", Style::default().fg(Color::Magenta)) } else { - // Try to find which host owns this key - let host_label = manifest + // Try to find which identity owns this key. Home identities are + // tagged and coloured distinctly from system identities. + match manifest .hosts .iter() .find(|(_, info)| info.keys.contains(key)) - .map(|(name, _)| format!(" ({})", name)) - .unwrap_or_default(); - Span::styled(host_label, Style::default().fg(Color::Cyan)) + { + Some((name, _)) if manifest.home_identities.contains(name) => Span::styled( + format!(" ({name}, home)"), + Style::default().fg(Color::Magenta), + ), + Some((name, _)) => { + Span::styled(format!(" ({name})"), Style::default().fg(Color::Cyan)) + } + None => Span::raw(""), + } }; lines.push(Line::from(vec![ @@ -168,7 +188,7 @@ fn render_secret_detail(state: &App, area: Rect, buf: &mut Buffer) { ])); } - let paragraph = Paragraph::new(lines).block(block).render(area, buf); + Paragraph::new(lines).block(block).render(area, buf); } fn render_placeholder(area: Rect, buf: &mut Buffer, title: &str, message: &str) {