diff --git a/modules/hosts/14900k/_private/displays.nix b/modules/hosts/14900k/_private/displays.nix index 2b69386..8b80efa 100644 --- a/modules/hosts/14900k/_private/displays.nix +++ b/modules/hosts/14900k/_private/displays.nix @@ -1,5 +1,5 @@ # Monitor layout for 14900k (ported from NixOS-New `hosts/clients/14900k/home.nix`). -# Niri: `chiasson.desktop.niri.extraSettings.extraConfig` (wrapper-modules / system package). +# Niri: `chiasson.desktop.niri.extraSettings` (`extraConfig` KDL + `binds` merged with defaults). # Hyprland: `chiasson.desktop.hyprland.settings` (merged in HM when `chiasson.desktop.hyprland.enable`). #TODO[epic=Moderate] Clean this up, move to host's configuration.nix. @@ -8,41 +8,50 @@ let gpuPassthrough = config.chiasson.system.vm.gpuPassthrough.enable; in { - chiasson.desktop.niri.extraSettings.extraConfig = - if gpuPassthrough then - '' - output "DP-1" { - mode "2560x1080@144" - scale 1.0 - position x=1920 y=0 - focus-at-startup - } - output "HDMI-A-2" { - mode "1920x1080@60" - scale 1.0 - position x=0 y=0 - } - '' - else - '' - output "DP-2" { - mode "2560x1080@144" - scale 1.0 - position x=0 y=0 - focus-at-startup - } - output "HDMI-A-3" { - mode "1920x1080@60" - scale 1.0 - position x=-1920 y=0 - } - output "DP-4" { - mode "1920x1080@144" - scale 1.0 - position x=0 y=-1080 - } + chiasson.desktop.niri.extraSettings = { + extraConfig = + if gpuPassthrough then + '' + output "DP-1" { + mode "2560x1080@144" + scale 1.0 + position x=1920 y=0 + focus-at-startup + } + output "HDMI-A-2" { + mode "2560x1080@60" + scale 1.0 + position x=0 y=0 + } + '' + else + '' + output "DP-2" { + mode "2560x1080@144" + scale 1.0 + position x=0 y=0 + focus-at-startup + } + output "HDMI-A-3" { + mode "1920x1080@60" + scale 1.0 + position x=-1920 y=0 + } + output "DP-4" { + mode "1920x1080@144" + scale 1.0 + position x=0 y=-1080 + } - ''; + ''; + + binds."XF86Tools".spawn = [ + "wpctl" + "set-mute" + "@DEFAULT_AUDIO_SOURCE@" + "toggle" + ]; + }; chiasson.desktop.hyprland.settings = lib.mkIf config.chiasson.desktop.hyprland.enable ( let diff --git a/modules/hosts/14900k/_private/printing-epson.nix b/modules/hosts/14900k/_private/printing-epson.nix new file mode 100644 index 0000000..6e36dac --- /dev/null +++ b/modules/hosts/14900k/_private/printing-epson.nix @@ -0,0 +1,21 @@ +# Epson ET-2760 on Wi‑Fi via IPP/mDNS. +# Import from configuration.nix when you need printing (see commented import there). +{ pkgs, ... }: +{ + services.printing = { + enable = true; + webInterface = true; + drivers = with pkgs; [ + epson-escpr2 + epson-escpr + ]; + }; + + services.avahi = { + enable = true; + nssmdns4 = true; + openFirewall = true; + }; + + networking.firewall.allowedTCPPorts = [ 631 ]; +} diff --git a/modules/hosts/14900k/configuration.nix b/modules/hosts/14900k/configuration.nix index ebbb529..f1a7de9 100644 --- a/modules/hosts/14900k/configuration.nix +++ b/modules/hosts/14900k/configuration.nix @@ -15,6 +15,7 @@ ./_private/platform.nix ./_private/nvidia.nix ./_private/peripherals.nix + # ./_private/printing-epson.nix ./_private/displays.nix ]; diff --git a/modules/lib/pi5-niri-kdl.nix b/modules/lib/pi5-niri-kdl.nix new file mode 100644 index 0000000..1fb9e02 --- /dev/null +++ b/modules/lib/pi5-niri-kdl.nix @@ -0,0 +1,39 @@ +# Pi5 + DSI DRM KDL snippets (`desktop.niri.raspberryPi5DrmWorkaround`) — lives in `flake.lib`. +{ ... }: +let + drmExtraConfig = '' + debug { + render-drm-device "/dev/dri/renderD128" + ignore-drm-device "/dev/dri/card1" + ignore-drm-device "/dev/dri/card2" + } + ''; +in +{ + flake.lib.pi5NiriKdl = { + inherit drmExtraConfig; + + # Keep in sync with DMS greeter niri template when upstream edits it. + dankGreeterCompositorConfig = '' + hotkey-overlay { + skip-at-startup + } + + environment { + DMS_RUN_GREETER "1" + } + + ${drmExtraConfig} + + gestures { + hot-corners { + off + } + } + + layout { + background-color "#000000" + } + ''; + }; +} diff --git a/modules/lib/users-merge.nix b/modules/lib/users-merge.nix new file mode 100644 index 0000000..bdc08f3 --- /dev/null +++ b/modules/lib/users-merge.nix @@ -0,0 +1,99 @@ +# Pure helpers: catalog → NixOS/HM/SSH shapes (`self.lib.usersMerge lib`). +{ ... }: { + flake.lib.usersMerge = + lib: + let + userHm = user: user.homeManager or { }; + userSsh = user: user.ssh or { }; + in + rec { + resolveHomeManagerModule = + moduleSpec: + let + t = builtins.typeOf moduleSpec; + in + if t == "path" || t == "string" then import moduleSpec else moduleSpec; + + selectedUsersAttr = + { catalog, enabled, hostOverrides }: + lib.listToAttrs ( + map (name: { + inherit name; + value = lib.recursiveUpdate catalog.${name} (hostOverrides.${name} or { }); + }) enabled + ); + + missingEnabledNames = catalog: enabled: builtins.filter (name: !(builtins.hasAttr name catalog)) enabled; + + strayHomeUserKeys = homeUsers: enabled: + builtins.filter (k: !(builtins.elem k enabled)) (builtins.attrNames homeUsers); + + mkNixosUser = + name: user: + { + isNormalUser = user.isNormalUser or true; + description = user.description or name; + extraGroups = user.extraGroups or [ ]; + } + // lib.optionalAttrs (user ? hashedPasswordFile && user.hashedPasswordFile != null) { + hashedPasswordFile = user.hashedPasswordFile; + }; + + hmWiredNames = + selectedUsers: + lib.attrNames ( + lib.filterAttrs (_: user: + let + hm = userHm user; + in + (hm.enable or false) && (hm.module or null) != null + ) selectedUsers + ); + + rbwOutboundSnippet = + name: user: + let + outboundCfg = ((userSsh user).outbound or { }).rbw or { }; + in + lib.mkIf (outboundCfg.enable or false) { + chiasson.ssh.outbound.rbw.enable = true; + chiasson.ssh.outbound.rbw.user = name; + chiasson.ssh.outbound.rbw.hosts = + if (outboundCfg.hosts or "all") == "all" then [ "all" ] else outboundCfg.hosts; + }; + + mkHmUserModule = + { name, user, hostExtraModules }: + let + hm = userHm user; + hmModule = resolveHomeManagerModule hm.module; + in + lib.mkMerge ( + [ + hmModule + (rbwOutboundSnippet name user) + ] + ++ (hm.extraModules or [ ]) + ++ hostExtraModules + ); + + inboundAuthorizedOrNull = + user: + let + inboundCfg = (userSsh user).inbound or { }; + in + if !(inboundCfg.enable or false) then + null + else if (inboundCfg.authorizedHosts or "all") == "all" then + "all" + else + inboundCfg.authorizedHosts; + + inboundHostsAttr = + selectedUsers: + lib.pipe selectedUsers [ + (lib.mapAttrs (_: inboundAuthorizedOrNull)) + (lib.filterAttrs (_: v: v != null)) + ]; + }; +} diff --git a/modules/ssh/home-manager/default.nix b/modules/ssh/home-manager/default.nix new file mode 100644 index 0000000..d84c8b9 --- /dev/null +++ b/modules/ssh/home-manager/default.nix @@ -0,0 +1,96 @@ +{ self, ... }: { + flake.homeManagerModules.sshOutboundRbw = { + config, + lib, + pkgs, + ... + }: + let + cfg = config.chiasson.ssh.outbound.rbw; + inventory = self.lib.sshInventory; + selectedHostNames = + if cfg.hosts == [ "all" ] then + builtins.attrNames inventory.activeHosts + else + cfg.hosts; + missing = builtins.filter (name: !(builtins.hasAttr name inventory.hosts)) selectedHostNames; + selectedHosts = builtins.listToAttrs ( + builtins.map (name: { + inherit name; + value = inventory.hosts.${name}; + }) selectedHostNames + ); + sshConfigTemplate = inventory.mkSshConfigTemplate { + selectedHosts = selectedHosts; + user = cfg.user; + }; + in + { + options.chiasson.ssh.outbound.rbw = { + enable = lib.mkEnableOption "Generated `~/.ssh/config` + rbw agent socket."; + user = lib.mkOption { + type = lib.types.str; + default = config.home.username; + description = "`User` in generated `Host` blocks."; + }; + hosts = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "all" ]; + description = "Inventory hosts to emit (or `[ \"all\" ]`)."; + }; + manageSshConfig = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Write `~/.ssh/config` from the template."; + }; + }; + + config = lib.mkIf cfg.enable (lib.mkMerge [ + { + assertions = [ + { + assertion = missing == [ ]; + message = "ssh.outbound.rbw: unknown host keys: ${builtins.concatStringsSep ", " missing}"; + } + ]; + } + { + home.packages = [ pkgs.rbw pkgs.pinentry-qt ]; + home.sessionVariables.SSH_AUTH_SOCK = "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket"; + home.file = inventory.mkIdentityFiles selectedHosts; + + programs.ssh.enable = lib.mkIf cfg.manageSshConfig false; + home.activation.rbwSshConfig = lib.mkIf cfg.manageSshConfig (lib.hm.dag.entryAfter [ "writeBoundary" ] '' + mkdir -p "$HOME/.ssh" + chmod 700 "$HOME/.ssh" + RBW_SSH_SOCK="/run/user/$(id -u)/rbw/ssh-agent-socket" + cat > "$HOME/.ssh/config" <<'EOF' +${sshConfigTemplate} +EOF + sed -i "s|__RBW_SSH_SOCK__|$RBW_SSH_SOCK|g" "$HOME/.ssh/config" + chmod 600 "$HOME/.ssh/config" + ''); + + systemd.user.services.rbw-agent-bootstrap = { + Unit = { + Description = "Bootstrap rbw SSH agent"; + PartOf = [ "graphical-session.target" ]; + After = [ "graphical-session.target" ]; + }; + Service = { + Type = "oneshot"; + ExecStart = "${pkgs.bash}/bin/bash -lc '${pkgs.rbw}/bin/rbw unlocked >/dev/null 2>&1 || true'"; + RemainAfterExit = true; + }; + Install = { + WantedBy = [ "graphical-session.target" ]; + }; + }; + + home.activation.rbwPinentryConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + ${pkgs.rbw}/bin/rbw config set pinentry "${pkgs.pinentry-qt}/bin/pinentry-qt" >/dev/null 2>&1 || true + ''; + } + ]); + }; +} diff --git a/modules/ssh/nixos/default.nix b/modules/ssh/nixos/default.nix new file mode 100644 index 0000000..08ff828 --- /dev/null +++ b/modules/ssh/nixos/default.nix @@ -0,0 +1,51 @@ +{ self, ... }: { + flake.nixosModules.sshInbound = { + config, + lib, + ... + }: + let + cfg = config.chiasson.ssh.inbound; + inventory = self.lib.sshInventory; + resolveSelection = + selection: + if selection == "all" then + inventory.authorizedKeys + else + let + missing = builtins.filter (name: !(builtins.hasAttr name inventory.hosts)) selection; + in + if missing != [ ] then + throw "ssh.inbound: unknown host keys: ${builtins.concatStringsSep ", " missing}" + else + lib.unique ( + builtins.filter (key: key != null) ( + builtins.map (hostName: inventory.hosts.${hostName}.publicKey) selection + ) + ); + in + { + options.chiasson.ssh.inbound = { + enable = lib.mkEnableOption "Apply `authorizedKeys` from the SSH inventory."; + userAuthorizedHosts = lib.mkOption { + type = lib.types.attrsOf (lib.types.either (lib.types.enum [ "all" ]) (lib.types.listOf lib.types.str)); + default = { }; + example = { + olivier = "all"; + admin = [ "14900k" "t2mbp" ]; + }; + description = '' + Per user: `"all"` or a list of inventory host names whose keys land in `authorized_keys`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + users.users = lib.mapAttrs + (_user: selection: { + openssh.authorizedKeys.keys = resolveSelection selection; + }) + cfg.userAuthorizedHosts; + }; + }; +} diff --git a/modules/system/audio.nix b/modules/system/audio.nix new file mode 100644 index 0000000..7a75064 --- /dev/null +++ b/modules/system/audio.nix @@ -0,0 +1,98 @@ +{ ... }: { + flake.nixosModules.systemAudio = + { config, lib, ... }: + let + cfg = config.chiasson.system.audio; + in + { + options.chiasson.system.audio = { + enable = lib.mkEnableOption '' + PipeWire (ALSA + Pulse), RTKit, optional WirePlumber tweak to kill idle suspend pop on devices. + ''; + + rtkit.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable `security.rtkit.enable` (recommended with PipeWire)."; + }; + + pipewire = { + alsa = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "PipeWire ALSA support."; + }; + support32Bit = lib.mkOption { + type = lib.types.bool; + default = true; + description = "32-bit ALSA support on x86_64."; + }; + }; + pulse.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "PipeWire PulseAudio compatibility (`pulseaudio` clients)."; + }; + jack.enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "PipeWire JACK support (e.g. pro-audio / low-latency apps)."; + }; + }; + + preventIdleSuspend = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + WirePlumber + clock hints so ALSA nodes do not sleep (~0.5s glitch on resume otherwise). + ''; + }; + }; + + config = lib.mkIf cfg.enable (lib.mkMerge [ + { + services.pulseaudio.enable = false; + security.rtkit.enable = cfg.rtkit.enable; + + services.pipewire = lib.mkMerge [ + { + enable = true; + alsa.enable = cfg.pipewire.alsa.enable; + alsa.support32Bit = cfg.pipewire.alsa.support32Bit; + pulse.enable = cfg.pipewire.pulse.enable; + } + (lib.mkIf cfg.pipewire.jack.enable { + jack.enable = true; + }) + (lib.mkIf cfg.preventIdleSuspend { + extraConfig.pipewire = { + "context.properties" = { + "default.clock.quantum" = 1024; + "default.clock.min-quantum" = 32; + "default.clock.max-quantum" = 8192; + }; + }; + }) + ]; + } + (lib.mkIf cfg.preventIdleSuspend { + environment.etc."wireplumber/wireplumber.conf.d/99-prevent-suspend.conf".text = '' + monitor.alsa.rules = [ + { + matches = [ + { node.name = "~alsa_output.*" } + { node.name = "~alsa_input.*" } + ] + actions = { + update-props = { + session.suspend-timeout-seconds = 0 + } + } + } + ] + ''; + }) + ]); + }; +} diff --git a/modules/system/default.nix b/modules/system/default.nix new file mode 100644 index 0000000..a85e08e --- /dev/null +++ b/modules/system/default.nix @@ -0,0 +1,27 @@ +{ self, inputs, ... }: { + flake.nixosModules.system = { + imports = [ + inputs.nur.modules.nixos.default + self.nixosModules.systemLocalization + self.nixosModules.systemFonts + self.nixosModules.systemNetworking + self.nixosModules.systemRemoteDesktop + self.nixosModules.systemLocalsend + self.nixosModules.systemMonitorInput + self.nixosModules.systemSpotify + self.nixosModules.systemPackagesDefaults + self.nixosModules.systemDocker + self.nixosModules.systemFlatpak + self.nixosModules.systemAudio + self.nixosModules.systemIdeapadMrubyOverlay + self.nixosModules.systemGaming + self.nixosModules.systemUconsoleKernelBuilder + self.nixosModules.systemLibrepods + self.nixosModules.systemPalera1n + self.nixosModules.systemCaching + inputs.swiftshare.nixosModules.systemServiceSwiftshare + self.nixosModules.systemServiceImmich + self.nixosModules.systemVM + ]; + }; +} diff --git a/modules/system/fonts.nix b/modules/system/fonts.nix new file mode 100644 index 0000000..fed174d --- /dev/null +++ b/modules/system/fonts.nix @@ -0,0 +1,69 @@ +{ ... }: { + flake.nixosModules.systemFonts = { + config, + lib, + pkgs, + ... + }: + let + cfg = config.chiasson.system.fonts; + in + { + options.chiasson.system.fonts = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Bundled fonts + fontconfig defaults."; + }; + + packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = with pkgs; [ + noto-fonts + noto-fonts-cjk-sans + noto-fonts-color-emoji + liberation_ttf + fira-code + fira-code-symbols + mplus-outline-fonts.githubRelease + dina-font + proggyfonts + nerd-fonts.fira-code + nerd-fonts.droid-sans-mono + nerd-fonts.jetbrains-mono + ]; + description = "System font packages to install."; + }; + + defaultFonts = { + monospace = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "FiraCode Nerd Font" "Fira Code" "DejaVu Sans Mono" ]; + description = "Default monospace font fallback chain."; + }; + sansSerif = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "Noto Sans" "DejaVu Sans" ]; + description = "Default sans-serif font fallback chain."; + }; + serif = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "Noto Serif" "DejaVu Serif" ]; + description = "Default serif font fallback chain."; + }; + }; + }; + + config = lib.mkIf cfg.enable { + fonts.packages = cfg.packages; + fonts.fontconfig = { + enable = true; + defaultFonts = { + monospace = cfg.defaultFonts.monospace; + sansSerif = cfg.defaultFonts.sansSerif; + serif = cfg.defaultFonts.serif; + }; + }; + }; + }; +} diff --git a/modules/system/monitor-input/default.nix b/modules/system/monitor-input/default.nix new file mode 100644 index 0000000..915bc56 --- /dev/null +++ b/modules/system/monitor-input/default.nix @@ -0,0 +1,22 @@ +# DDC/CI helper + i2c-dev + ddcutil (implementation under `package/`). +{ ... }: { + flake.nixosModules.systemMonitorInput = + { config, lib, pkgs, ... }: + let + cfg = config.chiasson.system.monitorInput; + monitorInputPkg = pkgs.callPackage ./package { }; + in + { + options.chiasson.system.monitorInput.enable = lib.mkEnableOption '' + `i2c-dev`, ddcutil, and `monitor-input` for DDC/CI input switching. + ''; + + config = lib.mkIf cfg.enable { + boot.kernelModules = [ "i2c-dev" ]; + environment.systemPackages = [ + pkgs.ddcutil + monitorInputPkg + ]; + }; + }; +} diff --git a/modules/system/monitor-input/package/default.nix b/modules/system/monitor-input/package/default.nix new file mode 100644 index 0000000..18509c9 --- /dev/null +++ b/modules/system/monitor-input/package/default.nix @@ -0,0 +1,12 @@ +# DDC/CI monitor input control: list displays, get/set input by bus or name. +{ pkgs }: +let + script = builtins.readFile ./monitor-input.sh; + unwrapped = pkgs.writeShellScriptBin "monitor-input" script; +in +pkgs.runCommand "monitor-input" { nativeBuildInputs = [ pkgs.makeWrapper ]; } '' + mkdir -p $out/bin + cp ${unwrapped}/bin/monitor-input $out/bin/monitor-input + chmod +x $out/bin/monitor-input + wrapProgram $out/bin/monitor-input --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.ddcutil ]} +'' diff --git a/modules/system/monitor-input/package/monitor-input.sh b/modules/system/monitor-input/package/monitor-input.sh new file mode 100644 index 0000000..e049113 --- /dev/null +++ b/modules/system/monitor-input/package/monitor-input.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail +VCP_INPUT=0x60 +CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/monitor-input" +CACHE_FILE="$CACHE_DIR/displays" +CACHE_TTL=60 + +# Output one line per DDC-capable display: BUS MODEL MFG (from ddcutil detect, cached for CACHE_TTL seconds). +list_ddc_displays() { + mkdir -p "$CACHE_DIR" + local now ts + now=$(date +%s) + ts=$(stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0) + if [[ -s "$CACHE_FILE" ]] && (( ts + CACHE_TTL > now )); then + cat "$CACHE_FILE" + return + fi + ddcutil detect 2>/dev/null | awk ' + /^Display [0-9]+$/ { bus=""; model=""; mfg=""; next } + /^Invalid display|^ DDC communication failed/ { next } + /^ I2C bus:/ { if (match($0, /i2c-[0-9]+/)) bus=substr($0, RSTART+4, RLENGTH-4); next } + /^ Mfg id:[[:space:]]+/ { mfg=substr($0, index($0,":")+1); gsub(/^[[:space:]]+/, "", mfg); next } + /^ Model:[[:space:]]+/ { model=substr($0, index($0,":")+1); gsub(/^[[:space:]]+/, "", model); next } + /^ VCP version:/ { if (bus != "") print bus "\t" model "\t" mfg } + ' | tee "$CACHE_FILE" +} + +# Resolve DISP (bus number or name substring) to bus number. Exits 1 if not found or ambiguous. +# When DISP is numeric, use it directly (no slow ddcutil detect). +resolve_display() { + local disp="$1" + if [[ "$disp" =~ ^[0-9]+$ ]]; then + echo "$disp" + return 0 + fi + local bus r + bus=$(list_ddc_displays | awk -v disp="$disp" ' + BEGIN { d = tolower(disp); found = 0 } + { + if (index(tolower($2), d) || index(tolower($3), d)) { + if (found) exit 2 + found = $1 + } + } + END { if (found) print found; exit (found ? 0 : 1) } + ') + r=$? + [[ $r -eq 2 ]] && { echo "monitor-input: '$disp' matches multiple displays (use bus number)" >&2; return 1; } + [[ $r -eq 0 && -n "$bus" ]] || { echo "monitor-input: no DDC display matching '$disp'" >&2; return 1; } + echo "$bus" +} + +# Map input name to VCP 0x60 value (common assignments; use hex for others). +input_to_hex() { + case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in + dp|dp1) echo "0x0f" ;; + dp2) echo "0x10" ;; + hdmi|hdmi1) echo "0x11" ;; + hdmi2) echo "0x12" ;; + 0x*) echo "$1" ;; + *) echo "monitor-input: unknown input '$1' (use dp, dp1, dp2, hdmi, hdmi1, hdmi2, or 0xNN)" >&2; return 1 ;; + esac +} + +case "${1:-}" in + list) + echo "Bus Model Mfg" + list_ddc_displays + ;; + *) + [[ -n "${1:-}" ]] || { echo "Usage: monitor-input list | monitor-input get|set [input]" >&2; exit 1; } + case "${2:-}" in + get) + bus=$(resolve_display "$1") || exit 1 + exec ddcutil --bus "$bus" getvcp "$VCP_INPUT" + ;; + set) + [[ -n "${3:-}" ]] || { echo "Usage: monitor-input set " >&2; exit 1; } + bus=$(resolve_display "$1") || exit 1 + hex=$(input_to_hex "$3") || exit 1 + exec ddcutil --bus "$bus" setvcp "$VCP_INPUT" "$hex" + ;; + *) + echo "Usage: monitor-input list" >&2 + echo " monitor-input get" >&2 + echo " monitor-input set " >&2 + echo " display: bus number (e.g. 23) or name match (e.g. benq, zowie, lg, 34GL750)" >&2 + echo " input: dp, dp1, dp2, hdmi, hdmi1, hdmi2, or 0xNN" >&2 + exit 1 + ;; + esac + ;; +esac diff --git a/modules/system/packages.nix b/modules/system/packages.nix new file mode 100644 index 0000000..35d687b --- /dev/null +++ b/modules/system/packages.nix @@ -0,0 +1,51 @@ +{ ... }: { + flake.nixosModules.systemPackagesDefaults = + { config, lib, pkgs, ... }: + let + cfg = config.chiasson.system; + in + { + options.chiasson.system.defaultPackages = { + enabled = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable shared default CLI/system package set."; + }; + packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = with pkgs; [ + git + wget + unzip + jq + pfetch + ]; + description = "Default packages to install on all hosts."; + }; + }; + + options.chiasson.system.extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = "Additional packages to install on all hosts."; + }; + + options.chiasson.system.allowUnfree = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Allow unfree packages from nixpkgs."; + }; + + config = lib.mkMerge [ + (lib.mkIf cfg.defaultPackages.enabled { + environment.systemPackages = cfg.defaultPackages.packages; + }) + (lib.mkIf (cfg.extraPackages != [ ]) { + environment.systemPackages = cfg.extraPackages; + }) + { + nixpkgs.config.allowUnfree = cfg.allowUnfree; + } + ]; + }; +} diff --git a/modules/system/palera1n.nix b/modules/system/palera1n.nix new file mode 100644 index 0000000..e4f380d --- /dev/null +++ b/modules/system/palera1n.nix @@ -0,0 +1,91 @@ +# checkm8 jailbreak CLI from upstream binaries; needs root. usbmuxd + idevice* help with USB iOS. +{ ... }: { + flake.nixosModules.systemPalera1n = + { config, lib, pkgs, ... }: + let + cfg = config.chiasson.system.palera1n; + + version = "2.2.1"; + + srcFor = + { + x86_64-linux = pkgs.fetchurl { + url = "https://github.com/palera1n/palera1n/releases/download/v${version}/palera1n-linux-x86_64"; + sha256 = "0l9ipabbiggkzvpy8hyi681kalln3z3396xsx4lz1393jw3c8dm2"; + }; + aarch64-linux = pkgs.fetchurl { + url = "https://github.com/palera1n/palera1n/releases/download/v${version}/palera1n-linux-arm64"; + sha256 = "12n0136g218b5ndq2s7ssymab4i6pbb55l681b94zs9m9lvacfa1"; + }; + } + .${pkgs.stdenv.hostPlatform.system} or null; + + palera1n = + if srcFor == null then + null + else + pkgs.stdenvNoCC.mkDerivation { + pname = "palera1n"; + inherit version; + src = srcFor; + + dontUnpack = true; + + installPhase = '' + install -Dm755 $src $out/bin/palera1n + ''; + + meta = with lib; { + description = "iOS jailbreak tool for checkm8 devices (A8–A11)"; + homepage = "https://palera.in"; + license = licenses.mit; + sourceProvenance = with sourceTypes; [ binaryNativeCode ]; + platforms = [ "x86_64-linux" "aarch64-linux" ]; + mainProgram = "palera1n"; + }; + }; + in + { + options.chiasson.system.palera1n = { + enable = lib.mkEnableOption '' + palera1n from GitHub releases + usbmuxd / libimobiledevice toggles (defaults on). + ''; + + usbmuxd = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "usbmuxd for iOS USB."; + }; + }; + + libimobiledevice = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "idevice* tools on PATH."; + }; + }; + }; + + config = lib.mkIf cfg.enable (lib.mkMerge [ + (lib.mkIf (palera1n == null) { + assertions = [ + { + assertion = false; + message = "system.palera1n: no upstream binary for ${pkgs.stdenv.hostPlatform.system}"; + } + ]; + }) + (lib.mkIf (palera1n != null) { + environment.systemPackages = [ palera1n ]; + }) + (lib.mkIf (cfg.enable && cfg.usbmuxd.enable) { + services.usbmuxd.enable = true; + }) + (lib.mkIf (cfg.enable && cfg.libimobiledevice.enable) { + environment.systemPackages = [ pkgs.libimobiledevice ]; + }) + ]); + }; +} diff --git a/modules/system/users/catalog-options.nix b/modules/system/users/catalog-options.nix new file mode 100644 index 0000000..1fde545 --- /dev/null +++ b/modules/system/users/catalog-options.nix @@ -0,0 +1,42 @@ +{ ... }: { + flake.nixosModules.usersCatalogOptions = + { lib, ... }: + { + options.chiasson.users = { + catalog = lib.mkOption { + type = lib.types.attrs; + default = { }; + description = '' + User records merged from `usersCatalogDefaults`; override with `hostOverrides` or `mkForce`. + ''; + }; + enabled = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Catalog names to materialize as `users.users` on this machine."; + }; + hostOverrides = lib.mkOption { + type = lib.types.attrs; + default = { }; + description = '' + `recursiveUpdate`d onto catalog users. + ''; + }; + extraModules = lib.mkOption { + type = lib.types.attrsOf (lib.types.listOf lib.types.unspecified); + default = { }; + description = '' + Per-user Home Manager `extraModules` keyed by catalog user name. + Keys must match `chiasson.users.enabled`. + ''; + }; + homeManager = { + autoWire = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Create HM users from the catalog when true."; + }; + }; + }; + }; +} diff --git a/modules/system/users/default.nix b/modules/system/users/default.nix new file mode 100644 index 0000000..8742cc8 --- /dev/null +++ b/modules/system/users/default.nix @@ -0,0 +1,27 @@ +# Catalog → NixOS `users.users` + Home Manager + SSH inbound. +{ self, ... }: { + flake.nixosModules.users = + { config, lib, ... }: + let + usersLib = self.lib.usersMerge lib; + selectUsers = + c: + let + uc = c.chiasson.users; + in + usersLib.selectedUsersAttr { + catalog = uc.catalog; + enabled = uc.enabled; + hostOverrides = uc.hostOverrides; + }; + in + { + imports = [ + self.nixosModules.sshInbound + self.nixosModules.usersCatalogOptions + self.nixosModules.usersCatalogDefaults + { _module.args = { inherit self usersLib selectUsers; }; } + self.nixosModules.usersHomeIntegration + ]; + }; +} diff --git a/modules/system/users/home-integration.nix b/modules/system/users/home-integration.nix new file mode 100644 index 0000000..a2cb5c2 --- /dev/null +++ b/modules/system/users/home-integration.nix @@ -0,0 +1,51 @@ +{ ... }: { + flake.nixosModules.usersHomeIntegration = + { config, options, lib, self, usersLib, selectUsers, ... }: + let + cfg = config.chiasson.users; + selected = selectUsers config; + missing = usersLib.missingEnabledNames cfg.catalog cfg.enabled; + stray = usersLib.strayHomeUserKeys cfg.extraModules cfg.enabled; + names = usersLib.hmWiredNames selected; + hmAvailable = lib.hasAttrByPath [ "home-manager" "users" ] options; + hmUsersAttr = lib.listToAttrs ( + map (name: { + inherit name; + value = usersLib.mkHmUserModule { + inherit name; + user = selected.${name}; + hostExtraModules = cfg.extraModules.${name} or [ ]; + }; + }) names + ); + inboundUsersAttr = usersLib.inboundHostsAttr selected; + in + { + config = lib.mkMerge [ + { + assertions = [ + { + assertion = missing == [ ]; + message = "chiasson.users.enabled references unknown catalog users: ${builtins.concatStringsSep ", " missing}"; + } + { + assertion = stray == [ ]; + message = "chiasson.users.extraModules has keys not in chiasson.users.enabled: ${builtins.concatStringsSep ", " stray}"; + } + ]; + } + { + users.users = lib.mapAttrs (name: user: usersLib.mkNixosUser name user) selected; + } + (lib.optionalAttrs hmAvailable { + "home-manager".useGlobalPkgs = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) true; + "home-manager".sharedModules = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) [ self.homeManagerModules.sshOutboundRbw ]; + "home-manager".users = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) hmUsersAttr; + }) + (lib.mkIf (inboundUsersAttr != { }) { + chiasson.ssh.inbound.enable = true; + chiasson.ssh.inbound.userAuthorizedHosts = inboundUsersAttr; + }) + ]; + }; +}