commit 072951659ac501bc94e4ba977f74d8e584c7d4b7 Author: Bryan Ramos Date: Sun Mar 15 11:08:33 2026 -0400 pruned 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/.git-crypt/.gitattributes b/.git-crypt/.gitattributes new file mode 100644 index 0000000..665b10e --- /dev/null +++ b/.git-crypt/.gitattributes @@ -0,0 +1,4 @@ +# Do not edit this file. To specify the files to encrypt, create your own +# .gitattributes file in the directory where your files are. +* !filter !diff +*.gpg binary diff --git a/.git-crypt/keys/default/0/AF6A8929FDBAD915B69065400908F4B4DB72C73D.gpg b/.git-crypt/keys/default/0/AF6A8929FDBAD915B69065400908F4B4DB72C73D.gpg new file mode 100644 index 0000000..52c4ede Binary files /dev/null and b/.git-crypt/keys/default/0/AF6A8929FDBAD915B69065400908F4B4DB72C73D.gpg differ diff --git a/.git-crypt/keys/default/0/B4B6203BEFAB54034918F2E0A68297986D710740.gpg b/.git-crypt/keys/default/0/B4B6203BEFAB54034918F2E0A68297986D710740.gpg new file mode 100644 index 0000000..74a5df9 Binary files /dev/null and b/.git-crypt/keys/default/0/B4B6203BEFAB54034918F2E0A68297986D710740.gpg differ diff --git a/.git-crypt/keys/default/0/BED465025664C2BF8209F1E5073C16CD71F334CC.gpg b/.git-crypt/keys/default/0/BED465025664C2BF8209F1E5073C16CD71F334CC.gpg new file mode 100644 index 0000000..5095b2f Binary files /dev/null and b/.git-crypt/keys/default/0/BED465025664C2BF8209F1E5073C16CD71F334CC.gpg differ diff --git a/.git-crypt/keys/default/0/F1F3466458452B2DF351F1E864D12BA95ACE1F2D.gpg b/.git-crypt/keys/default/0/F1F3466458452B2DF351F1E864D12BA95ACE1F2D.gpg new file mode 100644 index 0000000..d35bb40 Binary files /dev/null and b/.git-crypt/keys/default/0/F1F3466458452B2DF351F1E864D12BA95ACE1F2D.gpg differ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a47d6ed --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +**/*.key filter=git-crypt diff=git-crypt 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..ffbc68a Binary files /dev/null and b/system/keys/desktop/ssh.pub.key differ 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..ebf57f0 --- /dev/null +++ b/user/home.nix @@ -0,0 +1,39 @@ +{ lib, pkgs, config, ... }: + +let + pass = pkgs.pass.withExtensions (exts: with exts; [ + pass-audit + pass-otp + pass-update + ]); + +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/yubikey.pub.key b/user/keys/age/yubikey.pub.key new file mode 100644 index 0000000..026c9a8 Binary files /dev/null and b/user/keys/age/yubikey.pub.key differ 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/company.pub.key b/user/keys/pgp/company.pub.key new file mode 100755 index 0000000..6b4030a Binary files /dev/null and b/user/keys/pgp/company.pub.key differ diff --git a/user/keys/pgp/work.pub.key b/user/keys/pgp/work.pub.key new file mode 100755 index 0000000..722f959 Binary files /dev/null and b/user/keys/pgp/work.pub.key differ diff --git a/user/keys/pgp/yubikey.pub.key b/user/keys/pgp/yubikey.pub.key new file mode 100644 index 0000000..56c1b13 Binary files /dev/null and b/user/keys/pgp/yubikey.pub.key differ diff --git a/user/keys/ssh/graphone.pub.key b/user/keys/ssh/graphone.pub.key new file mode 100644 index 0000000..55e8f1b Binary files /dev/null and b/user/keys/ssh/graphone.pub.key differ diff --git a/user/keys/ssh/work.pub.key b/user/keys/ssh/work.pub.key new file mode 100644 index 0000000..3d61b38 Binary files /dev/null and b/user/keys/ssh/work.pub.key differ diff --git a/user/keys/ssh/yubikey.pub.key b/user/keys/ssh/yubikey.pub.key new file mode 100644 index 0000000..217a8e3 Binary files /dev/null and b/user/keys/ssh/yubikey.pub.key differ 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..bc3734a --- /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.company}"; + 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