diff --git a/.sops.yaml b/.sops.yaml index 3a1678a..d713073 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -12,6 +12,7 @@ keys: # (ssh-to-age). The host decrypts at runtime using /etc/ssh/ssh_host_ed25519_key. - &jupiter age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt - &neptun age1hp72xyx2cnd05937e4eww95g5kdtn0wsf9j2nypw330pa69gfdxqn0lpkp + - &terra age1rfcmu6zh40v4260l9hnf8ajs9vly0s06rx3ey76eu78dp9t7getqyhmkut # mercury (rpi) uses a dedicated age key (SD image, no ssh-host-key delivery); # the private key is dropped on its boot partition after flashing. - &mercury age1cpty7zrgnn6l97upq00w5wa8zcvnkxkdt2jvhlj97jh83exure4slha43t @@ -24,6 +25,9 @@ creation_rules: - path_regex: secrets/neptun\.yaml$ key_groups: - age: [ *admin, *neptun ] + - path_regex: secrets/terra\.yaml$ + key_groups: + - age: [ *admin, *terra ] - path_regex: secrets/mercury\.yaml$ key_groups: - age: [ *admin, *mercury ] diff --git a/dotfiles/mime/application-x-ms-sln.xml b/dotfiles/mime/application-x-ms-sln.xml new file mode 100644 index 0000000..a8c2365 --- /dev/null +++ b/dotfiles/mime/application-x-ms-sln.xml @@ -0,0 +1,16 @@ + + + + Visual Studio Solution + + + + + + + + Visual Studio XML Solution + + + + diff --git a/hosts/terra/configuration.nix b/hosts/terra/configuration.nix new file mode 100644 index 0000000..862381c --- /dev/null +++ b/hosts/terra/configuration.nix @@ -0,0 +1,47 @@ +{ config, pkgs, lib, ... }: + +# terra — Ryzen 9 5900X / Radeon RX 6800 XT desktop (MSI MS-7A32). Replaces +# CachyOS on the OS SSD (Kingston SA400, sdb). Dev-data disks (sdc ext4 +# /mnt/hdd_01, LVM vg_ssd /mnt/ssd_01) are kept out of disko and mounted here +# as plain filesystems so they're never wiped. The leftover ntfs disks +# (sda, sdf, nvme0n1) are ignored entirely — not referenced anywhere. +{ + imports = [ + ./hardware-configuration.nix + ./disk-config.nix # disko: OS-disk (sdb) partitions + filesystems + ./secrets.nix # sops-nix: darman password, tailscale key + ../../common.nix # shared base: user / ssh / nix / firewall + ../../services/vpn/tailscale.nix + ../../services/desktop/desktop-hyprland.nix + ../../services/desktop/desktop-apps.nix + ]; + + networking.hostName = "terra"; + + # ---- home-manager (user-level config for darman) ---- + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + home-manager.backupFileExtension = "hm-bak"; + home-manager.users.darman = import ./home.nix; + + # ---- Boot (UEFI) ---- + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + hardware.cpu.amd.updateMicrocode = true; + + # ---- Dev-data disks — NOT in disko, mounted read-write, never wiped ---- + # UUIDs captured from the running CachyOS box; verify after install + # (`lsblk -o NAME,UUID,MOUNTPOINT`) in case disko/kernel enumerates differently. + fileSystems."/mnt/hdd_01" = { + device = "/dev/disk/by-uuid/b8445126-ec6d-4f88-818a-d9e13031d9a4"; + fsType = "ext4"; + options = [ "nofail" ]; + }; + fileSystems."/mnt/ssd_01" = { + device = "/dev/disk/by-uuid/6ca18a9f-27bc-4e58-aea8-de43a0d0ed5d"; + fsType = "ext4"; + options = [ "nofail" ]; + }; + + system.stateVersion = "26.05"; +} diff --git a/hosts/terra/disk-config.nix b/hosts/terra/disk-config.nix new file mode 100644 index 0000000..61e7ac4 --- /dev/null +++ b/hosts/terra/disk-config.nix @@ -0,0 +1,42 @@ +{ ... }: + +# Declarative OS-disk layout (disko). UEFI: GPT with an ESP + ext4 root. +# disko both PARTITIONS/FORMATS this disk and generates the NixOS +# `fileSystems.*` entries, so hardware-configuration.nix must NOT define +# fileSystems for "/" or "/boot". +# +# ⚠️ This disk is WIPED on install. This is the Kingston SA400 SSD that +# currently holds CachyOS (btrfs root+subvols on sdb2, ESP on sdb1). +# The dev-data disks (sdc ext4 /mnt/hdd_01, LVM vg_ssd /mnt/ssd_01) and the +# leftover ntfs disks (sda, sdf, nvme0n1) are NOT listed here — they are +# mounted as plain fileSystems in configuration.nix (or, for the ntfs +# disks, ignored entirely) so they are never touched. +{ + disko.devices.disk.os = { + type = "disk"; + device = "/dev/disk/by-id/ata-KINGSTON_SA400S37480G_50026B738072F6C6"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; +} diff --git a/hosts/terra/hardware-configuration.nix b/hosts/terra/hardware-configuration.nix new file mode 100644 index 0000000..7c96807 --- /dev/null +++ b/hosts/terra/hardware-configuration.nix @@ -0,0 +1,22 @@ +# PLACEHOLDER — replaced on install. +# +# disko (disk-config.nix) owns "/" and "/boot", so this file only carries +# kernel modules + platform. nixos-anywhere regenerates it via: +# nixos-generate-config --no-filesystems +# Standard AMD desktop (SATA SSD, NVMe present but unused) — the generator +# should get ahci/nvme right on its own; these are a sane fallback so stage-1 +# still mounts root if it doesn't. +{ config, lib, pkgs, modulesPath, ... }: + +{ + boot.initrd.availableKernelModules = [ "ahci" "xhci_pci" "usb_storage" "usbhid" "sd_mod" "nvme" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-amd" ]; + boot.extraModulePackages = [ ]; + + # NO fileSystems here — disko defines "/" and "/boot". + + swapDevices = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/hosts/terra/home.nix b/hosts/terra/home.nix new file mode 100644 index 0000000..3ea3a53 --- /dev/null +++ b/hosts/terra/home.nix @@ -0,0 +1,101 @@ +{ pkgs, ... }: + +# home-manager profile for darman on terra. System-level Hyprland enable +# (session entry, portals) lives in ../../services/desktop/desktop-hyprland.nix; this +# manages the user's own hyprland.conf + session packages. +{ + home.stateVersion = "26.05"; + + wayland.windowManager.hyprland = { + enable = true; + # Starter config — replace with your real dotfiles. + settings = { + monitor = [ ",preferred,auto,1" ]; + "$mod" = "SUPER"; + bind = [ + "$mod, Return, exec, alacritty" + "$mod, Q, killactive" + "$mod, D, exec, wofi --show drun" + ]; + }; + }; + + programs.git.enable = true; + programs.home-manager.enable = true; + + # ---- Dracula theming (GTK + Qt) ---- + gtk = { + enable = true; + theme = { + name = "Dracula"; + package = pkgs.dracula-theme; + }; + }; + + # dracula-qt5-theme ships only a qt5ct color scheme (no style plugin), so + # Qt has to go through qt(5|6)ct rather than a direct style/platformTheme + # name. "qtct" pulls in both qt5ct and qt6ct; qt6ct reads its own config + # but understands the same scheme file format, so both point at it. + qt = { + enable = true; + platformTheme.name = "qtct"; + }; + xdg.configFile."qt5ct/qt5ct.conf".text = '' + [Appearance] + color_scheme_path=${pkgs.dracula-qt5-theme}/share/qt5ct/colors/Dracula.conf + custom_palette=true + style=Fusion + ''; + xdg.configFile."qt6ct/qt6ct.conf".text = '' + [Appearance] + color_scheme_path=${pkgs.dracula-qt5-theme}/share/qt5ct/colors/Dracula.conf + custom_palette=true + style=Fusion + ''; + + # Custom mime-info defs (sln/slnx). xdg.mime's update-mime-database only + # indexes share/mime/packages inside the hm profile itself, so this has to + # be a package in home.packages, not a plain xdg.dataFile. + xdg.mime.enable = true; + home.packages = [ + (pkgs.writeTextDir "share/mime/packages/application-x-ms-sln.xml" + (builtins.readFile ../../dotfiles/mime/application-x-ms-sln.xml)) + pkgs.claude-code + ]; + + programs.alacritty = { + enable = true; + settings = { + env.SHELL = "/bin/zsh"; + terminal.shell = { + program = "/bin/zsh"; + args = [ "-l" ]; + }; + window = { + padding = { x = 10; y = 10; }; + opacity = 0.8; + }; + font.normal = { + family = "DepartureMono Nerd Font"; + style = "Regular"; + }; + colors.primary = { + background = "#222831"; + foreground = "#ffd369"; + }; + hints.enabled = [ + { + hyperlinks = true; + regex = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)[^\\u0000-\\u001F\\u007F-\\u009F<>\"\\s{-}\\^⟨⟩`]+"; + command = "xdg-open"; + mouse.enabled = true; + } + ]; + keyboard.bindings = [ + # ESC + CR: nix strings have no \u escape, so fromJSON (which + # supports \u001B) is used to get the literal control chars here. + { key = "Return"; mods = "Shift"; chars = builtins.fromJSON ''"\u001B\r"''; } + ]; + }; + }; +} diff --git a/hosts/terra/secrets.nix b/hosts/terra/secrets.nix new file mode 100644 index 0000000..3e34d09 --- /dev/null +++ b/hosts/terra/secrets.nix @@ -0,0 +1,15 @@ +{ config, ... }: + +# sops-nix wiring for terra (desktop). Encrypted values in ../../secrets/terra.yaml, +# decrypted with terra's own SSH host key (recipient in ../../.sops.yaml). +# The host key is pre-generated on the laptop and shipped at install +# (nixos-anywhere --extra-files -> /etc/ssh/ssh_host_ed25519_key). +{ + sops.defaultSopsFile = ../../secrets/terra.yaml; + sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; + + sops.secrets.tailscale_authkey = { }; + + sops.secrets.darman_password.neededForUsers = true; + users.users.darman.hashedPasswordFile = config.sops.secrets.darman_password.path; +} diff --git a/secrets/terra.yaml b/secrets/terra.yaml new file mode 100644 index 0000000..5a6b311 --- /dev/null +++ b/secrets/terra.yaml @@ -0,0 +1,26 @@ +tailscale_authkey: ENC[AES256_GCM,data:5ZaIjgl4d380JGUC+GjhGUeoAkkrS8ky,iv:oRh/7v/od+Mxj+i3z2ouZ0H2NrUYujEiFR+fEODlaNU=,tag:xUbVxCoYqa1eGx2zvVRAnw==,type:str] +darman_password: ENC[AES256_GCM,data:aM6jV2KkNlfZBEDvIZ1VpJl+6VSS/zm0AxAohV+WBhcMYdCmIGrmJYArhxNjN5Kk9TyRrLZVNnpzeM5RB6g394NAhznWHumctMN8fiq3x5/YOXVEn9DGxvc+4wj3E3PWWu44nRmdXHo6Wg==,iv:SBh11ZB34TrzZNxKIw5TKLUFvoC65vMVDTuvfjjKd/g=,tag:dYwn+fdbMAQBOHsP9wasjA==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOWkt2MXlGUVZGUlYyRS81 + TDE2aFg5WGF3ZVcyMGEzSlR3aThwWU1QelNzClFYSll1K0kwMEYvQ3o5U2RKdnpu + UVZibUV2Z1FCcCtPRzROaW1RSk85M3cKLS0tIDdnRGpzS2dwYUc4ZlV5M1JlQURy + cWVDb1F2TTBSL2psTFdGaGRzb3JCaEEKBYdo2x/fjnU14hWI6ic3t2zmA8JioxVI + TEH3G9kPfWgcMdh1ORwykNnfcoSZcYM0EjomFDv5ZVjuZvRLDRDblA== + -----END AGE ENCRYPTED FILE----- + recipient: age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRZUFTNVJRdTBJbzZIZHQ4 + VWRRSVJTbHFxNVVGTEZGT1ZqZnduQm1OSXg0CjZTU2p2YU9sUWZQTmJwNUFSVUFG + ZmJuOUVoMHZNQjlCeGFOcU9GaE5UUG8KLS0tIGtIL0t2cjM4ekl0clN2bERrRFlH + QW8rZHpwRTJYbWxUbnNTNUplL2dRRmcKujP0nL2Tu8/kDALDQsvEpaMLFWeMAtsk + sHjKfw8VrrmAR4pQf1dsY+wcyh4FsZxhP3Q+QIVq3eCIXS9PeJkGAg== + -----END AGE ENCRYPTED FILE----- + recipient: age1rfcmu6zh40v4260l9hnf8ajs9vly0s06rx3ey76eu78dp9t7getqyhmkut + lastmodified: "2026-07-14T19:48:05Z" + mac: ENC[AES256_GCM,data:ne2YPbCG2U6ix/C+bua28qAEKEMJ4NkEmrg0qFw3iMs8l4gSKL0wVrEu7H6EimCUohdiZ24g4s8euRU60bPNrM1cKkFnDPfkIY4Lc33dI/JrKahjHrEafhpQNy85i2l7oxM1WnHOGfgkQ/XoicwInDwcM5Z8Ks4Sa1JnhYVwCFc=,iv:VnbreolrFp97Caim+sxB5MGW01ir3vyOTL3fJ8DDWwY=,tag:+4TusjszEXQFCUjP3F/IgQ==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.1 diff --git a/services/desktop/desktop-apps.nix b/services/desktop/desktop-apps.nix new file mode 100644 index 0000000..12b0f14 --- /dev/null +++ b/services/desktop/desktop-apps.nix @@ -0,0 +1,38 @@ +{ pkgs, lib, ... }: + +# Desktop applications for a workstation host (currently: terra). Split from +# desktop-hyprland.nix, which owns the *session* (compositor, greeter, audio, +# portals); this owns the things the user actually launches inside it. +{ + # gitkraken / jetbrains-toolbox / steam are unfree. Allow exactly those + # rather than flipping allowUnfree globally, so an unfree dep sneaking into + # a server host's closure still fails loudly. + nixpkgs.config.allowUnfreePredicate = pkg: + builtins.elem (lib.getName pkg) [ + "gitkraken" + "jetbrains-toolbox" + "steam" + "steam-unwrapped" + "steam-run" + "claude-code" + ]; + + # Steam needs its own module (not just the package): it sets up the FHS + # wrapper, the udev rules for controllers, and the 32-bit graphics stack. + programs.steam = { + enable = true; + remotePlay.openFirewall = true; # in-home streaming + dedicatedServer.openFirewall = false; + }; + hardware.graphics.enable32Bit = true; # required by steam's 32-bit games + + environment.systemPackages = with pkgs; [ + zed-editor + protonplus # manages Proton-GE / Wine-GE builds for steam + lutris + gitkraken + jetbrains-toolbox + kdePackages.dolphin + ]; + + fonts.packages = [ pkgs.nerd-fonts.departure-mono ]; # alacritty's font, see hosts/terra/home.nix +} diff --git a/services/desktop/desktop-hyprland.nix b/services/desktop/desktop-hyprland.nix new file mode 100644 index 0000000..a4efe07 --- /dev/null +++ b/services/desktop/desktop-hyprland.nix @@ -0,0 +1,36 @@ +{ pkgs, ... }: + +# Hyprland (wayland) desktop: compositor, login manager, audio, portals. +# Reusable for any host that wants a local GUI session (currently: terra). +{ + programs.hyprland.enable = true; + + services.greetd = { + enable = true; + settings.default_session.command = + "${pkgs.tuigreet}/bin/tuigreet --time --cmd Hyprland"; + }; + + # Audio (pipewire replaces pulseaudio/jack). + security.rtkit.enable = true; + services.pipewire = { + enable = true; + alsa.enable = true; + pulse.enable = true; + }; + + # Screen-share / file-picker portals for wayland apps. + xdg.portal = { + enable = true; + extraPortals = [ pkgs.xdg-desktop-portal-hyprland ]; + }; + + hardware.graphics.enable = true; # OpenGL/Vulkan for the compositor + apps + services.libinput.enable = true; + + environment.systemPackages = with pkgs; [ + alacritty + ]; + + networking.networkmanager.enable = true; +}