diff --git a/configs/nixos/guests/actualbudget.nix b/configs/nixos/guests/actualbudget.nix index 10a973a..5bd3157 100644 --- a/configs/nixos/guests/actualbudget.nix +++ b/configs/nixos/guests/actualbudget.nix @@ -4,7 +4,7 @@ ... }: let - cfg = config.guests.actualbudget; + cfg = config.guests.actualbudget; # NOLINT(let_order) dataDir = "/data"; in { diff --git a/configs/nixos/guests/copyparty.nix b/configs/nixos/guests/copyparty.nix index 63c57f3..fd4d9b7 100644 --- a/configs/nixos/guests/copyparty.nix +++ b/configs/nixos/guests/copyparty.nix @@ -4,7 +4,7 @@ ... }: let - cfg = config.guests.copyparty; + cfg = config.guests.copyparty; # NOLINT(let_order) volumeType = lib.types.submodule { options = { diff --git a/configs/nixos/guests/homepage.nix b/configs/nixos/guests/homepage.nix index da3e0b8..7461642 100644 --- a/configs/nixos/guests/homepage.nix +++ b/configs/nixos/guests/homepage.nix @@ -4,7 +4,7 @@ ... }: let - cfg = config.guests.homepage; + cfg = config.guests.homepage; # NOLINT(let_order) environmentFile = "/run/secrets/homepage.env"; in { diff --git a/configs/nixos/guests/palworld.nix b/configs/nixos/guests/palworld.nix index 9ae2208..057a90b 100644 --- a/configs/nixos/guests/palworld.nix +++ b/configs/nixos/guests/palworld.nix @@ -4,7 +4,7 @@ ... }: let - cfg = config.guests.palworld; + cfg = config.guests.palworld; # NOLINT(let_order) steamDir = "/data/steamcmd"; installDir = "/data/server"; stateDir = "/data/state"; diff --git a/configs/nixos/guests/valheim.nix b/configs/nixos/guests/valheim.nix index 6c38233..c1085d1 100644 --- a/configs/nixos/guests/valheim.nix +++ b/configs/nixos/guests/valheim.nix @@ -4,7 +4,7 @@ ... }: let - cfg = config.guests.valheim; + cfg = config.guests.valheim; # NOLINT(let_order) steamDir = "/data/steamcmd"; installDir = "/data/server"; stateDir = "/data/state"; diff --git a/configs/nixos/modules/desktop/apps/alacritty.nix b/configs/nixos/modules/desktop/apps/alacritty.nix index 785a71e..f8ce8d5 100644 --- a/configs/nixos/modules/desktop/apps/alacritty.nix +++ b/configs/nixos/modules/desktop/apps/alacritty.nix @@ -6,7 +6,7 @@ }: # Alacritty as the desktop terminal. let - cfg = config.modules.desktop.apps.alacritty; + cfg = config.modules.desktop.apps.alacritty; # NOLINT(let_order) user = config.user.name; in { diff --git a/configs/nixos/modules/desktop/apps/firefox.nix b/configs/nixos/modules/desktop/apps/firefox.nix index d47e3bd..e29035a 100644 --- a/configs/nixos/modules/desktop/apps/firefox.nix +++ b/configs/nixos/modules/desktop/apps/firefox.nix @@ -7,9 +7,9 @@ }: # Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy. let - cfg = config.modules.desktop.apps.firefox; - user = config.user.name; + cfg = config.modules.desktop.apps.firefox; # NOLINT(let_order) firefoxAddons = inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system}; + user = config.user.name; in { options.modules.desktop.apps.firefox = { diff --git a/configs/nixos/modules/desktop/apps/ghostty.nix b/configs/nixos/modules/desktop/apps/ghostty.nix index c39451e..a3b7853 100644 --- a/configs/nixos/modules/desktop/apps/ghostty.nix +++ b/configs/nixos/modules/desktop/apps/ghostty.nix @@ -6,7 +6,7 @@ }: # Ghostty as the desktop terminal. let - cfg = config.modules.desktop.apps.ghostty; + cfg = config.modules.desktop.apps.ghostty; # NOLINT(let_order) user = config.user.name; in { diff --git a/configs/nixos/modules/desktop/apps/obsidian.nix b/configs/nixos/modules/desktop/apps/obsidian.nix index 38b0deb..bc1f190 100644 --- a/configs/nixos/modules/desktop/apps/obsidian.nix +++ b/configs/nixos/modules/desktop/apps/obsidian.nix @@ -6,7 +6,7 @@ }: # Obsidian as the desktop note-taking vault. let - cfg = config.modules.desktop.apps.obsidian; + cfg = config.modules.desktop.apps.obsidian; # NOLINT(let_order) user = config.user.name; in { diff --git a/configs/nixos/modules/desktop/apps/r2modman.nix b/configs/nixos/modules/desktop/apps/r2modman.nix index 6607dfd..326f6b6 100644 --- a/configs/nixos/modules/desktop/apps/r2modman.nix +++ b/configs/nixos/modules/desktop/apps/r2modman.nix @@ -6,7 +6,7 @@ }: # r2modman, a Thunderstore mod manager. let - cfg = config.modules.desktop.apps.r2modman; + cfg = config.modules.desktop.apps.r2modman; # NOLINT(let_order) user = config.user.name; in { diff --git a/configs/nixos/modules/desktop/apps/signal.nix b/configs/nixos/modules/desktop/apps/signal.nix index 2c534d4..8d92ef4 100644 --- a/configs/nixos/modules/desktop/apps/signal.nix +++ b/configs/nixos/modules/desktop/apps/signal.nix @@ -6,7 +6,7 @@ }: # Signal desktop messenger. let - cfg = config.modules.desktop.apps.signal; + cfg = config.modules.desktop.apps.signal; # NOLINT(let_order) user = config.user.name; in { diff --git a/configs/nixos/modules/desktop/apps/steam.nix b/configs/nixos/modules/desktop/apps/steam.nix index 0bafc21..f2ca917 100644 --- a/configs/nixos/modules/desktop/apps/steam.nix +++ b/configs/nixos/modules/desktop/apps/steam.nix @@ -6,7 +6,7 @@ }: # Steam game launcher and runtime integration. let - cfg = config.modules.desktop.apps.steam; + cfg = config.modules.desktop.apps.steam; # NOLINT(let_order) in { options.modules.desktop.apps.steam.enable = lib.mkEnableOption "Steam game launcher"; diff --git a/configs/nixos/modules/desktop/apps/vencord.nix b/configs/nixos/modules/desktop/apps/vencord.nix index 53b6ecf..95a7f2c 100644 --- a/configs/nixos/modules/desktop/apps/vencord.nix +++ b/configs/nixos/modules/desktop/apps/vencord.nix @@ -6,7 +6,7 @@ }: # Vesktop, a Discord client with Vencord built in. let - cfg = config.modules.desktop.apps.vencord; + cfg = config.modules.desktop.apps.vencord; # NOLINT(let_order) user = config.user.name; in { diff --git a/configs/nixos/modules/desktop/default.nix b/configs/nixos/modules/desktop/default.nix index c9bf014..43e53a2 100644 --- a/configs/nixos/modules/desktop/default.nix +++ b/configs/nixos/modules/desktop/default.nix @@ -1,7 +1,7 @@ { config, lib, ... }: # The desktop aggregator: one flag brings up the whole graphical session. let - cfg = config.modules.desktop; + cfg = config.modules.desktop; # NOLINT(let_order) in { options.modules.desktop.enable = lib.mkEnableOption "the keyboard-driven Hyprland desktop"; diff --git a/configs/nixos/modules/desktop/session/hyprland/default.nix b/configs/nixos/modules/desktop/session/hyprland/default.nix index 445f293..7cc25db 100644 --- a/configs/nixos/modules/desktop/session/hyprland/default.nix +++ b/configs/nixos/modules/desktop/session/hyprland/default.nix @@ -6,8 +6,7 @@ }: # The Hyprland compositor. let - cfg = config.modules.desktop.session.hyprland; - user = config.user.name; + cfg = config.modules.desktop.session.hyprland; # NOLINT(let_order) cursor = config.stylix.cursor; # Numbered-workspace switch and move for 1..9, the operator's i3 muscle memory. @@ -16,6 +15,8 @@ let "$mod SHIFT, ${toString n}, movetoworkspace, ${toString n}" ]) (lib.range 1 9); + user = config.user.name; + # Flip the tiling strategy between the two built-in layouts, since neither a # dispatcher nor a keyword toggles it on its own. toggleLayout = pkgs.writeShellScript "hypr-toggle-layout" '' diff --git a/configs/nixos/modules/desktop/session/hyprland/hypridle.nix b/configs/nixos/modules/desktop/session/hyprland/hypridle.nix index 96411e3..a77e501 100644 --- a/configs/nixos/modules/desktop/session/hyprland/hypridle.nix +++ b/configs/nixos/modules/desktop/session/hyprland/hypridle.nix @@ -6,15 +6,16 @@ }: # hypridle: idle-triggered locking and display power management. let - cfg = config.modules.desktop.session.hyprland.hypridle; + cfg = config.modules.desktop.session.hyprland.hypridle; # NOLINT(let_order) + + # The guard drops the call when a locker is already up, so no idle trigger + # stacks a second hyprlock over the first. + lockCmd = "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"; user = config.user.name; hyprctl = "${config.programs.hyprland.package}/bin/hyprctl"; hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock"; - # The guard drops the call when a locker is already up, so no idle trigger - # stacks a second hyprlock over the first. - lockCmd = "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"; in { options.modules.desktop.session.hyprland.hypridle = { diff --git a/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix b/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix index 50a9782..ce7e8ec 100644 --- a/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix +++ b/configs/nixos/modules/desktop/session/hyprland/hyprlock.nix @@ -6,7 +6,7 @@ }: # The lock screen: a hyprlock session-lock surface the compositor owns. let - cfg = config.modules.desktop.session.hyprland.hyprlock; + cfg = config.modules.desktop.session.hyprland.hyprlock; # NOLINT(let_order) user = config.user.name; # The hyprlock this module installs, used by the lock keybind below. diff --git a/configs/nixos/modules/desktop/session/login.nix b/configs/nixos/modules/desktop/session/login.nix index d3f6cfd..b1aa8fb 100644 --- a/configs/nixos/modules/desktop/session/login.nix +++ b/configs/nixos/modules/desktop/session/login.nix @@ -7,7 +7,7 @@ # Text login: greetd running the tuigreet greeter, which starts the session # through the universal Wayland session manager. let - cfg = config.modules.desktop.session.login; + cfg = config.modules.desktop.session.login; # NOLINT(let_order) in { options.modules.desktop.session.login.enable = diff --git a/configs/nixos/modules/desktop/session/mako.nix b/configs/nixos/modules/desktop/session/mako.nix index fc1829f..0c4703c 100644 --- a/configs/nixos/modules/desktop/session/mako.nix +++ b/configs/nixos/modules/desktop/session/mako.nix @@ -6,7 +6,7 @@ }: # mako as the notification daemon: toasts, a do-not-disturb mode, and history recall. let - cfg = config.modules.desktop.session.mako; + cfg = config.modules.desktop.session.mako; # NOLINT(let_order) user = config.user.name; makoctl = "${pkgs.mako}/bin/makoctl"; diff --git a/configs/nixos/modules/desktop/session/portals.nix b/configs/nixos/modules/desktop/session/portals.nix index cd156a8..6132146 100644 --- a/configs/nixos/modules/desktop/session/portals.nix +++ b/configs/nixos/modules/desktop/session/portals.nix @@ -1,7 +1,7 @@ { config, lib, ... }: # XDG desktop portal routing for in-app screen sharing and file dialogs. let - cfg = config.modules.desktop.session.portals; + cfg = config.modules.desktop.session.portals; # NOLINT(let_order) in { options.modules.desktop.session.portals.enable = diff --git a/configs/nixos/modules/desktop/session/rofi.nix b/configs/nixos/modules/desktop/session/rofi.nix index 8a62c56..a39d0d3 100644 --- a/configs/nixos/modules/desktop/session/rofi.nix +++ b/configs/nixos/modules/desktop/session/rofi.nix @@ -6,7 +6,7 @@ }: # rofi as the desktop launcher and dmenu-style menu frontend. let - cfg = config.modules.desktop.session.rofi; + cfg = config.modules.desktop.session.rofi; # NOLINT(let_order) user = config.user.name; # A session menu built on the same themed rofi, so the power actions look diff --git a/configs/nixos/modules/desktop/session/waybar.nix b/configs/nixos/modules/desktop/session/waybar.nix index 330903f..3015552 100644 --- a/configs/nixos/modules/desktop/session/waybar.nix +++ b/configs/nixos/modules/desktop/session/waybar.nix @@ -6,13 +6,14 @@ }: # Waybar: a top status bar reading system state at a glance. let - cfg = config.modules.desktop.session.waybar; + cfg = config.modules.desktop.session.waybar; # NOLINT(let_order) vpnClient = config.modules.network.vpn-client; - user = config.user.name; # A Nerd Font Private-Use-Area codepoint as a literal character, decoded # through JSON so the glyph survives as bytes rather than an editor paste. g = code: builtins.fromJSON ''"\u${code}"''; + user = config.user.name; + sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"''; # Renders and toggles mako's do-not-disturb mode. diff --git a/configs/nixos/modules/desktop/utilities/audio.nix b/configs/nixos/modules/desktop/utilities/audio.nix index 0412bc9..375060b 100644 --- a/configs/nixos/modules/desktop/utilities/audio.nix +++ b/configs/nixos/modules/desktop/utilities/audio.nix @@ -1,7 +1,7 @@ { config, lib, ... }: # PipeWire as the desktop audio server. let - cfg = config.modules.desktop.utilities.audio; + cfg = config.modules.desktop.utilities.audio; # NOLINT(let_order) in { options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server"; diff --git a/configs/nixos/modules/desktop/utilities/clipboard.nix b/configs/nixos/modules/desktop/utilities/clipboard.nix index 612255f..981426b 100644 --- a/configs/nixos/modules/desktop/utilities/clipboard.nix +++ b/configs/nixos/modules/desktop/utilities/clipboard.nix @@ -6,7 +6,7 @@ }: # Clipboard history: cliphist records every copy, picked back through rofi. let - cfg = config.modules.desktop.utilities.clipboard; + cfg = config.modules.desktop.utilities.clipboard; # NOLINT(let_order) user = config.user.name; cliphist = "${pkgs.cliphist}/bin/cliphist"; diff --git a/configs/nixos/modules/desktop/utilities/osd.nix b/configs/nixos/modules/desktop/utilities/osd.nix index d6227c5..031fa7c 100644 --- a/configs/nixos/modules/desktop/utilities/osd.nix +++ b/configs/nixos/modules/desktop/utilities/osd.nix @@ -6,7 +6,7 @@ }: # SwayOSD: a transient on-screen popup for volume and brightness changes. let - cfg = config.modules.desktop.utilities.osd; + cfg = config.modules.desktop.utilities.osd; # NOLINT(let_order) user = config.user.name; in { diff --git a/configs/nixos/modules/desktop/utilities/recording.nix b/configs/nixos/modules/desktop/utilities/recording.nix index 7fa9ccc..15102a6 100644 --- a/configs/nixos/modules/desktop/utilities/recording.nix +++ b/configs/nixos/modules/desktop/utilities/recording.nix @@ -6,7 +6,7 @@ }: # Keybound screen recording: pick a region, then toggle a video-only capture. let - cfg = config.modules.desktop.utilities.recording; + cfg = config.modules.desktop.utilities.recording; # NOLINT(let_order) user = config.user.name; slurp = "${pkgs.slurp}/bin/slurp"; diff --git a/configs/nixos/modules/desktop/utilities/screenshot.nix b/configs/nixos/modules/desktop/utilities/screenshot.nix index ba18c98..ff33392 100644 --- a/configs/nixos/modules/desktop/utilities/screenshot.nix +++ b/configs/nixos/modules/desktop/utilities/screenshot.nix @@ -7,7 +7,7 @@ # Keyboard-driven screenshots that open in satty for annotation, then land in # both the clipboard and a dated file. let - cfg = config.modules.desktop.utilities.screenshot; + cfg = config.modules.desktop.utilities.screenshot; # NOLINT(let_order) user = config.user.name; grimblast = "${pkgs.grimblast}/bin/grimblast"; diff --git a/configs/nixos/modules/desktop/utilities/theming.nix b/configs/nixos/modules/desktop/utilities/theming.nix index 6c7768d..e1278fb 100644 --- a/configs/nixos/modules/desktop/utilities/theming.nix +++ b/configs/nixos/modules/desktop/utilities/theming.nix @@ -6,8 +6,7 @@ }: # Nord theming for the graphical layer, driven from one base16 scheme by Stylix. let - cfg = config.modules.desktop.utilities.theming; - user = config.user.name; + cfg = config.modules.desktop.utilities.theming; # NOLINT(let_order) # A single static Nord wallpaper, drawn as a vertical gradient across the # Polar Night shades so it needs no committed binary and no network fetch. @@ -16,6 +15,7 @@ let -size 2560x1440 gradient:'#2E3440'-'#3B4252' \ "$out" ''; + user = config.user.name; in { options.modules.desktop.utilities.theming.enable = diff --git a/configs/nixos/modules/desktop/utilities/userdirs.nix b/configs/nixos/modules/desktop/utilities/userdirs.nix index 7909f10..0b7a0f0 100644 --- a/configs/nixos/modules/desktop/utilities/userdirs.nix +++ b/configs/nixos/modules/desktop/utilities/userdirs.nix @@ -2,7 +2,7 @@ # Declares the XDG user directories, so a tool that queries them lands in a # well-known folder a host can relocate from one place. let - cfg = config.modules.desktop.utilities.userdirs; + cfg = config.modules.desktop.utilities.userdirs; # NOLINT(let_order) user = config.user.name; in { diff --git a/configs/nixos/modules/guest.nix b/configs/nixos/modules/guest.nix index b786050..588614e 100644 --- a/configs/nixos/modules/guest.nix +++ b/configs/nixos/modules/guest.nix @@ -1,6 +1,6 @@ { config, lib, ... }: let - cfg = config.modules.guest; + cfg = config.modules.guest; # NOLINT(let_order) in { options.modules.guest.enable = lib.mkEnableOption "nested-guest base configuration"; diff --git a/configs/nixos/modules/host.nix b/configs/nixos/modules/host.nix index ca4ce4d..3805c92 100644 --- a/configs/nixos/modules/host.nix +++ b/configs/nixos/modules/host.nix @@ -4,7 +4,7 @@ ... }: let - cfg = config.modules.host; + cfg = config.modules.host; # NOLINT(let_order) inherit (config) user; hostSecrets = ../secrets + "/${config.networking.hostName}.yaml"; passwordSecret = "${user.name}-password"; diff --git a/configs/nixos/modules/monorepo.nix b/configs/nixos/modules/monorepo.nix index 38c963e..b46806a 100644 --- a/configs/nixos/modules/monorepo.nix +++ b/configs/nixos/modules/monorepo.nix @@ -5,7 +5,7 @@ }: # Tools for developing this monorepo. let - cfg = config.modules.monorepo; + cfg = config.modules.monorepo; # NOLINT(let_order) in { options.modules.monorepo.enable = lib.mkEnableOption "tools for developing this monorepo"; diff --git a/configs/nixos/modules/network/ssh.nix b/configs/nixos/modules/network/ssh.nix index 45e8e75..4d7c2bb 100644 --- a/configs/nixos/modules/network/ssh.nix +++ b/configs/nixos/modules/network/ssh.nix @@ -6,33 +6,7 @@ }: # SSH on this machine, in both directions. let - cfg = config.modules.network.ssh; - user = config.user.name; - - inherit (lib) - concatLists - concatStringsSep - elem - filter - genAttrs - hasAttr - imap0 - listToAttrs - mapAttrs - mapAttrsToList - mkIf - mkMerge - mkOption - nameValuePair - optional - optionalAttrs - types - unique - ; - - hostKeySecret = type: "ssh-host-${type}-key"; - userKeySecret = "ssh-user-ed25519-key"; - + cfg = config.modules.network.ssh; # NOLINT(let_order) targetType = types.submodule ( { name, ... }: { @@ -90,54 +64,18 @@ let } ); - keyWithoutComment = key: concatStringsSep " " (lib.take 2 (lib.splitString " " key)); - - targetNamesIn = names: filter (name: hasAttr name cfg.targets) names; - - workstationNames = targetNamesIn cfg.hosts.workstations; - serverNames = targetNamesIn cfg.hosts.servers; - - clientKeysFor = names: filter (key: key != null) (map (name: cfg.targets.${name}.clientKey) names); - - currentHost = config.networking.hostName; - isServer = elem currentHost cfg.hosts.servers; - isWorkstation = elem currentHost cfg.hosts.workstations; - defaultAuthorizedKeys = lib.flatten ( clientKeysFor workstationNames ++ optional isServer (clientKeysFor serverNames) ); - outboundTargetNames = - let - groupTargets = - if isServer then - [ "gitea" ] ++ cfg.hosts.servers - else if isWorkstation then - [ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations - else - [ "gitea" ]; - in - filter (name: name != currentHost) (targetNamesIn groupTargets); + clientKeysFor = names: filter (key: key != null) (map (name: cfg.targets.${name}.clientKey) names); - sshSettingsFor = - name: - let - target = cfg.targets.${name}; - in - { - header = "Host ${concatStringsSep " " target.aliases}"; - HostName = target.hostName; - User = target.user; - } - // optionalAttrs (target.port != 22) { Port = target.port; }; + workstationNames = targetNamesIn cfg.hosts.workstations; + targetNamesIn = names: filter (name: hasAttr name cfg.targets) names; - knownHostNamesFor = - target: - let - names = unique (target.aliases ++ [ target.hostName ]); - withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}"; - in - map withPort names; + isServer = elem currentHost cfg.hosts.servers; + currentHost = config.networking.hostName; + serverNames = targetNamesIn cfg.hosts.servers; knownHosts = listToAttrs ( concatLists ( @@ -153,6 +91,68 @@ let ) cfg.targets ) ); + knownHostNamesFor = + target: + let + withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}"; + names = unique (target.aliases ++ [ target.hostName ]); + in + map withPort names; + + keyWithoutComment = key: concatStringsSep " " (lib.take 2 (lib.splitString " " key)); + + sshSettingsFor = + name: + let + target = cfg.targets.${name}; + in + { + header = "Host ${concatStringsSep " " target.aliases}"; + HostName = target.hostName; + User = target.user; + } + // optionalAttrs (target.port != 22) { Port = target.port; }; + + outboundTargetNames = + let + groupTargets = + if isServer then + [ "gitea" ] ++ cfg.hosts.servers + else if isWorkstation then + [ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations + else + [ "gitea" ]; + in + filter (name: name != currentHost) (targetNamesIn groupTargets); + + isWorkstation = elem currentHost cfg.hosts.workstations; + + hostKeySecret = type: "ssh-host-${type}-key"; + user = config.user.name; + + inherit (lib) + concatLists + concatStringsSep + elem + filter + genAttrs + hasAttr + imap0 + listToAttrs + mapAttrs + mapAttrsToList + mkIf + mkMerge + mkOption + nameValuePair + optional + optionalAttrs + types + unique + ; + + userKeySecret = "ssh-user-ed25519-key"; + in { options.modules.network.ssh = { diff --git a/configs/nixos/modules/network/vlans.nix b/configs/nixos/modules/network/vlans.nix index c945821..25d75de 100644 --- a/configs/nixos/modules/network/vlans.nix +++ b/configs/nixos/modules/network/vlans.nix @@ -6,7 +6,7 @@ }: # The host networking foundation: per-VLAN bridges over a tagged trunk. let - cfg = config.modules.network.vlans; + cfg = config.modules.network.vlans; # NOLINT(let_order) inherit (util) bridgeName; diff --git a/configs/nixos/modules/network/vpn-client.nix b/configs/nixos/modules/network/vpn-client.nix index 495b95a..19d4669 100644 --- a/configs/nixos/modules/network/vpn-client.nix +++ b/configs/nixos/modules/network/vpn-client.nix @@ -4,7 +4,7 @@ ... }: let - cfg = config.modules.network.vpn-client; + cfg = config.modules.network.vpn-client; # NOLINT(let_order) in { options.modules.network.vpn-client = { diff --git a/configs/nixos/modules/services/actualbudget.nix b/configs/nixos/modules/services/actualbudget.nix index 73e7bdd..eaddda6 100644 --- a/configs/nixos/modules/services/actualbudget.nix +++ b/configs/nixos/modules/services/actualbudget.nix @@ -4,7 +4,7 @@ ... }: let - cfg = config.modules.services.actualbudget; + cfg = config.modules.services.actualbudget; # NOLINT(let_order) in { options.modules.services.actualbudget = { diff --git a/configs/nixos/modules/services/copyparty.nix b/configs/nixos/modules/services/copyparty.nix index e1d0ce3..25c1459 100644 --- a/configs/nixos/modules/services/copyparty.nix +++ b/configs/nixos/modules/services/copyparty.nix @@ -5,7 +5,7 @@ ... }: let - cfg = config.modules.services.copyparty; + cfg = config.modules.services.copyparty; # NOLINT(let_order) volumeType = lib.types.submodule { options = { @@ -38,6 +38,31 @@ let }; }; + configScript = pkgs.writeShellScript "copyparty-config" '' + set -eu + + umask 077 + password="$(${pkgs.coreutils}/bin/tr -d '\n' < "$CREDENTIALS_DIRECTORY/password")" + + cat > /run/copyparty/copyparty.conf < /run/copyparty/copyparty.conf < 1) (serviceNamesByUid entries); serviceNamesByUid = entries: @@ -13,10 +10,6 @@ let groupBy (entry: toString entry.value) entries ); - duplicateUids = - entries: - filterAttrs (_uid: serviceNames: builtins.length serviceNames > 1) (serviceNamesByUid entries); - in { # Next stable UID: 20005 diff --git a/src/dotfiles/guests.nix b/src/dotfiles/guests.nix index 4085661..ba4cad9 100644 --- a/src/dotfiles/guests.nix +++ b/src/dotfiles/guests.nix @@ -8,6 +8,7 @@ util, }: let + files = util.collectNixFiles root; mkGuest = name: { config, lib, ... }: @@ -167,22 +168,22 @@ let config = lib.mkIf cfg.enable { assertions = let - hasDirectorySettings = mount: mount.owner != null || mount.group != null || mount.mode != null; - hasCompleteDirectorySettings = - mount: mount.owner != null && mount.group != null && mount.mode != null; - hasOneSource = mount: (mount.directory != null) != (mount.file != null); mountsWithInvalidSource = lib.attrNames ( lib.filterAttrs (_: mount: !hasOneSource mount) cfg.mounts ); + hasOneSource = mount: (mount.directory != null) != (mount.file != null); fileMountsWithHostSettings = lib.attrNames ( lib.filterAttrs (_: mount: mount.file != null && hasDirectorySettings mount) cfg.mounts ); + hasDirectorySettings = mount: mount.owner != null || mount.group != null || mount.mode != null; directoryMountsWithPartialHostSettings = lib.attrNames ( lib.filterAttrs ( _: mount: mount.directory != null && hasDirectorySettings mount && !hasCompleteDirectorySettings mount ) cfg.mounts ); + hasCompleteDirectorySettings = + mount: mount.owner != null && mount.group != null && mount.mode != null; in [ { @@ -275,11 +276,10 @@ let }; }; - files = util.collectNixFiles root; + names = map (file: lib.removeSuffix ".nix" file) (lib.attrNames guestFiles); guestFiles = lib.filterAttrs (file: type: type == "regular" && lib.hasSuffix ".nix" file) ( builtins.readDir root ); - names = map (file: lib.removeSuffix ".nix" file) (lib.attrNames guestFiles); in { imports = files ++ map mkGuest names; diff --git a/src/dotfiles/hosts.nix b/src/dotfiles/hosts.nix index 09ff2dd..5ce6e53 100644 --- a/src/dotfiles/hosts.nix +++ b/src/dotfiles/hosts.nix @@ -9,6 +9,8 @@ registryModule, }: let + names = lib.attrNames directories; + directories = lib.filterAttrs (_name: type: type == "directory") (builtins.readDir root); mkHost = { root, @@ -36,7 +38,5 @@ let ]; }; - directories = lib.filterAttrs (_name: type: type == "directory") (builtins.readDir root); - names = lib.attrNames directories; in lib.genAttrs names (name: mkHost { inherit root name; }) diff --git a/src/natu/ast.rs b/src/natu/ast.rs index c24b52c..61038d9 100644 --- a/src/natu/ast.rs +++ b/src/natu/ast.rs @@ -49,6 +49,7 @@ pub enum Expr { Ident(Ident), Select(Select), Let { + range: SourceRange, bindings: Vec, body: Box, }, diff --git a/src/natu/natu.rs b/src/natu/natu.rs index d942ea5..6197a54 100644 --- a/src/natu/natu.rs +++ b/src/natu/natu.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + mod ast; mod file_store; mod parser; @@ -23,7 +25,45 @@ impl Report { impl std::fmt::Display for Report { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut reported_let_order_files = HashSet::new(); + for diagnostic in &self.diagnostics { + if diagnostic.finding.rule_id == "let_order" { + if !reported_let_order_files.insert(diagnostic.file_id) { + continue; + } + + let file = self.files.get(diagnostic.file_id); + writeln!( + formatter, + "{} [let_order] Let bindings must follow dependency order", + file.path.display() + )?; + + let mut findings = self + .diagnostics + .iter() + .filter(|candidate| { + candidate.file_id == diagnostic.file_id + && candidate.finding.rule_id == "let_order" + }) + .collect::>(); + findings.sort_by_key(|finding| finding.finding.range.start); + + for finding in findings { + let (start, _) = line_range(&file.source, finding.finding.range); + let expected_order = + finding.finding.expected_order.as_ref().expect("let order"); + writeln!( + formatter, + " let at line {start}: {}", + expected_order.join(", ") + )?; + } + + continue; + } + let file = self.files.get(diagnostic.file_id); let (start, end) = line_range(&file.source, diagnostic.finding.range); let location = if start == end { @@ -71,7 +111,7 @@ pub fn future_sight(files: Vec) -> status::Result { } let graph = ScopeGraph::from_file(&parsed); - diagnostics.extend(rules::run_rules(&parsed, &graph)); + diagnostics.extend(rules::run_rules(&parsed, &graph, &file.source)); } Ok(Report { diff --git a/src/natu/natu_test.rs b/src/natu/natu_test.rs index 45087bd..7584791 100644 --- a/src/natu/natu_test.rs +++ b/src/natu/natu_test.rs @@ -50,7 +50,30 @@ fn natu_reports_out_of_order_let_bindings() -> googletest::Result<()> { .iter() .filter(|diagnostic| diagnostic.finding.rule_id == "let_order") .count(); - expect_that!(let_order_count, eq(4)); + expect_that!(let_order_count, eq(2)); + expect_that!( + report.to_string(), + eq(concat!( + "testdata/lint_findings.nix:11 [manual_inherit] Use `inherit` for this assignment\n", + "testdata/lint_findings.nix:12 [manual_inherit_from] Use `inherit` for this selected attribute\n", + "testdata/lint_findings.nix:13 [unquoted_uri] URI literals should be quoted\n", + "testdata/lint_findings.nix [let_order] Let bindings must follow dependency order\n", + " let at line 1: first, second, nested\n", + " let at line 4: nestedSecond, nestedFirst\n", + )) + ); + Ok(()) +} + +#[gtest] +fn natu_preserves_suppressed_bindings() -> googletest::Result<()> { + let report = natu::future_sight(vec![SourceFile { + path: PathBuf::from("testdata/nolint.nix"), + source: "{ config, ... }: let\n cfg = config.example; # NOLINT(let_order)\n value = cfg;\nin value\n".into(), + }]) + .or_fail()?; + + expect_that!(report.is_clean(), is_true()); Ok(()) } diff --git a/src/natu/parser.rs b/src/natu/parser.rs index 419188c..9ece9db 100644 --- a/src/natu/parser.rs +++ b/src/natu/parser.rs @@ -43,6 +43,7 @@ fn convert_expr(expr: rnix::ast::Expr) -> ast::Expr { match expr { rnix::ast::Expr::Ident(ident) => ast::Expr::Ident(ident.into()), rnix::ast::Expr::LetIn(let_in) => ast::Expr::Let { + range: let_in.syntax().text_range().into(), bindings: let_in .attrpath_values() .filter_map(convert_binding_ast) diff --git a/src/natu/rules.rs b/src/natu/rules.rs index 558e302..b295ecd 100644 --- a/src/natu/rules.rs +++ b/src/natu/rules.rs @@ -22,6 +22,7 @@ pub struct Finding { pub range: ast::SourceRange, pub message: &'static str, pub binding: Option, + pub expected_order: Option>, } impl std::fmt::Display for Diagnostic { @@ -58,7 +59,7 @@ pub trait TokenRule { fn check(token: &Self::Token) -> Option<&'static str>; } -pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec { +pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, source: &str) -> Vec { let node_diagnostics = file .nodes .iter() @@ -75,16 +76,21 @@ pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec .flat_map(|token| [run_token_rule::(token)]) .flatten(); - let let_order_diagnostics = let_order::check(graph).into_iter().map(|finding| Finding { - rule_id: "let_order", - range: finding.range, - message: "Let bindings must follow dependency order", - binding: Some(finding.binding), - }); + let let_order_diagnostics = + let_order::check(graph, source) + .into_iter() + .map(|finding| Finding { + rule_id: "let_order", + range: finding.range, + message: "Let bindings must follow dependency order", + binding: None, + expected_order: Some(finding.expected_order), + }); node_diagnostics .chain(token_diagnostics) .chain(let_order_diagnostics) + .filter(|finding| !is_suppressed(source, finding.range, finding.rule_id)) .map(|finding| Diagnostic { file_id: file.file_id, finding, @@ -92,6 +98,24 @@ pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec .collect() } +pub fn is_suppressed(source: &str, range: ast::SourceRange, rule_id: &str) -> bool { + let line = source[range.start..].lines().next().unwrap_or_default(); + let Some(comment) = line.split_once('#').map(|(_, comment)| comment.trim()) else { + return false; + }; + if comment == "NOLINT" { + return true; + } + comment + .strip_prefix("NOLINT(") + .and_then(|rules| rules.strip_suffix(')')) + .is_some_and(|rules| { + rules + .split(',') + .any(|candidate| candidate.trim() == rule_id) + }) +} + fn run_node_rule(node: &ast::Node) -> Option where R: NodeRule, @@ -104,6 +128,7 @@ where range: node.range, message, binding: None, + expected_order: None, }) } @@ -119,5 +144,6 @@ where range: token.range, message, binding: None, + expected_order: None, }) } diff --git a/src/natu/rules/let_order.rs b/src/natu/rules/let_order.rs index 7d01ebf..ca7daf0 100644 --- a/src/natu/rules/let_order.rs +++ b/src/natu/rules/let_order.rs @@ -1,43 +1,59 @@ use crate::{ ast, + rules::is_suppressed, scope_graph::{LetScope, NodeId, ScopeGraph}, }; pub struct Finding { pub range: ast::SourceRange, - pub binding: String, + pub expected_order: Vec, } -pub fn check(graph: &ScopeGraph) -> Vec { +pub fn check(graph: &ScopeGraph, source: &str) -> Vec { graph .let_scopes() .iter() .flat_map(|let_scope| { - let expected = expected_order(graph, let_scope); - let positions = expected - .iter() - .enumerate() - .map(|(position, declaration)| (*declaration, position)) + let expected = expected_order(graph, let_scope) + .into_iter() + .filter(|declaration| !binding_suppressed(let_scope, *declaration, source)) .collect::>(); - - let_scope + let actual = let_scope .bindings .iter() - .enumerate() - .filter_map(move |(position, binding)| { - positions - .iter() - .find(|(declaration, _)| *declaration == binding.declaration) - .filter(|(_, expected_position)| position != *expected_position) - .map(|_| Finding { - range: binding.range, - binding: binding.name.clone(), - }) - }) + .filter(|binding| !is_suppressed(source, binding.range, "let_order")) + .map(|binding| binding.declaration) + .collect::>(); + + (!actual.starts_with(&expected)).then(|| Finding { + range: let_scope.range, + expected_order: expected + .iter() + .map(|declaration| binding_name(let_scope, *declaration).to_owned()) + .collect(), + }) }) .collect() } +fn binding_suppressed(let_scope: &LetScope, declaration: NodeId, source: &str) -> bool { + let binding = let_scope + .bindings + .iter() + .find(|binding| binding.declaration == declaration) + .expect("let declaration"); + is_suppressed(source, binding.range, "let_order") +} + +fn binding_name(let_scope: &LetScope, declaration: NodeId) -> &str { + &let_scope + .bindings + .iter() + .find(|binding| binding.declaration == declaration) + .expect("let declaration") + .name +} + fn expected_order(graph: &ScopeGraph, let_scope: &LetScope) -> Vec { let mut order = Vec::new(); visit_references(graph, &let_scope.entry_references, let_scope, &mut order); diff --git a/src/natu/scope_graph.rs b/src/natu/scope_graph.rs index 6795458..e509f6d 100644 --- a/src/natu/scope_graph.rs +++ b/src/natu/scope_graph.rs @@ -26,7 +26,11 @@ impl ScopeGraph { fn build_expr(&mut self, expr: &ast::Expr, scope: NodeId) -> Vec { match expr { ast::Expr::Ident(ident) => vec![self.add_reference(&ident.name, scope)], - ast::Expr::Let { bindings, body } => self.build_let(bindings, body, scope), + ast::Expr::Let { + range, + bindings, + body, + } => self.build_let(*range, bindings, body, scope), ast::Expr::AttrSet(bindings) => bindings .iter() .flat_map(|binding| self.build_expr(&binding.value, scope)) @@ -48,6 +52,7 @@ impl ScopeGraph { fn build_let( &mut self, + range: ast::SourceRange, bindings: &[ast::Binding], body: &ast::Expr, parent: NodeId, @@ -74,6 +79,7 @@ impl ScopeGraph { .chain(entry_references.iter().copied()) .collect(); self.lets.push(LetScope { + range, bindings: graph_bindings, entry_references, }); @@ -181,6 +187,7 @@ struct Edge { #[derive(Debug)] pub(crate) struct LetScope { + pub(crate) range: ast::SourceRange, pub(crate) bindings: Vec, pub(crate) entry_references: Vec, }