commit 259d9ed5a0847ad256471688221f5a4c978d7677 Author: Bryan Ramos Date: Sun Mar 15 02:43:16 2026 -0400 init diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..619d00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.qcow2 +result +.direnv diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d6d2845 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,15 @@ +[submodule "nvim"] + path = user/modules/neovim/nvim + url = https://github.com/itme-brain/nvim.git + +[submodule "vim"] + path = user/modules/vim/vim + url = https://github.com/itme-brain/vim.git + +[submodule "git"] + path = user/modules/git/git + url = https://github.com/itme-brain/git.git + +[submodule "bash"] + path = user/modules/bash/bash + url = https://github.com/itme-brain/bash.git diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..831afe6 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,19 @@ +# sops-nix configuration +# Per-machine age keys - add new machines here to grant access + +keys: + # Machines + - &desktop age17ejyzyk52unr6eyaa9rpunxpmf7u9726v6sx7me3ww3mdu5xzgjqsgj9gl + - &server age198jg29ryg3c0qj3yg6y9ha4ce2ue4hjdaa9kalf49fxju74dhchsquvjzp + +creation_rules: + # Desktop secrets + - path_regex: secrets/system/wifi\.yaml$ # Home WIFI Credentials + key_groups: + - age: + - *desktop + # Server secrets (cameras) + - path_regex: secrets/system/cameras\.yaml$ # RTSP Feed + key_groups: + - age: + - *server diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bc89c4 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# NixOS Configuration + +Modular NixOS flake configuration with home-manager integration. + +## Requirements + +- [Nix with Flakes](https://nixos.wiki/wiki/Flakes#Enable_flakes_permanently_in_NixOS) +- [NixOS](https://www.nixos.org/) for system configurations +- [Home-Manager](https://nix-community.github.io/home-manager/) for user configurations + +## Flake Outputs + +| Configuration | Description | +|---------------|-------------| +| `desktop` | Primary workstation | +| `server` | Home server | +| `wsl` | Windows Subsystem for Linux | + +## Fresh Install + +From the NixOS live installer: + +```bash +# Enable flakes +echo "experimental-features = nix-command flakes" | sudo tee -a /etc/nix/nix.conf + +# Clone repo +nix run nixpkgs#git -- clone --recurse-submodules https://github.com/itme-brain/nixos.git +cd nixos + +# Enter dev shell and install +nix develop +just install desktop +``` + +## Getting Started + +```bash +git clone --recurse-submodules git@github.com:itme-brain/nixos.git +cd nixos +nix develop +just +``` + +**Note:** Replace `hardware.nix` in `system/machines/` with output from `nixos-generate-config` for your hardware. + +## Directory Structure + +``` +. +├── flake.nix +├── flake.lock +├── justfile +│ +├── system/ +│ ├── keys/ # Machine SSH keys +│ │ └── desktop/ +│ └── machines/ +│ ├── desktop/ +│ │ ├── default.nix # Machine entry point +│ │ ├── hardware.nix # Hardware config +│ │ ├── system.nix # System settings +│ │ └── modules/ +│ │ ├── disko/ # Disk partitioning +│ │ └── home-manager/ # Home-manager integration +│ ├── server/ # Server (same structure) +│ └── wsl/ # WSL (same structure) +│ +└── user/ + ├── default.nix # User options (name, email, keys) + ├── home.nix # Shared home-manager defaults + ├── bookmarks/ + ├── keys/ + │ ├── age/ + │ ├── pgp/ + │ └── ssh/ + └── modules/ + ├── bash/bash/ # Shell (submodule) + ├── git/git/ # Git (submodule) + ├── neovim/nvim/ # Neovim (submodule) + ├── vim/vim/ # Vim (submodule) + ├── tmux/ + ├── dev/ # CLI dev tools + ├── security/ + │ ├── gpg/ + │ └── yubikey/ + ├── utils/ + │ ├── dev/ # Dev tools (claude-code, direnv, etc.) + │ ├── email/ + │ ├── irc/ + │ └── writing/ + └── gui/ + ├── default.nix # Browser-focused mimeApps + ├── wm/ + │ ├── hyprland/ + │ └── sway/ + ├── browsers/ + ├── alacritty/ + ├── dev/ + │ ├── pcb/ # Arduino, KiCad + │ └── design/ # Penpot + ├── corn/ + ├── fun/ + └── utils/ +``` + +## Architecture + +**flake.nix** defines NixOS configurations that reference machines under `system/machines/`. +Each machine imports its hardware, system settings, and home-manager config. + +**user/home.nix** provides shared defaults for all users: +- Essential packages +- Default modules + +**Machine home.nix** imports user defaults and enables machine-specific modules. + +## Resources + +- [nixpkgs Packages](https://search.nixos.org/packages) +- [nixpkgs Options](https://search.nixos.org/options) +- [Home-Manager Options](https://home-manager-options.extranix.com) diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..aa50bbb --- /dev/null +++ b/flake.lock @@ -0,0 +1,204 @@ +{ + "nodes": { + "disko": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1773025010, + "narHash": "sha256-khlHllTsovXgT2GZ0WxT4+RvuMjNeR5OW0UYeEHPYQo=", + "owner": "nix-community", + "repo": "disko", + "rev": "7b9f7f88ab3b339f8142dc246445abb3c370d3d3", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nur", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1772985280, + "narHash": "sha256-FdrNykOoY9VStevU4zjSUdvsL9SzJTcXt4omdEDZDLk=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "8f736f007139d7f70752657dff6a401a585d6cbc", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-25.11", + "repo": "home-manager", + "type": "github" + } + }, + "nixos-wsl": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1739577062, + "narHash": "sha256-u/trdPzJO8UotNq48RbG7m6Pe8761IEMCOY0QidNjY4=", + "owner": "nix-community", + "repo": "NixOS-WSL", + "rev": "0b2b8b31f69f24e9a75b4b18a32c771a48612d5e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "2411.6.0", + "repo": "NixOS-WSL", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1773375660, + "narHash": "sha256-SEzUWw2Rf5Ki3bcM26nSKgbeoqi2uYy8IHVBqOKjX3w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3e20095fe3c6cbb1ddcef89b26969a69a1570776", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1773389992, + "narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c06b4ae3d6599a672a6210b7021d699c351eebda", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1772963539, + "narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9dcb002ca1690658be4a04645215baea8b95f31d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nur": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1773108757, + "narHash": "sha256-3BAoe2R6YA6Xjdsgx3urZ4Ns3LeTy0E/w5d1wPny910=", + "owner": "nix-community", + "repo": "NUR", + "rev": "9f2c583704f122828e6f9893416ca3b007464ee6", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "NUR", + "type": "github" + } + }, + "root": { + "inputs": { + "disko": "disko", + "home-manager": "home-manager", + "nixos-wsl": "nixos-wsl", + "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable", + "nur": "nur", + "sops-nix": "sops-nix" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1773550941, + "narHash": "sha256-wa/++bL2QeMUreNFBZEWluQfOYB0MnQIeGNMuaX9sfs=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "c469b6885f0dcd5c7c56bd935a0f08dbcd9e79e1", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..708ffac --- /dev/null +++ b/flake.nix @@ -0,0 +1,78 @@ +{ + description = "My Nix Configs"; + + inputs = + { + self.submodules = true; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; + nur = { + url = "github:nix-community/NUR"; + }; + home-manager = { + url = "github:nix-community/home-manager/release-25.11"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + nixos-wsl = { + url = "github:nix-community/NixOS-WSL/2411.6.0"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { nixpkgs, nixpkgs-unstable, nur, ... }@inputs: + let + mkPkgs = system: import nixpkgs { + inherit system; + config = { + allowUnfree = true; + nvidia.acceptLicense = true; + }; + overlays = [ + nur.overlays.default + # Make unstable packages available as pkgs.unstable.* + (final: prev: { + unstable = import nixpkgs-unstable { + inherit system; + config.allowUnfree = true; + }; + }) + ]; + }; + + mkSystem = { path, system ? "x86_64-linux" }: + let pkgs = mkPkgs system; + in nixpkgs.lib.nixosSystem { + inherit system pkgs; + specialArgs = { inherit inputs; }; + modules = [ + inputs.sops-nix.nixosModules.sops + path + ]; + }; + + in + { + nixosConfigurations = { + desktop = mkSystem { path = ./system/machines/desktop; }; + server = mkSystem { path = ./system/machines/server; }; + wsl = mkSystem { path = ./system/machines/wsl; }; + }; + + devShells.x86_64-linux.default = with mkPkgs "x86_64-linux"; mkShell { + name = "devShell"; + packages = [ + just + age + sops + ]; + }; + }; +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..f2aea1d --- /dev/null +++ b/justfile @@ -0,0 +1,366 @@ +SYSTEM := "$(echo $HOSTNAME)" +VALID_SYSTEMS := "desktop server wsl" + +# Print this list +default: + @just --list + +# Validate system argument +[private] +_validate SYSTEM: + #!/usr/bin/env bash + case "{{SYSTEM}}" in + desktop|server|wsl) ;; + *) echo "Error: Unknown system '{{SYSTEM}}'. Use one of: {{VALID_SYSTEMS}}"; exit 1 ;; + esac + +# Helper to parse submodules from .gitmodules +[private] +_subs_init := ''' + declare -A SUBS + while read -r key path; do + name="${key#submodule.}"; name="${name%.path}" + SUBS[$name]="$path" + done < <(git config -f .gitmodules --get-regexp 'submodule\..*\.path') +''' + +# Clean up build artifacts +[group('nix')] +clean: + #!/usr/bin/env bash + set -euo pipefail + echo "Cleaning build artifacts" + rm -f result + rm -f ./*.qcow2 + echo "Done" + +# Output what derivations will be built +[group('nix')] +out SYSTEM="desktop": (_validate SYSTEM) + @echo "Outputting derivations to be built for {{SYSTEM}}..." + @nix build --dry-run .#nixosConfigurations."{{SYSTEM}}".config.system.build.toplevel -L + +# Test switch into the next generation +[group('nixos')] +test SYSTEM=SYSTEM: (_validate SYSTEM) + @echo "Testing switching to next NixOS generation for {{SYSTEM}}..." + @sudo nixos-rebuild test --flake .#{{SYSTEM}} + +# Build the nix expression and hydrate the results directory +[group('nix')] +build SYSTEM="desktop": (_validate SYSTEM) + @echo "Building NixOS configuration for {{SYSTEM}}..." + @nix build .#nixosConfigurations."{{SYSTEM}}".config.system.build.toplevel -L + @echo -e "\033[32mBuild success - result directory hydrated\033[0m" + +# Deploy a vm of the defined system +[group('nixos')] +vm SYSTEM: (_validate SYSTEM) + #!/usr/bin/env bash + set -euo pipefail + echo "Building VM for {{SYSTEM}}..." + nixos-rebuild build-vm --flake .#{{SYSTEM}} + if [[ -f result/bin/run-{{SYSTEM}}-vm ]]; then + result/bin/run-{{SYSTEM}}-vm + else + echo "Error: VM build failed!" + exit 1 + fi + +# grep nixpkgs for PKG +[group('nix')] +search PKG: + nix search nixpkgs {{PKG}} + +# Open nixos packages in the browser +[group('nix')] +pkgs: + @xdg-open https://search.nixos.org/packages + +# Open nixos options in the browser +[group('nix')] +options: + @xdg-open https://search.nixos.org/options + +# NixOS-rebuild switch for the current system +[group('nixos')] +switch: + @echo -e "\033[32m->> Switching to next generation ->>\033[0m" + @sudo nixos-rebuild switch --flake .#{{SYSTEM}} + +# Rollback to previous generation +[group('nixos')] +rollback: + @sudo nixos-rebuild switch --rollback + +# NixOS-rebuild boot for the current system +[group('nixos')] +boot: + @echo -e "\033[34m->> Reboot to new generation ->>\033[0m" + @sudo nixos-rebuild boot --flake .#{{SYSTEM}} + +# Partition disk only (interactive disk selection) +[group('nixos')] +partition SYSTEM: + #!/usr/bin/env bash + set -euo pipefail + + DISKO_CONFIG="./system/machines/{{SYSTEM}}/modules/disko/default.nix" + + if [[ ! -f "$DISKO_CONFIG" ]]; then + echo "Error: No disko config for '{{SYSTEM}}'" + exit 1 + fi + + # Build array of disk options with readable info + declare -a DISK_IDS + declare -a DISK_OPTIONS + + for id in /dev/disk/by-id/*; do + name=$(basename "$id") + [[ "$name" =~ part ]] && continue + [[ ! "$name" =~ ^(ata|nvme|scsi)- ]] && continue + + dev=$(readlink -f "$id") + dev_name=$(basename "$dev") + size=$(lsblk -dn -o SIZE "$dev" 2>/dev/null) || continue + model=$(lsblk -dn -o MODEL "$dev" 2>/dev/null | xargs) || model="" + + DISK_IDS+=("$id") + DISK_OPTIONS+=("$dev_name $size $model") + done + + if [[ ${#DISK_IDS[@]} -eq 0 ]]; then + echo "No disks found!" + exit 1 + fi + + echo "Select a disk:" + select opt in "${DISK_OPTIONS[@]}"; do + if [[ -n "$opt" ]]; then + idx=$((REPLY - 1)) + DISK="${DISK_IDS[$idx]}" + break + else + echo "Invalid selection" + fi + done + + echo "" + echo -e "\033[31m!! WARNING: This will DESTROY all data on $DISK !!\033[0m" + read -p "Continue? [y/N]: " confirm + case "${confirm,,}" in + y|yes) ;; + *) echo "Aborted."; exit 1 ;; + esac + + echo "Writing disk '$DISK' to disko config..." + sed -i "s|device = \"/dev/disk/by-id/[^\"]*\";|device = \"$DISK\";|" "$DISKO_CONFIG" + + echo "Partitioning $DISK..." + sudo nix \ + --extra-experimental-features "nix-command flakes" \ + run github:nix-community/disko -- \ + --mode destroy,format,mount \ + "$DISKO_CONFIG" + + echo -e "\033[32mPartitioning complete. Disk mounted at /mnt.\033[0m" + +# Install NixOS (partition + install in one shot) +[group('nixos')] +install SYSTEM: + #!/usr/bin/env bash + set -euo pipefail + + DISKO_CONFIG="./system/machines/{{SYSTEM}}/modules/disko/default.nix" + + if [[ ! -f "$DISKO_CONFIG" ]]; then + echo "Error: No disko config for '{{SYSTEM}}'" + exit 1 + fi + + # Build array of disk options with readable info + declare -a DISK_IDS + declare -a DISK_OPTIONS + + for id in /dev/disk/by-id/*; do + name=$(basename "$id") + [[ "$name" =~ part ]] && continue + [[ ! "$name" =~ ^(ata|nvme|scsi)- ]] && continue + + dev=$(readlink -f "$id") + dev_name=$(basename "$dev") + size=$(lsblk -dn -o SIZE "$dev" 2>/dev/null) || continue + model=$(lsblk -dn -o MODEL "$dev" 2>/dev/null | xargs) || model="" + + DISK_IDS+=("$id") + DISK_OPTIONS+=("$dev_name $size $model") + done + + if [[ ${#DISK_IDS[@]} -eq 0 ]]; then + echo "No disks found!" + exit 1 + fi + + echo "Select a disk:" + select opt in "${DISK_OPTIONS[@]}"; do + if [[ -n "$opt" ]]; then + idx=$((REPLY - 1)) + DISK="${DISK_IDS[$idx]}" + break + else + echo "Invalid selection" + fi + done + + echo "" + echo -e "\033[31m!! WARNING: This will DESTROY all data on $DISK !!\033[0m" + read -p "Continue? [y/N]: " confirm + case "${confirm,,}" in + y|yes) ;; + *) echo "Aborted."; exit 1 ;; + esac + + echo "Writing disk '$DISK' to disko config..." + sed -i "s|device = \"/dev/disk/by-id/[^\"]*\";|device = \"$DISK\";|" "$DISKO_CONFIG" + + echo "Partitioning and installing NixOS..." + sudo nix \ + --extra-experimental-features "nix-command flakes" \ + run github:nix-community/disko/latest#disko-install -- \ + --flake .#{{SYSTEM}} \ + --disk main "$DISK" + + echo -e "\033[32mDone! Reboot to start NixOS.\033[0m" + +# Commit all changes and push to upstream +[group('git')] +gh COMMIT_MESSAGE: + #!/usr/bin/env bash + set -euo pipefail + git add -A + git commit -m "{{COMMIT_MESSAGE}}" + git push + +# Show status of submodules with changes +[group('submodule')] +sstatus: + #!/usr/bin/env bash + {{_subs_init}} + for name in "${!SUBS[@]}"; do + status=$(git -C "${SUBS[$name]}" status -s) + [[ -n "$status" ]] && echo -e "\033[34m$name:\033[0m" && echo "$status" + done + +# Pull all submodules and parent +[group('submodule')] +spull: + #!/usr/bin/env bash + set -euo pipefail + {{_subs_init}} + git pull + for name in "${!SUBS[@]}"; do + echo -e "\033[34m$name:\033[0m" + git -C "${SUBS[$name]}" pull + done + +# Push submodules and parent +[group('submodule')] +spush NAME="": + #!/usr/bin/env bash + set -euo pipefail + {{_subs_init}} + if [[ -n "{{NAME}}" ]]; then + path="${SUBS[{{NAME}}]:-}" + [[ -z "$path" ]] && echo "Unknown: {{NAME}}. Available: ${!SUBS[*]}" && exit 1 + git -C "$path" push + else + for path in "${SUBS[@]}"; do git -C "$path" push; done + fi + git push + +# Commit submodule changes and update parent +[group('submodule')] +scommit NAME="": + #!/usr/bin/env bash + set -euo pipefail + {{_subs_init}} + MSGS=() + + commit_sub() { + local name="$1" path="$2" + [[ -z "$(git -C "$path" status -s)" ]] && return 0 + echo -e "\033[34m$name:\033[0m" + git -C "$path" status -s + read -p "Commit message: " MSG + [[ -z "$MSG" ]] && return 0 + git -C "$path" add -A && git -C "$path" commit -m "$MSG" + git add "$path" + MSGS+=("$name: $MSG") + } + + if [[ -n "{{NAME}}" ]]; then + path="${SUBS[{{NAME}}]:-}" + [[ -z "$path" ]] && echo "Unknown: {{NAME}}. Available: ${!SUBS[*]}" && exit 1 + commit_sub "{{NAME}}" "$path" + else + for name in "${!SUBS[@]}"; do commit_sub "$name" "${SUBS[$name]}"; done + fi + + if ! git diff --cached --quiet; then + COMMIT_MSG="updated submodules"$'\n' + for m in "${MSGS[@]}"; do COMMIT_MSG+="- $m"$'\n'; done + git commit -m "$COMMIT_MSG" + fi + +# Commit and push submodules + parent +[group('submodule')] +ssync NAME="": + #!/usr/bin/env bash + set -euo pipefail + {{_subs_init}} + MSGS=() + + sync_sub() { + local name="$1" path="$2" + [[ -z "$(git -C "$path" status -s)" ]] && return 0 + echo -e "\033[34m$name:\033[0m" + git -C "$path" status -s + read -p "Commit message: " MSG + [[ -z "$MSG" ]] && return 0 + git -C "$path" add -A && git -C "$path" commit -m "$MSG" + git -C "$path" push + git add "$path" + MSGS+=("$name: $MSG") + } + + if [[ -n "{{NAME}}" ]]; then + path="${SUBS[{{NAME}}]:-}" + [[ -z "$path" ]] && echo "Unknown: {{NAME}}. Available: ${!SUBS[*]}" && exit 1 + sync_sub "{{NAME}}" "$path" + else + for name in "${!SUBS[@]}"; do sync_sub "$name" "${SUBS[$name]}"; done + fi + + if ! git diff --cached --quiet; then + COMMIT_MSG="updated submodules"$'\n' + for m in "${MSGS[@]}"; do COMMIT_MSG+="- $m"$'\n'; done + git commit -m "$COMMIT_MSG" + fi + git push + +# Fetch resources and compute sha256 hash +[group('nix')] +hash URL: + #!/usr/bin/env bash + set -euo pipefail + + if [[ "{{URL}}" =~ \.(tar(\.gz)?|tgz|gz|zip)$ ]]; then + CONTENTS=$(nix-prefetch-url --unpack {{URL}}) + else + CONTENTS=$(nix-prefetch-url {{URL}}) + fi + + HASH=$(nix hash convert --hash-algo sha256 "$CONTENTS") + + echo -e "\033[32m$HASH\033[0m" diff --git a/secrets/README.md b/secrets/README.md new file mode 100644 index 0000000..56eb406 --- /dev/null +++ b/secrets/README.md @@ -0,0 +1,140 @@ +# Secrets Management + +``` +secrets/ +├── system/ # System-level secrets (WiFi, VPN, etc.) +└── user/ # User-level secrets (password-store, API keys, etc.) +``` + +## Prerequisites + +Age identity files are stored in `src/user/config/keys/age/` and deployed automatically. + +```bash +# For testing with a local key: +age-keygen > src/user/config/keys/age/local + +# For Yubikey (see "Migrating to Yubikey" below): +age-plugin-yubikey --identity > src/user/config/keys/age/yubikey + +# Add the public key to .sops.yaml in repo root +``` + +After rebuild, the identity is written to `~/.config/sops/age/keys.txt`. + +## Adding Secrets + +1. Create or edit a YAML file: + ```bash + vim secrets/system/example.yaml + ``` + +2. Encrypt in place: + ```bash + sops -e -i secrets/system/example.yaml + ``` + +3. Reference in NixOS config: + ```nix + sops.secrets."SECRET_NAME" = { + sopsFile = path/to/example.yaml; + }; + ``` + +## Editing Secrets + +```bash +# Opens decrypted in $EDITOR, re-encrypts on save +sops secrets/system/wifi.yaml +``` + +## Viewing Secrets + +```bash +# Decrypt to stdout +sops -d secrets/system/wifi.yaml +``` + +## Removing Secrets + +1. Remove from NixOS config +2. Delete the encrypted file or remove the key from it via `sops` + +## Re-keying (after adding/removing age keys) + +```bash +# Update .sops.yaml with new keys, then: +sops updatekeys secrets/system/wifi.yaml +``` + +## Migrating to Yubikey + +### 1. Generate a new age identity on Yubikey + +```bash +# Insert Yubikey and run interactive setup +age-plugin-yubikey + +# Follow prompts: +# - Select slot (default: 1) +# - Set PIN policy (default: once per session) +# - Set touch policy (recommended: always) +# +# This generates a NEW key on the Yubikey - you will not know the private key. +# Save the identity to the keys directory: +age-plugin-yubikey --identity > src/user/config/keys/age/yubikey +``` + +The identity file only contains a *reference* to the Yubikey, not the private key. +It will be deployed to `~/.config/sops/age/keys.txt` on rebuild. + +### 2. Update .sops.yaml with Yubikey public key + +```bash +# Get the public key (age1yubikey1...) +age-plugin-yubikey --list + +# Edit .sops.yaml and replace/add the key: +vim .sops.yaml +``` + +```yaml +keys: + - &yubikey age1yubikey1q... # your Yubikey public key + +creation_rules: + - path_regex: secrets/.*\.yaml$ + key_groups: + - age: + - *yubikey +``` + +### 3. Re-key all secrets against the new key + +```bash +# This decrypts with your OLD key and re-encrypts with the NEW key +find secrets -name "*.yaml" -exec sops updatekeys {} \; +``` + +You'll need your old key available during this step. + +### 4. Remove the old age key (optional) + +```bash +# Once all secrets are re-keyed and tested: +# 1. Remove old key from .sops.yaml +# 2. Delete the old key file from the repo: +rm src/user/config/keys/age/local # or whatever your test key was named +``` + +### 5. Test decryption with Yubikey + +```bash +# Should prompt for Yubikey touch/PIN +sops -d secrets/system/wifi.yaml + +# Test a full rebuild +sudo nixos-rebuild switch --flake .#desktop +``` + +If decryption works, your migration is complete. diff --git a/secrets/system/cameras.yaml b/secrets/system/cameras.yaml new file mode 100644 index 0000000..448cc03 --- /dev/null +++ b/secrets/system/cameras.yaml @@ -0,0 +1,17 @@ +RTSP_USER: ENC[AES256_GCM,data:yketGXU=,iv:KQVYzBjzkkDepiD+hjGWLjvyC3iySK6JMZ9Fyrdo1Eo=,tag:7sHqOYROk6qNd56xWex1Bw==,type:str] +RTSP_PASS: ENC[AES256_GCM,data:QGfg7bZVdGAjuw==,iv:uS/6XpHlMgpZ812tVxGFjwMeyqX5YvfBNJUVuc0C+z8=,tag:5SIdu/yGVxzhYclyOUrOCg==,type:str] +sops: + age: + - recipient: age198jg29ryg3c0qj3yg6y9ha4ce2ue4hjdaa9kalf49fxju74dhchsquvjzp + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwSys1ZzdwRmRybkR2TGFn + RG1wVGI4aTNkYTZpOUtUSlBJQTVnU1JsdmpzCklLdUY0K1ZjSzhId3NVNXcvUWl0 + eE95cmVHWGNsZVNYWHQvSXlNZjl5WWMKLS0tIFBpek81aGlhUXUxWm91ZjV1RFk0 + SzZFalY2NXJOMFNSVFVxbDZPb1Q1amsKaDZqJvFfqxhqVcd5ldRHC+3XC/lBb9N7 + VUQ/hQZM5a1WUk321Y2bBXTN6cE/06UYrl6HXwZgxTVydou4eHywww== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-03-15T06:01:25Z" + mac: ENC[AES256_GCM,data:EwPCYlEKUgjcrZ0c75UH7n9FjkbF+WEMQzJ7Xb1+fXkD0zIIVgjudgCNtwwJTbSVupyuCVcJfCKN9n4kBpG+HyIqDZQl1MTy5YzcvvMoj3rkPLIRMfkLXFs4FRe/cFKFdxARbQrlEJqfgQME8/M07Bl+VcZRIq0mz7HlrxZFbgg=,iv:WDGCSNFT8l+MEOQCWSDDtYTj9gdDoCk+kl8UdQg+9mw=,tag:4b9vRle/waBqQX284cIiNA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.1 diff --git a/secrets/system/wifi.yaml b/secrets/system/wifi.yaml new file mode 100644 index 0000000..6224546 --- /dev/null +++ b/secrets/system/wifi.yaml @@ -0,0 +1,19 @@ +WIFI_HOME_SSID: ENC[AES256_GCM,data:xZl6DE4=,iv:koEKZTW3O+bctlwoSzZCBLRT4iG380RmP/olukUd8Xc=,tag:4HM6d+FslbM1hRYcn3JTqA==,type:str] +WIFI_HOME_PSK: ENC[AES256_GCM,data:jyC4VXzhpIE=,iv:fN33x0y4kmRrPQe7ydWGdeTQaR5a3ekBaUKHX9FpHk0=,tag:tQUUj5LU6kidYTTI2RWf8w==,type:str] +WIFI_CAMS_SSID: ENC[AES256_GCM,data:yJ/oUCfSbaw=,iv:foswCMqFLOUyPQP9KL08Mhix0j2+Jt4sHHaPV49RFe4=,tag:rBG9IyQDmbNsUp4E+tnmZg==,type:str] +WIFI_CAMS_PSK: ENC[AES256_GCM,data:VlVxqxbHof6rmqSRJrXEQsT15BNl1lrghg==,iv:B6si07a0Z5ZJfMkK0HN9fa5zvQDzf7lvIQt1ZBpBZdk=,tag:21622mki8lITWA5fh7bKrg==,type:str] +sops: + age: + - recipient: age17ejyzyk52unr6eyaa9rpunxpmf7u9726v6sx7me3ww3mdu5xzgjqsgj9gl + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuaFRWRnNGVm9TTkhJVFAv + RWQ1Q2Q3RStBa0E4V2hFYUV2ZHFPZnJGdkFJClY0WThYbWk2Nmx6V0g4UU9WSGRZ + bFpNalZJRlZyWjFTMU1JK1dpWndPS3cKLS0tIHI0M3ZUVlI3TTV6c2h1WmdrdW1l + VWtxaFNVUUFHT20xVTZpSjVWRHozTzQKAAsNbFf6bU6eelqOX7Ei+Zrtw0aw0WgQ + 5zOWrxd92MaG/AvVpL0jC1LuWtZeK3MK7Qpgtm8t0rgugUas16KYpA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-03-15T06:25:12Z" + mac: ENC[AES256_GCM,data:+Lhmcr2Jg1htfcMMMPu8AxrDhvlm4yLVIunxAcs4adX8NeJccD+/UVvZO+qtzF6iQmXCdTvRDo3shqmJKHvs6ZUJVe3jokTKMJoQdIbSIS0fSwULUV8evK5Incf8qzpnHd2J1Kg4qCL8oWeN9t4TBJTPVrNJzd/sOF1Kp2g9IBE=,iv:/ORst/Lnj3h16fJQWxAaJ5vMWKMN2lGhGoIQjNxNpGQ=,tag:NURI5mwbfECaWTgbSs6clA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.1 diff --git a/system/keys/default.nix b/system/keys/default.nix new file mode 100644 index 0000000..c946ac3 --- /dev/null +++ b/system/keys/default.nix @@ -0,0 +1,43 @@ +{ lib, ... }: + +with lib; +with builtins; +let + extractName = filename: + let + noKey = removeSuffix ".key" filename; + noMarkers = replaceStrings + [ ".pub" ".priv" ".public" ".private" ] + [ "" "" "" "" ] + noKey; + in noMarkers; + + constructKeys = dir: ( + listToAttrs ( + map (subdir: { + name = subdir; + value = listToAttrs ( + map (file: { + name = extractName file; + value = readFile "${dir}/${subdir}/${file}"; + }) (filter (file: + (readDir "${dir}/${subdir}").${file} == "regular" && + hasSuffix ".key" file + ) (attrNames (readDir "${dir}/${subdir}"))) + ); + }) (filter (node: (readDir dir).${node} == "directory") (attrNames (readDir dir))) + ) + ); + +in +{ + options = { + machines = mkOption { + description = "Machine Configurations"; + type = types.attrs; + default = { + keys = constructKeys ./.; + }; + }; + }; +} diff --git a/system/keys/desktop/ssh.pub.key b/system/keys/desktop/ssh.pub.key new file mode 100644 index 0000000..4604ca9 --- /dev/null +++ b/system/keys/desktop/ssh.pub.key @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYXfu4Jc/HtdyhOfAdCXYzhqCubIq3Bz6Kl9NDUov76 bryan@desktop diff --git a/system/machines/desktop/README.md b/system/machines/desktop/README.md new file mode 100644 index 0000000..92c75d7 --- /dev/null +++ b/system/machines/desktop/README.md @@ -0,0 +1,19 @@ +## Hardware + +| Component | Model | +|-------------|------------------------------------| +| Motherboard | MSI B760 GAMING PLUS WIFI | +| CPU | Intel Core i7-12700KF (12th Gen) | +| GPU | NVIDIA GeForce GTX 1650 | +| Storage | 2x 2TB Crucial MX500 SSD | + +## Memory + +| Slot | Size | Manufacturer | Part Number | Speed | +|---------|------|----------------|-------------|------------| +| DIMM A1 | - | - | - | - | +| DIMM A2 | 16GB | Team Group Inc | UD5-6000 | 4800 MT/s | +| DIMM B1 | - | - | - | - | +| DIMM B2 | 16GB | Team Group Inc | UD5-6000 | 4800 MT/s | + +**Total: 32GB DDR5** diff --git a/system/machines/desktop/default.nix b/system/machines/desktop/default.nix new file mode 100644 index 0000000..c7f50e0 --- /dev/null +++ b/system/machines/desktop/default.nix @@ -0,0 +1,16 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.disko.nixosModules.disko + (import ./modules/disko) + inputs.home-manager.nixosModules.home-manager + { home-manager.sharedModules = [ inputs.sops-nix.homeManagerModules.sops ]; } + (import ./modules/home-manager) + ../../../user + ../../keys + ../../modules/sops + ./hardware.nix + ./system.nix + ]; +} diff --git a/system/machines/desktop/hardware.nix b/system/machines/desktop/hardware.nix new file mode 100644 index 0000000..a4183c8 --- /dev/null +++ b/system/machines/desktop/hardware.nix @@ -0,0 +1,88 @@ +{ config, lib, pkgs, modulesPath, ... }: + +with lib; +{ + imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; + + options.monitors = mkOption { + type = types.listOf (types.submodule { + options = { + name = mkOption { type = types.str; example = "HDMI-A-1"; }; + width = mkOption { type = types.int; }; + height = mkOption { type = types.int; }; + x = mkOption { type = types.int; }; + y = mkOption { type = types.int; }; + scale = mkOption { type = types.float; }; + refreshRate = mkOption { type = types.int; }; + }; + }); + default = []; + description = "System monitor configuration"; + }; + + config = { + monitors = [ + { name = "HDMI-A-1"; width = 1920; height = 1080; x = 0; y = 0; scale = 1.0; refreshRate = 60; } + { name = "DP-1"; width = 1920; height = 1080; x = 1920; y = 0; scale = 1.0; refreshRate = 60; } + ]; + + boot = { + initrd = { + availableKernelModules = [ "vmd" "xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" ]; + kernelModules = [ "dm-snapshot" ]; + }; + extraModulePackages = [ ]; + kernelPackages = pkgs.linuxPackages_zen; + kernelParams = [ "intel_iommu=on" ]; + kernelModules = [ "kvm-intel" "virtio" "vfio-pci" "coretemp" ]; + }; + + environment.systemPackages = with pkgs; [ + linuxHeaders + + vulkan-headers + vulkan-loader + vulkan-tools + vulkan-extension-layer + + mesa + mesa-demos + + cudaPackages.cudatoolkit + cudaPackages.cudnn + + nvidia-vaapi-driver + ]; + + hardware = { + cpu = { + intel = { + updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; + }; + }; + nvidia = { + open = true; + modesetting.enable = true; + nvidiaSettings = true; + package = config.boot.kernelPackages.nvidiaPackages.stable; + }; + graphics = { + enable = true; + enable32Bit = true; + }; + }; + + # Despite confusing name, this configures userspace nvidia libraries + services.xserver.videoDrivers = [ "nvidia" ]; + + virtualisation.libvirtd = { + enable = true; + qemu = { + runAsRoot = true; + }; + }; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + powerManagement.cpuFreqGovernor = lib.mkDefault "performance"; + }; +} diff --git a/system/machines/desktop/modules/disko/default.nix b/system/machines/desktop/modules/disko/default.nix new file mode 100644 index 0000000..fd39485 --- /dev/null +++ b/system/machines/desktop/modules/disko/default.nix @@ -0,0 +1,57 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/disk/by-id/ata-CT2000MX500SSD1_2137E5D2D47D"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1G"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + primary = { + size = "100%"; + content = { + type = "lvm_pv"; + vg = "nix"; + }; + }; + }; + }; + }; + }; + + lvm_vg = { + nix = { + type = "lvm_vg"; + lvs = { + root = { + size = "5%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + mountOptions = [ "defaults" ]; + }; + }; + home = { + size = "100%FREE"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/home"; + }; + }; + }; + }; + }; + }; +} diff --git a/system/machines/desktop/modules/home-manager/default.nix b/system/machines/desktop/modules/home-manager/default.nix new file mode 100644 index 0000000..86de83f --- /dev/null +++ b/system/machines/desktop/modules/home-manager/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./home.nix + ]; +} diff --git a/system/machines/desktop/modules/home-manager/home.nix b/system/machines/desktop/modules/home-manager/home.nix new file mode 100644 index 0000000..59b2299 --- /dev/null +++ b/system/machines/desktop/modules/home-manager/home.nix @@ -0,0 +1,73 @@ +{ config, pkgs, ... }: + +{ + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + home-manager.extraSpecialArgs = { + monitors = config.monitors; + }; + home-manager.users.${config.user.name} = { + imports = [ + ../../../../../user + ../../../../../user/home.nix + ../../../../../user/modules + ]; + + home.stateVersion = "23.11"; + + home.packages = [ pkgs.sshfs ]; + + systemd.user.services.nvr-mount = { + Unit = { + Description = "Mount Frigate recordings via SSHFS"; + After = [ "network-online.target" ]; + }; + Service = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p %h/Media/nvr"; + ExecStart = "${pkgs.sshfs}/bin/sshfs -o reconnect,ServerAliveInterval=15 server:/var/lib/frigate/recordings %h/Media/nvr"; + ExecStop = "${pkgs.fuse}/bin/fusermount -u %h/Media/nvr"; + }; + Install = { + WantedBy = [ "default.target" ]; + }; + }; + + programs.ssh = { + enable = true; + enableDefaultConfig = false; + matchBlocks = { + "*" = { + serverAliveInterval = 60; + serverAliveCountMax = 3; + }; + "server" = { + hostname = "192.168.0.154"; + user = "bryan"; + }; + }; + }; + + # Machine-specific modules + modules.user = { + vim.enable = false; + security.yubikey.enable = true; + + utils = { + dev.enable = true; + irc.enable = true; + writing.enable = true; + }; + + gui = { + wm.hyprland.enable = true; + browser.firefox.enable = true; + alacritty.enable = true; + corn.enable = true; + fun.enable = true; + utils.enable = true; + }; + }; + }; +} diff --git a/system/machines/desktop/system.nix b/system/machines/desktop/system.nix new file mode 100644 index 0000000..d734365 --- /dev/null +++ b/system/machines/desktop/system.nix @@ -0,0 +1,206 @@ +{ pkgs, lib, config, ... }: + +let + gpgEnabled = lib.any + (user: user.modules.user.security.gpg.enable or false) + (lib.attrValues config.home-manager.users); + +in +{ system.stateVersion = "23.11"; + + modules.system.sops.enable = true; + + # WiFi secrets + sops.secrets = let wifi = { sopsFile = ../../../secrets/system/wifi.yaml; }; in { + "WIFI_HOME_SSID" = wifi; + "WIFI_HOME_PSK" = wifi; + "WIFI_CAMS_SSID" = wifi; + "WIFI_CAMS_PSK" = wifi; + }; + + sops.templates."wifi-env".content = '' + WIFI_HOME_SSID=${config.sops.placeholder."WIFI_HOME_SSID"} + WIFI_HOME_PSK=${config.sops.placeholder."WIFI_HOME_PSK"} + WIFI_CAMS_SSID=${config.sops.placeholder."WIFI_CAMS_SSID"} + WIFI_CAMS_PSK=${config.sops.placeholder."WIFI_CAMS_PSK"} + ''; + + users.users = { + ${config.user.name} = { + isNormalUser = true; + extraGroups = config.user.groups + ++ [ "video" "audio" "kvm" "libvirtd" "dialout" ]; + openssh.authorizedKeys.keys = [ "${config.user.keys.ssh.graphone}" ]; + }; + }; + + nix = { + channel.enable = false; + package = pkgs.nixVersions.stable; + extraOptions = '' + experimental-features = nix-command flakes + keep-going = true + ''; + settings = { + auto-optimise-store = true; + trusted-users = [ "${config.user.name}" ]; + substitute = true; + max-jobs = "auto"; + }; + gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 7d"; + }; + }; + + boot.loader = { + systemd-boot = { + enable = true; + configurationLimit = 5; + #memtest86.enable = true; + }; + + efi = { + canTouchEfiVariables = true; + }; + #timeout = null; + }; + + environment = { + systemPackages = with pkgs; [ + vim + git + usbutils + ]; + pathsToLink = [ + "/share/applications" + "/share/xdg-desktop-portal" + ]; + }; + + fonts.packages = with pkgs; [ + nerd-fonts.terminess-ttf + ]; + + security = { + sudo = { + wheelNeedsPassword = false; + execWheelOnly = true; + }; + polkit.enable = true; + }; + + time = { + timeZone = "America/New_York"; + hardwareClockInLocalTime = true; + }; + + i18n.defaultLocale = "en_US.UTF-8"; + + console = { + font = "Lat2-Terminus16"; + useXkbConfig = true; + }; + + networking = { + hostName = "desktop"; + useDHCP = lib.mkDefault true; + networkmanager = { + enable = true; + ensureProfiles = { + environmentFiles = [ config.sops.templates."wifi-env".path ]; + profiles.wifi = { + connection = { + id = "$WIFI_HOME_SSID"; + type = "wifi"; + interface-name = "wlo1"; + autoconnect = false; # Manual connection via nmcli + }; + wifi = { + ssid = "$WIFI_HOME_SSID"; + mode = "infrastructure"; + }; + wifi-security = { + key-mgmt = "wpa-psk"; + psk = "$WIFI_HOME_PSK"; + }; + ipv4.method = "auto"; + ipv6.method = "auto"; + }; + profiles.cams = { + connection = { + id = "$WIFI_CAMS_SSID"; + type = "wifi"; + interface-name = "wlo1"; + autoconnect = false; + }; + wifi = { + ssid = "$WIFI_CAMS_SSID"; + mode = "infrastructure"; + }; + wifi-security = { + key-mgmt = "wpa-psk"; + psk = "$WIFI_CAMS_PSK"; + }; + ipv4.method = "auto"; + ipv6.method = "auto"; + }; + }; + }; + firewall = { + enable = true; + allowedTCPPorts = [ 22 80 443 ]; + }; + }; + + services.dnsmasq = { + enable = true; + settings = { + # Explicit subdomains -> local server + address = [ + "/git.ramos.codes/192.168.0.154" + "/ln.ramos.codes/192.168.0.154" + "/photos.ramos.codes/192.168.0.154" + "/test.ramos.codes/192.168.0.154" + "/electrum.ramos.codes/192.168.0.154" + "/immich.ramos.codes/192.168.0.154" + "/forgejo.ramos.codes/192.168.0.154" + "/frigate.ramos.codes/192.168.0.154" + ]; + server = [ "192.168.0.1" ]; + }; + }; + + services = { + pcscd.enable = gpgEnabled; + timesyncd = lib.mkDefault { + enable = true; + servers = [ + "0.pool.ntp.org" + "1.pool.ntp.org" + "2.pool.ntp.org" + "3.pool.ntp.org" + ]; + }; + pipewire = { + enable = true; + audio.enable = true; + + wireplumber.enable = true; + + pulse.enable = true; + jack.enable = true; + alsa.enable = true; + alsa.support32Bit = true; + }; + openssh = { + enable = true; + startWhenNeeded = false; + settings = { + X11Forwarding = false; + PasswordAuthentication = false; + }; + }; + }; +} diff --git a/system/machines/server/README.md b/system/machines/server/README.md new file mode 100644 index 0000000..56c6cb5 --- /dev/null +++ b/system/machines/server/README.md @@ -0,0 +1,20 @@ +## Hardware + +| Component | Model | +|-----------|--------------------------------| +| System | HP Z230 SFF Workstation | +| CPU | Intel Core i7-4770 @ 3.40GHz | +| GPU | Integrated | +| Storage | 6TB Seagate ST6000NM0024 | +| Network | Intel (onboard) | + +## Memory + +| Slot | Size | Manufacturer | Part Number | Speed | +|-------|------|---------------|-------------------|-----------| +| DIMM1 | 4GB | Hynix/Hyundai | HMT451U6AFR8C-PB | 1600 MT/s | +| DIMM2 | 4GB | Hynix/Hyundai | HMT451U6AFR8C-PB | 1600 MT/s | +| DIMM3 | 4GB | Hynix/Hyundai | HMT451U6AFR8C-PB | 1600 MT/s | +| DIMM4 | 4GB | Hynix/Hyundai | HMT451U6AFR8C-PB | 1600 MT/s | + +**Total: 16GB DDR3** diff --git a/system/machines/server/default.nix b/system/machines/server/default.nix new file mode 100644 index 0000000..c7f50e0 --- /dev/null +++ b/system/machines/server/default.nix @@ -0,0 +1,16 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.disko.nixosModules.disko + (import ./modules/disko) + inputs.home-manager.nixosModules.home-manager + { home-manager.sharedModules = [ inputs.sops-nix.homeManagerModules.sops ]; } + (import ./modules/home-manager) + ../../../user + ../../keys + ../../modules/sops + ./hardware.nix + ./system.nix + ]; +} diff --git a/system/machines/server/hardware.nix b/system/machines/server/hardware.nix new file mode 100644 index 0000000..8e9e3c5 --- /dev/null +++ b/system/machines/server/hardware.nix @@ -0,0 +1,27 @@ +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; + + boot = { + initrd = { + availableKernelModules = [ "xhci_pci" "ehci_pci" "ahci" "usbhid" "sd_mod" "sr_mod" ]; + kernelModules = [ ]; + }; + kernelModules = [ "kvm-intel" ]; + extraModulePackages = [ ]; + }; + + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; + + # Enable VAAPI for hardware video acceleration + hardware.graphics = { + enable = true; + extraPackages = with pkgs; [ + intel-vaapi-driver # i965 driver for Haswell + ]; + }; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand"; +} diff --git a/system/machines/server/modules/backup/default.nix b/system/machines/server/modules/backup/default.nix new file mode 100644 index 0000000..511b332 --- /dev/null +++ b/system/machines/server/modules/backup/default.nix @@ -0,0 +1,103 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.backup; + + recipientArgs = concatMapStrings (r: "-r '${lib.strings.trim r}' ") cfg.recipients; + + # Convert absolute paths to relative for tar, preserving structure + # e.g., /var/lib/forgejo -> var/lib/forgejo + tarPaths = map (p: removePrefix "/" p) cfg.paths; + excludeArgs = concatMapStrings (e: "--exclude='${e}' ") cfg.exclude; + + backupScript = pkgs.writeShellScript "backup" '' + set -euo pipefail + + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + BACKUP_NAME="backup-$TIMESTAMP.tar.age" + TEMP_DIR=$(mktemp -d) + trap "rm -rf $TEMP_DIR" EXIT + + echo "Starting backup: $BACKUP_NAME" + echo "Paths: ${concatStringsSep " " cfg.paths}" + + export PATH="${pkgs.age-plugin-yubikey}/bin:$PATH" + ${pkgs.gnutar}/bin/tar -C / ${excludeArgs}-cf - ${concatStringsSep " " tarPaths} | \ + ${pkgs.age}/bin/age ${recipientArgs} -o "$TEMP_DIR/$BACKUP_NAME" + + ${pkgs.rclone}/bin/rclone --config /root/.config/rclone/rclone.conf copy "$TEMP_DIR/$BACKUP_NAME" "${cfg.destination}" + + # Prune old backups + ${pkgs.rclone}/bin/rclone --config /root/.config/rclone/rclone.conf lsf "${cfg.destination}" | \ + sort -r | \ + tail -n +$((${toString cfg.keepLast} + 1)) | \ + while read -r old; do + ${pkgs.rclone}/bin/rclone --config /root/.config/rclone/rclone.conf delete "${cfg.destination}/$old" + done + + echo "Backup complete" + ''; + +in +{ + options.modules.system.backup = { + enable = mkEnableOption "Encrypted backups"; + + paths = mkOption { + type = types.listOf types.str; + default = []; + description = "Absolute paths to include in backup (structure preserved)"; + }; + + exclude = mkOption { + type = types.listOf types.str; + default = []; + description = "Patterns to exclude (passed to tar --exclude)"; + }; + + recipients = mkOption { + type = types.listOf types.str; + default = []; + description = "Age public keys for encryption"; + }; + + destination = mkOption { + type = types.str; + default = ""; + description = "Rclone destination"; + }; + + schedule = mkOption { + type = types.str; + default = "daily"; + description = "Systemd calendar expression"; + }; + + keepLast = mkOption { + type = types.int; + default = 3; + description = "Number of backups to keep"; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ pkgs.rclone ]; + + systemd.services.backup = { + description = "Encrypted backup"; + serviceConfig = { + Type = "oneshot"; + ExecStart = backupScript; + }; + }; + + systemd.timers.backup = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = cfg.schedule; + Persistent = true; + }; + }; + }; +} diff --git a/system/machines/server/modules/bitcoin/config/bitcoin.conf b/system/machines/server/modules/bitcoin/config/bitcoin.conf new file mode 100644 index 0000000..d3ed9eb --- /dev/null +++ b/system/machines/server/modules/bitcoin/config/bitcoin.conf @@ -0,0 +1,20 @@ +server=1 + +rpccookiefile=/var/lib/bitcoin/.cookie +rpccookieperms=group +rpcbind=127.0.0.1 +rpcallowip=127.0.0.1 + +dnsseed=0 +onlynet=onion + +bind=127.0.0.1 +proxy=127.0.0.1:9050 + +listen=1 +listenonion=1 +torcontrol=127.0.0.1:9051 + +txindex=1 + +dbcache=1024 diff --git a/system/machines/server/modules/bitcoin/default.nix b/system/machines/server/modules/bitcoin/default.nix new file mode 100644 index 0000000..e7e12a0 --- /dev/null +++ b/system/machines/server/modules/bitcoin/default.nix @@ -0,0 +1,80 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.bitcoin; + nginx = config.modules.system.nginx; + + home = "/var/lib/bitcoin"; + + bitcoinConf = pkgs.writeTextFile { + name = "bitcoin.conf"; + text = builtins.readFile ./config/bitcoin.conf; + }; + +in +{ options.modules.system.bitcoin = { enable = mkEnableOption "Bitcoin Server"; }; + config = mkIf cfg.enable { + modules.system.tor.enable = true; + + environment.systemPackages = with pkgs; [ + bitcoind + ]; + + users = { + users = { + "btc" = { + inherit home; + description = "Bitcoin Core system user"; + isSystemUser = true; + group = "bitcoin"; + extraGroups = [ "tor" ]; + createHome = true; + }; + "nginx" = { + extraGroups = mkIf nginx.enable [ + "bitcoin" + ]; + }; + }; + groups = { + "bitcoin" = { + members = [ + "btc" + config.user.name + ]; + }; + }; + }; + + programs.bash.shellAliases = { + btc = "bitcoin-cli"; + }; + + services.bitcoind = { + "mainnet" = { + enable = true; + user = "btc"; + group = "bitcoin"; + configFile = bitcoinConf; + dataDir = home; + pidFile = "${home}/bitcoind.pid"; + }; + }; + + # Make data dir group-accessible so electrs/clightning can read cookie + systemd.tmpfiles.rules = [ + "d ${home} 0750 btc bitcoin -" + ]; + + systemd.services.bitcoind-mainnet = { + wants = [ "tor.service" ]; + after = [ "tor.service" ]; + serviceConfig.ExecStartPre = "+${pkgs.coreutils}/bin/chmod 750 /var/lib/tor"; + }; + + modules.system.backup.paths = [ + "${home}/wallets" + ]; + }; +} diff --git a/system/machines/server/modules/bitcoin/modules/clightning/config/lightning.conf b/system/machines/server/modules/bitcoin/modules/clightning/config/lightning.conf new file mode 100644 index 0000000..def24ec --- /dev/null +++ b/system/machines/server/modules/bitcoin/modules/clightning/config/lightning.conf @@ -0,0 +1,31 @@ +alias=OrdSux + +network=bitcoin +bitcoin-datadir=/var/lib/bitcoin +bitcoin-rpcconnect=127.0.0.1 +bitcoin-rpcport=8332 + +lightning-dir=/var/lib/clightning +plugin-dir=/var/lib/clightning/plugins + +log-file=/var/lib/clightning/lightningd.log +log-level=info +rpc-file-mode=0660 + +# Bind RPC locally only +bind-addr=127.0.0.1:9736 + +# Auto-create Tor hidden service for peer connections +addr=autotor:127.0.0.1:9051 + +# Route outbound through Tor +proxy=127.0.0.1:9050 +always-use-proxy=true + +large-channels +fee-base=1000 +fee-per-satoshi=10 +min-capacity-sat=10000 +htlc-minimum-msat=0 +funding-confirms=3 +max-concurrent-htlcs=30 diff --git a/system/machines/server/modules/bitcoin/modules/clightning/default.nix b/system/machines/server/modules/bitcoin/modules/clightning/default.nix new file mode 100644 index 0000000..7889819 --- /dev/null +++ b/system/machines/server/modules/bitcoin/modules/clightning/default.nix @@ -0,0 +1,115 @@ +{ lib, pkgs, config, ... }: + +with lib; +let + cfg = config.modules.system.bitcoin.clightning; + btc = config.modules.system.bitcoin; + nginx = config.modules.system.nginx; + home = "/var/lib/clightning"; + domain = "ramos.codes"; + + clnrest = pkgs.callPackage ./plugins/clnrest.nix { }; + + clnConfig = pkgs.writeTextFile { + name = "lightning.conf"; + text = '' + ${builtins.readFile ./config/lightning.conf} + bitcoin-cli=${pkgs.bitcoind}/bin/bitcoin-cli + + # CLNRest configuration + clnrest-port=3010 + clnrest-host=127.0.0.1 + clnrest-protocol=https + ''; + }; + +in +{ options.modules.system.bitcoin.clightning = { enable = mkEnableOption "Core Lightning Server"; }; + config = mkIf (cfg.enable && btc.enable) { + environment.systemPackages = with pkgs; [ + clightning + ]; + + users = { + users = { + "clightning" = { + inherit home; + description = "Core Lightning system user"; + isSystemUser = true; + group = "bitcoin"; + extraGroups = [ "tor" ]; + createHome = true; + }; + }; + groups = { + "bitcoin" = { + members = mkAfter [ + "clightning" + ]; + }; + }; + }; + + programs.bash.shellAliases = { + cln = "lightning-cli"; + }; + + systemd.services.lightningd = { + description = "Core Lightning Daemon"; + wantedBy = [ "multi-user.target" ]; + + wants = [ "bitcoind-mainnet.service" "tor.service" ]; + after = [ + "bitcoind-mainnet.service" + "tor.service" + "network.target" + ]; + + serviceConfig = { + ExecStartPre = "+${pkgs.coreutils}/bin/chmod 750 /var/lib/bitcoin /var/lib/tor ${home} ${home}/bitcoin"; + ExecStart = "${pkgs.clightning}/bin/lightningd --conf=${clnConfig}"; + User = "clightning"; + Group = "bitcoin"; + WorkingDirectory = home; + + Type = "simple"; + KillMode = "process"; + TimeoutSec = 60; + Restart = "always"; + RestartSec = 60; + }; + }; + + # Bind mount from /data + fileSystems.${home} = { + device = "/data/clightning"; + fsType = "none"; + options = [ "bind" ]; + }; + + # Ensure data directory exists with correct permissions + systemd.tmpfiles.rules = mkAfter [ + "d /data/clightning 0750 clightning bitcoin -" + "d /data/clightning/bitcoin 0750 clightning bitcoin -" + "d /data/clightning/plugins 0750 clightning bitcoin -" + "L+ /home/${config.user.name}/.lightning - - - - ${home}" + "L+ ${home}/plugins/clnrest - - - - ${clnrest}/libexec/c-lightning/plugins/clnrest" + ]; + + modules.system.backup.paths = [ + "${home}/bitcoin/hsm_secret" + "${home}/bitcoin/emergency.recover" + ]; + + services.nginx.virtualHosts."ln.${domain}" = mkIf nginx.enable { + useACMEHost = domain; + forceSSL = true; + locations."/" = { + proxyPass = "https://127.0.0.1:3010"; + extraConfig = '' + proxy_ssl_verify off; + ''; + }; + }; + }; +} diff --git a/system/machines/server/modules/bitcoin/modules/clightning/plugins/clnrest.nix b/system/machines/server/modules/bitcoin/modules/clightning/plugins/clnrest.nix new file mode 100644 index 0000000..b4124cf --- /dev/null +++ b/system/machines/server/modules/bitcoin/modules/clightning/plugins/clnrest.nix @@ -0,0 +1,54 @@ +{ + lib, + rustPlatform, + fetchFromGitHub, + pkg-config, + openssl, + protobuf, +}: + +rustPlatform.buildRustPackage rec { + pname = "clnrest"; + version = "25.02.2"; + + src = fetchFromGitHub { + owner = "ElementsProject"; + repo = "lightning"; + rev = "v${version}"; + hash = "sha256-SiPYB463l9279+zawsxmql1Ui/dTdah5KgJgmrWsR2A="; + }; + + cargoLock = { + lockFile = "${src}/Cargo.lock"; + }; + + cargoBuildFlags = [ + "-p" + "clnrest" + ]; + cargoTestFlags = [ + "-p" + "clnrest" + ]; + + nativeBuildInputs = [ + pkg-config + protobuf + ]; + + buildInputs = [ openssl ]; + + postInstall = '' + mkdir -p $out/libexec/c-lightning/plugins + mv $out/bin/clnrest $out/libexec/c-lightning/plugins/ + rmdir $out/bin + ''; + + meta = { + description = "Transforms RPC calls into REST APIs"; + homepage = "https://docs.corelightning.org/docs/rest"; + license = lib.licenses.mit; + platforms = lib.platforms.linux; + mainProgram = "clnrest"; + }; +} diff --git a/system/machines/server/modules/bitcoin/modules/electrum/config/config.toml b/system/machines/server/modules/bitcoin/modules/electrum/config/config.toml new file mode 100644 index 0000000..9f05fe2 --- /dev/null +++ b/system/machines/server/modules/bitcoin/modules/electrum/config/config.toml @@ -0,0 +1,13 @@ +network = "bitcoin" + +electrum_rpc_addr = "127.0.0.1:50001" + +cookie_file = "/var/lib/bitcoin/.cookie" + +db_dir = "/var/lib/electrs" + +log_filters = "INFO" + +daemon_rpc_addr = "127.0.0.1:8332" +daemon_p2p_addr = "127.0.0.1:8333" +daemon_dir = "/var/lib/bitcoin" diff --git a/system/machines/server/modules/bitcoin/modules/electrum/default.nix b/system/machines/server/modules/bitcoin/modules/electrum/default.nix new file mode 100644 index 0000000..5a85770 --- /dev/null +++ b/system/machines/server/modules/bitcoin/modules/electrum/default.nix @@ -0,0 +1,121 @@ +{ lib, pkgs, config, ... }: + +with lib; +let + cfg = config.modules.system.bitcoin.electrum; + nginx = config.modules.system.nginx; + home = "/var/lib/electrs"; + + btc = config.modules.system.bitcoin; + domain = "ramos.codes"; + + electrsConfig = pkgs.writeTextFile { + name = "config.toml"; + text = builtins.readFile ./config/config.toml; + }; + +in +{ options.modules.system.bitcoin.electrum = { enable = mkEnableOption "Electrs Server"; }; + config = mkIf (cfg.enable && btc.enable) { + #TODO: Fix the failing overlay due to `cargoHash/cargoSha256` + #nixpkgs.overlays = [ + # (final: prev: { + # electrs = prev.electrs.overrideAttrs (old: rec { + # pname = "electrs"; + # version = "0.10.8"; + # src = pkgs.fetchFromGitHub { + # owner = "romanz"; + # repo = pname; + # rev = "v${version}"; + # hash = "sha256-L26jzAn8vwnw9kFd6ciyYS/OLEFTbN8doNKy3P8qKRE="; + # }; + # #cargoDeps = old.cargoDeps.overrideAttrs (const { + # # name = "electrs-${version}.tar.gz"; + # # inherit src; + # # sha256 = ""; + # #}); + # cargoHash = "sha256-lBRcq73ri0HR3duo6Z8PdSjnC8okqmG5yWeHxH/LmcU="; + # }); + # }) + #]; + + environment.systemPackages = with pkgs; [ + electrs + ]; + + users = { + users = { + "electrs" = { + inherit home; + description = "Electrs system user"; + isSystemUser = true; + group = "bitcoin"; + createHome = true; + }; + }; + groups = { + "bitcoin" = { + members = mkAfter [ + "electrs" + ]; + }; + }; + }; + + + systemd.services.electrs = { + description = "Electrs Bitcoin Indexer"; + wantedBy = [ "multi-user.target" ]; + + wants = [ "bitcoind-mainnet.service" ]; + after = [ + "bitcoind-mainnet.service" + "network.target" + ]; + + serviceConfig = { + ExecStartPre = "+${pkgs.coreutils}/bin/chmod 750 /var/lib/bitcoin"; + ExecStart = "${pkgs.electrs}/bin/electrs --conf=${electrsConfig}"; + User = "electrs"; + Group = "bitcoin"; + WorkingDirectory = home; + + Type = "simple"; + KillMode = "process"; + TimeoutSec = 60; + Restart = "always"; + RestartSec = 60; + }; + }; + + # Bind mount from /data + fileSystems.${home} = { + device = "/data/electrs"; + fsType = "none"; + options = [ "bind" ]; + }; + + # Ensure db directory exists with correct permissions + systemd.tmpfiles.rules = [ + "d /data/electrs 0750 electrs bitcoin -" + ]; + + # Nginx SSL proxy for Electrum protocol (TCP) + networking.firewall.allowedTCPPorts = mkIf nginx.enable [ 50002 ]; + + services.nginx.streamConfig = mkIf nginx.enable '' + map $ssl_server_name $electrs_backend { + electrum.${domain} 127.0.0.1:50001; + default ""; + } + + server { + listen 50002 ssl; + proxy_pass $electrs_backend; + + ssl_certificate /var/lib/acme/${domain}/fullchain.pem; + ssl_certificate_key /var/lib/acme/${domain}/key.pem; + } + ''; + }; +} diff --git a/system/machines/server/modules/default.nix b/system/machines/server/modules/default.nix new file mode 100644 index 0000000..b34257d --- /dev/null +++ b/system/machines/server/modules/default.nix @@ -0,0 +1,35 @@ +let + mkModules = dir: isRoot: + let + entries = builtins.readDir dir; + names = builtins.attrNames entries; + + isModuleDir = path: + builtins.pathExists path && + builtins.readFileType path == "directory" && + builtins.baseNameOf path != "config" && + builtins.baseNameOf path != "plugins" && + builtins.baseNameOf path != "home-manager" && + builtins.baseNameOf path != "disko"; + isModule = file: file == "default.nix"; + isNix = file: builtins.match ".*\\.nix" file != null && file != "default.nix"; + + in + builtins.concatMap (name: + let + path = "${dir}/${name}"; + in + if isModuleDir path then + mkModules path false + else if isModule name && !isRoot then + [dir] + else if isNix name then + [path] + else + [] + ) names; + +in +{ + imports = mkModules ./. true; +} diff --git a/system/machines/server/modules/disko/default.nix b/system/machines/server/modules/disko/default.nix new file mode 100644 index 0000000..8f5d43e --- /dev/null +++ b/system/machines/server/modules/disko/default.nix @@ -0,0 +1,75 @@ +{ lib, ... }: + +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/sda"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + lvm = { + size = "100%"; + content = { + type = "lvm_pv"; + vg = "vg0"; + }; + }; + }; + }; + }; + }; + + lvm_vg = { + vg0 = { + type = "lvm_vg"; + lvs = { + root = { + size = "200G"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + data = { + size = "1T"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/data"; + }; + }; + bitcoin = { + size = "1T"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/var/lib/bitcoin"; + }; + }; + frigate = { + size = "3T"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/var/lib/frigate"; + }; + }; + # ~300GB left unallocated for future growth + }; + }; + }; + }; +} diff --git a/system/machines/server/modules/forgejo/default.nix b/system/machines/server/modules/forgejo/default.nix new file mode 100644 index 0000000..a4dcc42 --- /dev/null +++ b/system/machines/server/modules/forgejo/default.nix @@ -0,0 +1,100 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.forgejo; + nginx = config.modules.system.nginx; + domain = "ramos.codes"; + socketPath = "/run/forgejo/forgejo.sock"; + +in +{ + options.modules.system.forgejo = { + enable = mkEnableOption "Forgejo Server"; + }; + + config = mkIf cfg.enable { + users.groups.git = {}; + users.users.git = { + isSystemUser = true; + group = "git"; + home = "/var/lib/forgejo"; + shell = "${pkgs.bash}/bin/bash"; + }; + + users.users.nginx = mkIf nginx.enable { + extraGroups = [ "git" ]; + }; + + # Bind mount from /data + fileSystems."/var/lib/forgejo" = { + device = "/data/forgejo"; + fsType = "none"; + options = [ "bind" ]; + }; + + systemd.tmpfiles.rules = [ + "d /data/forgejo 0750 git git -" + "d /data/forgejo/.ssh 0700 git git -" + "d /data/forgejo/custom 0750 git git -" + "d /data/forgejo/data 0750 git git -" + ]; + + services.forgejo = { + enable = true; + user = "git"; + group = "git"; + stateDir = "/var/lib/forgejo"; + + settings = { + DEFAULT = { + APP_NAME = "Git Server"; + APP_SLOGAN = ""; + }; + + service.REQUIRE_SIGNIN_VIEW = false; + server = { + DOMAIN = "git.${domain}"; + ROOT_URL = "https://git.${domain}/"; + PROTOCOL = "http+unix"; + HTTP_ADDR = socketPath; + SSH_DOMAIN = "git.${domain}"; + SSH_PORT = 22; + START_SSH_SERVER = false; + LANDING_PAGE = "explore"; + }; + + service = { + REGISTER_MANUAL_CONFIRM = true; + DISABLE_REGISTRATION = false; + DEFAULT_ALLOW_CREATE_ORGANIZATION = false; + }; + + admin = { + DISABLE_REGULAR_ORG_CREATION = true; + }; + + auth = { + ENABLE_BASIC_AUTHENTICATION = true; + }; + }; + + database = { + type = "sqlite3"; + path = "/var/lib/forgejo/data/forgejo.db"; + }; + }; + + modules.system.backup.paths = [ + "/var/lib/forgejo" + ]; + + services.nginx.virtualHosts."git.${domain}" = mkIf nginx.enable { + useACMEHost = domain; + forceSSL = true; + locations."/" = { + proxyPass = "http://unix:${socketPath}"; + }; + }; + }; +} diff --git a/system/machines/server/modules/frigate/README.md b/system/machines/server/modules/frigate/README.md new file mode 100644 index 0000000..56b685c --- /dev/null +++ b/system/machines/server/modules/frigate/README.md @@ -0,0 +1,162 @@ +# Frigate Camera Setup + +## Camera Models + +| Camera | Model | MAC | IP | +|--------|-------|-----|-----| +| cam4 | W461ASC | 00:1f:54:c2:d1:b1 | 192.168.1.194 | +| cam1 | B463AJ | 00:1f:54:a9:81:d1 | 192.168.1.167 | +| cam2 | W463AQ (ch1) | 00:1f:54:b2:9b:1d | 192.168.1.147 | +| cam3 | W463AQ (ch2) | 00:1f:54:b2:9b:1d | 192.168.1.147 | +| cam5 | SL300 | | | | + +## Network Architecture + +- Camera network: 192.168.1.0/24 (isolated, no internet) +- Server NIC: enp2s0f1 @ 192.168.1.1 +- WiFi AP: TP-Link RE315 @ 192.168.1.254 +- DHCP range: 192.168.1.100-200 + +## RTSP URL Format + +``` +rtsp://admin:ocu?u3Su@/cam/realmonitor?channel=&subtype=0 +``` + +- channel=1 for single-camera devices +- channel=1,2 for dual-camera devices (W463AQ) +- subtype=0 for main stream, subtype=1 for sub stream + +## Camera Reset Procedures + +### W461ASC (cam4) +1. Keep camera powered on +2. Reset button is on the back of the camera +3. Press and hold reset button for 30-60 seconds until chime sounds + +### B463AJ (cam1) +1. Remove doorbell from mount +2. Locate reset button on the back +3. Press and hold until you hear chime reset sound +4. Reconnect via Lorex app as new device + +### W463AQ (cam2/cam3) +1. Keep camera powered on +2. Rotate the lens upwards to reveal hidden reset button +3. Press and hold reset button until you hear audio prompt +4. Flashing green Smart Security Lighting confirms reset +5. Solid green = not fully reset, repeat if needed + +### SL300 (cam5) +1. Keep camera powered on +2. Tilt camera lens upwards to reveal reset/microSD card cover +3. Remove the cover +4. Press and hold reset button until audio prompt +5. Replace cover quickly +6. Wait for green LED flash + audio confirmation + +## Initial Setup + +1. Temporarily enable internet for camera network: + ```bash + sudo iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o enp2s0f0 -j MASQUERADE + sudo sysctl -w net.ipv4.ip_forward=1 + ``` + +2. Connect camera to "cams" WiFi network + +3. Use Lorex app to configure camera (requires cloud - CCP middleman) + +4. Get camera MAC from DHCP leases: + ```bash + cat /var/lib/dnsmasq/dnsmasq.leases + ``` + +5. Add DHCP reservation in `system.nix`: + ```nix + dhcp-host = [ + "aa:bb:cc:dd:ee:ff,192.168.1.XXX,camera_name" + ]; + ``` + +6. Add MAC to firewall block list in `system.nix`: + ```nix + iptables -A FORWARD -m mac --mac-source aa:bb:cc:dd:ee:ff -j DROP + ``` + +7. Update camera IP in `frigate/default.nix` and enable + +8. Deploy and disable internet: + ```bash + nixos-rebuild switch --flake .#server --target-host server + sudo iptables -t nat -D POSTROUTING -s 192.168.1.0/24 -o enp2s0f0 -j MASQUERADE + sudo sysctl -w net.ipv4.ip_forward=0 + ``` + +## Storage + +| Path | Bind Mount | Contents | +|------|------------|----------| +| /var/lib/frigate | /data/frigate/lib | Database, recordings, clips | + +## Notes + +- Lorex cameras are cloud-only for configuration (no local web UI responds) +- RTSP works locally without internet +- Cameras phone home aggressively when internet is available - keep isolated +- Haswell CPU cannot hardware decode HEVC - using CPU decode +- Consider T400 GPU for hardware acceleration if scaling to more cameras + +## Port Scan Results (W461ASC) + +- 80/tcp - HTTP (non-responsive, proprietary) +- 554/tcp - RTSP (working) +- 8086/tcp - Proprietary +- 35000/tcp - Proprietary + +## Planned Upgrades + +Replace Lorex cameras with proper RTSP/ONVIF cameras for reliable Frigate integration. + +| Current | Replacement | Price | Notes | +|---------|-------------|-------|-------| +| cam1 (B861AJ) | Reolink Video Doorbell WiFi | ~$120 | 5MP, wired power + WiFi, always-on | +| cam4 (W461ASC) | TP-Link Tapo C110 | ~$30 | 3MP, compact, window-friendly | +| cam2 + cam3 (W463AQ) | Reolink E1 Pro | ~$45 | 4MP, 355° pan | +| cam5 (SL300) | **Remove** | - | Obstructed, overlaps with cam4 | + +**Total: ~$195** + +### Reolink Video Doorbell WiFi + +- URL: https://reolink.com/us/product/reolink-video-doorbell-wifi +- Model: SKU 2267808 +- Resolution: 5MP (2560x1920 @ 20fps) +- Dimensions: Standard doorbell form factor +- Power: Hardwired 12-24VAC or DC 24V (always-on, no battery) +- Network: 2.4GHz/5GHz WiFi +- Protocols: RTSP, ONVIF, RTMP, HTTPS +- FOV: 180° diagonal (135° H, 100° V) + +### TP-Link Tapo C110 + +- URL: https://www.tp-link.com/us/home-networking/cloud-camera/tapo-c110/ +- Resolution: 3MP (2304x1296 @ 15fps) +- Dimensions: 2.66" x 2.15" x 3.89" (compact cube, similar to Lorex W461ASC) +- Power: 9V DC adapter +- Network: 2.4GHz WiFi +- Protocols: RTSP, ONVIF (officially supported NVR mode) +- RTSP URL: `rtsp://user:pass@IP:554/stream1` (main), `stream2` (sub) +- Frigate: Confirmed working - https://www.simonam.dev/tapo-c110-frigate-config/ + +### Reolink E1 Pro + +- URL: https://reolink.com/us/product/e1-pro/ +- Resolution: 4MP (2560x1440) +- Dimensions: ~4" dome with pan/tilt +- Power: 5V DC adapter +- Network: 2.4GHz/5GHz WiFi +- Protocols: RTSP, ONVIF +- Features: Pan 355°, Tilt 50°, person/pet detection + +**Why replace Lorex:** Cloud-dependent config, no ONVIF, doorbell sleeps on battery, aggressive phone-home behavior requires network isolation. diff --git a/system/machines/server/modules/frigate/default.nix b/system/machines/server/modules/frigate/default.nix new file mode 100644 index 0000000..4b5f0c1 --- /dev/null +++ b/system/machines/server/modules/frigate/default.nix @@ -0,0 +1,294 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.frigate; + nginx = config.modules.system.nginx; + domain = "ramos.codes"; + user = config.sops.placeholder."RTSP_USER"; + pass = config.sops.placeholder."RTSP_PASS"; + +in +{ + options.modules.system.frigate = { + enable = mkEnableOption "Enable Frigate NVR"; + }; + + config = mkIf cfg.enable { + # Allow user to access frigate recordings via SSHFS + users.users.${config.user.name}.extraGroups = [ "frigate" ]; + + # go2rtc config with credentials from sops + sops.templates."go2rtc.yaml" = { + content = '' + rtsp: + listen: ":8554" + webrtc: + listen: ":8555" + streams: + cam1: "rtsp://${user}:${pass}@192.168.1.167/cam/realmonitor?channel=1&subtype=0#backchannel=1" + cam1_sub: "rtsp://${user}:${pass}@192.168.1.167/cam/realmonitor?channel=1&subtype=1" + cam2: "rtsp://${user}:${pass}@192.168.1.147/cam/realmonitor?channel=1&subtype=0#backchannel=1" + cam2_sub: "rtsp://${user}:${pass}@192.168.1.147/cam/realmonitor?channel=1&subtype=1" + cam3: "rtsp://${user}:${pass}@192.168.1.147/cam/realmonitor?channel=2&subtype=0#backchannel=1" + cam3_sub: "rtsp://${user}:${pass}@192.168.1.147/cam/realmonitor?channel=2&subtype=1" + cam4: "rtsp://${user}:${pass}@192.168.1.194/cam/realmonitor?channel=1&subtype=0" + cam4_sub: "rtsp://${user}:${pass}@192.168.1.194/cam/realmonitor?channel=1&subtype=1" + ''; + mode = "0444"; # go2rtc runs as dynamic user, needs read access + }; + + # go2rtc service using sops-templated config + services.go2rtc.enable = true; + systemd.services.go2rtc = { + serviceConfig.ExecStart = mkForce "${pkgs.go2rtc}/bin/go2rtc -config ${config.sops.templates."go2rtc.yaml".path}"; + after = [ "sops-nix.service" ]; + wants = [ "sops-nix.service" ]; + }; + + services.frigate = { + enable = true; + package = pkgs.unstable.frigate; + hostname = "frigate.${domain}"; + vaapiDriver = "i965"; # Haswell iGPU for H.264 decode + settings = { + mqtt.enabled = false; + + ffmpeg = { + hwaccel_args = "preset-vaapi"; # VAAPI for H.264 substream detection + input_args = "preset-rtsp-restream"; # TCP transport for go2rtc + }; + + birdseye = { + mode = "continuous"; + width = 1280; + height = 720; + quality = 8; # 8 - 31 + }; + + motion = { + enabled = true; + }; + + detect = { + enabled = true; + min_initialized = 3; + max_disappeared = 25; + width = 1280; + height = 720; + }; + + audio = { + enabled = true; + max_not_heard = 30; + min_volume = 600; + listen = [ + "glass" + "shatter" + "fire_alarm" + "boom" + "thump" + "siren" + "alarm" + "explosion" + "burst" + ]; + }; + + audio_transcription = { + enabled = true; + model_size = "small"; + language = "en"; + }; + + record = { + enabled = true; + continuous.days = 3; # Full 24/7 footage + motion.days = 7; # Motion segments after continuous expires + detections.retain = { + days = 14; # Any tracked object (person, car, etc.) + mode = "motion"; + }; + alerts.retain = { + days = 30; # Zone violations, loitering - important stuff + mode = "all"; + }; + }; + + snapshots = { + enabled = true; + retain = { + default = 3; + }; + quality = 80; + }; + + + cameras = { + cam1 = { + enabled = true; + ffmpeg.inputs = [ + { + path = "rtsp://127.0.0.1:8554/cam1"; + roles = [ "record" ]; + } + { + path = "rtsp://127.0.0.1:8554/cam1_sub"; + roles = [ "detect" "audio" ]; + } + ]; + }; + + cam2 = { + enabled = true; + motion.enabled = false; + detect.enabled = false; + objects.mask = [ "0.969,0.078,0.846,0.075,0.845,0.034,0.97,0.037" ]; + ffmpeg.inputs = [ + { + path = "rtsp://127.0.0.1:8554/cam2"; + roles = [ "record" ]; + } + { + path = "rtsp://127.0.0.1:8554/cam2_sub"; + roles = [ "detect" "audio" ]; + } + ]; + }; + + cam3 = { + enabled = true; + motion.enabled = false; + detect.enabled = false; + ffmpeg.inputs = [ + { + path = "rtsp://127.0.0.1:8554/cam3"; + roles = [ "record" ]; + } + { + path = "rtsp://127.0.0.1:8554/cam3_sub"; + roles = [ "detect" "audio" ]; + } + ]; + }; + + cam4 = { + enabled = true; + audio.enabled = false; + motion.mask = [ "0.811,0.109,0.954,0.111,0.959,0.065,0.811,0.055" ]; + zones.zone1 = { + friendly_name = "lot"; + coordinates = "0.299,0.438,0.191,0.951,0.453,0.964,0.453,0.437"; + loitering_time = 10; + }; + ffmpeg.inputs = [ + { + path = "rtsp://127.0.0.1:8554/cam4"; + roles = [ "record" ]; + } + { + path = "rtsp://127.0.0.1:8554/cam4_sub"; + roles = [ "detect" ]; + } + ]; + }; + }; + + classification = { + custom = { + "door" = { + enabled = true; + name = "door"; + threshold = 0.8; + state_config = { + cameras = { + cam2.crop = [ + 0.8595647692717828 + 0.39901413156128707 + 0.9903488513256276 + 0.6315191663236775 + ]; + cam3.crop = [ + 0.0008617338314475493 + 0.3909394833748086 + 0.12040036569190293 + 0.6034526066822848 + ]; + }; + motion = true; + }; + }; + "lot" = { + enabled = true; + name = "lot"; + threshold = 0.8; + state_config = { + cameras = { + cam4.crop = [ + 0.2757899560295573 + 0.5156825410706086 + 0.4445399560295573 + 0.8156825410706086 + ]; + }; + motion = true; + }; + }; + }; + }; + }; + }; + + # Add SSL to frigate's nginx virtualHost + services.nginx.virtualHosts."frigate.${domain}" = mkIf nginx.enable { + useACMEHost = domain; + forceSSL = true; + locations."/go2rtc/" = { + proxyPass = "http://127.0.0.1:1984/"; + proxyWebsockets = true; + }; + }; + + # Frigate segment cache in RAM (reduces disk writes) + fileSystems."/var/cache/frigate" = { + device = "tmpfs"; + fsType = "tmpfs"; + options = [ "size=512M" "mode=0755" ]; + }; + + systemd.tmpfiles.rules = [ + # Set ownership after tmpfs mount + "d /var/cache/frigate 0750 frigate frigate -" + # Create log directories for Frigate API (NixOS uses journald, but API expects these) + "d /dev/shm/logs 0755 frigate frigate -" + "d /dev/shm/logs/frigate 0755 frigate frigate -" + "d /dev/shm/logs/nginx 0755 frigate frigate -" + "d /dev/shm/logs/go2rtc 0755 frigate frigate -" + ]; + + # Pipe journald logs to files for Frigate GUI + systemd.services.frigate-log-pipe = { + description = "Pipe logs to /dev/shm for Frigate GUI"; + wantedBy = [ "multi-user.target" ]; + after = [ "frigate.service" "go2rtc.service" "nginx.service" ]; + serviceConfig = { + Type = "simple"; + Restart = "always"; + ExecStart = pkgs.writeShellScript "frigate-log-pipe" '' + while true; do + ${pkgs.systemd}/bin/journalctl -u frigate -n 500 -o cat > /dev/shm/logs/frigate/current 2>/dev/null + ${pkgs.systemd}/bin/journalctl -u go2rtc -n 500 -o cat > /dev/shm/logs/go2rtc/current 2>/dev/null + ${pkgs.systemd}/bin/journalctl -u nginx -n 500 -o cat > /dev/shm/logs/nginx/current 2>/dev/null + chown frigate:frigate /dev/shm/logs/*/current + sleep 5 + done + ''; + }; + }; + + # Backup recordings/database + modules.system.backup = { + paths = [ "/var/lib/frigate" ]; + }; + }; +} diff --git a/system/machines/server/modules/home-manager/default.nix b/system/machines/server/modules/home-manager/default.nix new file mode 100644 index 0000000..c3a558b --- /dev/null +++ b/system/machines/server/modules/home-manager/default.nix @@ -0,0 +1,23 @@ +{ config, ... }: + +{ + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + home-manager.users.${config.user.name} = { + imports = [ + ../../../../../user + ../../../../../user/home.nix + ../../../../../user/modules + ]; + + home.stateVersion = "25.11"; + + # Machine-specific modules + modules.user = { + neovim.enable = false; + vim.enable = true; + tmux.enable = false; + utils.dev.enable = true; + }; + }; +} diff --git a/system/machines/server/modules/immich/default.nix b/system/machines/server/modules/immich/default.nix new file mode 100644 index 0000000..031336d --- /dev/null +++ b/system/machines/server/modules/immich/default.nix @@ -0,0 +1,57 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.immich; + nginx = config.modules.system.nginx; + domain = "ramos.codes"; + port = 2283; + +in +{ + options.modules.system.immich = { + enable = mkEnableOption "Immich Photo Server"; + }; + + config = mkIf cfg.enable { + # Bind mount from /data + systemd.tmpfiles.rules = [ + "d /data/immich 0750 immich immich -" + "d /data/postgresql 0750 postgres postgres -" + ]; + + fileSystems."/var/lib/immich" = { + device = "/data/immich"; + fsType = "none"; + options = [ "bind" ]; + }; + + fileSystems."/var/lib/postgresql" = { + device = "/data/postgresql"; + fsType = "none"; + options = [ "bind" ]; + }; + + services.immich = { + enable = true; + port = port; + host = "127.0.0.1"; + mediaLocation = "/var/lib/immich"; + machine-learning.enable = false; + }; + + modules.system.backup.paths = [ + "/var/lib/immich" + "/var/lib/postgresql" + ]; + + services.nginx.virtualHosts."photos.${domain}" = mkIf nginx.enable { + useACMEHost = domain; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString port}"; + proxyWebsockets = true; + }; + }; + }; +} diff --git a/system/machines/server/modules/nginx/default.nix b/system/machines/server/modules/nginx/default.nix new file mode 100644 index 0000000..7f508f0 --- /dev/null +++ b/system/machines/server/modules/nginx/default.nix @@ -0,0 +1,76 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.nginx; + domain = "ramos.codes"; + +in +{ + options.modules.system.nginx = { + enable = mkEnableOption "Nginx Reverse Proxy"; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = [ 80 443 ]; + + systemd.services.nginx.serviceConfig.LimitNOFILE = 65536; + + security.acme = { + acceptTerms = true; + defaults.email = config.user.email; + + certs."${domain}" = { + domain = "*.${domain}"; + dnsProvider = "namecheap"; + environmentFile = "/var/lib/acme/namecheap.env"; + group = "nginx"; + }; + }; + + services.sslh = { + enable = true; + listenAddresses = [ "0.0.0.0" ]; + port = 443; + settings = { + protocols = [ + { name = "ssh"; host = "127.0.0.1"; port = "22"; } + { name = "tls"; host = "127.0.0.1"; port = "4443"; } + ]; + }; + }; + + services.nginx = { + enable = true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + eventsConfig = "worker_connections 4096;"; + defaultSSLListenPort = 4443; + + # Catch-all default - friendly error for unknown subdomains + virtualHosts."_" = { + default = true; + useACMEHost = domain; + forceSSL = true; + locations."/" = { + return = "404 'Not Found: This subdomain does not exist.'"; + extraConfig = '' + add_header Content-Type text/plain; + ''; + }; + }; + + virtualHosts."test.${domain}" = { + useACMEHost = domain; + forceSSL = true; + locations."/" = { + return = "200 'nginx is working'"; + extraConfig = '' + add_header Content-Type text/plain; + ''; + }; + }; + }; + }; +} diff --git a/system/machines/server/modules/tor/default.nix b/system/machines/server/modules/tor/default.nix new file mode 100644 index 0000000..37c2e95 --- /dev/null +++ b/system/machines/server/modules/tor/default.nix @@ -0,0 +1,30 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.tor; + +in +{ + options.modules.system.tor = { + enable = mkEnableOption "Tor"; + }; + + config = mkIf cfg.enable { + services.tor = { + enable = true; + + client = { + enable = true; + # SOCKS proxy on 127.0.0.1:9050 + }; + + settings = { + ControlPort = 9051; + CookieAuthentication = true; + CookieAuthFileGroupReadable = true; + DataDirectoryGroupReadable = true; + }; + }; + }; +} diff --git a/system/machines/server/modules/webdav/default.nix b/system/machines/server/modules/webdav/default.nix new file mode 100644 index 0000000..1b90573 --- /dev/null +++ b/system/machines/server/modules/webdav/default.nix @@ -0,0 +1,69 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.webdav; + domain = "ramos.codes"; + +in +{ + options.modules.system.webdav = { + enable = mkEnableOption "WebDAV server for phone backups"; + + directory = mkOption { + type = types.path; + default = "/var/lib/seedvault"; + description = "Directory to store backups"; + }; + }; + + config = mkIf cfg.enable { + # Create backup directory + systemd.tmpfiles.rules = [ + "d ${cfg.directory} 0750 webdav webdav -" + ]; + + services.webdav = { + enable = true; + # Credentials in /var/lib/webdav/env: + # WEBDAV_USERNAME=seedvault + # WEBDAV_PASSWORD=your-secure-password + environmentFile = "/var/lib/webdav/env"; + settings = { + address = "127.0.0.1"; + port = 8090; + directory = cfg.directory; + behindProxy = true; + permissions = "CRUD"; # Create, Read, Update, Delete + users = [ + { + username = "{env}WEBDAV_USERNAME"; + password = "{env}WEBDAV_PASSWORD"; + } + ]; + }; + }; + + services.nginx.virtualHosts."backup.${domain}" = { + useACMEHost = domain; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8090"; + extraConfig = '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebDAV needs these + proxy_pass_request_headers on; + proxy_set_header Destination $http_destination; + + # Large file uploads for backups + client_max_body_size 0; + proxy_request_buffering off; + ''; + }; + }; + }; +} diff --git a/system/machines/server/system.nix b/system/machines/server/system.nix new file mode 100644 index 0000000..69bcf99 --- /dev/null +++ b/system/machines/server/system.nix @@ -0,0 +1,192 @@ +{ pkgs, lib, config, ... }: + +{ system.stateVersion = "25.11"; + + imports = [ ./modules ]; + + modules.system.sops.enable = true; + + # Camera RTSP credentials (used by frigate/go2rtc) + sops.secrets = let cameras = { sopsFile = ../../../secrets/system/cameras.yaml; }; in { + "RTSP_USER" = cameras; + "RTSP_PASS" = cameras; + }; + + modules.system = { + nginx.enable = true; + forgejo.enable = true; + frigate.enable = true; + immich.enable = true; + webdav.enable = false; + # bitcoin = { + # enable = true; + # electrum.enable = true; + # clightning.enable = true; + # }; + + backup = { + enable = true; + recipients = [ + "${config.user.keys.age.yubikey}" + "${config.machines.keys.desktop.ssh}" + ]; + paths = [ "/root/.config/rclone" ]; + destination = "gdrive:backups/server"; + schedule = "daily"; + keepLast = 2; + }; + }; + + users.users = { + ${config.user.name} = { + isNormalUser = true; + extraGroups = config.user.groups; + openssh.authorizedKeys.keys = [ + "${config.machines.keys.desktop.ssh}" + ]; + }; + }; + + nix = { + channel.enable = false; + package = pkgs.nixVersions.stable; + extraOptions = "experimental-features = nix-command flakes"; + settings = { + auto-optimise-store = true; + trusted-users = [ "${config.user.name}" ]; + }; + gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + }; + + boot.loader = { + timeout = 3; + grub = { + enable = true; + devices = [ "nodev" ]; + efiSupport = true; + configurationLimit = 5; + splashImage = null; + }; + + efi = { + canTouchEfiVariables = true; + }; + }; + + environment.systemPackages = with pkgs; [ + wget + git + vim + htop + ]; + + security.sudo = { + wheelNeedsPassword = false; + execWheelOnly = true; + }; + + time = { + timeZone = "America/New_York"; + hardwareClockInLocalTime = true; + }; + + services.timesyncd = lib.mkDefault { + enable = true; + servers = [ + "0.pool.ntp.org" + "1.pool.ntp.org" + "2.pool.ntp.org" + "3.pool.ntp.org" + ]; + }; + + i18n.defaultLocale = "en_US.UTF-8"; + + console.font = "Lat2-Terminus16"; + + networking = { + hostName = "server"; + useDHCP = false; + interfaces.enp2s0f0 = { + ipv4.addresses = [{ + address = "192.168.0.154"; + prefixLength = 24; + }]; + }; + # Camera network - isolated, no gateway + interfaces.enp2s0f1 = { + ipv4.addresses = [{ + address = "192.168.1.1"; + prefixLength = 24; + }]; + }; + defaultGateway = "192.168.0.1"; + nameservers = [ "1.1.1.1" "8.8.8.8" ]; + firewall = { + enable = true; + allowedTCPPorts = [ 22 ]; + allowedUDPPorts = [ 53 67 ]; # DNS + DHCP + extraCommands = '' + # Block camera MACs from forwarding (instant DROP, no timeouts) + iptables -A FORWARD -m mac --mac-source 00:1f:54:c2:d1:b1 -j DROP # cam4 + iptables -A FORWARD -m mac --mac-source 00:1f:54:b2:9b:1d -j DROP # cam2/cam3 + iptables -A FORWARD -m mac --mac-source 00:1f:54:a9:81:d1 -j DROP # cam1 + ''; + extraStopCommands = '' + iptables -D FORWARD -m mac --mac-source 00:1f:54:c2:d1:b1 -j DROP || true + iptables -D FORWARD -m mac --mac-source 00:1f:54:b2:9b:1d -j DROP || true + iptables -D FORWARD -m mac --mac-source 00:1f:54:a9:81:d1 -j DROP || true + ''; + }; + }; + + services.dnsmasq = { + enable = true; + settings = { + # All *.ramos.codes subdomains -> local server + address = "/.ramos.codes/192.168.0.154"; + # Except www, http, https and bare domain -> forward to upstream + server = [ + "/www.ramos.codes/1.1.1.1" + "/http.ramos.codes/1.1.1.1" + "/https.ramos.codes/1.1.1.1" + "/ramos.codes/1.1.1.1" + "1.1.1.1" + "8.8.8.8" + ]; + cache-size = 1000; + + # Camera network DHCP (isolated - no gateway = no internet) + interface = "enp2s0f1"; + bind-interfaces = true; + dhcp-range = "192.168.1.100,192.168.1.200,24h"; + + # Static DHCP reservations for cameras + dhcp-host = [ + "00:1f:54:c2:d1:b1,192.168.1.194,cam4" + "00:1f:54:b2:9b:1d,192.168.1.147,cam2" + "00:1f:54:a9:81:d1,192.168.1.167,cam1" + ]; + }; + }; + + services.fail2ban = { + enable = true; + maxretry = 5; + bantime = "1h"; + }; + + services.openssh = { + enable = true; + startWhenNeeded = true; + settings = { + X11Forwarding = false; + PasswordAuthentication = false; + PermitRootLogin = "no"; + }; + }; +} diff --git a/system/machines/wsl/default.nix b/system/machines/wsl/default.nix new file mode 100644 index 0000000..9fb4e88 --- /dev/null +++ b/system/machines/wsl/default.nix @@ -0,0 +1,15 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.nixos-wsl.nixosModules.wsl + (import ./modules/wsl) + inputs.home-manager.nixosModules.home-manager + { home-manager.sharedModules = [ inputs.sops-nix.homeManagerModules.sops ]; } + (import ./modules/home-manager) + ../../../user + ../../keys + ../../modules/sops + ./system.nix + ]; +} diff --git a/system/machines/wsl/modules/home-manager/default.nix b/system/machines/wsl/modules/home-manager/default.nix new file mode 100644 index 0000000..86de83f --- /dev/null +++ b/system/machines/wsl/modules/home-manager/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./home.nix + ]; +} diff --git a/system/machines/wsl/modules/home-manager/home.nix b/system/machines/wsl/modules/home-manager/home.nix new file mode 100644 index 0000000..dc8a221 --- /dev/null +++ b/system/machines/wsl/modules/home-manager/home.nix @@ -0,0 +1,24 @@ +{ config, ... }: + +{ + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + home-manager.users.${config.user.name} = { + imports = [ + ../../../../../user + ../../../../../user/home.nix + ../../../../../user/modules + ]; + + home.stateVersion = "23.11"; + + # Machine-specific modules + modules.user = { + utils = { + dev.enable = true; + email.enable = true; + irc.enable = true; + }; + }; + }; +} diff --git a/system/machines/wsl/modules/wsl/default.nix b/system/machines/wsl/modules/wsl/default.nix new file mode 100644 index 0000000..3cceea6 --- /dev/null +++ b/system/machines/wsl/modules/wsl/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./wsl.nix + ]; +} diff --git a/system/machines/wsl/modules/wsl/wsl.nix b/system/machines/wsl/modules/wsl/wsl.nix new file mode 100644 index 0000000..8bf5fb5 --- /dev/null +++ b/system/machines/wsl/modules/wsl/wsl.nix @@ -0,0 +1,20 @@ +{ config, lib, ... }: + +{ + imports = [ ../../../../../user ]; + + wsl = rec { + enable = true; + defaultUser = lib.mkDefault config.user.name; + useWindowsDriver = true; + + wslConf = { + user.default = lib.mkDefault defaultUser; + boot.command = "cd"; + network = { + hostname = "${config.networking.hostName}"; + generateHosts = true; + }; + }; + }; +} diff --git a/system/machines/wsl/system.nix b/system/machines/wsl/system.nix new file mode 100644 index 0000000..729213f --- /dev/null +++ b/system/machines/wsl/system.nix @@ -0,0 +1,75 @@ +{ pkgs, lib, config, ... }: + +{ + system.stateVersion = "23.11"; + boot.isContainer = true; + + users.users = { + ${config.user.name} = { + isNormalUser = true; + extraGroups = config.user.groups; + openssh.authorizedKeys.keys = [ + "${config.user.keys.ssh.yubikey}" + ]; + }; + }; + + nix = { + channel.enable = false; + package = pkgs.nixVersions.stable; + extraOptions = '' + experimental-features = nix-command flakes + ''; + settings = { + auto-optimise-store = true; + trusted-users = [ "${config.user.name}" ]; + }; + gc = { + automatic = true; + dates = "daily"; + options = "--delete-older-than 7d"; + }; + }; + + security.sudo = { + wheelNeedsPassword = false; + execWheelOnly = true; + }; + + time = { + timeZone = "America/New_York"; + }; + + i18n.defaultLocale = "en_US.UTF-8"; + + console = { + font = "Lat2-Terminus16"; + useXkbConfig = true; + }; + + networking = { + hostName = "wsl"; + useDHCP = lib.mkDefault true; + firewall = { + enable = true; + allowedTCPPorts = [ 22 80 443 ]; + }; + }; + + services = { + openssh = { + enable = true; + startWhenNeeded = true; + settings = { + X11Forwarding = false; + PasswordAuthentication = false; + }; + }; + timesyncd = lib.mkDefault { + enable = true; + servers = [ + "time.windows.com" + ]; + }; + }; +} diff --git a/system/modules/sops/default.nix b/system/modules/sops/default.nix new file mode 100644 index 0000000..e7c2240 --- /dev/null +++ b/system/modules/sops/default.nix @@ -0,0 +1,31 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.sops; + +in +{ + options.modules.system.sops = { enable = mkEnableOption "Enable sops-nix"; }; + + config = mkIf cfg.enable { + # Smartcard daemon for Yubikey (GPG, etc.) + services.pcscd.enable = true; + services.udev.packages = [ pkgs.yubikey-personalization ]; + + environment.systemPackages = with pkgs; [ + age + sops + ]; + + # Per-machine age key for system secrets (boot-time, unattended) + # This is the sops-nix default path + sops.age.keyFile = "/var/lib/sops-nix/key.txt"; + + # Symlink for root so `sudo sops` finds the key automatically + systemd.tmpfiles.rules = [ + "d /root/.config/sops/age 0700 root root -" + "L+ /root/.config/sops/age/keys.txt - - - - /var/lib/sops-nix/key.txt" + ]; + }; +} diff --git a/user/bookmarks/default.nix b/user/bookmarks/default.nix new file mode 100644 index 0000000..0aa60e1 --- /dev/null +++ b/user/bookmarks/default.nix @@ -0,0 +1,323 @@ +[ + { + name = "toolbar"; + toolbar = true; + bookmarks = [ + { + name = "ArchWiki"; + url = "https://wiki.archlinux.org/"; + tags = [ "dev" "linux" "docs" ]; + } + { + name = "NixOS"; + bookmarks = [ + { + name = "Nixpkgs"; + url = "https://search.nixos.org/packages"; + tags = [ "nix" "dev" "linux" ]; + keyword = "pkgs"; + } + { + name = "Home Manager"; + bookmarks = [ + { + name = "Home Manager Option Docs"; + url = "https://nix-community.github.io/home-manager/options.xhtml"; + tags = [ "nix" "dev" "linux" ]; + keyword = "hm"; + } + { + name = "Home Manager Option Search"; + url = "https://home-manager-options.extranix.com"; + tags = [ "nix" "dev" "linux" ]; + keyword = "hm"; + } + ]; + } + { + name = "Nix Docs"; + bookmarks = [ + { + name = "nix.dev"; + url = "https://nix.dev"; + tags = [ "nix" "dev" "docs" ]; + keyword = "nix"; + } + ]; + } + ]; + } + { + name = "Gentoo Wiki"; + url = "https://wiki.gentoo.org"; + tags = [ "dev" "linux" "docs" ]; + keyword = "gentoo"; + } + { + name = "Email"; + bookmarks = [ + { + name = "Gmail"; + url = "https://gmail.com"; + tags = [ "google" "email" ]; + keyword = "gmail"; + } + { + name = "ProtonMail"; + url = "https://mail.protonmail.com"; + tags = [ "email" "personal" "work" ]; + keyword = "email"; + } + ]; + } + { + name = "Work"; + bookmarks = [ + { + name = "Outlook"; + url = "https://outlook.office365.com"; + tags = [ "work" "email" "microsoft" ]; + keyword = "work"; + } + { + name = "Teams"; + url = "https://teams.microsoft.com"; + tags = [ "work" "microsoft" ]; + keyword = "teams"; + } + ]; + } + { + name = "Youtube"; + url = "https://youtube.com"; + tags = [ "social" "entertainment" "google" ]; + keyword = "youtube"; + } + { + name = "Substack"; + url = "https://substack.com"; + tags = [ "social" "blogging" "personal" ]; + keyword = "blog"; + } + { + name = "Amazon"; + url = "https://amazon.com"; + tags = [ "shopping" "amazon" ]; + keyword = "amazon"; + } + { + name = "Social"; + bookmarks = [ + { + name = "Twitter"; + url = "https://x.com"; + tags = [ "social" "forum" ]; + keyword = "x"; + } + { + name = "Reddit"; + url = "https://reddit.com"; + tags = [ "social" "forum" ]; + keyword = "reddit"; + } + { + name = "Twitch"; + url = "https://twitch.com"; + tags = [ "social" "entertainment" "amazon" ]; + keyword = "twitch"; + } + { + name = "Nostr"; + url = "https://primal.net"; + tags = [ "social" "nostr" "bitcoin" ]; + keyword = "nostr"; + } + ]; + } + { + name = "ChatGPT"; + url = "https://chat.openai.com"; + tags = [ "dev" "ai" "microsoft" ]; + keyword = "ai"; + } + { + name = "Dev"; + bookmarks = [ + { + name = "Github"; + url = "https://github.com"; + tags = [ "dev" "work" "personal" "microsoft" ]; + keyword = "git"; + } + { + name = "Gist"; + url = "https://gist.github.com"; + tags = [ "dev" "work" "personal" "microsoft" "blogging" ]; + keyword = "gist"; + } + { + name = "Stack Overflow"; + url = "https://stackoverflow.com"; + tags = [ "dev" "work" "forum" ]; + } + { + name = "Learning"; + bookmarks = [ + { + name = "Coding"; + bookmarks = [ + { + name = "Leetcode"; + url = "https://leetcode.com"; + } + { + name = "CodeWars"; + url = "https://codewars.com"; + } + ]; + } + { + name = "Projects"; + bookmarks = [ + { + name = "Linux From Scratch"; + url = "https://linuxfromscratch.org/lfs/view/stable/index.html"; + } + ]; + } + ]; + } + { + name = "Documentation"; + bookmarks = [ + { + name = "MDN"; + url = "https://developer.mozilla.org"; + tags = [ "dev" "docs" ]; + keyword = "mdn"; + } + { + name = "DevDocs"; + url = "https://devdocs.io"; + tags = [ "dev" "docs" ]; + keyword = "docs"; + } + { + name = "Linux Kernel"; + url = "https://docs.kernel.org"; + } + { + name = "References"; + bookmarks = [ + { + name = "ASCII Table"; + url = "https://asciitable.com"; + tags = [ "dev" ]; + keyword = "ascii"; + } + { + name = "Regex Cheat Sheet"; + url = "https://rexegg.com/regex-quickstart.php"; + tags = [ "dev" ]; + keyword = "regex"; + } + ]; + } + ]; + } + { + name = "Tools"; + bookmarks = [ + { + name = "GitBook"; + url = "https://gitbook.com"; + tags = [ "dev" "docs" ]; + } + { + name = "Namecheap"; + url = "https://namecheap.com"; + tags = [ "dev" "shopping" "hosting" ]; + } + { + name = "LetsEncrypt"; + url = "https://letsencrypt.com"; + tags = [ "dev" "hosting" ]; + } + { + name = "Gitea"; + url = "https://gitea.com"; + tags = [ "dev" "hosting" ]; + } + { + name = "Hosting"; + bookmarks = [ + { + name = "DigitalOcean"; + url = "https://digitalocean.com"; + tags = [ "dev" "hosting" ]; + } + { + name = "Supabase"; + url = "https://supabase.com"; + tags = [ "dev" "hosting" ]; + } + { + name = "Vercel"; + url = "https://vercel.com"; + tags = [ "dev" "hosting" ]; + } + { + name = "AWS"; + url = "https://aws.amazon.com"; + tags = [ "dev" "hosting" ]; + } + { + name = "Azure"; + url = "https://azure.microsoft.com"; + tags = [ "dev" "hosting" ]; + } + { + name = "Firebase"; + url = "https://firebase.google.com"; + tags = [ "dev" "hosting" ]; + } + ]; + } + ]; + } + ]; + } + { + name = "Financials"; + bookmarks = [ + { + name = "Fidelity"; + url = "https://fidelity.com"; + tags = [ "banking" ]; + keyword = "bank"; + } + { + name = "Chase"; + url = "https://chase.com"; + tags = [ "banking" ]; + } + { + name = "Wells Fargo"; + url = "https://wellsfargo.com"; + tags = [ "banking" ]; + } + { + name = "Crapto"; + bookmarks = [ + { + name = "Coinbase"; + url = "https://coinbase.com"; + tags = [ "banking" ]; + } + ]; + } + ]; + } + ]; + } +] diff --git a/user/config/keys/age/yubikey b/user/config/keys/age/yubikey new file mode 100644 index 0000000..93be935 --- /dev/null +++ b/user/config/keys/age/yubikey @@ -0,0 +1 @@ +AGE-PLUGIN-YUBIKEY-1C8CXVQVZUK7AV2CFWEKFP diff --git a/user/default.nix b/user/default.nix new file mode 100644 index 0000000..bc4ddfa --- /dev/null +++ b/user/default.nix @@ -0,0 +1,20 @@ +{ lib, pkgs, ... }: + +with lib; +{ + options = { + user = mkOption { + description = "User Configurations"; + type = types.attrs; + default = with pkgs; rec { + name = "bryan"; + email = "bryan@ramos.codes"; + shell = bash; + keys = import ./keys { inherit lib; }; + + groups = [ "wheel" "networkmanager" "home-manager" "input" ]; + bookmarks = import ./bookmarks; + }; + }; + }; +} diff --git a/user/home.nix b/user/home.nix new file mode 100644 index 0000000..6b93564 --- /dev/null +++ b/user/home.nix @@ -0,0 +1,40 @@ +{ lib, pkgs, config, ... }: + +let + pass = pkgs.pass.withExtensions (exts: with exts; [ + pass-audit + pass-otp + pass-update + pass-tomb + ]); + +in +{ + programs.home-manager.enable = true; + + home.username = config.user.name; + home.homeDirectory = "/home/${config.user.name}"; + + # Essential packages for all users + home.packages = with pkgs; [ + pass + wget curl fastfetch fd + unzip zip rsync + calc calcurse + age + sops + ]; + + programs.bash.shellAliases = { + cal = "${pkgs.calcurse}/bin/calcurse"; + calendar = "${pkgs.calcurse}/bin/calcurse"; + }; + + # Default modules for all users (machines can override with mkForce false) + modules.user = { + bash.enable = lib.mkDefault true; + git.enable = lib.mkDefault true; + neovim.enable = lib.mkDefault true; + security.gpg.enable = lib.mkDefault true; + }; +} diff --git a/user/keys/age/README.md b/user/keys/age/README.md new file mode 100644 index 0000000..92284a8 --- /dev/null +++ b/user/keys/age/README.md @@ -0,0 +1,3 @@ +# Age Keys + +yubikey.pub.key - Cold storage backup for age encryption diff --git a/user/keys/age/yubikey.pub.key b/user/keys/age/yubikey.pub.key new file mode 100644 index 0000000..559bc52 --- /dev/null +++ b/user/keys/age/yubikey.pub.key @@ -0,0 +1 @@ +age1yubikey1qfapxqnnkh92zkgayzzm9n0gtpkwaqcvrzy4d4xa4rxnjua8vjhy72hh9r9 diff --git a/user/keys/default.nix b/user/keys/default.nix new file mode 100644 index 0000000..e3f3aaf --- /dev/null +++ b/user/keys/default.nix @@ -0,0 +1,33 @@ +{ lib }: + +with builtins; +let + extractName = filename: + let + # Remove .key extension + noKey = lib.removeSuffix ".key" filename; + # Remove .pub/.priv/.public/.private markers + noMarkers = replaceStrings + [ ".pub" ".priv" ".public" ".private" ] + [ "" "" "" "" ] + noKey; + in noMarkers; + + constructKeys = dir: ( + listToAttrs ( + map (subdir: { + name = subdir; + value = listToAttrs ( + map (file: { + name = extractName file; + value = readFile "${dir}/${subdir}/${file}"; + }) (filter (file: + (readDir "${dir}/${subdir}").${file} == "regular" && + lib.hasSuffix ".key" file + ) (attrNames (readDir "${dir}/${subdir}"))) + ); + }) (filter (node: (readDir dir).${node} == "directory") (attrNames (readDir dir))) + ) + ); +in + constructKeys ./. diff --git a/user/keys/pgp/README.md b/user/keys/pgp/README.md new file mode 100644 index 0000000..50fb051 --- /dev/null +++ b/user/keys/pgp/README.md @@ -0,0 +1,5 @@ +# PGP Keys + +yubikey.pub.key - +work.pub.key -> bryan.ramos@concurrent-rt.com +ccur.pub.key -> ? diff --git a/user/keys/pgp/ccur.pub.key b/user/keys/pgp/ccur.pub.key new file mode 100755 index 0000000..3ddf45c --- /dev/null +++ b/user/keys/pgp/ccur.pub.key @@ -0,0 +1,53 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +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 +uWsnbjV0Rg== +=56r2 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/user/keys/pgp/work.pub.key b/user/keys/pgp/work.pub.key new file mode 100755 index 0000000..31e14fb --- /dev/null +++ b/user/keys/pgp/work.pub.key @@ -0,0 +1,121 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +Vv2c5XcFneMcDVBQ28VlwQ2fbIoDokz3Iw== +=my/o +-----END PGP PUBLIC KEY BLOCK----- diff --git a/user/keys/pgp/yubikey.pub.key b/user/keys/pgp/yubikey.pub.key new file mode 100644 index 0000000..a15a521 --- /dev/null +++ b/user/keys/pgp/yubikey.pub.key @@ -0,0 +1,109 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +1hbflYLyMYo1BxGR6bPS9gJA2w== +=5uun +-----END PGP PUBLIC KEY BLOCK----- diff --git a/user/keys/ssh/README.md b/user/keys/ssh/README.md new file mode 100644 index 0000000..2ebbe16 --- /dev/null +++ b/user/keys/ssh/README.md @@ -0,0 +1,5 @@ +# SSH Keys + +yubikey.pub.key -> PGP derived from `pgp.yubikey.pub.key` +work.pub.key - ? +graphone.pub.key -> For Android `pass` diff --git a/user/keys/ssh/graphone.pub.key b/user/keys/ssh/graphone.pub.key new file mode 100644 index 0000000..d07e510 --- /dev/null +++ b/user/keys/ssh/graphone.pub.key @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJM1HutPcWXdeTaAXY7ha8SlgeZFtLJGwNa3Kd/DL/R38fq5+fkh3iCoHgv+iiKcordtVTMhbOsHhz3H+Jm274c= diff --git a/user/keys/ssh/work.pub.key b/user/keys/ssh/work.pub.key new file mode 100644 index 0000000..c4b3a55 --- /dev/null +++ b/user/keys/ssh/work.pub.key @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDXYU5c7AUD5tQQdpzQ73yy3ti3R7dArZ+f/wETN7L2Z2Hw6zo6hDid9/Q4yxdgM/FlTj/Ok2DHBWqxJsEe3S4shwsT9l2qJatjdcUK6zH3/0nFPxGYIaByj87aZ+5dwMoWNGlioPWciUdKeovvau1PwvdBxPabHHap6nwC9yPaSIVbZi4GgYv/zEvOB4LVYLuxLqr0pPdMNz1ddjmjsQCq5alC33jSZWkABERw3GlF02dNHbUq6cZlFq9BudbNWBQ8zFgj/C8amK4DHUSeU8w+ckTmO5PjDjINOnFr8kytDap+/5AQ6kr618evJ2JCwnBj6txb3SVGhcvn3/DJjf2H7flVhZEWIMEMu7452SXfz9mxp3Vu3UMJkjHUj6Lxl302M318k9j+w1fa8EHO7OQHQZajNKrEP5/UK2CDfpP2KIybX5HnEqBcEqoSKhRt7ytNX6VGzURk3/mmk9L+An5z7ve+zqlgNOA8uaIoebB4476+n5pGiNIedO3FRjPofEidYjf5NTZ9YDpqFc5KbfbhduuP63G/kqmgTxXMuTsWINY2xKEc0BPnlEGfezMN+eQpwWINOUxW1ZEk3OYMvC91EndbVwxVbm3aze9894T3+wVTipJ88xARCQeQpu1eaWDSaNduD+8LAouQiTA4whM+jBEeQoWZe6Wf6W4tBeCZ6Q== diff --git a/user/keys/ssh/yubikey.pub.key b/user/keys/ssh/yubikey.pub.key new file mode 100644 index 0000000..a840349 --- /dev/null +++ b/user/keys/ssh/yubikey.pub.key @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDl4895aB9P5p/lp8Hq5rHun4clvhyTSHFi3U2d6OOBoW5Fm+VcQnW/xbjmCBsXk5BdiowsBxQhwnzdfz/KJL7J5RobomUEaVRwb9UwT88eJveLp14BG8j2J3SjfyhrCX+4jkPx0bPQk1HGcuYY+tPEXf1q/ps88Dhu0CARBIzYQOTYY6b1qWzxpDoFZGHjKG8g5iY6FIu65yKKvvVy1f8IgZ3l3IpwBWVamxgkTcYY0QYSrmzo1n7TXxwrWbvenAqBsQ0cBPs+gVa3uIr+1TJl0Az5SElBVGu3LvUdlk58trtPUj6TQR3YUkg7Vjll7WHOdqhux5ZQNhjkOsHerf0Tw86e6cEzgeTuIbQHIb0LcsUunwKcuh2+au7RO599cvHn0+xZE5MZBxloDDaJ3JsiliM8kyPP/U3ERj03cWLW7BqbT+sfjAOl21RCzk0iQxk1wt/8VmtCr9Adv7IyrtaYvf/bwRP+g+9ldmzKGt8Mdb605uVzZ70H/LLm17f40Te+QHaex5by/6p6cuwEEZtgIg53Wpglu0rA6UxrBfQEHKl/Jt3FLeE0mnEyYkkR2MnHNtyWRIXtuqYZMAm2Ub1pFHH7jQV1gGiDVTw6a2eIwK21a/hXtRjFUpFd1nB1n+KNfJBE4zT3wm3Ud7mKw/6rWnoRyhYZvGXkFdp+iEs49Q== diff --git a/user/modules/bash/bash b/user/modules/bash/bash new file mode 160000 index 0000000..a90d892 --- /dev/null +++ b/user/modules/bash/bash @@ -0,0 +1 @@ +Subproject commit a90d89277c4bbd363d6929f434eef633bea439f5 diff --git a/user/modules/bash/default.nix b/user/modules/bash/default.nix new file mode 100644 index 0000000..a1420a7 --- /dev/null +++ b/user/modules/bash/default.nix @@ -0,0 +1,32 @@ +{ lib, config, ... }: + +with lib; +let + cfg = config.modules.user.bash; + +in +{ options.modules.user.bash = { enable = mkEnableOption "user.bash"; }; + config = mkIf cfg.enable { + programs.bash = { + enable = true; + initExtra = "source ~/.config/bash/bashrc"; + }; + + home.file.".config/bash" = { + source = ./bash; + recursive = true; + }; + + programs = { + ripgrep.enable = true; + eza = { + enable = true; + enableBashIntegration = true; + enableFishIntegration = false; + enableZshIntegration = false; + enableNushellIntegration = false; + enableIonIntegration = false; + }; + }; + }; +} diff --git a/user/modules/default.nix b/user/modules/default.nix new file mode 100644 index 0000000..dc0f32a --- /dev/null +++ b/user/modules/default.nix @@ -0,0 +1,34 @@ +let + mkModules = dir: isRoot: + let + entries = builtins.readDir dir; + names = builtins.attrNames entries; + + excludedDirs = [ "config" "scripts" ]; + isSubmodule = path: + builtins.pathExists "${path}/.git" && + builtins.readFileType "${path}/.git" == "regular"; + isModuleDir = path: + builtins.pathExists path && + builtins.readFileType path == "directory" && + !(builtins.elem (builtins.baseNameOf path) excludedDirs) && + !(isSubmodule path); + isModule = file: file == "default.nix"; + + in + builtins.concatMap (name: + let + path = "${dir}/${name}"; + in + if isModuleDir path then + mkModules path false + else if isModule name && !isRoot then + [ dir ] + else + [] + ) names; + +in +{ + imports = mkModules ./. true; +} diff --git a/user/modules/git/default.nix b/user/modules/git/default.nix new file mode 100644 index 0000000..26baea5 --- /dev/null +++ b/user/modules/git/default.nix @@ -0,0 +1,32 @@ +{ lib, pkgs, config, ... }: + +with lib; +let + cfg = config.modules.user.git; + +in +{ options.modules.user.git = { enable = mkEnableOption "user.git"; }; + config = mkIf cfg.enable { + programs = { + git = { + enable = true; + }; + gh = { + enable = true; + settings.git_protocol = "ssh"; + }; + }; + + home = { + packages = with pkgs; [ + git-crypt + ]; + file.".config/git" = { + source = ./git; + recursive = true; + }; + }; + + programs.bash.initExtra = import ./scripts/cdg.nix; + }; +} diff --git a/user/modules/git/git b/user/modules/git/git new file mode 160000 index 0000000..d394ee0 --- /dev/null +++ b/user/modules/git/git @@ -0,0 +1 @@ +Subproject commit d394ee0594e8b1162f05547c3f7da817b6fcb62a diff --git a/user/modules/git/scripts/cdg.nix b/user/modules/git/scripts/cdg.nix new file mode 100644 index 0000000..00f7238 --- /dev/null +++ b/user/modules/git/scripts/cdg.nix @@ -0,0 +1,24 @@ +'' +function cdg() { + if [[ $1 == "--help" ]]; then + echo "A simple utility for navigating to the root of a git repo" + return 0 + fi + + if [[ -n "$1" ]]; then + echo "Invalid command: $1. Try 'cdg --help'." + return 1 + fi + + local root_dir + root_dir=$(git rev-parse --show-toplevel 2>/dev/null) + local git_status=$? + + if [ $git_status -ne 0 ]; then + echo "Error: Not a git repo." + return 1 + fi + + cd "$root_dir" +} +'' diff --git a/user/modules/gui/alacritty/config/alacritty.nix b/user/modules/gui/alacritty/config/alacritty.nix new file mode 100644 index 0000000..b396d7c --- /dev/null +++ b/user/modules/gui/alacritty/config/alacritty.nix @@ -0,0 +1,83 @@ +{ config, ... }: + +let +hyprland = config.modules.user.gui.wm.hyprland; + +in +{ + scrolling = { + history = 10000; + multiplier = 3; + }; + + window = { + opacity = if hyprland.enable then 0.9 else 1; + }; + + keyboard.bindings = [ + { + key = "Enter"; + mods = "Alt | Shift"; + action = "SpawnNewInstance"; + } + ]; + + colors = { + primary = { + background = "#000000"; + foreground = "#cdd6f4"; + }; + + normal = { + black = "#1e2127"; + red = "#e06c75"; + green = "#98c379"; + yellow = "#d19a66"; + blue = "#61afef"; + magenta = "#c678dd"; + cyan = "#56b6c2"; + white = "#abb2bf"; + }; + + bright = { + black = "#5c6370"; + red = "#e06c75"; + green = "#98c379"; + yellow = "#d19a66"; + blue = "#61afef"; + magenta = "#c678dd"; + cyan = "#56b6c2"; + white = "#ffffff"; + }; + }; + + font = { + size = 12; + normal = { + family = "Terminess Nerd Font Propo"; + style = "Regular"; + }; + + bold = { + family = "Terminess Nerd Font Propo"; + style = "Bold"; + }; + + italic = { + family = "Terminess Nerd Font Propo"; + style = "Italic"; + }; + + bold_italic = { + family = "Terminess Nerd Font Propo"; + style = "Bold Italic"; + }; + }; + + + #cursor = { + # shape = "Block"; + # blinking = "Always"; + # blink_interval = 750; + #}; +} diff --git a/user/modules/gui/alacritty/default.nix b/user/modules/gui/alacritty/default.nix new file mode 100644 index 0000000..290e19f --- /dev/null +++ b/user/modules/gui/alacritty/default.nix @@ -0,0 +1,15 @@ +{ lib, config, ... }: + +with lib; +let + cfg = config.modules.user.gui.alacritty; + +in +{ options.modules.user.gui.alacritty = { enable = mkEnableOption "Enable Alacritty terminal"; }; + config = mkIf cfg.enable { + programs.alacritty = { + enable = true; + settings = import ./config/alacritty.nix { inherit config; }; + }; + }; +} diff --git a/user/modules/gui/browsers/chromium/default.nix b/user/modules/gui/browsers/chromium/default.nix new file mode 100644 index 0000000..bf9c59c --- /dev/null +++ b/user/modules/gui/browsers/chromium/default.nix @@ -0,0 +1,53 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.gui.browser.chromium; + +in +{ options.modules.user.gui.browser.chromium = { enable = mkEnableOption "Enable Chromium browser"; }; + config = mkIf cfg.enable { + programs = { + chromium = rec { + enable = true; + package = pkgs.ungoogled-chromium; + extensions = + let + vrs = package.version; + in + [ + rec { + id = "cjpalhdlnbpafiamejdnhcphjbkeiagm"; + crxPath = builtins.fetchurl { + url = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=${vrs}&acceptformat=crx2,crx3&x=id%3D${id}%26uc"; + name = "ublock_${version}.crx"; + sha256 = "0ycnkna72n969crgxfy2lc1qbndjqrj46b9gr5l9b7pgfxi5q0ll"; + }; + version = "1.62.0"; + } + rec { + id = "dbepggeogbaibhgnhhndojpepiihcmeb"; + crxPath = builtins.fetchurl { + url = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=${vrs}&acceptformat=crx2,crx3&x=id%3D${id}%26uc"; + name = "vimium_${version}.crx"; + sha256 = "0m8xski05w2r8igj675sxrlkzxlrl59j3a7m0r6c8pwcvka0r88d"; + }; + version = "2.1.2"; + } + rec { + id = "naepdomgkenhinolocfifgehidddafch"; + crxPath = builtins.fetchurl { + url = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=${vrs}&acceptformat=crx2,crx3&x=id%3D${id}%26uc"; + name = "browserpass_${version}.crx"; + sha256 = "074sc9hxh7vh5j79yjhsrnhb5k4dv3bh5vip0jr30hkkni7nygbd"; + }; + version = "3.9.0"; + } + ]; + }; + browserpass = { + enable = true; + }; + }; + }; +} diff --git a/user/modules/gui/browsers/firefox/default.nix b/user/modules/gui/browsers/firefox/default.nix new file mode 100644 index 0000000..c8069c3 --- /dev/null +++ b/user/modules/gui/browsers/firefox/default.nix @@ -0,0 +1,338 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.gui.browser.firefox; + passffHost= { + home.packages = with pkgs; [ + passff-host + ]; + home.file = { + ".mozilla/native-messaging-hosts/passff.json" = { + text = '' + { + "name": "passff", + "description": "Host for communicating with zx2c4 pass", + "path": "${pkgs.passff-host}/share/passff-host/passff.py", + "type": "stdio", + "allowed_extensions": [ "passff@invicem.pro" ] + } + ''; + }; + }; + assertions = + let + pinentry = config.services.gpg-agent.pinentry.package; + in + [ + { + assertion = pinentry != pkgs.pinentry-curses || pinentry != pkgs.pinentry-tty; + message = "Firefox plugin passff requires graphical pinentry"; + } + ]; + }; + +in +{ + options.modules.user.gui.browser.firefox = { enable = mkEnableOption "Enable Firefox browser"; }; + config = mkIf cfg.enable (passffHost // { + programs.firefox = { + enable = true; + profiles = { + "${config.user.name}" = { + isDefault = true; + #bookmarks = config.user.bookmarks; + + extensions = { + packages = with pkgs.nur.repos.rycee.firefox-addons; [ + ublock-origin + tridactyl + #darkreader + tampermonkey + clearurls + passff + multi-account-containers + ]; + }; + + search = { + force = true; + default = "google"; + engines = { + "Startpage" = { + urls = [{ + template = "https://www.startpage.com/sp/search?q={searchTerms}"; + }]; + icon = "https://www.startpage.com/sp/cdn/favicons/favicon--default.ico"; + }; + }; + }; + + containersForce = true; + containers = { + Banking = { + color = "green"; + icon = "dollar"; + id = 1; + }; + Personal = { + color = "orange"; + icon = "fingerprint"; + id = 2; + }; + Work = { + color = "yellow"; + icon = "briefcase"; + id = 3; + }; + Personal_Work = { + color = "turquoise"; + icon = "briefcase"; + id = 4; + }; + Social = { + color = "red"; + icon = "chill"; + id = 5; + }; + Shopping = { + color = "purple"; + icon = "cart"; + id = 6; + }; + Google = { + color = "pink"; + icon = "vacation"; + id = 7; + }; + }; + + settings = { + "layout.spellcheckDefault" = 0; + "ui.key.menuAccessKeyFocuses" = false; + "signon.rememberSignons" = false; + "extensions.pocket.enabled" = false; + "extensions.autoDisableScopes" = 0; + + # May break extensions due to Nix + "extensions.enabledScopes" = 5; + + # May break stuff but increases privacy + #"extensions.webextensions.restrictedDomains" = ""; + #"privacy.resistFingerprinting" = true; + #"privacy.resistFingerprinting.letterboxing" = true; + #"privacy.resistFingerprinting.block_mozAddonManager" = true; + + "browser.startup.homepage_override.mstone" = "ignore"; + + "browser.aboutConfig.showWarning" = false; + "browser.startup.page" = 0; + "browser.formfill.enable" = false; + "places.history.enabled" = false; + + "browser.urlbar.suggest.history" = false; + "browser.urlbar.suggest.topsites" = false; + "browser.urlbar.sponsoredTopSites" = false; + "browser.urlbar.autoFill" = false; + "browser.urlbar.suggest.pocket" = false; + "browser.urlbar.suggest.quicksuggest.nonsponsored" = false; + "browser.urlbar.suggest.quicksuggest.sponsored" = false; + "browser.toolbars.bookmarks.showOtherBookmarks" = false; + "browser.aboutwelcome.showModal" = false; + "browser.migrate.content-modal.about-welcome-behavior" = ""; + + "browser.newtabpage.enabled" = false; + "browser.newtabpage.activity-stream.showSponsored" = false; + "browser.newtabpage.activity-stream.showSponsoredTopSites" = false; + "browser.newtabpage.activity-stream.default.sites" = ""; + + "extensions.getAddons.showPane" = false; + "extensions.htmlaboutaddons.recommendations.enabled" = false; + "browser.discovery.enabled" = false; + "browser.shopping.experience2023.enabled" = false; + + "datareporting.policy.dataSubmissionEnabled" = false; + "datareporting.healthreport.uploadEnabled" = false; + + "toolkit.telemetry.unified" = false; + "toolkit.telemetry.enabled" = false; + "toolkit.telemetry.server" = ""; + "toolkit.telemetry.archive.enabled" = false; + "toolkit.telemetry.newProfilePing.enabled" = false; + "toolkit.telemetry.shutdownPingSender.enabled" = false; + "toolkit.telemetry.updatePing.enabled" = false; + "toolkit.telemetry.bhrPing.enabled" = false; + "toolkit.telemetry.firstShutdownPing.enabled" = false; + "toolkit.telemetry.coverage.opt-out" = false; + "toolkit.coverage.endpoint.base" = ""; + + "browser.newtabpage.activity-stream.feeds.telemetry" = false; + "browser.newtabpage.activity-stream.telemetry" = false; + + "app.shield.optoutstudies.enabled" = false; + "app.normandy.enabled" = false; + "app.normandy.api_url" = ""; + + "breakpad.reportURL" = false; + "browser.tabs.crashReporting.sendReport" = false; + "browser.crashReports.unsubmittedCheck.autoSubmit2" = false; + + "captivedetect.canonicalURL" = ""; + "network.captive-portal-service.enabled" = false; + "network.connectivity-service.enabled" = false; + + "browser.safebrowsing.downloads.remote.enabled" = false; + + "network.prefetch-next" = false; + "network.dns.disablePrefetch" = true; + "network.predictor.enabled" = false; + "network.predictor.enable-prefetch" = false; + "network.http.speculative-parallel-limit" = 0; + "network.http.speculativeConnect.enabled" = false; + + "network.proxy.sock_remote_dns" = true; + "network.file.disable_unc_paths" = true; + "network.gio.supported-protocols" = ""; + + "browser.urlbar.speculativeConnect.enabled" = false; + "browser.search.suggest.enabled" = false; + "browser.urlbar.suggest.searches" = false; + + "browser.urlbar.clipboard.featureGate" = false; + "browser.urlbar.richSuggestions.featureGate" = false; + "browser.urlbar.trending.featureGate" = false; + "browser.urlbar.addons.featureGate" = false; + "browser.urlbar.pocket.featureGate" = false; + "browser.urlbar.weather.featureGate" = false; + "browser.urlbar.yelp.featureGate" = false; + "browser.urlbar.suggest.engines" = false; + + "signon.autofillForms" = false; + "signon.formlessCapture.enabled" = false; + + "network.auth.subresource-http-auth-allow" = 1; + + "browser.privatebrowsing.forceMediaMemoryCache" = true; + "media.memory_cache_max_size" = 65536; + "browser.sessionstore.privacy_level" = 2; + "browser.shell.shortcutFavicons" = false; + + "security.ssl.require_safe_negotiation" = true; + "security.tls.enable_0rtt_data" = false; + "security.OCSP.enabled" = true; + "security.OCSP.require" = true; + + "security.cert_pinning.enforcement_level" = 2; + "security.remote_settings.crlite_filters.enabled" = true; + "security.pki.crlite_mode" = 2; + + "dom.security.https_only_mode" = true; + "dom.security.https_only_mode_send_http_background_request" = false; + + "security.ssl.treat_unsafe_negotiation_as_broken" = true; + "browser.xul.error_pages.expert_bad_cert" = true; + + "network.http.referer.XOriginTrimmingPolicy" = 2; + + "privacy.userContext.enabled" = true; + "privacy.userContext.ui.enabled" = true; + + "media.peerconnection.ice.proxy_only_if_behind_proxy" = true; + "media.peerconnection.ice.default_address_only" = true; + + "dom.disable_window_move_resize" = true; + + "browser.download.start_downloads_in_tmp_dir" = false; + "browser.helperApps.deleteTempFileOnExit" = true; + "browser.uitour.enabled" = false; + + "devtools.debugger.remote-enabled" = false; + "permissions.manager.defaultsUrl" = ""; + "webchannel.allowObject.urlWhitelist" = ""; + "network.IDN_show_punycode" = true; + + "pdfjs.disabled" = false; + "pdfjs.enableScripting" = false; + + "browser.tabs.searchclipboardfor.middleclick" = false; + "browser.contentanalysis.default_allow" = false; + + "browser.download.useDownloadDir" = true; + "browser.download.alwaysOpenPanel" = true; + "browser.download.manager.addToRecentDocs" = false; + "browser.download.always_ask_before_handling_new_types" = true; + "extensions.postDownloadThirdPartyPrompt" = true; + + "browser.contentblocking.category" = "strict"; + + "privacy.sanitize.sanitizeOnShutdown" = true; + "privacy.clearOnShutdown.cache" = true; + "privacy.clearOnShutdown_v2.cache" = true; + "privacy.clearOnShutdown.downloads" = true; + "privacy.clearOnShutdown.formdata" = true; + "privacy.clearOnShutdown.history" = true; + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads" = true; + + "privacy.clearOnShutdown.cookies" = false; + "privacy.clearOnShutdown.offlineApss" = true; + "privacy.clearOnShutdown.sessions" = true; + "privacy.clearOnShutdown_v2.cookiesAndStorage" = false; + + "privacy.clearSiteData.cache" = true; + "privacy.clearSiteData.cookiesAndStorage" = false; + "privacy.clearSiteData.historyFormDataAndDownloads" = true; + + "privacy.cpd.cache" = true; + "privacy.clearHistory.cache" = true; + "privacy.cpd.formdata" = true; + "privacy.cpd.history" = true; + "privacy.clearHistory.historyFormDataAndDownloads" = true; + "privacy.cpd.cookies" = false; + "privacy.cpd.sessions" = true; + "privacy.cpd.offlineApps" = false; + "privacy.clearHistory.cookiesAndStorage" = false; + + "privacy.sanitize.timeSpan" = 0; + + "privacy.window.maxInnerWidth" = 1600; + "privacy.window.maxInnerHeight" = 900; + + "privacy.spoof_english" = 1; + + "browser.display.use_system_colors" = false; + "widget.non-native-theme.enabled" = true; + + "browser.link.open_newwindow" = 3; + "browser.link.open_newwindow.restriction" = 0; + "browser.chrome.site_icons" = false; + "browser.download.forbid_open_with" = true; + + "extensions.blocklist.enabled" = true; + "network.http.referer.spoofSource" = false; + "security.dialog_enable_delay" = 1000; + "privacy.firstparty.isolate" = false; + "extensions.webcompat.enable_shims" = true; + "security.tls.version.enable-deprecated" = false; + "extensions.webcompat-reporter.enabled" = false; + "extensions.quarantinedDomains.enabled" = true; + + "media.videocontrols.picture-in-picture.enabled" = false; + + # VA-API hardware video acceleration (NVIDIA) + "media.ffmpeg.vaapi.enabled" = true; + "media.rdd-ffmpeg.enabled" = true; + "media.av1.enabled" = false; # GTX 1650 doesn't support AV1 decode + "gfx.x11-egl.force-enabled" = true; + }; + }; + }; + policies = { + WebsiteFilter = { + Block = [ + "*://*.pokemonshowdown.com/*" + ]; + }; + }; + }; + }); +} diff --git a/user/modules/gui/corn/default.nix b/user/modules/gui/corn/default.nix new file mode 100644 index 0000000..712c6fb --- /dev/null +++ b/user/modules/gui/corn/default.nix @@ -0,0 +1,36 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.gui.corn; + +in +{ options.modules.user.gui.corn = { enable = mkEnableOption "Enable Bitcoin client applications"; }; + config = mkIf cfg.enable { + home.packages = with pkgs; [ + #trezor-suite + #trezorctl + #trezord + + sparrow + ]; + + #systemd.user.services = { + # trezord = { + # Unit = { + # Description = "Trezor Bridge"; + # After = [ "network.target" ]; + # Wants = [ "network.target" ]; + # PartOf = [ "graphical-session.target" ]; + # }; + # Service = { + # ExecStart = "${pkgs.trezord}/bin/trezord-go"; + # Restart = "always"; + # }; + # Install = { + # WantedBy = [ "default.target" ]; + # }; + # }; + #}; + }; +} diff --git a/user/modules/gui/default.nix b/user/modules/gui/default.nix new file mode 100644 index 0000000..6b9286c --- /dev/null +++ b/user/modules/gui/default.nix @@ -0,0 +1,30 @@ +{ lib, config, ... }: + +let + programs = config.programs; + + defaultBrowser = + if programs.firefox.enable then "firefox.desktop" + else if programs.brave.enable then "brave-browser.desktop" + else if programs.chromium.enable then "chromium.desktop" + else null; + + types = [ + "text/html" "application/xhtml+xml" + "x-scheme-handler/http" "x-scheme-handler/https" + "application/pdf" + "image/png" "image/jpeg" "image/jpg" "image/gif" + "image/webp" "image/avif" "image/bmp" "image/tiff" "image/svg+xml" + "video/mp4" "video/webm" "video/mkv" "video/avi" + "video/x-matroska" "video/quicktime" + ]; + +in +{ + xdg.mimeApps = lib.mkIf (defaultBrowser != null) { + enable = true; + defaultApplications = builtins.listToAttrs ( + map (t: { name = t; value = [ defaultBrowser ]; }) types + ); + }; +} diff --git a/user/modules/gui/dev/design/default.nix b/user/modules/gui/dev/design/default.nix new file mode 100644 index 0000000..392da8f --- /dev/null +++ b/user/modules/gui/dev/design/default.nix @@ -0,0 +1,14 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.gui.dev.design; + +in +{ options.modules.user.gui.dev.design = { enable = mkEnableOption "Enable design tools"; }; + config = mkIf cfg.enable { + home.packages = with pkgs; [ + penpot-desktop + ]; + }; +} diff --git a/user/modules/gui/dev/pcb/default.nix b/user/modules/gui/dev/pcb/default.nix new file mode 100644 index 0000000..59d89d9 --- /dev/null +++ b/user/modules/gui/dev/pcb/default.nix @@ -0,0 +1,16 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.gui.dev.pcb; + +in +{ options.modules.user.gui.dev.pcb = { enable = mkEnableOption "Enable PCB development suite"; }; + config = mkIf cfg.enable { + home.packages = with pkgs; [ + arduino-ide + kicad-small + ngspice + ]; + }; +} diff --git a/user/modules/gui/fun/config/discord.config.json b/user/modules/gui/fun/config/discord.config.json new file mode 100644 index 0000000..3e8d9fa --- /dev/null +++ b/user/modules/gui/fun/config/discord.config.json @@ -0,0 +1,82 @@ +{ + "settings": { + "general": { + "menuBar": { + "hide": false + }, + "tray": { + "disable": false + }, + "taskbar": { + "flash": true + }, + "window": { + "transparent": false, + "hideOnClose": false + } + }, + "privacy": { + "blockApi": { + "science": true, + "typingIndicator": false, + "fingerprinting": true + }, + "permissions": { + "video": null, + "audio": true, + "fullscreen": true, + "notifications": null, + "display-capture": true, + "background-sync": false + } + }, + "advanced": { + "csp": { + "enabled": true + }, + "cspThirdParty": { + "spotify": true, + "gif": true, + "hcaptcha": true, + "youtube": true, + "twitter": true, + "twitch": true, + "streamable": true, + "vimeo": true, + "soundcloud": true, + "paypal": true, + "audius": true, + "algolia": true, + "reddit": true, + "googleStorageApi": true + }, + "currentInstance": { + "radio": 0 + }, + "devel": { + "enabled": true + }, + "redirection": { + "warn": true + }, + "optimize": { + "gpu": true + }, + "webApi": { + "webGl": true + }, + "unix": { + "autoscroll": false + } + } + }, + "update": { + "notification": { + "version": "", + "till": "" + } + }, + "screenShareStore": { + "audio": false + } +} \ No newline at end of file diff --git a/user/modules/gui/fun/default.nix b/user/modules/gui/fun/default.nix new file mode 100644 index 0000000..4b075c0 --- /dev/null +++ b/user/modules/gui/fun/default.nix @@ -0,0 +1,26 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.gui.fun; + +in +{ options.modules.user.gui.fun = { enable = mkEnableOption "Enable entertainment apps"; }; + config = mkIf cfg.enable { + #programs.obs-studio = { + # enable = true; + # plugins = with pkgs.obs-studio-plugins; [ + # wlrobs + # obs-pipewire-audio-capture + # input-overlay + # ]; + #}; + + home.packages = with pkgs; [ + ytmdesktop + #discordo + #webcord + discord + ]; + }; +} diff --git a/user/modules/gui/utils/default.nix b/user/modules/gui/utils/default.nix new file mode 100644 index 0000000..f162ad3 --- /dev/null +++ b/user/modules/gui/utils/default.nix @@ -0,0 +1,16 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.gui.utils; + +in +{ options.modules.user.gui.utils = { enable = mkEnableOption "Enable desktop utils"; }; + config = mkIf cfg.enable { + programs.btop.enable = true; + home.packages = with pkgs; [ + gimp + libreoffice + ]; + }; +} diff --git a/user/modules/gui/wm/hyprland/config/rofi/config/config.rasi b/user/modules/gui/wm/hyprland/config/rofi/config/config.rasi new file mode 100644 index 0000000..2e38cf2 --- /dev/null +++ b/user/modules/gui/wm/hyprland/config/rofi/config/config.rasi @@ -0,0 +1,7 @@ +configuration { + font: "SF Pro Rounded 10"; + show-icons: true; + kb-cancel: "Escape,Alt+F1"; +} + +@theme "~/.config/rofi/material-ocean.rasi" diff --git a/user/modules/gui/wm/hyprland/config/rofi/config/material-ocean.rasi b/user/modules/gui/wm/hyprland/config/rofi/config/material-ocean.rasi new file mode 100644 index 0000000..3533a13 --- /dev/null +++ b/user/modules/gui/wm/hyprland/config/rofi/config/material-ocean.rasi @@ -0,0 +1,95 @@ +* { + background: #0f111a; + foreground: #f1f1f1; + selected: #ff4151; +} + +window { + transparency: "real"; + background-color: @background; + text-color: @foreground; +} + +prompt { + enabled: true; + padding: 4px 4px 6px 6px; + background-color: @background; + text-color: @foreground; +} + +textbox-prompt-colon { + expand: false; + background-color: @background; + padding: 4px 0px 0px 6px; +} + +inputbar { + children: [ textbox-prompt-colon, entry ]; + background-color: @background; + text-color: @foreground; + expand: false; + border: 0px 0px 0px 0px; + border-radius: 0px; + border-color: @selected; + margin: 0px 0px 0px 0px; + padding: 0px 0px 4px 0px; + position: center; +} + +entry { + background-color: @background; + text-color: @foreground; + placeholder-color: @foreground; + expand: true; + horizontal-align: 0; + blink: true; + padding: 4px 0px 0px 4px; +} + +case-indicator { + background-color: @background; + text-color: @foreground; + spacing: 0; +} + +listview { + background-color: @background; + columns: 1; + spacing: 5px; + cycle: true; + dynamic: true; + layout: vertical; +} + +mainbox { + background-color: @background; + children: [ inputbar, listview ]; + spacing: 5px; + padding: 5px 5px 5px 5px; +} + +element { + background-color: @background; + text-color: @foreground; + orientation: horizontal; + border-radius: 4px; + padding: 6px 6px 6px 6px; +} + +element-text, element-icon { + background-color: inherit; + text-color: inherit; +} + +element-icon { + size: 18px; + border: 4px; +} + +element selected { + background-color: @selected; + text-color: @background; + border: 0px; + border-radius: 0px; + border-color: @selected; +} diff --git a/user/modules/gui/wm/hyprland/config/rofi/default.nix b/user/modules/gui/wm/hyprland/config/rofi/default.nix new file mode 100644 index 0000000..724fd55 --- /dev/null +++ b/user/modules/gui/wm/hyprland/config/rofi/default.nix @@ -0,0 +1,183 @@ +{ pkgs, config, ... }: +let + inherit (config.lib.formats.rasi) mkLiteral; + +in +{ + enable = true; + package = pkgs.rofi; + location = "center"; + terminal = "\${pkgs.alacritty}/bin/alacritty"; + plugins = with pkgs; [ + rofi-emoji + ]; + + #theme = { + # "*" = { + # nord0 = mkLiteral "#2e3440"; + # nord1 = mkLiteral "#3b4252"; + # nord2 = mkLiteral "#434c5e"; + # nord3 = mkLiteral "#4c566a"; + # nord4 = mkLiteral "#d8dee9"; + # nord5 = mkLiteral "#e5e9f0"; + # nord6 = mkLiteral "#eceff4"; + # nord7 = mkLiteral "#8fbcbb"; + # nord8 = mkLiteral "#88c0d0"; + # nord9 = mkLiteral "#81a1c1"; + # nord10 = mkLiteral "#5e81ac"; + # nord11 = mkLiteral "#bf616a"; + # nord12 = mkLiteral "#d08770"; + # nord13 = mkLiteral "#ebcb8b"; + # nord14 = mkLiteral "#a3be8c"; + # nord15 = mkLiteral "#b48ead"; + # spacing = 2; + # background-color = mkLiteral "var(nord1)"; + # background = mkLiteral "var(nord1)"; + # foreground = mkLiteral "var(nord4)"; + # normal-background = mkLiteral "var(background)"; + # normal-foreground = mkLiteral "var(foreground)"; + # alternate-normal-background = mkLiteral "var(background)"; + # alternate-normal-foreground = mkLiteral "var(foreground)"; + # selected-normal-background = mkLiteral "var(nord8)"; + # selected-normal-foreground = mkLiteral "var(background)"; + # active-background = mkLiteral "var(background)"; + # active-foreground = mkLiteral "var(nord10)"; + # alternate-active-background = mkLiteral "var(background)"; + # alternate-active-foreground = mkLiteral "var(nord10)"; + # selected-active-background = mkLiteral "var(nord10)"; + # selected-active-foreground = mkLiteral "var(background)"; + # urgent-background = mkLiteral "var(background)"; + # urgent-foreground = mkLiteral "var(nord11)"; + # alternate-urgent-background = mkLiteral "var(background)"; + # alternate-urgent-foreground = mkLiteral "var(nord11)"; + # selected-urgent-background = mkLiteral "var(nord11)"; + # selected-urgent-foreground = mkLiteral "var(background)"; + # }; + # + # element = { + # padding = mkLiteral "0px 0px 0px 7px"; + # spacing = mkLiteral "5px"; + # border = 0; + # cursor = mkLiteral "pointer"; + # }; + + # "element normal.normal" = { + # background-color = mkLiteral "var(normal-background)"; + # text-color = mkLiteral "var(normal-foreground)"; + # }; + + # "element normal.urgent" = { + # background-color = mkLiteral "var(urgent-background)"; + # text-color = mkLiteral "var(urgent-foreground)"; + # }; + + # "element normal.active" = { + # background-color = mkLiteral "var(active-background)"; + # text-color = mkLiteral "var(active-foreground)"; + # }; + + # "element selected.normal" = { + # background-color = mkLiteral "var(selected-normal-background)"; + # text-color = mkLiteral "var(selected-normal-foreground)"; + # }; + + # "element selected.urgent" = { + # background-color = mkLiteral "var(selected-urgent-background)"; + # text-color = mkLiteral "var(selected-urgent-foreground)"; + # }; + + # "element selected.active" = { + # background-color = mkLiteral "var(selected-active-background)"; + # text-color = mkLiteral "var(selected-active-foreground)"; + # }; + + # "element alternate.normal" = { + # background-color = mkLiteral "var(alternate-normal-background)"; + # text-color = mkLiteral "var(alternate-normal-foreground)"; + # }; + + # "element alternate.urgent" = { + # background-color = mkLiteral "var(alternate-urgent-background)"; + # text-color = mkLiteral "var(alternate-urgent-foreground)"; + # }; + + # "element alternate.active" = { + # background-color = mkLiteral "var(alternate-active-background)"; + # text-color = mkLiteral "var(alternate-active-foreground)"; + # }; + + # "element-text" = { + # background-color = mkLiteral "rgba(0, 0, 0, 0%)"; + # text-color = mkLiteral "inherit"; + # highlight = mkLiteral "inherit"; + # cursor = mkLiteral "inherit"; + # }; + + # "element-icon" = { + # background-color = mkLiteral "rgba(0, 0, 0, 0%)"; + # size = mkLiteral "1.0000em"; + # text-color = mkLiteral "inherit"; + # cursor = mkLiteral "inherit"; + # }; + + # window = { + # padding = 0; + # border = 0; + # background-color = mkLiteral "var(background)"; + # }; + + # mainbox = { + # padding = 0; + # border = 0; + # }; + + # message = { + # margin = mkLiteral "0px 7px"; + # }; + + # textbox = { + # text-color = mkLiteral "var(foreground)"; + # }; + + # listview = { + # margin = mkLiteral "0px 0px 5px"; + # scrollbar = true; + # spacing = mkLiteral "2px"; + # fixed-height = 0; + # }; + + # scrollbar = { + # padding = 0; + # handle-width = mkLiteral "14px"; + # border = 0; + # handle-color = mkLiteral "var(nord3)"; + # }; + + # button = { + # spacing = 0; + # text-color = mkLiteral "var(normal-foreground)"; + # cursor = mkLiteral "pointer"; + # }; + + # "button selected" = { + # background-color = mkLiteral "var(selected-normal-background)"; + # text-color = mkLiteral "var(selected-normal-foreground)"; + # }; + + # inputbar = { + # padding = mkLiteral "7px"; + # margin = mkLiteral "7px"; + # spacing = 0; + # text-color = mkLiteral "var(normal-foreground)"; + # background-color = mkLiteral "var(nord3)"; + # children = [ "entry" ]; + # }; + + # entry = { + # spacing = 0; + # cursor = mkLiteral "text"; + # text-color = mkLiteral "var(normal-foreground)"; + # background-color = mkLiteral "var(nord3)"; + # }; + #}; +} diff --git a/user/modules/gui/wm/hyprland/config/waybar/config b/user/modules/gui/wm/hyprland/config/waybar/config new file mode 100644 index 0000000..3bb7b94 --- /dev/null +++ b/user/modules/gui/wm/hyprland/config/waybar/config @@ -0,0 +1,126 @@ +{ + "layer": "top", + "position": "top", + "output": "HDMI-A-1", + "modules-left": [ "custom/logo", "clock", "custom/blockheight", "custom/price", "memory", "cpu" ], + "modules-center": [ "hyprland/workspaces" ], + "modules-right": [ "tray", "pulseaudio", "network" ], + "reload_style_on_change":true, + + "custom/logo": { + "format": "", + "tooltip": false, + "on-click": "alacritty --class sys-specs -e bash -c 'fastfetch; read -n 1'" + }, + + "hyprland/workspaces": { + "format": "{icon}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "6": "", + "active": "", + "default": "" + }, + "persistent-workspaces": { + "*": [ 2, 3, 4, 5, 6 ] + } + }, + + "custom/weather": { + "format": "{}", + "return-type": "json", + "exec": "~/.config/waybar/scripts/weather.sh", + "interval": 10, + }, + + "custom/blockheight": { + "format": "󰠓 {} ", + "interval": 30, + "exec": "~/.config/waybar/scripts/getBlock", + "on-click": "xdg-open https://www.mempool.space", + }, + + "custom/price": { + "format": "${}", + "interval": 10, + "exec": "~/.config/waybar/scripts/getPrice", + "on-click": "xdg-open https://www.coinbase.com/price/bitcoin", + }, + + "clock": { + "format": "{:%I:%M:%S %p}", + "interval":1, + "tooltip-format": "\n{:%d %A}\n{calendar}", + "calendar-weeks-pos": "right", + "today-format": "{}", + "format-calendar": "{}", + "format-calendar-weeks": "W{:%V}", + "format-calendar-weekdays": "{}" + }, + + "network": { + "format-wifi": "", + "format-ethernet":"󰌘", + "format-disconnected": "", + "tooltip-format": "{ipaddr}", + "tooltip-format-wifi": "{essid} ({signalStrength}%) | {ipaddr}", + "tooltip-format-ethernet": "{ifname} | {ipaddr}", + "tooltip-format-disconnected": "Offline", + "on-click": "alacritty -e nmtui" + }, + + "cpu": { + "interval": 1, + "format": " {usage}%", + "min-length": 6, + "max-length": 6, + "format-icons": ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"], + }, + + "memory": { + "format": "󱐋{percentage}%" + }, + + "temperature": { + "format": "󱩱 :{temperatureC}°C", + "format-critical": "󰈸:{temperatureC}°C", + "interval": 1, + "critical-threshold": 80, + "on-click": "alacritty -e btop", + }, + + "pulseaudio": { + "format": "{icon}", + "format-bluetooth":"󰂰", + "format-muted": "", + "format-icons": { + "headphones": "", + "bluetooth": "󰥰", + "handsfree": "", + "headset": "󱡬", + "phone": "", + "portable": "", + "car": "", + "default": ["","",""] + }, + "justify": "center", + "on-click": "alacritty -e pulsemixer", + "tooltip-format": "{volume}%" + }, + + "jack": { + "format": "{} 󱎔", + "format-xrun": "{xruns} xruns", + "format-disconnected": "DSP off", + "realtime": true + }, + + "tray": { + "icon-size": 14, + "spacing": 10 + }, +} diff --git a/user/modules/gui/wm/hyprland/config/waybar/scripts/getBlock b/user/modules/gui/wm/hyprland/config/waybar/scripts/getBlock new file mode 100755 index 0000000..a6b2903 Binary files /dev/null and b/user/modules/gui/wm/hyprland/config/waybar/scripts/getBlock differ diff --git a/user/modules/gui/wm/hyprland/config/waybar/scripts/getPrice b/user/modules/gui/wm/hyprland/config/waybar/scripts/getPrice new file mode 100755 index 0000000..5d85c7f --- /dev/null +++ b/user/modules/gui/wm/hyprland/config/waybar/scripts/getPrice @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +response=$(curl -s "https://api.coinbase.com/v2/prices/BTC-USD/spot") +price=$(echo "$response" | jq -r .data.amount | awk -F. '{print $1}') + +if [ -z "$price" ]; then + echo -e "\033[31mErr\033[0m" + exit 1 +fi + +echo "$price" +exit 0 diff --git a/user/modules/gui/wm/hyprland/config/waybar/scripts/weather.sh b/user/modules/gui/wm/hyprland/config/waybar/scripts/weather.sh new file mode 100755 index 0000000..0eaeaf9 --- /dev/null +++ b/user/modules/gui/wm/hyprland/config/waybar/scripts/weather.sh @@ -0,0 +1,24 @@ +#!/bin/sh +BSSIDS="$(nmcli device wifi list | + awk 'NR>1 {if ($1 != "*") {print $1}}' | + tr -d ":" | + tr "\n" ",")" + +LOC="" +REQUEST_GEO="$(wget -qO - http://openwifi.su/api/v1/bssids/"$BSSIDS")" +if [[ "$(jq ".count_results" <<< "$REQUEST_GEO")" -gt 0 ]] ; then + LAT="$(jq ".lat" <<< "$REQUEST_GEO")" + LON="$(jq ".lon" <<< "$REQUEST_GEO")" + LOC="$LAT,$LON" +fi + +text="$(curl -s "https://wttr.in/$LOC?format=1" | sed 's/ //g')" +tooltip="$(curl -s "https://wttr.in/$LOC?0QT" | + sed 's/\\/\\\\/g' | + sed ':a;N;$!ba;s/\n/\\n/g' | + sed 's/"/\\"/g')" + +if ! grep -q "Unknown location" <<< "$text"; then + echo "{\"text\": \"$text\", \"tooltip\": \"$tooltip\", \"class\": \"weather\"}" +fi + diff --git a/user/modules/gui/wm/hyprland/config/waybar/style.css b/user/modules/gui/wm/hyprland/config/waybar/style.css new file mode 100644 index 0000000..06911c7 --- /dev/null +++ b/user/modules/gui/wm/hyprland/config/waybar/style.css @@ -0,0 +1,73 @@ +* { + border: none; + font-size: 14px; + font-family: "Terminus Nerd Font Propo" ; + min-height: 25px; +} + +window#waybar { + background: transparent; + margin: 5px; + } + +#custom-logo { + padding: 0 10px; + color: #5277c3; +} + +.modules-right { + padding-left: 5px; + border-radius: 15px 0 0 15px; + margin-top: 2px; + background: #000000; +} + +.modules-center { + padding: 0 15px; + margin-top: 2px; + border-radius: 15px 15px 15px 15px; + background: #000000; +} + +.modules-left { + border-radius: 0 15px 15px 0; + margin-top: 2px; + background: #000000; +} + +#custom-clipboard, +#pulseaudio, +#network, +#disk, +#memory, +#backlight, +#cpu, +#temperature, +#custom-weather, +#jack, +#tray, +#window, +#workspaces, +#clock { + padding: 0 5px; +} +#pulseaudio { + padding-top: 3px; +} + +#temperature.critical, +#pulseaudio.muted { + color: #FF0000; + padding-top: 0; +} + +#clock{ + color: #5fd1fa; +} + +@keyframes blink { + to { + background-color: #ffffff; + color: #000000; + } +} diff --git a/user/modules/gui/wm/hyprland/default.nix b/user/modules/gui/wm/hyprland/default.nix new file mode 100644 index 0000000..d8c7aba --- /dev/null +++ b/user/modules/gui/wm/hyprland/default.nix @@ -0,0 +1,246 @@ +{ pkgs, lib, config, monitors ? [], ... }: + +with lib; +let + cfg = config.modules.user.gui.wm.hyprland; + + wallpaper = builtins.fetchurl { + url = "https://images6.alphacoders.com/117/1174033.png"; + sha256 = "1ph5m9s57076jx6042iipqx2ifzadmd5z4lf5l49wgq4jb92mp16"; + }; + + toHyprlandMonitor = m: + "${m.name}, ${toString m.width}x${toString m.height}@${toString m.refreshRate}, ${toString m.x}x${toString m.y}, ${toString m.scale}"; + +in +{ options.modules.user.gui.wm.hyprland = { enable = mkEnableOption "Enable Hyprland WM"; }; + config = mkIf cfg.enable { + wayland.windowManager.hyprland = { + enable = true; + xwayland.enable = true; + + settings = { + "$mod" = "ALT"; + "$terminal" = "${pkgs.alacritty}/bin/alacritty"; + "$menu" = "rofi -show drun -show-icons -drun-icon-theme Qogir -font 'Noto Sans 14'"; + + monitor = if monitors != [] + then map toHyprlandMonitor monitors + else [ ", preferred, auto, 1" ]; + + exec-once = [ + "waybar" + "hyprctl setcursor Vanilla-DMZ 24" + ]; + + bind = [ + "$mod, Return, exec, $terminal" + "$mod, q, killactive" + + "$mod, J, movefocus, d" + "$mod, K, movefocus, u" + "$mod, H, movefocus, l" + "$mod, L, movefocus, r" + + "$mod&SHIFT, J, movewindow, d" + "$mod&SHIFT, K, movewindow, u" + "$mod&SHIFT, H, movewindow, l" + "$mod&SHIFT, L, movewindow, r" + + "$mod, F, fullscreen" + + ", Print, exec, grim ~/Pictures/screenshot-$(date +'%Y%m%d-%H%M%S').png" + "$mod&SHIFT, Print, exec, grim -g \"$(slurp)\" ~/Pictures/screenshot-$(date +'%Y%m%d-%H%M%S').png" + "$mod&SHIFT, F, exec, alacritty -e sh -c 'EDITOR=nvim ranger'" + ''SHIFT, Print, exec, grim -g "$(hyprctl activewindow -j | jq -r '"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"')" ~/Pictures/screenshot-$(date +'%Y%m%d-%H%M%S').png'' + + "$mod, D, exec, $menu" + "$mod&SHIFT, D, exec, rofi -modi emoji -show emoji" + ] ++ ( builtins.concatLists (builtins.genList ( + x: let + ws = let + c = (x + 1) / 10; + in + builtins.toString (x + 1 - (c * 10)); + in + [ + "$mod, ${ws}, workspace, ${toString (x + 1)}" + "$mod SHIFT, ${ws}, movetoworkspace, ${toString (x + 1)}" + ]) + 10) + ); + + bindm = [ + "$mod, mouse:272, movewindow" + ]; + + windowrulev2 = [ + "float, title:(Android Emulator)" + "float, title: Extension: (PassFF)" + "float, size 400 600, stayfocused, class:sys-specs" + ]; + + general = { + layout = "master"; + border_size = 0; + }; + + decoration = { + rounding = 10; + }; + + master = { + drop_at_cursor = false; + #new_is_master = false; + }; + + input = { + kb_layout = "us"; + follow_mouse = 1; + accel_profile = "flat"; + sensitivity = 0.35; + }; + + cursor = { + inactive_timeout = 0; + no_hardware_cursors = true; + hide_on_touch = false; + use_cpu_buffer = 0; + enable_hyprcursor = false; + }; + + env = [ + "HYPRCURSOR_THEME,Vanilla-DMZ" + "HYPRCURSOR_SIZE,24" + "GTK_THEME,Juno" + + "LIBVA_DRIVER_NAME,nvidia" + "XDG_SESSION_TYPE,wayland" + "GBM_BACKEND,nvidia-drm" + "__GLX_VENDOR_LIBRARY_NAME,nvidia" + ]; + }; + }; + + programs.rofi = { + enable = true; + package = pkgs.rofi; + location = "center"; + terminal = "alacritty"; + plugins = with pkgs; [ + rofi-emoji + ]; + }; + + home = { + file = { + ".config/rofi" = { + source = ./config/rofi/config; + recursive = true; + }; + ".config/waybar" = { + source = ./config/waybar; + recursive = true; + }; + }; + + packages = with pkgs; [ + pulsemixer + xdg-utils + wl-clipboard + cliphist + + dconf + + grim + jq + slurp + + ranger + highlight + + noto-fonts + noto-fonts-cjk-sans + noto-fonts-color-emoji + ]; + + sessionVariables = { + NIXOS_OZONE_WL = 1; + }; + }; + + programs.waybar = { + enable = true; + }; + + services.hyprpaper = { + enable = true; + settings = { + ipc = "on"; + splash = false; + splash_offset = 2.0; + + preload = + [ "${wallpaper}" ]; + + wallpaper = [ + ",${wallpaper}" + ]; + }; + }; + + gtk = { + enable = true; + theme = { + name = "Juno"; + package = pkgs.juno-theme; + }; + iconTheme = { + name = "Qogir"; + package = pkgs.qogir-icon-theme; + }; + cursorTheme = { + package = pkgs.vanilla-dmz; + name = "Vanilla-DMZ"; + }; + gtk3.extraConfig = { + gtk-application-prefer-dark-theme = 1; + }; + gtk4.extraConfig = { + gtk-application-prefer-dark-theme = 1; + }; + }; + + qt = { + enable = true; + style = { + name = "juno"; + package = pkgs.juno-theme; + }; + platformTheme.name = "gtk"; + }; + + xdg.portal = { + enable = true; + extraPortals = with pkgs; [ + xdg-desktop-portal-hyprland + ]; + config.common.default = "*"; + }; + + programs = { + imv.enable = true; + mpv.enable = true; + zathura.enable = true; + }; + + fonts.fontconfig.enable = true; + + # Auto-start Hyprland on tty1 + programs.bash.profileExtra = '' + if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then + exec Hyprland + fi + ''; + }; +} diff --git a/user/modules/gui/wm/sway/config/rofi/config/config.rasi b/user/modules/gui/wm/sway/config/rofi/config/config.rasi new file mode 100644 index 0000000..2e38cf2 --- /dev/null +++ b/user/modules/gui/wm/sway/config/rofi/config/config.rasi @@ -0,0 +1,7 @@ +configuration { + font: "SF Pro Rounded 10"; + show-icons: true; + kb-cancel: "Escape,Alt+F1"; +} + +@theme "~/.config/rofi/material-ocean.rasi" diff --git a/user/modules/gui/wm/sway/config/rofi/config/material-ocean.rasi b/user/modules/gui/wm/sway/config/rofi/config/material-ocean.rasi new file mode 100644 index 0000000..3533a13 --- /dev/null +++ b/user/modules/gui/wm/sway/config/rofi/config/material-ocean.rasi @@ -0,0 +1,95 @@ +* { + background: #0f111a; + foreground: #f1f1f1; + selected: #ff4151; +} + +window { + transparency: "real"; + background-color: @background; + text-color: @foreground; +} + +prompt { + enabled: true; + padding: 4px 4px 6px 6px; + background-color: @background; + text-color: @foreground; +} + +textbox-prompt-colon { + expand: false; + background-color: @background; + padding: 4px 0px 0px 6px; +} + +inputbar { + children: [ textbox-prompt-colon, entry ]; + background-color: @background; + text-color: @foreground; + expand: false; + border: 0px 0px 0px 0px; + border-radius: 0px; + border-color: @selected; + margin: 0px 0px 0px 0px; + padding: 0px 0px 4px 0px; + position: center; +} + +entry { + background-color: @background; + text-color: @foreground; + placeholder-color: @foreground; + expand: true; + horizontal-align: 0; + blink: true; + padding: 4px 0px 0px 4px; +} + +case-indicator { + background-color: @background; + text-color: @foreground; + spacing: 0; +} + +listview { + background-color: @background; + columns: 1; + spacing: 5px; + cycle: true; + dynamic: true; + layout: vertical; +} + +mainbox { + background-color: @background; + children: [ inputbar, listview ]; + spacing: 5px; + padding: 5px 5px 5px 5px; +} + +element { + background-color: @background; + text-color: @foreground; + orientation: horizontal; + border-radius: 4px; + padding: 6px 6px 6px 6px; +} + +element-text, element-icon { + background-color: inherit; + text-color: inherit; +} + +element-icon { + size: 18px; + border: 4px; +} + +element selected { + background-color: @selected; + text-color: @background; + border: 0px; + border-radius: 0px; + border-color: @selected; +} diff --git a/user/modules/gui/wm/sway/config/rofi/default.nix b/user/modules/gui/wm/sway/config/rofi/default.nix new file mode 100644 index 0000000..724fd55 --- /dev/null +++ b/user/modules/gui/wm/sway/config/rofi/default.nix @@ -0,0 +1,183 @@ +{ pkgs, config, ... }: +let + inherit (config.lib.formats.rasi) mkLiteral; + +in +{ + enable = true; + package = pkgs.rofi; + location = "center"; + terminal = "\${pkgs.alacritty}/bin/alacritty"; + plugins = with pkgs; [ + rofi-emoji + ]; + + #theme = { + # "*" = { + # nord0 = mkLiteral "#2e3440"; + # nord1 = mkLiteral "#3b4252"; + # nord2 = mkLiteral "#434c5e"; + # nord3 = mkLiteral "#4c566a"; + # nord4 = mkLiteral "#d8dee9"; + # nord5 = mkLiteral "#e5e9f0"; + # nord6 = mkLiteral "#eceff4"; + # nord7 = mkLiteral "#8fbcbb"; + # nord8 = mkLiteral "#88c0d0"; + # nord9 = mkLiteral "#81a1c1"; + # nord10 = mkLiteral "#5e81ac"; + # nord11 = mkLiteral "#bf616a"; + # nord12 = mkLiteral "#d08770"; + # nord13 = mkLiteral "#ebcb8b"; + # nord14 = mkLiteral "#a3be8c"; + # nord15 = mkLiteral "#b48ead"; + # spacing = 2; + # background-color = mkLiteral "var(nord1)"; + # background = mkLiteral "var(nord1)"; + # foreground = mkLiteral "var(nord4)"; + # normal-background = mkLiteral "var(background)"; + # normal-foreground = mkLiteral "var(foreground)"; + # alternate-normal-background = mkLiteral "var(background)"; + # alternate-normal-foreground = mkLiteral "var(foreground)"; + # selected-normal-background = mkLiteral "var(nord8)"; + # selected-normal-foreground = mkLiteral "var(background)"; + # active-background = mkLiteral "var(background)"; + # active-foreground = mkLiteral "var(nord10)"; + # alternate-active-background = mkLiteral "var(background)"; + # alternate-active-foreground = mkLiteral "var(nord10)"; + # selected-active-background = mkLiteral "var(nord10)"; + # selected-active-foreground = mkLiteral "var(background)"; + # urgent-background = mkLiteral "var(background)"; + # urgent-foreground = mkLiteral "var(nord11)"; + # alternate-urgent-background = mkLiteral "var(background)"; + # alternate-urgent-foreground = mkLiteral "var(nord11)"; + # selected-urgent-background = mkLiteral "var(nord11)"; + # selected-urgent-foreground = mkLiteral "var(background)"; + # }; + # + # element = { + # padding = mkLiteral "0px 0px 0px 7px"; + # spacing = mkLiteral "5px"; + # border = 0; + # cursor = mkLiteral "pointer"; + # }; + + # "element normal.normal" = { + # background-color = mkLiteral "var(normal-background)"; + # text-color = mkLiteral "var(normal-foreground)"; + # }; + + # "element normal.urgent" = { + # background-color = mkLiteral "var(urgent-background)"; + # text-color = mkLiteral "var(urgent-foreground)"; + # }; + + # "element normal.active" = { + # background-color = mkLiteral "var(active-background)"; + # text-color = mkLiteral "var(active-foreground)"; + # }; + + # "element selected.normal" = { + # background-color = mkLiteral "var(selected-normal-background)"; + # text-color = mkLiteral "var(selected-normal-foreground)"; + # }; + + # "element selected.urgent" = { + # background-color = mkLiteral "var(selected-urgent-background)"; + # text-color = mkLiteral "var(selected-urgent-foreground)"; + # }; + + # "element selected.active" = { + # background-color = mkLiteral "var(selected-active-background)"; + # text-color = mkLiteral "var(selected-active-foreground)"; + # }; + + # "element alternate.normal" = { + # background-color = mkLiteral "var(alternate-normal-background)"; + # text-color = mkLiteral "var(alternate-normal-foreground)"; + # }; + + # "element alternate.urgent" = { + # background-color = mkLiteral "var(alternate-urgent-background)"; + # text-color = mkLiteral "var(alternate-urgent-foreground)"; + # }; + + # "element alternate.active" = { + # background-color = mkLiteral "var(alternate-active-background)"; + # text-color = mkLiteral "var(alternate-active-foreground)"; + # }; + + # "element-text" = { + # background-color = mkLiteral "rgba(0, 0, 0, 0%)"; + # text-color = mkLiteral "inherit"; + # highlight = mkLiteral "inherit"; + # cursor = mkLiteral "inherit"; + # }; + + # "element-icon" = { + # background-color = mkLiteral "rgba(0, 0, 0, 0%)"; + # size = mkLiteral "1.0000em"; + # text-color = mkLiteral "inherit"; + # cursor = mkLiteral "inherit"; + # }; + + # window = { + # padding = 0; + # border = 0; + # background-color = mkLiteral "var(background)"; + # }; + + # mainbox = { + # padding = 0; + # border = 0; + # }; + + # message = { + # margin = mkLiteral "0px 7px"; + # }; + + # textbox = { + # text-color = mkLiteral "var(foreground)"; + # }; + + # listview = { + # margin = mkLiteral "0px 0px 5px"; + # scrollbar = true; + # spacing = mkLiteral "2px"; + # fixed-height = 0; + # }; + + # scrollbar = { + # padding = 0; + # handle-width = mkLiteral "14px"; + # border = 0; + # handle-color = mkLiteral "var(nord3)"; + # }; + + # button = { + # spacing = 0; + # text-color = mkLiteral "var(normal-foreground)"; + # cursor = mkLiteral "pointer"; + # }; + + # "button selected" = { + # background-color = mkLiteral "var(selected-normal-background)"; + # text-color = mkLiteral "var(selected-normal-foreground)"; + # }; + + # inputbar = { + # padding = mkLiteral "7px"; + # margin = mkLiteral "7px"; + # spacing = 0; + # text-color = mkLiteral "var(normal-foreground)"; + # background-color = mkLiteral "var(nord3)"; + # children = [ "entry" ]; + # }; + + # entry = { + # spacing = 0; + # cursor = mkLiteral "text"; + # text-color = mkLiteral "var(normal-foreground)"; + # background-color = mkLiteral "var(nord3)"; + # }; + #}; +} diff --git a/user/modules/gui/wm/sway/default.nix b/user/modules/gui/wm/sway/default.nix new file mode 100644 index 0000000..f0d297b --- /dev/null +++ b/user/modules/gui/wm/sway/default.nix @@ -0,0 +1,184 @@ +{ pkgs, lib, config, monitors ? [], ... }: + +with lib; +let + cfg = config.modules.user.gui.wm.sway; + modifier = config.wayland.windowManager.sway.config.modifier; + + wallpaper = builtins.fetchurl { + url = "https://images6.alphacoders.com/117/1174033.png"; + sha256 = "1ph5m9s57076jx6042iipqx2ifzadmd5z4lf5l49wgq4jb92mp16"; + }; + + barStatus = pkgs.writeShellScript "status.sh" '' + #!/usr/bin/env bash + while :; do + echo "$(ip -4 addr show eno1 | awk '/inet / {print $2}' | cut -d'/' -f1) | $(free -h | awk '/^Mem/ {print $3}') | $(date +'%I:%M:%S %p') | $(date +'%m-%d-%Y')"; sleep 1; + done + ''; + + toSwayOutput = m: { + "${m.name}" = { + resolution = "${toString m.width}x${toString m.height}@${toString m.refreshRate}Hz"; + position = "${toString m.x} ${toString m.y}"; + scale = toString m.scale; + bg = "${wallpaper} fill"; + }; + }; + + outputConfig = if monitors != [] + then lib.mkMerge (map toSwayOutput monitors) + else { + "*" = { bg = "${wallpaper} fill"; }; + }; + +in +{ options.modules.user.gui.wm.sway = { enable = mkEnableOption "Enable Sway WM"; }; + config = mkIf cfg.enable { + wayland.windowManager.sway = { + enable = true; + xwayland = true; + wrapperFeatures.gtk = true; + + extraSessionCommands = '' + export _JAVA_AWT_WM_NONREPARENTING=1 + export GTK_THEME=Adwaita-Dark + ''; + + config = { + defaultWorkspace = "workspace number 1"; + + fonts = { + names = [ "Terminus" ]; + }; + + output = outputConfig; + modifier = "Mod1"; + menu = "rofi -show drun -show-icons -drun-icon-theme Qogir -font 'Noto Sans 14'"; + terminal = "${pkgs.alacritty}/bin/alacritty"; + + input = { + keyboard = { + xkb_numlock = "enabled"; + xkb_layout = "us"; + }; + pointer = { + accel_profile = "flat"; + pointer_accel = "0.65"; + }; + }; + + bars = [ + { + position = "top"; + statusCommand = "${barStatus}"; + fonts = { + names = [ "Terminus" ]; + size = 12.0; + }; + colors = { + background = "#0A0E14"; + statusline = "#FFFFFF"; + }; + } + ]; + + gaps = { + smartGaps = false; + inner = 10; + }; + + floating = { + titlebar = false; + border = 0; + criteria = [ + { + title = "Android Emulator"; + } + ]; + }; + + window = { + titlebar = false; + border= 0; + }; + + keybindings = lib.mkOptionDefault { + "${modifier}+q" = "kill"; + "Print" = "exec grim ~/Pictures/screenshot-$(date +'%Y%m%d-%H%M%S').png"; + "${modifier}+Shift+Print" = "exec grim -g \"$(slurp)\" ~/Pictures/screenshot-$(date +'%Y%m%d-%H%M%S').png"; + "${modifier}+Print" = ''exec sh -c 'grim -g "$(swaymsg -t get_tree | jq -j '"'"'.. | select(.type?) | select(.focused).rect | "\(.x),\(.y) \(.width)x\(.height)"'"'"')" ~/Pictures/screenshot-$(date +'%Y%m%d-%H%M%S').png' ''; + "${modifier}+Shift+f" = "exec alacritty -e sh -c 'EDITOR=nvim ranger'"; + "${modifier}+Shift+d" = "exec rofi -modi emoji -show emoji"; + }; + }; + + extraConfig = '' + exec_always ${pkgs.autotiling}/bin/autotiling -sr "1.61" + ''; + }; + + programs.rofi = import ./config/rofi { inherit pkgs config lib; }; + + home.file.".config/rofi" = { + source = ./config/rofi/config; + recursive = true; + }; + + xdg = { + portal = { + enable = true; + extraPortals = with pkgs; [ + xdg-desktop-portal-wlr + ]; + config.common.default = "*"; + }; + }; + + #gtk = { + # enable = true; + # theme.package = pkgs.juno-theme; + # theme.name = "Juno-ocean"; + # iconTheme.package = pkgs.qogir-icon-theme; + # iconTheme.name = "Qogir"; + #}; + + qt = { + enable = true; + style.package = pkgs.juno-theme; + platformTheme.name = "gtk"; + }; + + home.packages = with pkgs; [ + pavucontrol + xdg-utils + wl-clipboard + autotiling + + grim + jq + slurp + + ranger + highlight + + nerd-fonts.terminess-ttf + noto-fonts + noto-fonts-cjk-sans + noto-fonts-color-emoji + ]; + + programs = { + imv.enable = true; + }; + + fonts.fontconfig.enable = true; + + # Auto-start sway on tty1 + programs.bash.profileExtra = '' + if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then + exec sway + fi + ''; + }; +} diff --git a/user/modules/neovim/.luarc.json b/user/modules/neovim/.luarc.json new file mode 100644 index 0000000..6779f74 --- /dev/null +++ b/user/modules/neovim/.luarc.json @@ -0,0 +1,5 @@ +{ + "diagnostics.disable": [ + "missing-fields" + ] +} \ No newline at end of file diff --git a/user/modules/neovim/default.nix b/user/modules/neovim/default.nix new file mode 100644 index 0000000..9a90d08 --- /dev/null +++ b/user/modules/neovim/default.nix @@ -0,0 +1,23 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.neovim; + +in +{ options.modules.user.neovim = { enable = mkEnableOption "user.neovim"; }; + config = mkIf cfg.enable { + programs.neovim = { + enable = true; + defaultEditor = true; + vimAlias = true; + vimdiffAlias = true; + extraPackages = import ./pkgs.nix { inherit pkgs; }; + }; + + home.file.".config/nvim" = { + source = ./nvim; + recursive = true; + }; + }; +} diff --git a/user/modules/neovim/nvim b/user/modules/neovim/nvim new file mode 160000 index 0000000..c341ac8 --- /dev/null +++ b/user/modules/neovim/nvim @@ -0,0 +1 @@ +Subproject commit c341ac8840e8a19ab98bcc5084f51157ddaf8730 diff --git a/user/modules/neovim/pkgs.nix b/user/modules/neovim/pkgs.nix new file mode 100644 index 0000000..04dcd86 --- /dev/null +++ b/user/modules/neovim/pkgs.nix @@ -0,0 +1,24 @@ +{ pkgs, ... }: + +let + # Essential LSPs for config files (project-specific LSPs go in devShells) + lsp = with pkgs; [ + nixd + lua-language-server + marksman + taplo + ]; + + lsp' = with pkgs.nodePackages; [ + vscode-langservers-extracted # jsonls, html, cssls + bash-language-server + yaml-language-server + ]; + + extraPackages = with pkgs; [ + lazygit + gcc + ]; + +in + extraPackages ++ lsp ++ lsp' diff --git a/user/modules/security/gpg/default.nix b/user/modules/security/gpg/default.nix new file mode 100644 index 0000000..244eee1 --- /dev/null +++ b/user/modules/security/gpg/default.nix @@ -0,0 +1,50 @@ +{ pkgs, lib, config, osConfig, ... }: + +with lib; +let + cfg = config.modules.user.security.gpg; + wm = config.modules.user.gui.wm; + gui = { + enable = builtins.any (mod: mod.enable or false) (builtins.attrValues wm); + }; + +in +{ options.modules.user.security.gpg = { enable = mkEnableOption "Enable GPG module"; }; + config = mkIf cfg.enable { + programs.gpg = { + enable = true; + # Use pcscd instead of direct CCID access (avoids conflicts with age-plugin-yubikey) + scdaemonSettings = mkIf osConfig.services.pcscd.enable { + disable-ccid = true; + }; + publicKeys = [ + { + text = "${config.user.keys.pgp.yubikey}"; + trust = 5; + } + ] ++ optionals (osConfig.networking.hostName == "workstation") [ + { + text = "${config.user.keys.pgp.work}"; + trust = 5; + } + { + text = "${config.user.keys.pgp.ccur}"; + trust = 5; + } + ]; + }; + + services.gpg-agent = { + enable = true; + enableSshSupport = true; + enableBashIntegration = true; + enableScDaemon = true; + + pinentry.package = + if gui.enable then + pkgs.pinentry-gnome3 + else + pkgs.pinentry-tty; + }; + }; +} diff --git a/user/modules/security/yubikey/default.nix b/user/modules/security/yubikey/default.nix new file mode 100644 index 0000000..62f3ead --- /dev/null +++ b/user/modules/security/yubikey/default.nix @@ -0,0 +1,16 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.security.yubikey; + +in +{ options.modules.user.security.yubikey = { enable = mkEnableOption "Enable Yubikey support"; }; + config = mkIf cfg.enable { + home.packages = with pkgs; [ + yubikey-manager + age-plugin-yubikey + yubico-piv-tool + ]; + }; +} diff --git a/user/modules/tmux/config/tmux.nix b/user/modules/tmux/config/tmux.nix new file mode 100644 index 0000000..9ad7ae4 --- /dev/null +++ b/user/modules/tmux/config/tmux.nix @@ -0,0 +1,44 @@ +'' +bind -n M-C source-file ~/.config/tmux/tmux.conf + +# Navigation (matches hyprland Alt+hjkl) +bind-key -n M-h select-pane -L +bind-key -n M-j select-pane -D +bind-key -n M-k select-pane -U +bind-key -n M-l select-pane -R + +# Move/swap pane (matches hyprland Alt+Shift+hjkl) +bind-key -n M-H swap-pane -s '{left-of}' +bind-key -n M-J swap-pane -s '{down-of}' +bind-key -n M-K swap-pane -s '{up-of}' +bind-key -n M-L swap-pane -s '{right-of}' + +# Actions +bind-key -n M-q kill-pane +bind-key -n M-Return split-window -c "#{pane_current_path}" +bind-key -n M-f resize-pane -Z + +# Windows (like workspaces) +bind-key -n M-1 select-window -t 1 +bind-key -n M-2 select-window -t 2 +bind-key -n M-3 select-window -t 3 +bind-key -n M-4 select-window -t 4 +bind-key -n M-5 select-window -t 5 +bind-key -n M-6 select-window -t 6 +bind-key -n M-7 select-window -t 7 +bind-key -n M-8 select-window -t 8 +bind-key -n M-9 select-window -t 9 +bind-key -n M-0 select-window -t 10 + +# Move pane to window (like move to workspace) +bind-key -n M-! join-pane -t :1 +bind-key -n M-@ join-pane -t :2 +bind-key -n M-'#' join-pane -t :3 +bind-key -n M-'$' join-pane -t :4 +bind-key -n M-% join-pane -t :5 +bind-key -n M-^ join-pane -t :6 +bind-key -n M-& join-pane -t :7 +bind-key -n M-* join-pane -t :8 +bind-key -n M-( join-pane -t :9 +bind-key -n M-) join-pane -t :10 +'' diff --git a/user/modules/tmux/default.nix b/user/modules/tmux/default.nix new file mode 100644 index 0000000..9bf2c47 --- /dev/null +++ b/user/modules/tmux/default.nix @@ -0,0 +1,44 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.tmux; + gui = config.modules.user.gui.wm; + + wm = { + enable = builtins.any (mod: mod.enable or false) (builtins.attrValues gui); + }; + +in +{ options.modules.user.tmux = { enable = mkEnableOption "Enable tmux module"; }; + config = mkIf cfg.enable { + programs.tmux = { + enable = true; + newSession = true; + disableConfirmationPrompt = true; + keyMode = "vi"; + mouse = if wm.enable then true else false; + + prefix = "M"; + #shell = "\${pkgs.bash}/bin/bash"; + + plugins = with pkgs.tmuxPlugins; [ + { + plugin = tilish; + extraConfig = '' + set -g @tilish-default 'tiled' + ''; + } + ]; + + extraConfig = import ./config/tmux.nix; + }; + + # Auto-start tmux only on local TTY (not SSH, not in tmux already) + programs.bash.profileExtra = mkIf (!wm.enable) '' + if [ -t 0 ] && [[ $- == *i* ]] && [ -z "$DISPLAY" ] && [ -z "$TMUX" ] && [ -z "$SSH_TTY" ] && [ -z "$SSH_CONNECTION" ]; then + exec tmux + fi + ''; + }; +} diff --git a/user/modules/utils/dev/default.nix b/user/modules/utils/dev/default.nix new file mode 100644 index 0000000..102807c --- /dev/null +++ b/user/modules/utils/dev/default.nix @@ -0,0 +1,56 @@ +{ pkgs, lib, config, osConfig, ... }: + +with lib; +let + cfg = config.modules.user.utils.dev; + +in +{ options.modules.user.utils.dev = { enable = mkEnableOption "user.utils.dev"; }; + config = mkIf cfg.enable { + home.packages = with pkgs; [ + claude-code + + nix-init + nix-prefetch-git + nurl + + pkg-config + qrencode + + docker + + # Network/system tools + fping + wireguard-tools + pciutils + lshw + ] ++ optionals (osConfig.virtualisation.libvirtd.enable) [ + virt-manager + ]; + + programs = { + #bash = { + # initExtra = import ./config/penpot.nix; + #}; + direnv = { + enable = true; + enableBashIntegration = true; + nix-direnv.enable = true; + }; + }; + + home = { + sessionVariables = { + DIRENV_LOG_FORMAT = ""; + }; + + # Workaround for direnv_log bug + # https://github.com/direnv/direnv/issues/1418#issuecomment-2820125413 + file.".config/direnv/direnv.toml" = { + enable = true; + force = true; + text = ""; + }; + }; + }; +} diff --git a/user/modules/utils/email/config/aerc.conf b/user/modules/utils/email/config/aerc.conf new file mode 100644 index 0000000..e7e0bda --- /dev/null +++ b/user/modules/utils/email/config/aerc.conf @@ -0,0 +1,561 @@ +# +# aerc main configuration + +[general] +# +# Used as a default path for save operations if no other path is specified. +# ~ is expanded to the current user home dir. +# +#default-save-path= + +# If set to "gpg", aerc will use system gpg binary and keystore for all crypto +# operations. If set to "internal", the internal openpgp keyring will be used. +# If set to "auto", the system gpg will be preferred unless the internal +# keyring already exists, in which case the latter will be used. +# +# Default: auto +pgp-provider=auto + +# By default, the file permissions of accounts.conf must be restrictive and +# only allow reading by the file owner (0600). Set this option to true to +# ignore this permission check. Use this with care as it may expose your +# credentials. +# +# Default: false +#unsafe-accounts-conf=false + +# Output log messages to specified file. A path starting with ~/ is expanded to +# the user home dir. When redirecting aerc's output to a file using > shell +# redirection, this setting is ignored and log messages are printed to stdout. +# +#log-file= + +# Only log messages above the specified level to log-file. Supported levels +# are: trace, debug, info, warn and error. When redirecting aerc's output to +# a file using > shell redirection, this setting is ignored and the log level +# is forced to trace. +# +# Default: info +#log-level=info + +# Set the $TERM environment variable used for the embedded terminal. +# +# Default: xterm-256color +term=xterm-256color + +# Display OSC8 strings in the embedded terminal +# +# Default: false +#enable-osc8=false + +[ui] +# +# Describes the format for each row in a mailbox view. This is a comma +# separated list of column names with an optional align and width suffix. After +# the column name, one of the '<' (left), ':' (center) or '>' (right) alignment +# characters can be added (by default, left) followed by an optional width +# specifier. The width is either an integer representing a fixed number of +# characters, or a percentage between 1% and 99% representing a fraction of the +# terminal width. It can also be one of the '*' (auto) or '=' (fit) special +# width specifiers. Auto width columns will be equally attributed the remaining +# terminal width. Fit width columns take the width of their contents. If no +# width specifier is set, '*' is used by default. +# +# Default: date<20,name<17,flags>4,subject<* +#index-columns=date<20,name<17,flags>4,subject<* + +# +# Each name in index-columns must have a corresponding column-$name setting. +# All column-$name settings accept golang text/template syntax. See +# aerc-templates(7) for available template attributes and functions. +# +# Default settings +#column-date={{.DateAutoFormat .Date.Local}} +#column-name={{index (.From | names) 0}} +#column-flags={{.Flags | join ""}} +#column-subject={{.ThreadPrefix}}{{.Subject}} + +# +# String separator inserted between columns. When the column width specifier is +# an exact number of characters, the separator is added to it (i.e. the exact +# width will be fully available for the column contents). +# +# Default: " " +#column-separator=" " + +# +# See time.Time#Format at https://godoc.org/time#Time.Format +# +# Default: 2006-01-02 03:04 PM (ISO 8601 + 12 hour time) +#timestamp-format=2006-01-02 03:04 PM + +# +# Index-only time format for messages that were received/sent today. +# If this is not specified, timestamp-format is used instead. +# +#this-day-time-format= + +# +# Index-only time format for messages that were received/sent within the last +# 7 days. If this is not specified, timestamp-format is used instead. +# +#this-week-time-format= + +# +# Index-only time format for messages that were received/sent this year. +# If this is not specified, timestamp-format is used instead. +# +#this-year-time-format= + +# +# Width of the sidebar, including the border. +# +# Default: 20 +#sidebar-width=20 + +# +# Message to display when viewing an empty folder. +# +# Default: (no messages) +#empty-message=(no messages) + +# Message to display when no folders exists or are all filtered +# +# Default: (no folders) +#empty-dirlist=(no folders) + +# Enable mouse events in the ui, e.g. clicking and scrolling with the mousewheel +# +# Default: false +#mouse-enabled=false + +# +# Ring the bell when new messages are received +# +# Default: true +#new-message-bell=true + +# +# Template to use for Account tab titles +# +# Default: {{.Account}} +#tab-title-account={{.Account}} + +# Marker to show before a pinned tab's name. +# +# Default: ` +#pinned-tab-marker='`' + +# Template for the left side of the directory list. +# See aerc-templates(7) for all available fields and functions. +# +# Default: {{.Folder}} +#dirlist-left={{.Folder}} + +# Template for the right side of the directory list. +# See aerc-templates(7) for all available fields and functions. +# +# Default: {{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}} +#dirlist-right={{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}} + +# Delay after which the messages are actually listed when entering a directory. +# This avoids loading messages when skipping over folders and makes the UI more +# responsive. If you do not want that, set it to 0s. +# +# Default: 200ms +#dirlist-delay=200ms + +# Display the directory list as a foldable tree that allows to collapse and +# expand the folders. +# +# Default: false +#dirlist-tree=false + +# If dirlist-tree is enabled, set level at which folders are collapsed by +# default. Set to 0 to disable. +# +# Default: 0 +#dirlist-collapse=0 + +# List of space-separated criteria to sort the messages by, see *sort* +# command in *aerc*(1) for reference. Prefixing a criterion with "-r " +# reverses that criterion. +# +# Example: "from -r date" +# +#sort= + +# Moves to next message when the current message is deleted +# +# Default: true +#next-message-on-delete=true + +# Automatically set the "seen" flag when a message is opened in the message +# viewer. +# +# Default: true +#auto-mark-read=true + +# The directories where the stylesets are stored. It takes a colon-separated +# list of directories. If this is unset or if a styleset cannot be found, the +# following paths will be used as a fallback in that order: +# +# ${XDG_CONFIG_HOME:-~/.config}/aerc/stylesets +# ${XDG_DATA_HOME:-~/.local/share}/aerc/stylesets +# /nix/store/jzlsc52f1zsczi5rmjrbff45i7ng3cph-aerc-0.15.2/share/aerc/stylesets +# +#stylesets-dirs= + +# Uncomment to use box-drawing characters for vertical and horizontal borders. +# +# Default: " " +#border-char-vertical=" " +#border-char-horizontal=" " + +# Sets the styleset to use for the aerc ui elements. +# +# Default: default +#styleset-name=default + +# Activates fuzzy search in commands and their arguments: the typed string is +# searched in the command or option in any position, and need not be +# consecutive characters in the command or option. +# +# Default: false +#fuzzy-complete=false + +# How long to wait after the last input before auto-completion is triggered. +# +# Default: 250ms +#completion-delay=250ms + +# The minimum required characters to allow auto-completion to be triggered after +# completion-delay. +# +# Default: 1 +#completion-min-chars=1 + +# +# Global switch for completion popovers +# +# Default: true +#completion-popovers=true + +# Uncomment to use UTF-8 symbols to indicate PGP status of messages +# +# Default: ASCII +#icon-unencrypted= +#icon-encrypted=✔ +#icon-signed=✔ +#icon-signed-encrypted=✔ +#icon-unknown=✘ +#icon-invalid=⚠ + +# Reverses the order of the message list. By default, the message list is +# ordered with the newest (highest UID) message on top. Reversing the order +# will put the oldest (lowest UID) message on top. This can be useful in cases +# where the backend does not support sorting. +# +# Default: false +#reverse-msglist-order = false + +# Reverse display of the mesage threads. Default order is the the intial +# message is on the top with all the replies being displayed below. The +# reverse option will put the initial message at the bottom with the +# replies on top. +# +# Default: false +#reverse-thread-order=false + +# Sort the thread siblings according to the sort criteria for the messages. If +# sort-thread-siblings is false, the thread siblings will be sorted based on +# the message UID in ascending order. This option is only applicable for +# client-side threading with a backend that enables sorting. Note that there's +# a performance impact when sorting is activated. +# +# Default: false +#sort-thread-siblings=false + +#[ui:account=foo] +# +# Enable a threaded view of messages. If this is not supported by the backend +# (IMAP server or notmuch), threads will be built by the client. +# +# Default: false +#threading-enabled=false + +# Force client-side thread building +# +# Default: false +#force-client-threads=false + +# Debounce client-side thread building +# +# Default: 50ms +#client-threads-delay=50ms + +[statusline] +# +# Describes the format for the status line. This is a comma separated list of +# column names with an optional align and width suffix. See [ui].index-columns +# for more details. To completely mute the status line except for push +# notifications, explicitly set status-columns to an empty string. +# +# Default: left<*,center:=,right>* +#status-columns=left<*,center:=,right>* + +# +# Each name in status-columns must have a corresponding column-$name setting. +# All column-$name settings accept golang text/template syntax. See +# aerc-templates(7) for available template attributes and functions. +# +# Default settings +#column-left=[{{.Account}}] {{.StatusInfo}} +#column-center={{.PendingKeys}} +#column-right={{.TrayInfo}} + +# +# String separator inserted between columns. +# See [ui].column-separator for more details. +# +#column-separator=" " + +# Specifies the separator between grouped statusline elements. +# +# Default: " | " +#separator=" | " + +# Defines the mode for displaying the status elements. +# Options: text, icon +# +# Default: text +#display-mode=text + +[viewer] +# +# Specifies the pager to use when displaying emails. Note that some filters +# may add ANSI codes to add color to rendered emails, so you may want to use a +# pager which supports ANSI codes. +# +# Default: less -R +#pager=less -R + +# +# If an email offers several versions (multipart), you can configure which +# mimetype to prefer. For example, this can be used to prefer plaintext over +# html emails. +# +# Default: text/plain,text/html +#alternatives=text/plain,text/html + +# +# Default setting to determine whether to show full headers or only parsed +# ones in message viewer. +# +# Default: false +#show-headers=false + +# +# Layout of headers when viewing a message. To display multiple headers in the +# same row, separate them with a pipe, e.g. "From|To". Rows will be hidden if +# none of their specified headers are present in the message. +# +# Default: From|To,Cc|Bcc,Date,Subject +#header-layout=From|To,Cc|Bcc,Date,Subject + +# Whether to always show the mimetype of an email, even when it is just a single part +# +# Default: false +#always-show-mime=false + +# Parses and extracts http links when viewing a message. Links can then be +# accessed with the open-link command. +# +# Default: true +#parse-http-links=true + +[compose] +# +# Specifies the command to run the editor with. It will be shown in an embedded +# terminal, though it may also launch a graphical window if the environment +# supports it. Defaults to $EDITOR, or vi. +#editor= + +# +# Default header fields to display when composing a message. To display +# multiple headers in the same row, separate them with a pipe, e.g. "To|From". +# +# Default: To|From,Subject +#header-layout=To|From,Subject + +# +# Specifies the command to be used to tab-complete email addresses. Any +# occurrence of "%s" in the address-book-cmd will be replaced with what the +# user has typed so far. +# +# The command must output the completions to standard output, one completion +# per line. Each line must be tab-delimited, with an email address occurring as +# the first field. Only the email address field is required. The second field, +# if present, will be treated as the contact name. Additional fields are +# ignored. +# +# This parameter can also be set per account in accounts.conf. +#address-book-cmd= + +# Specifies the command to be used to select attachments. Any occurence of '%s' +# in the file-picker-cmd will be replaced the argument to :attach -m +# . +# +# The command must output the selected files to standard output, one file per +# line. +#file-picker-cmd= + +# +# Allow to address yourself when replying +# +# Default: true +#reply-to-self=true + +# +# Warn before sending an email that matches the specified regexp but does not +# have any attachments. Leave empty to disable this feature. +# +# Uses Go's regexp syntax, documented at https://golang.org/s/re2syntax. The +# "(?im)" flags are set by default (case-insensitive and multi-line). +# +# Example: +# no-attachment-warning=^[^>]*attach(ed|ment) +# +#no-attachment-warning= + +# +# When set, aerc will generate "format=flowed" bodies with a content type of +# "text/plain; format=flowed" as described in RFC3676. This format is easier to +# handle for some mailing software, and generally just looks like ordinary +# text. To actually make use of this format's features, you'll need support in +# your editor. +# +#format-flowed=false + +[multipart-converters] +# +# Converters allow to generate multipart/alternative messages by converting the +# main text/plain part into any other MIME type. Only exact MIME types are +# accepted. The commands are invoked with sh -c and are expected to output +# valid UTF-8 text. +# +# Example (obviously, this requires that you write your main text/plain body +# using the markdown syntax): +#text/html=pandoc -f markdown -t html --standalone + +[filters] +# +# Filters allow you to pipe an email body through a shell command to render +# certain emails differently, e.g. highlighting them with ANSI escape codes. +# +# The commands are invoked with sh -c. The following folders are appended to +# the system $PATH to allow referencing filters from their name only: +# +# ${XDG_CONFIG_HOME:-~/.config}/aerc/filters +# ${XDG_DATA_HOME:-~/.local/share}/aerc/filters +# $PREFIX/share/aerc/filters +# /usr/share/aerc/filters +# +# The following variables are defined in the filter command environment: +# +# AERC_MIME_TYPE the part MIME type/subtype +# AERC_FORMAT the part content type format= parameter +# AERC_FILENAME the attachment filename (if any) +# AERC_SUBJECT the message Subject header value +# AERC_FROM the message From header value +# +# The first filter which matches the email's mimetype will be used, so order +# them from most to least specific. +# +# You can also match on non-mimetypes, by prefixing with the header to match +# against (non-case-sensitive) and a comma, e.g. subject,text will match a +# subject which contains "text". Use header,~regex to match against a regex. +# +text/plain=colorize +text/calendar=calendar +message/delivery-status=colorize +message/rfc822=colorize +#text/html=pandoc -f html -t plain | colorize +#text/html=html | colorize +#text/*=bat -fP --file-name="$AERC_FILENAME" +#application/x-sh=bat -fP -l sh +#image/*=catimg -w $(tput cols) - +#subject,~Git(hub|lab)=lolcat -f +#from,thatguywhodoesnothardwraphismessages=wrap -w 100 | colorize + +# This special filter is only used to post-process email headers when +# [viewer].show-headers=true +# By default, headers are piped directly into the pager. +# +.headers=colorize + +[openers] +# +# Openers allow you to specify the command to use for the :open and :open-link +# actions on a per-MIME-type basis. The :open-link URL scheme is used to +# determine the MIME type as follows: x-scheme-handler/. +# +# {} is expanded as the temporary filename to be opened. If it is not +# encountered in the command, the temporary filename will be appened to the end +# of the command. +# +# Like [filters], openers support basic shell globbing. The first opener which +# matches the part's MIME type (or URL scheme handler MIME type) will be used, +# so order them from most to least specific. +# +# Examples: +# x-scheme-handler/irc=hexchat +# x-scheme-handler/http*=firefox +# text/html=surf -dfgms +# text/plain=gvim {} +125 +# message/rfc822=thunderbird + +[hooks] +# +# Hooks are triggered whenever the associated event occurs. + +# +# Executed when a new email arrives in the selected folder +#mail-received=notify-send "New mail from $AERC_FROM_NAME" "$AERC_SUBJECT" + +# +# Executed when aerc starts +#aerc-startup=aerc :terminal calcurse && aerc :next-tab + +# +# Executed when aerc shuts down. +#aerc-shutdown= + +[templates] +# Templates are used to populate email bodies automatically. +# + +# The directories where the templates are stored. It takes a colon-separated +# list of directories. If this is unset or if a template cannot be found, the +# following paths will be used as a fallback in that order: +# +# ${XDG_CONFIG_HOME:-~/.config}/aerc/templates +# ${XDG_DATA_HOME:-~/.local/share}/aerc/templates +# /nix/store/jzlsc52f1zsczi5rmjrbff45i7ng3cph-aerc-0.15.2/share/aerc/templates +# +#template-dirs= + +# The default template to be used for new messages. +# +# default: new_message +#new-message=new_message + +# The default template to be used for quoted replies. +# +# default: quoted_reply +#quoted-reply=quoted_reply + +# The default template to be used for forward as body. +# +# default: forward_as_body +#forwards=forward_as_body diff --git a/user/modules/utils/email/config/binds.conf b/user/modules/utils/email/config/binds.conf new file mode 100644 index 0000000..e7a743f --- /dev/null +++ b/user/modules/utils/email/config/binds.conf @@ -0,0 +1,129 @@ +# Binds are of the form = +# To use '=' in a key sequence, substitute it with "Eq": "" +# If you wish to bind #, you can wrap the key sequence in quotes: "#" = quit + = :prev-tab + = :next-tab + = :term +? = :help keys + +[messages] +q = :quit + +j = :next + = :next + = :next 50% + = :next 100% + = :next 100% + +k = :prev + = :prev + = :prev 50% + = :prev 100% + = :prev 100% +g = :select 0 +G = :select -1 + +J = :next-folder +K = :prev-folder +H = :collapse-folder +L = :expand-folder + +v = :mark -t +V = :mark -v + +T = :toggle-threads + + = :view +d = :prompt 'Really delete this message?' 'delete-message' +D = :delete +A = :archive flat + +C = :compose + +rr = :reply -a +rq = :reply -aq +Rr = :reply +Rq = :reply -q + +c = :cf +$ = :term +! = :term +| = :pipe + +/ = :search +\ = :filter +n = :next-result +N = :prev-result + = :clear + +[messages:folder=Drafts] + = :recall + +[view] +/ = :toggle-key-passthrough/ +q = :close +O = :open +S = :save +| = :pipe +D = :delete +A = :archive flat + + = :open-link + +f = :forward +rr = :reply -a +rq = :reply -aq +Rr = :reply +Rq = :reply -q + +H = :toggle-headers + = :prev-part + = :next-part +J = :next +K = :prev + +[view::passthrough] +$noinherit = true +$ex = + = :toggle-key-passthrough + +[compose] +# Keybindings used when the embedded terminal is not selected in the compose +# view +$noinherit = true +$ex = + = :prev-field + = :next-field + = :switch-account -p + = :switch-account -n + = :next-field + = :prev-field + = :prev-tab + = :next-tab + +[compose::editor] +# Keybindings used when the embedded terminal is selected in the compose view +$noinherit = true +$ex = + = :prev-field + = :next-field + = :prev-tab + = :next-tab + +[compose::review] +# Keybindings used when reviewing a message to be sent +y = :send +n = :abort +v = :preview +p = :postpone +q = :choose -o d discard abort -o p postpone postpone +e = :edit +a = :attach +d = :detach + +[terminal] +$noinherit = true +$ex = + + = :prev-tab + = :next-tab diff --git a/user/modules/utils/email/default.nix b/user/modules/utils/email/default.nix new file mode 100644 index 0000000..aa4babd --- /dev/null +++ b/user/modules/utils/email/default.nix @@ -0,0 +1,19 @@ +{ lib, config, ... }: + +with lib; +let + cfg = config.modules.user.utils.email; + +in +{ options.modules.user.utils.email = { enable = mkEnableOption "user.utils.email"; }; + config = mkIf cfg.enable { + programs.aerc = { + enable = true; + }; + + home.file.".config/aerc" = { + source = ./config; + recursive = true; + }; + }; +} diff --git a/user/modules/utils/irc/default.nix b/user/modules/utils/irc/default.nix new file mode 100644 index 0000000..119e926 --- /dev/null +++ b/user/modules/utils/irc/default.nix @@ -0,0 +1,17 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.utils.irc; + +in +{ options.modules.user.utils.irc = { enable = mkEnableOption "user.utils.irc"; }; + config = mkIf cfg.enable { + home.packages = with pkgs; [ + weechat + ]; + programs.bash.shellAliases = { + chat = "weechat"; + }; + }; +} diff --git a/user/modules/utils/writing/default.nix b/user/modules/utils/writing/default.nix new file mode 100644 index 0000000..5d83096 --- /dev/null +++ b/user/modules/utils/writing/default.nix @@ -0,0 +1,16 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.utils.writing; + +in +{ options.modules.user.utils.writing = { enable = mkEnableOption "Enable writing tools"; }; + config = mkIf cfg.enable { + home.packages = with pkgs; [ + mdbook + pandoc + asciidoctor + ]; + }; +} diff --git a/user/modules/vim/default.nix b/user/modules/vim/default.nix new file mode 100644 index 0000000..68aed8a --- /dev/null +++ b/user/modules/vim/default.nix @@ -0,0 +1,24 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.user.vim; + +in +{ options.modules.user.vim = { enable = mkEnableOption "user.vim"; }; + config = mkIf cfg.enable { + programs.bash.shellAliases = { + vi = "${pkgs.vim}/bin/vim"; + }; + + home = { + packages = with pkgs; [ + vim + ]; + file.".vim" = { + source = ./vim; + recursive = true; + }; + }; + }; +} diff --git a/user/modules/vim/vim b/user/modules/vim/vim new file mode 160000 index 0000000..e5ff26b --- /dev/null +++ b/user/modules/vim/vim @@ -0,0 +1 @@ +Subproject commit e5ff26b6f6ec9b8e9f8737dc5418d6a64a68ec4b