commit 864c69fe610ec0648bb148821b19e3f6600149b2 Author: Bryan Ramos Date: Wed Apr 15 20:58:07 2026 -0400 init diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fad0876 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.qcow2 +result +.direnv +.claude diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8823f91 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,19 @@ +[submodule "nvim"] + path = user/modules/neovim/nvim + url = https://github.com/itme-brain/nvim.git + branch = master + +[submodule "vim"] + path = user/modules/vim/vim + url = https://github.com/itme-brain/vim.git + branch = master + +[submodule "git"] + path = user/modules/git/git + url = https://github.com/itme-brain/git.git + branch = master + +[submodule "bash"] + path = user/modules/bash/bash + url = https://github.com/itme-brain/bash.git + branch = master diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..6fa100a --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,30 @@ +# 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 + # Shared secrets (desktop + server) + - path_regex: secrets/system/llama\.yaml$ # llama.cpp API key + key_groups: + - age: + - *desktop + - *server + # Server secrets (cameras) + - path_regex: secrets/system/cameras\.yaml$ # RTSP Feed + key_groups: + - age: + - *server + # Server secrets (searxng) + - path_regex: secrets/system/searxng\.yaml$ # searxng token + 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/external/README.md b/external/README.md new file mode 100644 index 0000000..3ceaaf9 --- /dev/null +++ b/external/README.md @@ -0,0 +1,6 @@ +# External Automation + +This directory contains automation for systems that are not managed as NixOS +hosts inside this repository. + +- `rigby/`: Ubuntu-based AI rig recovery and service automation. diff --git a/external/rigby/README.md b/external/rigby/README.md new file mode 100644 index 0000000..2fe3d06 --- /dev/null +++ b/external/rigby/README.md @@ -0,0 +1,64 @@ +# Rigby Recovery + +This directory contains disaster-recovery automation for `rigby`, the Ubuntu +AI rig at `192.168.0.23`. + +## Scope + +This automation manages the host state after a manual Ubuntu install. + +It is intended to restore the working state we validated for: + +- AMD ROCm `7.2.1` +- `amdgpu-dkms` +- `amdgpu.cwsr_enable=0` +- pinned ComfyUI checkout +- `uv`-managed Python `3.13` venv +- ROCm PyTorch +- ComfyUI service layout +- output sharing over Samba +- required groups and permissions + +## Manual Prerequisites + +These are intentionally documented, not automated: + +- Install Ubuntu `24.04.4` +- Update BIOS to the known-good version for the board +- Verify BIOS settings: + - `Above 4G Decoding = Enabled` + - `SVM = Enabled` + - UEFI boot + - sane PCIe slot configuration +- Ensure host SSH is reachable as `bryan` +- Ensure passwordless sudo works for `bryan` +- Ensure the initial DHCP lease is known so recovery can begin + +## Recovery Flow + +1. Install Ubuntu manually. +2. Clone this repository onto the operator machine. +3. From the repo root, run `just rigby-check HOST=`. +4. Run `just rigby-recover HOST=`. +5. Reboot `rigby`. +6. Validate: + - `rocminfo` + - `rocm-smi` + - ComfyUI startup + +## Notes + +- The AMD repo and package installs are automated here, but BIOS and physical + host setup remain manual. +- ComfyUI itself is deployed as an application under `/home/comfy/ComfyUI`. +- The `comfyui.service` unit is installed but left disabled so the service is + started on demand. +- Models, LoRAs, VAEs, outputs, and other AI assets are not restored by this + automation. `rigby` is the source of truth for that data, so disaster + recovery for models requires a separate backup strategy. +- The `just` entrypoints accept `HOST=` so recovery does not depend on a + fixed DHCP lease. +- Recovery installs the configured SSH key for `bryan`. +- Static IP configuration is applied at the end of the playbook via netplan. + The SSH session used for recovery may be interrupted once the new address is + applied, and subsequent access should use the final static IP. diff --git a/external/rigby/ansible.cfg b/external/rigby/ansible.cfg new file mode 100644 index 0000000..0e18eec --- /dev/null +++ b/external/rigby/ansible.cfg @@ -0,0 +1,9 @@ +[defaults] +inventory = inventory.ini +host_key_checking = False +stdout_callback = yaml +retry_files_enabled = False +interpreter_python = auto_silent + +[ssh_connection] +pipelining = True diff --git a/external/rigby/inventory.ini b/external/rigby/inventory.ini new file mode 100644 index 0000000..cb7f2b4 --- /dev/null +++ b/external/rigby/inventory.ini @@ -0,0 +1,2 @@ +[ai_rig] +rigby ansible_host=192.168.0.23 ansible_user=bryan diff --git a/external/rigby/playbooks/recover.yml b/external/rigby/playbooks/recover.yml new file mode 100644 index 0000000..1cdb476 --- /dev/null +++ b/external/rigby/playbooks/recover.yml @@ -0,0 +1,442 @@ +--- +- name: Recover rigby AI rig + hosts: ai_rig + become: true + vars: + rigby_user: bryan + rigby_recovery_ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDl4895aB9P5p/lp8Hq5rHun4clvhyTSHFi3U2d6OOBoW5Fm+VcQnW/xbjmCBsXk5BdiowsBxQhwnzdfz/KJL7J5RobomUEaVRwb9UwT88eJveLp14BG8j2J3SjfyhrCX+4jkPx0bPQk1HGcuYY+tPEXf1q/ps88Dhu0CARBIzYQOTYY6b1qWzxpDoFZGHjKG8g5iY6FIu65yKKvvVy1f8IgZ3l3IpwBWVamxgkTcYY0QYSrmzo1n7TXxwrWbvenAqBsQ0cBPs+gVa3uIr+1TJl0Az5SElBVGu3LvUdlk58trtPUj6TQR3YUkg7Vjll7WHOdqhux5ZQNhjkOsHerf0Tw86e6cEzgeTuIbQHIb0LcsUunwKcuh2+au7RO599cvHn0+xZE5MZBxloDDaJ3JsiliM8kyPP/U3ERj03cWLW7BqbT+sfjAOl21RCzk0iQxk1wt/8VmtCr9Adv7IyrtaYvf/bwRP+g+9ldmzKGt8Mdb605uVzZ70H/LLm17f40Te+QHaex5by/6p6cuwEEZtgIg53Wpglu0rA6UxrBfQEHKl/Jt3FLeE0mnEyYkkR2MnHNtyWRIXtuqYZMAm2Ub1pFHH7jQV1gGiDVTw6a2eIwK21a/hXtRjFUpFd1nB1n+KNfJBE4zT3wm3Ud7mKw/6rWnoRyhYZvGXkFdp+iEs49Q== itme-brain@github/78120816 + rigby_static_network_enabled: true + rigby_interface: eno1 + rigby_static_ip: 192.168.0.23/24 + rigby_gateway: 192.168.0.1 + rigby_dns: + - 192.168.0.1 + - 1.1.1.1 + comfy_user: comfy + comfy_group: comfy + comfy_home: /home/comfy + comfy_root: /home/comfy/ComfyUI + comfy_venv: /home/comfy/comfy-venv + comfy_python_version: "3.13" + comfy_port: 8188 + comfy_output_dir: /home/comfy/ComfyUI/output + comfy_repo_url: https://github.com/comfy-org/ComfyUI + comfy_repo_version: a1344238901efc5ea199d8094cb16fca36ceb28b + comfy_manager_version: "4.1" + comfy_torch_index_url: https://download.pytorch.org/whl/rocm7.2 + grub_cmdline_linux_default: "amdgpu.cwsr_enable=0" + amd_driver_deb: amdgpu-install_7.2.1.70201-1_all.deb + amd_driver_url: https://repo.radeon.com/amdgpu-install/7.2.1/ubuntu/noble/amdgpu-install_7.2.1.70201-1_all.deb + rigby_packages: + - curl + - git + - rsync + - software-properties-common + - python-is-python3 + - python3.13 + - python3.13-venv + - python3.13-dev + - build-essential + - linux-headers-{{ ansible_kernel }} + - linux-modules-extra-{{ ansible_kernel }} + - samba + - just + - python3.12 + - python3.12-venv + - docker.io + vllm_user: vllm + vllm_home: /home/vllm + vllm_venv: /home/vllm/vllm-venv + vllm_models: /home/vllm/models + vllm_port: 8000 + vllm_gpu_memory_utilization: "0.95" + vllm_rocm_wheels_url: https://wheels.vllm.ai/rocm/0.19.0/rocm721 + vllm_models_list: + - name: Qwen2.5-Coder-14B + recipe: coder + dir: Qwen2.5-Coder-14B-Instruct + max_model_len: 4096 + - name: Qwen2.5-7B-Instruct + recipe: qwen7b + dir: Qwen2.5-7B-Instruct + max_model_len: 8192 + tool_call_parser: hermes + librechat_root: /home/bryan/LibreChat + librechat_repo_url: https://github.com/danny-avila/LibreChat + tasks: + - name: Ensure deadsnakes PPA is configured + ansible.builtin.apt_repository: + repo: ppa:deadsnakes/ppa + state: present + update_cache: true + + - name: Install required Ubuntu packages + ansible.builtin.apt: + name: "{{ rigby_packages }}" + state: present + update_cache: true + + - name: Ensure AMD installer package is present + ansible.builtin.get_url: + url: "{{ amd_driver_url }}" + dest: "/tmp/{{ amd_driver_deb }}" + mode: "0644" + + - name: Install AMD installer package + ansible.builtin.apt: + deb: "/tmp/{{ amd_driver_deb }}" + state: present + + - name: Install AMD GPU DKMS driver + ansible.builtin.apt: + name: amdgpu-dkms + state: present + update_cache: true + + - name: Install ROCm stack + ansible.builtin.apt: + name: rocm + state: present + + - name: Ensure required groups exist + ansible.builtin.group: + name: "{{ item }}" + state: present + loop: + - render + - video + - "{{ comfy_group }}" + + - name: Ensure comfy user exists + ansible.builtin.user: + name: "{{ comfy_user }}" + group: "{{ comfy_group }}" + groups: + - render + - video + append: true + create_home: true + shell: /bin/bash + + - name: Ensure bryan is in required groups + ansible.builtin.user: + name: "{{ rigby_user }}" + groups: + - render + - video + - "{{ comfy_group }}" + append: true + + - name: Ensure recovery SSH keys are present for bryan + ansible.posix.authorized_key: + user: "{{ rigby_user }}" + state: present + key: "{{ item }}" + loop: "{{ rigby_recovery_ssh_keys }}" + + - name: Configure GRUB default kernel args + ansible.builtin.lineinfile: + path: /etc/default/grub + regexp: '^GRUB_CMDLINE_LINUX_DEFAULT=' + line: 'GRUB_CMDLINE_LINUX_DEFAULT="{{ grub_cmdline_linux_default }}"' + + - name: Ensure GRUB menu is shown + ansible.builtin.lineinfile: + path: /etc/default/grub + regexp: '^{{ item.key }}=' + line: "{{ item.key }}={{ item.value }}" + loop: + - { key: GRUB_TIMEOUT_STYLE, value: "menu" } + - { key: GRUB_TIMEOUT, value: "5" } + + - name: Regenerate grub config + ansible.builtin.command: update-grub + changed_when: true + + - name: Ensure Comfy directories exist + ansible.builtin.file: + path: "{{ item.path }}" + state: directory + owner: "{{ comfy_user }}" + group: "{{ comfy_group }}" + mode: "{{ item.mode }}" + loop: + - { path: "{{ comfy_home }}", mode: "0775" } + - { path: "{{ comfy_root }}", mode: "0775" } + - { path: "{{ comfy_output_dir }}", mode: "2775" } + - { path: "{{ comfy_home }}/.local/bin", mode: "0775" } + - { path: "{{ comfy_home }}/piptmp", mode: "0775" } + + - name: Ensure uv is installed for comfy + ansible.builtin.shell: | + set -euo pipefail + curl -LsSf https://astral.sh/uv/install.sh | sh + args: + creates: "{{ comfy_home }}/.local/bin/uv" + become_user: "{{ comfy_user }}" + + - name: Ensure ComfyUI repo is present at pinned revision + ansible.builtin.git: + repo: "{{ comfy_repo_url }}" + dest: "{{ comfy_root }}" + version: "{{ comfy_repo_version }}" + update: true + become_user: "{{ comfy_user }}" + + - name: Ensure ComfyUI venv exists + ansible.builtin.command: + argv: + - "{{ comfy_home }}/.local/bin/uv" + - venv + - --python + - "{{ comfy_python_version }}" + - "{{ comfy_venv }}" + args: + creates: "{{ comfy_venv }}/bin/python" + become_user: "{{ comfy_user }}" + + - name: Install base Python packaging tools in Comfy venv + ansible.builtin.command: + argv: + - "{{ comfy_home }}/.local/bin/uv" + - pip + - install + - --python + - "{{ comfy_venv }}/bin/python" + - --upgrade + - pip + - setuptools + - wheel + become_user: "{{ comfy_user }}" + + - name: Install ROCm PyTorch in Comfy venv + ansible.builtin.command: + argv: + - "{{ comfy_home }}/.local/bin/uv" + - pip + - install + - --python + - "{{ comfy_venv }}/bin/python" + - --index-url + - "{{ comfy_torch_index_url }}" + - torch + - torchvision + - torchaudio + environment: + TMPDIR: "{{ comfy_home }}/piptmp" + become_user: "{{ comfy_user }}" + + - name: Install ComfyUI requirements in Comfy venv + ansible.builtin.command: + argv: + - "{{ comfy_home }}/.local/bin/uv" + - pip + - install + - --python + - "{{ comfy_venv }}/bin/python" + - -r + - "{{ comfy_root }}/requirements.txt" + environment: + TMPDIR: "{{ comfy_home }}/piptmp" + become_user: "{{ comfy_user }}" + + - name: Install ComfyUI-Manager in Comfy venv + ansible.builtin.command: + argv: + - "{{ comfy_home }}/.local/bin/uv" + - pip + - install + - --python + - "{{ comfy_venv }}/bin/python" + - "comfyui-manager=={{ comfy_manager_version }}" + environment: + TMPDIR: "{{ comfy_home }}/piptmp" + become_user: "{{ comfy_user }}" + + - name: Ensure output directories have group inheritance + ansible.builtin.shell: | + set -euo pipefail + find "{{ comfy_output_dir }}" -type d -exec chown {{ comfy_user }}:{{ comfy_group }} {} + + find "{{ comfy_output_dir }}" -type d -exec chmod 2775 {} + + changed_when: true + + - name: Ensure output files are group writable + ansible.builtin.shell: | + set -euo pipefail + find "{{ comfy_output_dir }}" -type f -exec chown {{ comfy_user }}:{{ comfy_group }} {} + + find "{{ comfy_output_dir }}" -type f -exec chmod 0664 {} + + changed_when: true + + - name: Install ComfyUI systemd unit + ansible.builtin.template: + src: ../templates/comfyui.service.j2 + dest: /etc/systemd/system/comfyui.service + owner: root + group: root + mode: "0644" + + - name: Ensure Samba include directory exists + ansible.builtin.file: + path: /etc/samba/smb.conf.d + state: directory + owner: root + group: root + mode: "0755" + + - name: Install Samba share config for Comfy outputs + ansible.builtin.template: + src: ../templates/comfy-output.conf.j2 + dest: /etc/samba/smb.conf.d/comfy-output.conf + owner: root + group: root + mode: "0644" + + - name: Ensure Samba includes conf.d snippets + ansible.builtin.blockinfile: + path: /etc/samba/smb.conf + marker: "; {mark} ANSIBLE MANAGED COMFY OUTPUT INCLUDE" + block: | + include = /etc/samba/smb.conf.d/comfy-output.conf + + - name: Reload systemd + ansible.builtin.systemd_service: + daemon_reload: true + + - name: Ensure ComfyUI service is installed but disabled + ansible.builtin.systemd_service: + name: comfyui.service + enabled: false + + - name: Ensure Samba service is enabled and running + ansible.builtin.systemd_service: + name: smbd.service + enabled: true + state: started + + - name: Install netplan static IP config for rigby + ansible.builtin.template: + src: ../templates/99-rigby-static.yaml.j2 + dest: /etc/netplan/99-rigby-static.yaml + owner: root + group: root + mode: "0644" + when: rigby_static_network_enabled | bool + + - name: Apply static netplan configuration as final step + ansible.builtin.command: netplan apply + when: rigby_static_network_enabled | bool + changed_when: true + + # --- vLLM --- + + - name: Ensure vllm user exists + ansible.builtin.user: + name: "{{ vllm_user }}" + groups: + - render + - video + append: true + create_home: true + shell: /bin/bash + + - name: Ensure vllm models directory exists + ansible.builtin.file: + path: "{{ vllm_models }}" + state: directory + owner: "{{ vllm_user }}" + group: "{{ vllm_user }}" + mode: "0755" + + - name: Ensure uv is installed for vllm user + ansible.builtin.shell: | + set -euo pipefail + curl -LsSf https://astral.sh/uv/install.sh | sh + args: + creates: "{{ vllm_home }}/.local/bin/uv" + become_user: "{{ vllm_user }}" + + - name: Ensure vllm venv exists + ansible.builtin.command: + argv: + - "{{ vllm_home }}/.local/bin/uv" + - venv + - --python + - "3.12" + - "{{ vllm_venv }}" + args: + creates: "{{ vllm_venv }}/bin/python" + become_user: "{{ vllm_user }}" + + - name: Install vLLM in venv + ansible.builtin.command: + argv: + - "{{ vllm_home }}/.local/bin/uv" + - pip + - install + - --python + - "{{ vllm_venv }}/bin/python" + - vllm + - --extra-index-url + - "{{ vllm_rocm_wheels_url }}" + args: + creates: "{{ vllm_venv }}/bin/vllm" + become_user: "{{ vllm_user }}" + + - name: Install vllm justfile + ansible.builtin.template: + src: ../templates/vllm-justfile.j2 + dest: "{{ vllm_home }}/justfile" + owner: "{{ vllm_user }}" + group: "{{ vllm_user }}" + mode: "0644" + + - name: Ensure vllm bashrc sources api key from file + ansible.builtin.lineinfile: + path: "{{ vllm_home }}/.bashrc" + line: "export VLLM_API_KEY=$(cat {{ vllm_home }}/.api_key)" + state: present + + # --- LibreChat --- + + - name: Ensure Docker service is enabled and running + ansible.builtin.systemd_service: + name: docker + enabled: true + state: started + + - name: Ensure bryan is in docker group + ansible.builtin.user: + name: "{{ rigby_user }}" + groups: + - docker + append: true + + - name: Ensure LibreChat repo is present + ansible.builtin.git: + repo: "{{ librechat_repo_url }}" + dest: "{{ librechat_root }}" + update: false + become_user: "{{ rigby_user }}" + + - name: Install librechat.yaml config + ansible.builtin.template: + src: ../templates/librechat.yaml.j2 + dest: "{{ librechat_root }}/librechat.yaml" + owner: "{{ rigby_user }}" + group: "{{ rigby_user }}" + mode: "0644" + + - name: Install librechat systemd unit + ansible.builtin.template: + src: ../templates/librechat.service.j2 + dest: /etc/systemd/system/librechat.service + owner: root + group: root + mode: "0644" + + - name: Reload systemd and enable librechat service + ansible.builtin.systemd_service: + name: librechat.service + daemon_reload: true + enabled: true diff --git a/external/rigby/templates/99-rigby-static.yaml.j2 b/external/rigby/templates/99-rigby-static.yaml.j2 new file mode 100644 index 0000000..209d127 --- /dev/null +++ b/external/rigby/templates/99-rigby-static.yaml.j2 @@ -0,0 +1,16 @@ +network: + version: 2 + renderer: networkd + ethernets: + {{ rigby_interface }}: + dhcp4: false + addresses: + - {{ rigby_static_ip }} + routes: + - to: default + via: {{ rigby_gateway }} + nameservers: + addresses: +{% for dns in rigby_dns %} + - {{ dns }} +{% endfor %} diff --git a/external/rigby/templates/comfy-output.conf.j2 b/external/rigby/templates/comfy-output.conf.j2 new file mode 100644 index 0000000..32a7c86 --- /dev/null +++ b/external/rigby/templates/comfy-output.conf.j2 @@ -0,0 +1,9 @@ +[comfy-output] + path = {{ comfy_output_dir }} + browseable = yes + read only = no + guest ok = yes + force user = {{ comfy_user }} + force group = {{ comfy_group }} + create mask = 0664 + directory mask = 2775 diff --git a/external/rigby/templates/comfyui.service.j2 b/external/rigby/templates/comfyui.service.j2 new file mode 100644 index 0000000..aa0ac7d --- /dev/null +++ b/external/rigby/templates/comfyui.service.j2 @@ -0,0 +1,22 @@ +[Unit] +Description=ComfyUI +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User={{ comfy_user }} +Group={{ comfy_group }} +UMask=0002 +WorkingDirectory={{ comfy_root }} +Environment=HOME={{ comfy_home }} +Environment=COMFYUI_PATH={{ comfy_root }} +Environment=PATH={{ comfy_home }}/.local/bin:{{ comfy_venv }}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +ExecStart={{ comfy_venv }}/bin/python {{ comfy_root }}/main.py --highvram --enable-manager --listen 0.0.0.0 --port {{ comfy_port }} --disable-auto-launch +Restart=on-failure +RestartSec=5 +NoNewPrivileges=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/external/rigby/templates/librechat.service.j2 b/external/rigby/templates/librechat.service.j2 new file mode 100644 index 0000000..608dcbd --- /dev/null +++ b/external/rigby/templates/librechat.service.j2 @@ -0,0 +1,18 @@ +[Unit] +Description=LibreChat +After=network-online.target docker.service +Wants=network-online.target +Requires=docker.service + +[Service] +Type=simple +User={{ rigby_user }} +Group=docker +WorkingDirectory={{ librechat_root }} +ExecStart=/usr/bin/docker compose up +ExecStop=/usr/bin/docker compose down +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/external/rigby/templates/librechat.yaml.j2 b/external/rigby/templates/librechat.yaml.j2 new file mode 100644 index 0000000..640fd7c --- /dev/null +++ b/external/rigby/templates/librechat.yaml.j2 @@ -0,0 +1,43 @@ +version: 1.3.5 + +cache: true + +interface: + webSearch: false + runCode: false + mcpServers: + use: true + create: true + share: false + public: false + +mcpServers: + searxng: + command: npx + args: + - -y + - mcp-searxng + env: + SEARXNG_URL: http://searxng:8080 + timeout: 60000 + fetch: + command: uvx + args: + - mcp-server-fetch + - --ignore-robots-txt + timeout: 60000 + +endpoints: + custom: + - name: "rigby-vllm" + apiKey: "${VLLM_API_KEY}" + baseURL: "http://host.docker.internal:{{ vllm_port }}/v1" + models: + default: [] + fetch: true + titleConvo: true + titleModel: "current_model" + titleMessageRole: "user" + summarize: false + summaryModel: "current_model" + modelDisplayLabel: "Rigby vLLM" diff --git a/external/rigby/templates/vllm-justfile.j2 b/external/rigby/templates/vllm-justfile.j2 new file mode 100644 index 0000000..33e0f11 --- /dev/null +++ b/external/rigby/templates/vllm-justfile.j2 @@ -0,0 +1,58 @@ +{% raw %}# List available recipes +[private] +default: + @just --list + +# Show currently running vLLM server +status: + @pgrep -a -f "vllm serve" || echo "No vLLM server running" + +# Tail the vLLM log +logs: + @tail -f {% endraw %}{{ vllm_home }}/vllm.log{% raw %} + +# Stop any running vLLM server and wait for VRAM to free +stop: + #!/usr/bin/env bash + set -euo pipefail + if pgrep -f "vllm serve" > /dev/null; then + echo "Stopping vLLM..." + pkill -TERM -f "vllm serve" || true + sleep 2 + pkill -KILL -f "vllm serve" 2>/dev/null || true + fi + echo "Waiting for VRAM to release..." + for i in $(seq 1 30); do + used=$(rocm-smi --showmeminfo vram 2>/dev/null | awk '/VRAM Total Used Memory/ {print $NF}') + total=$(rocm-smi --showmeminfo vram 2>/dev/null | awk '/VRAM Total Memory \(B\)/ {print $NF}') + if [ -n "$used" ] && [ -n "$total" ] && [ "$total" -gt 0 ]; then + pct=$(( used * 100 / total )) + echo " VRAM: ${pct}%" + if [ "$pct" -lt 10 ]; then + echo "VRAM free." + exit 0 + fi + fi + sleep 2 + done + echo "Warning: VRAM did not fully release after 60s" +{% endraw %} +{% for model in vllm_models_list %} +# Serve {{ model.name }} +{{ model.recipe }}: stop + #!/usr/bin/env bash + source {{ vllm_home }}/.bashrc + nohup {{ vllm_home }}/vllm-venv/bin/vllm serve {{ vllm_models }}/{{ model.dir }} \ + --served-model-name {{ model.name }} \ + --host 0.0.0.0 \ + --port {{ vllm_port }} \ + --api-key ${VLLM_API_KEY} \ + --dtype auto \ + --max-model-len {{ model.max_model_len }} \ + --gpu-memory-utilization {{ vllm_gpu_memory_utilization }}{% if model.tool_call_parser is defined %} \ + --enable-auto-tool-choice \ + --tool-call-parser {{ model.tool_call_parser }}{% endif %} \ + > {{ vllm_home }}/vllm.log 2>&1 & + echo "Started {{ model.name }} (pid $!). Run 'just logs' to follow." + +{% endfor %} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..aa265c3 --- /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": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", + "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..1ecc950 --- /dev/null +++ b/flake.nix @@ -0,0 +1,89 @@ +{ + 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 = { self, 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 + rclone + ansible + + age + sops + ssh-to-age + + git + git-crypt + gnupg + + yubikey-manager + age-plugin-yubikey + ]; + }; + }; +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..2869dcb --- /dev/null +++ b/justfile @@ -0,0 +1,383 @@ +SYSTEM := "$(echo $HOSTNAME)" +VALID_SYSTEMS := "desktop server wsl" +RIGBY_DIR := "external/rigby" +RIGBY_HOST := "192.168.0.23" + +# Print this list +default: + @just --list + +# Verify SSH connectivity and Ansible access to the Ubuntu AI rig. +[group('rigby')] +rigby-check HOST=RIGBY_HOST: + @cd {{RIGBY_DIR}} && ansible -i "{{HOST}}," all -u bryan -m ping + +# Apply the disaster-recovery playbook for the Ubuntu AI rig. +[group('rigby')] +rigby-recover HOST=RIGBY_HOST: + @cd {{RIGBY_DIR}} && ansible-playbook -i "{{HOST}}," -u bryan playbooks/recover.yml + +# Preview rig recovery changes without modifying the target host. +[group('rigby')] +rigby-recover-dry-run HOST=RIGBY_HOST: + @cd {{RIGBY_DIR}} && ansible-playbook -i "{{HOST}}," -u bryan playbooks/recover.yml --check --diff + +# 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..92e28d4 --- /dev/null +++ b/secrets/README.md @@ -0,0 +1,68 @@ +# 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 +``` 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/llama.yaml b/secrets/system/llama.yaml new file mode 100644 index 0000000..6df19c1 --- /dev/null +++ b/secrets/system/llama.yaml @@ -0,0 +1,25 @@ +LLAMA_API_KEY: ENC[AES256_GCM,data:ZVDpwGAxnHbHxt+JW3mYGyyBU5JfFAbjc/byq6Ok9wTlpQZBx969Z0wV74F5pR4axmpdGs7XlZDh1rJaQTn7lg==,iv:oAG9G25x+1FRkRNBRzLW2UJmbSxgx5Cu64Qo/6VzAyw=,tag:nkO/SdzjjLxH4fkgIdwUYQ==,type:str] +sops: + age: + - recipient: age17ejyzyk52unr6eyaa9rpunxpmf7u9726v6sx7me3ww3mdu5xzgjqsgj9gl + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzUmV6Q2dCMWU3TUFkZ0I0 + dHA3dXd2U0RSRzNtL3YvdG8rYWdnOTZoTkMwCkNnYnVlVmMyRDNnS1FmWktlNU9N + UW1OMlJYODVzSHNIZWZMRkpPY05Ed3cKLS0tIDg0b0VkT0NrS3NIWE9EdWtWYXc1 + NjNESHpYbVptcnVRYWFKb3RlYkJ6OWMK3JsRXPDvJdKv2UyYIH8kr/WKbXgUDXbc + fYOD0Huo73BA0vr8PlrsF4STVgJr/arKCMdI1C0bDdcwjExKnR1tIw== + -----END AGE ENCRYPTED FILE----- + - recipient: age198jg29ryg3c0qj3yg6y9ha4ce2ue4hjdaa9kalf49fxju74dhchsquvjzp + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFTGNKOWczaityaXowWi9I + dmh0MjJoelV3bVlzeGpLZmVTVzJjckwwQUFzCk81ZHlTcm5oWHRQNklreUR4bWNS + OVdQelQ4YXkzeWZqOWZoNWlOVkZpWUkKLS0tIDZKQUU3LzV0UUhnRHVHQkFadkxm + djRyUEYyZ2srMlVxR0JtQlFqSWV1QWcKMIF9Sq4TUUmpVZAukjTjFbIrMxcE3+el + QSrHIm1HXLXwCKLDQ2N6b8Q9iUo/XMV0wsD3TLxdnUfegpQpfsDhag== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-04-14T05:45:37Z" + mac: ENC[AES256_GCM,data:G+o6OhNF5AFBDKQEU3f1MZ+GOkxQj/m7NNk4Ti8PxPPOHdByoCrauvgB78SdQf5ubcfupElcNB0yF5QsG3/m7eGaSA+8J0cDL6jB3NEE5EUbW1Fuzzg2Ez1JnFu4BstkLiDRD/TribXMNFAjykmNrHt4zee6fhU3H0MOn7+Acok=,iv:IqBLSBq1kOMRHQn1IvU8OgmWGn6EFJcef/rNr38txmY=,tag:/mSWgbPbhUNoIm3x+6zyRA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.1 diff --git a/secrets/system/searxng.yaml b/secrets/system/searxng.yaml new file mode 100644 index 0000000..2834056 --- /dev/null +++ b/secrets/system/searxng.yaml @@ -0,0 +1,16 @@ +SEARXNG_TOKEN: ENC[AES256_GCM,data:6hI9+Gk9D7OjgcNV7WHUkcT8Kzta+QbJ8bq5uDv1AU/n1lpD/41RSWAZ91v5f0VSAldKvDMIuRdjxmKaE0ITOA==,iv:LURC0t6YwectCMllBBx8TIGxM80vXS84pkvczmWtO6U=,tag:ZSRbU7B+LKsLw8R7Yi9uzg==,type:str] +sops: + age: + - recipient: age198jg29ryg3c0qj3yg6y9ha4ce2ue4hjdaa9kalf49fxju74dhchsquvjzp + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWkRjc0xWR3o5ZFlnUXRY + bnNKUWhNL3dFcnJ0a0tLZHNuRU1obEwzam44CjczSEEvdVUyUXN5MEpsYnA5Y3NQ + M2N6VXZpM3ZxMHd0ZWV1MG5qT3ZnZzgKLS0tIGN3UG01eW4xZ1A4bkF3TzNKVkdv + eE9uRmpId2R5VTJSeDhRVUkvSWt4RHMK/oXVHDAWN5SY/4hPCm0QsTo2ubBD+uBf + fOZr/4HNDOyq8AIfbRVbilC7l/Ozg8snu8chRo1keCjqHp+Pt+Yzhw== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-04-13T19:21:23Z" + mac: ENC[AES256_GCM,data:qxBRF1wSTXeFEvjs+5HiRmk6wqt1Rtx1kNFigpqicfd/IJsZTJY/6g3SmZXrJNkpkYwyOBNlblGfLAfKMWm6/eg1KYBJHlSAAqkH/xECdDqKY0rTkMj3rfPTZzLGmi4kVp6v8jg9OO5SwK8sLGtbaK2S/VjdTI0NXlMMnsB30Sg=,iv:GacmqywEsxCyKQKmCPu42uyqy6Q0JhR7STDAMFvW7kQ=,tag:G1Lbu6DKuO5yLWs6kXjwHA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.1 diff --git a/secrets/system/wifi.yaml b/secrets/system/wifi.yaml new file mode 100644 index 0000000..6224546 --- /dev/null +++ b/secrets/system/wifi.yaml @@ -0,0 +1,19 @@ +WIFI_HOME_SSID: ENC[AES256_GCM,data:xZl6DE4=,iv:koEKZTW3O+bctlwoSzZCBLRT4iG380RmP/olukUd8Xc=,tag:4HM6d+FslbM1hRYcn3JTqA==,type:str] +WIFI_HOME_PSK: ENC[AES256_GCM,data:jyC4VXzhpIE=,iv:fN33x0y4kmRrPQe7ydWGdeTQaR5a3ekBaUKHX9FpHk0=,tag:tQUUj5LU6kidYTTI2RWf8w==,type:str] +WIFI_CAMS_SSID: ENC[AES256_GCM,data:yJ/oUCfSbaw=,iv:foswCMqFLOUyPQP9KL08Mhix0j2+Jt4sHHaPV49RFe4=,tag:rBG9IyQDmbNsUp4E+tnmZg==,type:str] +WIFI_CAMS_PSK: ENC[AES256_GCM,data:VlVxqxbHof6rmqSRJrXEQsT15BNl1lrghg==,iv:B6si07a0Z5ZJfMkK0HN9fa5zvQDzf7lvIQt1ZBpBZdk=,tag:21622mki8lITWA5fh7bKrg==,type:str] +sops: + age: + - recipient: age17ejyzyk52unr6eyaa9rpunxpmf7u9726v6sx7me3ww3mdu5xzgjqsgj9gl + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuaFRWRnNGVm9TTkhJVFAv + RWQ1Q2Q3RStBa0E4V2hFYUV2ZHFPZnJGdkFJClY0WThYbWk2Nmx6V0g4UU9WSGRZ + bFpNalZJRlZyWjFTMU1JK1dpWndPS3cKLS0tIHI0M3ZUVlI3TTV6c2h1WmdrdW1l + VWtxaFNVUUFHT20xVTZpSjVWRHozTzQKAAsNbFf6bU6eelqOX7Ei+Zrtw0aw0WgQ + 5zOWrxd92MaG/AvVpL0jC1LuWtZeK3MK7Qpgtm8t0rgugUas16KYpA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-03-15T06:25:12Z" + mac: ENC[AES256_GCM,data:+Lhmcr2Jg1htfcMMMPu8AxrDhvlm4yLVIunxAcs4adX8NeJccD+/UVvZO+qtzF6iQmXCdTvRDo3shqmJKHvs6ZUJVe3jokTKMJoQdIbSIS0fSwULUV8evK5Incf8qzpnHd2J1Kg4qCL8oWeN9t4TBJTPVrNJzd/sOF1Kp2g9IBE=,iv:/ORst/Lnj3h16fJQWxAaJ5vMWKMN2lGhGoIQjNxNpGQ=,tag:NURI5mwbfECaWTgbSs6clA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.1 diff --git a/system/keys/default.nix b/system/keys/default.nix new file mode 100644 index 0000000..c946ac3 --- /dev/null +++ b/system/keys/default.nix @@ -0,0 +1,43 @@ +{ lib, ... }: + +with lib; +with builtins; +let + extractName = filename: + let + noKey = removeSuffix ".key" filename; + noMarkers = replaceStrings + [ ".pub" ".priv" ".public" ".private" ] + [ "" "" "" "" ] + noKey; + in noMarkers; + + constructKeys = dir: ( + listToAttrs ( + map (subdir: { + name = subdir; + value = listToAttrs ( + map (file: { + name = extractName file; + value = readFile "${dir}/${subdir}/${file}"; + }) (filter (file: + (readDir "${dir}/${subdir}").${file} == "regular" && + hasSuffix ".key" file + ) (attrNames (readDir "${dir}/${subdir}"))) + ); + }) (filter (node: (readDir dir).${node} == "directory") (attrNames (readDir dir))) + ) + ); + +in +{ + options = { + machines = mkOption { + description = "Machine Configurations"; + type = types.attrs; + default = { + keys = constructKeys ./.; + }; + }; + }; +} diff --git a/system/keys/desktop/ssh.pub.key b/system/keys/desktop/ssh.pub.key new file mode 100644 index 0000000..4604ca9 --- /dev/null +++ b/system/keys/desktop/ssh.pub.key @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYXfu4Jc/HtdyhOfAdCXYzhqCubIq3Bz6Kl9NDUov76 bryan@desktop diff --git a/system/machines/desktop/README.md b/system/machines/desktop/README.md new file mode 100644 index 0000000..92c75d7 --- /dev/null +++ b/system/machines/desktop/README.md @@ -0,0 +1,19 @@ +## Hardware + +| Component | Model | +|-------------|------------------------------------| +| Motherboard | MSI B760 GAMING PLUS WIFI | +| CPU | Intel Core i7-12700KF (12th Gen) | +| GPU | NVIDIA GeForce GTX 1650 | +| Storage | 2x 2TB Crucial MX500 SSD | + +## Memory + +| Slot | Size | Manufacturer | Part Number | Speed | +|---------|------|----------------|-------------|------------| +| DIMM A1 | - | - | - | - | +| DIMM A2 | 16GB | Team Group Inc | UD5-6000 | 4800 MT/s | +| DIMM B1 | - | - | - | - | +| DIMM B2 | 16GB | Team Group Inc | UD5-6000 | 4800 MT/s | + +**Total: 32GB DDR5** diff --git a/system/machines/desktop/default.nix b/system/machines/desktop/default.nix new file mode 100644 index 0000000..539aa63 --- /dev/null +++ b/system/machines/desktop/default.nix @@ -0,0 +1,17 @@ +{ 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 + ../../modules/docker + ./hardware.nix + ./system.nix + ]; +} diff --git a/system/machines/desktop/hardware.nix b/system/machines/desktop/hardware.nix new file mode 100644 index 0000000..052bdfa --- /dev/null +++ b/system/machines/desktop/hardware.nix @@ -0,0 +1,89 @@ +{ 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; + }; + nvidia-container-toolkit.enable = true; + 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..41e88bc --- /dev/null +++ b/system/machines/desktop/modules/home-manager/home.nix @@ -0,0 +1,94 @@ +{ 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" ]; + }; + }; + + systemd.user.services.comfy-mount = { + Unit = { + Description = "Mount ComfyUI outputs via SSHFS"; + After = [ "network-online.target" ]; + }; + Service = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p %h/Media/Comfy"; + ExecStart = "${pkgs.sshfs}/bin/sshfs -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 rigby:/home/comfy/ComfyUI/output %h/Media/Comfy"; + ExecStop = "${pkgs.fuse}/bin/fusermount -u %h/Media/Comfy"; + }; + Install = { + WantedBy = [ "default.target" ]; + }; + }; + + programs.ssh = { + enable = true; + enableDefaultConfig = false; + matchBlocks = { + "*" = { + serverAliveInterval = 60; + serverAliveCountMax = 3; + }; + "server" = { + hostname = "192.168.0.154"; + user = "bryan"; + }; + "rigby" = { + hostname = "192.168.0.23"; + 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..e981ab8 --- /dev/null +++ b/system/machines/desktop/system.nix @@ -0,0 +1,212 @@ +{ pkgs, lib, config, ... }: + +let + gpgEnabled = lib.any + (user: user.modules.user.security.gpg.enable or false) + (lib.attrValues config.home-manager.users); + + devEnabled = lib.any + (user: user.modules.user.utils.dev.enable or false) + (lib.attrValues config.home-manager.users); + + sysModules = config.modules.system; + +in +{ system.stateVersion = "23.11"; + + modules.system.sops.enable = true; + modules.system.docker.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; + } // lib.optionalAttrs devEnabled { + "LLAMA_API_KEY" = { + sopsFile = ../../../secrets/system/llama.yaml; + owner = config.user.name; + }; + }; + + 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; [ + ansible + 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 = [ + "/*.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..cf02ea0 --- /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.gz.age" + TEMP_DIR=$(mktemp -d) + trap "rm -rf $TEMP_DIR" EXIT + + echo "Starting backup: $BACKUP_NAME" + echo "Paths: ${concatStringsSep " " cfg.paths}" + + export PATH="${pkgs.gzip}/bin:${pkgs.age-plugin-yubikey}/bin:$PATH" + ${pkgs.gnutar}/bin/tar -C / ${excludeArgs}-czf - ${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..95fc9de --- /dev/null +++ b/system/machines/server/modules/forgejo/default.nix @@ -0,0 +1,106 @@ +{ 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"; + LFS_MAX_FILE_SIZE = 0; + }; + + "repository.upload" = { + FILE_MAX_SIZE = 0; + }; + + 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; + extraConfig = "client_max_body_size 0;"; + 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..402cfcc --- /dev/null +++ b/system/machines/server/modules/frigate/default.nix @@ -0,0 +1,295 @@ +{ 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"; + privateAccessRules = concatMapStringsSep "\n" (cidr: "allow ${cidr};") nginx.privateAllowCidrs + "\ndeny all;"; + +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 = false; + + 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."/" = { + extraConfig = privateAccessRules; + }; + locations."/go2rtc/" = { + proxyPass = "http://127.0.0.1:1984/"; + proxyWebsockets = true; + extraConfig = privateAccessRules; + }; + }; + + # 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..f38e079 --- /dev/null +++ b/system/machines/server/modules/immich/default.nix @@ -0,0 +1,59 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.immich; + nginx = config.modules.system.nginx; + domain = "ramos.codes"; + port = 2283; + privateAccessRules = concatMapStringsSep "\n" (cidr: "allow ${cidr};") nginx.privateAllowCidrs + "\ndeny all;"; + +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; + extraConfig = privateAccessRules; + }; + }; + }; +} diff --git a/system/machines/server/modules/nginx/default.nix b/system/machines/server/modules/nginx/default.nix new file mode 100644 index 0000000..3f4b0f2 --- /dev/null +++ b/system/machines/server/modules/nginx/default.nix @@ -0,0 +1,163 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.nginx; + domain = "ramos.codes"; + privateAccessRules = concatMapStringsSep "\n" (cidr: "allow ${cidr};") cfg.privateAllowCidrs + "\ndeny all;"; + +in +{ + options.modules.system.nginx = { + enable = mkEnableOption "Nginx Reverse Proxy"; + + privateAllowCidrs = mkOption { + type = types.listOf types.str; + default = [ + "192.168.0.0/24" + "10.8.0.0/24" + ]; + description = '' + CIDR ranges allowed to access private vhosts (LAN + WireGuard). + ''; + }; + + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = [ 80 443 ]; + + systemd.services.nginx.serviceConfig.LimitNOFILE = 65536; + + environment.etc."fail2ban/filter.d/nginx-404.conf".text = '' + [Definition] + failregex = ^ - .+ "(GET|POST|HEAD|PUT|DELETE|PATCH) .+ HTTP/[0-9.]+" 404 + ignoreregex = + ''; + + environment.etc."fail2ban/filter.d/nginx-401.conf".text = '' + [Definition] + failregex = ^ - .+ "(GET|POST|HEAD|PUT|DELETE|PATCH) .+ HTTP/[0-9.]+" 401 + ignoreregex = + ''; + + services.fail2ban.jails.nginx-404 = '' + enabled = true + filter = nginx-404 + logpath = /var/log/nginx/access.log + maxretry = 10 + findtime = 10m + bantime = 24h + ''; + + services.fail2ban.jails.nginx-401 = '' + enabled = true + filter = nginx-401 + logpath = /var/log/nginx/access.log + maxretry = 5 + findtime = 10m + bantime = 24h + ''; + + security.acme = { + acceptTerms = true; + defaults.email = config.user.email; + + certs."${domain}" = { + domain = "*.${domain}"; + dnsProvider = "namecheap"; + environmentFile = "/var/lib/acme/namecheap.env"; + group = "nginx"; + }; + }; + + services.nginx = { + enable = true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + eventsConfig = "worker_connections 4096;"; + + # 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."wg.${domain}" = { + useACMEHost = domain; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString config.modules.system.wstunnel.listenPort}"; + proxyWebsockets = true; + extraConfig = '' + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + ''; + }; + }; + + virtualHosts."ai.${domain}" = let + apiKeyAuth = '' + set $api_key ""; + if ($http_authorization ~* "^Bearer (.+)$") { + set $api_key $1; + } + if ($api_key = "") { + return 401 '{"error": "Missing Authorization header"}'; + } + include ${config.sops.templates."nginx-ai-auth.conf".path}; + ''; + in { + useACMEHost = domain; + forceSSL = true; + + # Web UI + llama.cpp API (browser, /v1/* calls from the UI) + # Auth handled by llama.cpp itself (--api-key flag) + locations."/" = { + proxyPass = "http://192.168.0.23:8000"; + proxyWebsockets = true; + }; + + # Llama Stack API (opencode, programmatic clients) + # Clients use baseURL: https://ai.ramos.codes/stack/v1 + locations."/stack/v1/" = { + proxyPass = "http://192.168.0.23:8321/v1/"; + proxyWebsockets = true; + extraConfig = apiKeyAuth + '' + proxy_read_timeout 300s; + proxy_send_timeout 300s; + ''; + }; + + # MCP servers (namespaced, for llama.cpp web UI + direct access) + locations."/mcp/web_search/" = { + proxyPass = "http://192.168.0.23:8002/"; + proxyWebsockets = true; + extraConfig = '' + include ${config.sops.templates."nginx-mcp-auth.conf".path}; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + ''; + }; + }; + + virtualHosts."comfy.${domain}" = { + useACMEHost = domain; + forceSSL = true; + locations."/" = { + proxyPass = "http://192.168.0.23:8188"; + proxyWebsockets = true; + extraConfig = privateAccessRules; + }; + }; + }; + }; +} diff --git a/system/machines/server/modules/sandpack/default.nix b/system/machines/server/modules/sandpack/default.nix new file mode 100644 index 0000000..d8b46a1 --- /dev/null +++ b/system/machines/server/modules/sandpack/default.nix @@ -0,0 +1,136 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.sandpack; + domain = "ramos.codes"; + privateAccessRules = concatMapStringsSep "\n" (cidr: "allow ${cidr};") config.modules.system.nginx.privateAllowCidrs + "\ndeny all;"; + + staticBrowserServer = pkgs.stdenvNoCC.mkDerivation (finalAttrs: let + pnpm = pkgs.pnpm_10; + in { + pname = "static-browser-server"; + version = "1.0.6"; + + src = pkgs.fetchFromGitHub { + owner = "LibreChat-AI"; + repo = "static-browser-server"; + rev = "30de7ae4ebf5433acc0fb640649fb77426a79e04"; + hash = "sha256-OVAGnoh7KRmTPY2bXE0kvCMiPx3tXAooDa8n8ujugYM="; + }; + + patches = [ ./pnpm-lock.patch ]; + + pnpmDeps = pkgs.fetchPnpmDeps { + inherit (finalAttrs) pname version src patches; + pnpm = pnpm; + fetcherVersion = 3; + hash = "sha256-+Gz8tQy4rkoi365To9GI6sShPTjuKEmZxtV5mEB2UYk="; + }; + + nativeBuildInputs = [ + pkgs.makeWrapper + pkgs.nodejs + pkgs.pnpmConfigHook + pnpm + ]; + + buildPhase = '' + runHook preBuild + pnpm build + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out/libexec/static-browser-server $out/bin + cp -r out $out/libexec/ + + pnpm exec esbuild \ + ./servers/demo-server.ts \ + --bundle \ + --platform=node \ + --format=cjs \ + --outfile=$out/libexec/static-browser-server/demo-server.js + + makeWrapper ${pkgs.nodejs}/bin/node $out/bin/static-browser-server \ + --add-flags $out/libexec/static-browser-server/demo-server.js + + runHook postInstall + ''; + }); +in +{ + options.modules.system.sandpack = { + enable = mkEnableOption "Sandpack services"; + }; + + config = mkIf cfg.enable { + virtualisation.oci-containers = { + backend = "podman"; + containers.sandpack-bundler = { + image = "ghcr.io/librechat-ai/codesandbox-client/bundler:latest"; + ports = [ "127.0.0.1:4333:80" ]; + }; + }; + + systemd.services.sandpack-preview = { + description = "Sandpack static preview server"; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${staticBrowserServer}/bin/static-browser-server"; + WorkingDirectory = "${staticBrowserServer}/libexec/static-browser-server"; + Restart = "always"; + RestartSec = 5; + DynamicUser = true; + Environment = [ + "HOST=127.0.0.1" + "PORT=4324" + ]; + }; + }; + + services.nginx.virtualHosts."bundler.${domain}" = { + useACMEHost = domain; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:4333"; + extraConfig = '' + ${privateAccessRules} + + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; + add_header Access-Control-Max-Age "3600" always; + + if ($request_method = OPTIONS) { + return 204; + } + ''; + }; + }; + + services.nginx.virtualHosts."preview.${domain}" = { + useACMEHost = domain; + forceSSL = true; + serverAliases = [ "~^.+-preview\\.ramos\\.codes$" ]; + locations."/" = { + proxyPass = "http://127.0.0.1:4324"; + extraConfig = '' + ${privateAccessRules} + + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; + add_header Access-Control-Max-Age "3600" always; + + if ($request_method = OPTIONS) { + return 204; + } + ''; + }; + }; + }; +} diff --git a/system/machines/server/modules/sandpack/pnpm-lock.patch b/system/machines/server/modules/sandpack/pnpm-lock.patch new file mode 100644 index 0000000..95eb7f6 --- /dev/null +++ b/system/machines/server/modules/sandpack/pnpm-lock.patch @@ -0,0 +1,1238 @@ +--- a/pnpm-lock.yaml ++++ b/pnpm-lock.yaml +@@ -8,6 +8,9 @@ + + .: + dependencies: ++ '@fastify/cors': ++ specifier: ^11.0.1 ++ version: 11.2.0 + '@open-draft/deferred-promise': + specifier: ^2.1.0 + version: 2.2.0 +@@ -22,8 +25,8 @@ + version: 1.4.3 + devDependencies: + '@fastify/static': +- specifier: ^6.9.0 +- version: 6.12.0 ++ specifier: ^8.1.1 ++ version: 8.3.0 + '@types/mime-db': + specifier: ^1.43.1 + version: 1.43.5 +@@ -37,14 +40,14 @@ + specifier: ^4.0.0 + version: 4.0.1 + esbuild: +- specifier: ^0.17.10 +- version: 0.17.19 ++ specifier: ^0.25.2 ++ version: 0.25.12 + esbuild-register: + specifier: ^3.4.2 +- version: 3.6.0(esbuild@0.17.19) ++ version: 3.6.0(esbuild@0.25.12) + fastify: +- specifier: ^4.13.0 +- version: 4.29.0 ++ specifier: ^5.2.2 ++ version: 5.8.4 + mustache: + specifier: ^4.2.0 + version: 4.2.0 +@@ -54,159 +57,195 @@ + + packages: + +- '@esbuild/android-arm64@0.17.19': +- resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} +- engines: {node: '>=12'} ++ '@esbuild/aix-ppc64@0.25.12': ++ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} ++ engines: {node: '>=18'} ++ cpu: [ppc64] ++ os: [aix] ++ ++ '@esbuild/android-arm64@0.25.12': ++ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} ++ engines: {node: '>=18'} + cpu: [arm64] + os: [android] + +- '@esbuild/android-arm@0.17.19': +- resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} +- engines: {node: '>=12'} ++ '@esbuild/android-arm@0.25.12': ++ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} ++ engines: {node: '>=18'} + cpu: [arm] + os: [android] + +- '@esbuild/android-x64@0.17.19': +- resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} +- engines: {node: '>=12'} ++ '@esbuild/android-x64@0.25.12': ++ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} ++ engines: {node: '>=18'} + cpu: [x64] + os: [android] + +- '@esbuild/darwin-arm64@0.17.19': +- resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} +- engines: {node: '>=12'} ++ '@esbuild/darwin-arm64@0.25.12': ++ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} ++ engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + +- '@esbuild/darwin-x64@0.17.19': +- resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} +- engines: {node: '>=12'} ++ '@esbuild/darwin-x64@0.25.12': ++ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} ++ engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + +- '@esbuild/freebsd-arm64@0.17.19': +- resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} +- engines: {node: '>=12'} ++ '@esbuild/freebsd-arm64@0.25.12': ++ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} ++ engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + +- '@esbuild/freebsd-x64@0.17.19': +- resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} +- engines: {node: '>=12'} ++ '@esbuild/freebsd-x64@0.25.12': ++ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} ++ engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + +- '@esbuild/linux-arm64@0.17.19': +- resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} +- engines: {node: '>=12'} ++ '@esbuild/linux-arm64@0.25.12': ++ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} ++ engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + +- '@esbuild/linux-arm@0.17.19': +- resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} +- engines: {node: '>=12'} ++ '@esbuild/linux-arm@0.25.12': ++ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} ++ engines: {node: '>=18'} + cpu: [arm] + os: [linux] + +- '@esbuild/linux-ia32@0.17.19': +- resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} +- engines: {node: '>=12'} ++ '@esbuild/linux-ia32@0.25.12': ++ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} ++ engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + +- '@esbuild/linux-loong64@0.17.19': +- resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} +- engines: {node: '>=12'} ++ '@esbuild/linux-loong64@0.25.12': ++ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} ++ engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + +- '@esbuild/linux-mips64el@0.17.19': +- resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} +- engines: {node: '>=12'} ++ '@esbuild/linux-mips64el@0.25.12': ++ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} ++ engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + +- '@esbuild/linux-ppc64@0.17.19': +- resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} +- engines: {node: '>=12'} ++ '@esbuild/linux-ppc64@0.25.12': ++ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} ++ engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + +- '@esbuild/linux-riscv64@0.17.19': +- resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} +- engines: {node: '>=12'} ++ '@esbuild/linux-riscv64@0.25.12': ++ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} ++ engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + +- '@esbuild/linux-s390x@0.17.19': +- resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} +- engines: {node: '>=12'} ++ '@esbuild/linux-s390x@0.25.12': ++ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} ++ engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + +- '@esbuild/linux-x64@0.17.19': +- resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} +- engines: {node: '>=12'} ++ '@esbuild/linux-x64@0.25.12': ++ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} ++ engines: {node: '>=18'} + cpu: [x64] + os: [linux] + +- '@esbuild/netbsd-x64@0.17.19': +- resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} +- engines: {node: '>=12'} ++ '@esbuild/netbsd-arm64@0.25.12': ++ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} ++ engines: {node: '>=18'} ++ cpu: [arm64] ++ os: [netbsd] ++ ++ '@esbuild/netbsd-x64@0.25.12': ++ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} ++ engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + +- '@esbuild/openbsd-x64@0.17.19': +- resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} +- engines: {node: '>=12'} ++ '@esbuild/openbsd-arm64@0.25.12': ++ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} ++ engines: {node: '>=18'} ++ cpu: [arm64] ++ os: [openbsd] ++ ++ '@esbuild/openbsd-x64@0.25.12': ++ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} ++ engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + +- '@esbuild/sunos-x64@0.17.19': +- resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} +- engines: {node: '>=12'} ++ '@esbuild/openharmony-arm64@0.25.12': ++ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} ++ engines: {node: '>=18'} ++ cpu: [arm64] ++ os: [openharmony] ++ ++ '@esbuild/sunos-x64@0.25.12': ++ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} ++ engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + +- '@esbuild/win32-arm64@0.17.19': +- resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} +- engines: {node: '>=12'} ++ '@esbuild/win32-arm64@0.25.12': ++ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} ++ engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + +- '@esbuild/win32-ia32@0.17.19': +- resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} +- engines: {node: '>=12'} ++ '@esbuild/win32-ia32@0.25.12': ++ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} ++ engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + +- '@esbuild/win32-x64@0.17.19': +- resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} +- engines: {node: '>=12'} ++ '@esbuild/win32-x64@0.25.12': ++ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} ++ engines: {node: '>=18'} + cpu: [x64] + os: [win32] + +- '@fastify/accept-negotiator@1.1.0': +- resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} +- engines: {node: '>=14'} ++ '@fastify/accept-negotiator@2.0.1': ++ resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} ++ ++ '@fastify/ajv-compiler@4.0.5': ++ resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + +- '@fastify/ajv-compiler@3.6.0': +- resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} ++ '@fastify/cors@11.2.0': ++ resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==} + +- '@fastify/error@3.4.1': +- resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} ++ '@fastify/error@4.2.0': ++ resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + +- '@fastify/fast-json-stringify-compiler@4.3.0': +- resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} ++ '@fastify/fast-json-stringify-compiler@5.0.3': ++ resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + +- '@fastify/merge-json-schemas@0.1.1': +- resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} ++ '@fastify/forwarded@3.0.1': ++ resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + +- '@fastify/send@2.1.0': +- resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==} ++ '@fastify/merge-json-schemas@0.2.1': ++ resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + +- '@fastify/static@6.12.0': +- resolution: {integrity: sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==} ++ '@fastify/proxy-addr@5.1.0': ++ resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} ++ ++ '@fastify/send@4.1.0': ++ resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} ++ ++ '@fastify/static@8.3.0': ++ resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} ++ ++ '@isaacs/cliui@9.0.0': ++ resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} ++ engines: {node: '>=18'} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} +@@ -215,6 +254,9 @@ + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + ++ '@pinojs/redact@0.4.0': ++ resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} ++ + '@types/mime-db@1.43.5': + resolution: {integrity: sha512-/bfTiIUTNPUBnwnYvUxXAre5MhD88jgagLEQiQtIASjU+bwxd8kS/ASDA4a8ufd8m0Lheu6eeMJHEUpLHoJ28A==} + +@@ -227,14 +269,6 @@ + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + +- ajv-formats@2.1.1: +- resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} +- peerDependencies: +- ajv: ^8.0.0 +- peerDependenciesMeta: +- ajv: +- optional: true +- + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: +@@ -250,25 +284,31 @@ + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + +- avvio@8.4.0: +- resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==} ++ avvio@9.2.0: ++ resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + +- balanced-match@1.0.2: +- resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} ++ balanced-match@4.0.4: ++ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} ++ engines: {node: 18 || 20 || >=22} + + base-x@4.0.1: + resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==} + +- brace-expansion@2.0.1: +- resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} ++ brace-expansion@5.0.5: ++ resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} ++ engines: {node: 18 || 20 || >=22} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + +- cookie@0.7.2: +- resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} +- engines: {node: '>= 0.6'} ++ cookie@1.1.1: ++ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} ++ engines: {node: '>=18'} ++ ++ cross-spawn@7.0.6: ++ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} ++ engines: {node: '>= 8'} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} +@@ -283,6 +323,10 @@ + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + ++ dequal@2.0.3: ++ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} ++ engines: {node: '>=6'} ++ + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} +@@ -292,87 +336,82 @@ + peerDependencies: + esbuild: '>=0.12 <1' + +- esbuild@0.17.19: +- resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} +- engines: {node: '>=12'} ++ esbuild@0.25.12: ++ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} ++ engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + +- fast-content-type-parse@1.1.0: +- resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} +- + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + +- fast-json-stringify@5.16.1: +- resolution: {integrity: sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==} ++ fast-json-stringify@6.3.0: ++ resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + +- fast-redact@3.5.0: +- resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} +- engines: {node: '>=6'} +- +- fast-uri@2.4.0: +- resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==} +- + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + +- fastify-plugin@4.5.1: +- resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} ++ fastify-plugin@5.1.0: ++ resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + +- fastify@4.29.0: +- resolution: {integrity: sha512-MaaUHUGcCgC8fXQDsDtioaCcag1fmPJ9j64vAKunqZF4aSub040ZGi/ag8NGE2714yREPOKZuHCfpPzuUD3UQQ==} ++ fastify@5.8.4: ++ resolution: {integrity: sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + +- find-my-way@8.2.2: +- resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==} +- engines: {node: '>=14'} +- +- forwarded@0.2.0: +- resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} +- engines: {node: '>= 0.6'} ++ find-my-way@9.5.0: ++ resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} ++ engines: {node: '>=20'} + +- fs.realpath@1.0.0: +- resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} ++ foreground-child@3.3.1: ++ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} ++ engines: {node: '>=14'} + +- glob@8.1.0: +- resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} +- engines: {node: '>=12'} +- deprecated: Glob versions prior to v9 are no longer supported ++ glob@11.1.0: ++ resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} ++ engines: {node: 20 || >=22} ++ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me ++ hasBin: true + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + +- inflight@1.0.6: +- resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} +- deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. +- + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + +- ipaddr.js@1.9.1: +- resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} +- engines: {node: '>= 0.10'} ++ ipaddr.js@2.3.0: ++ resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} ++ engines: {node: '>= 10'} ++ ++ isexe@2.0.0: ++ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} ++ ++ jackspeak@4.2.3: ++ resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} ++ engines: {node: 20 || >=22} + +- json-schema-ref-resolver@1.0.1: +- resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} ++ json-schema-ref-resolver@3.0.0: ++ resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + +- light-my-request@5.14.0: +- resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} ++ light-my-request@6.6.0: ++ resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} ++ ++ lru-cache@11.3.3: ++ resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} ++ engines: {node: 20 || >=22} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} +@@ -383,9 +422,13 @@ + engines: {node: '>=10.0.0'} + hasBin: true + +- minimatch@5.1.6: +- resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} +- engines: {node: '>=10'} ++ minimatch@10.2.5: ++ resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} ++ engines: {node: 18 || 20 || >=22} ++ ++ minipass@7.1.3: ++ resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} ++ engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} +@@ -398,35 +441,35 @@ + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + +- once@1.4.0: +- resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} +- + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + +- p-limit@3.1.0: +- resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} +- engines: {node: '>=10'} ++ package-json-from-dist@1.0.1: ++ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + +- pino-abstract-transport@2.0.0: +- resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} ++ path-key@3.1.1: ++ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} ++ engines: {node: '>=8'} ++ ++ path-scurry@2.0.2: ++ resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} ++ engines: {node: 18 || 20 || >=22} ++ ++ pino-abstract-transport@3.0.0: ++ resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + +- pino@9.6.0: +- resolution: {integrity: sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==} ++ pino@10.3.1: ++ resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + +- process-warning@3.0.0: +- resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} +- + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + +- proxy-addr@2.0.7: +- resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} +- engines: {node: '>= 0.10'} ++ process-warning@5.0.0: ++ resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} +@@ -439,8 +482,8 @@ + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + +- ret@0.4.3: +- resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==} ++ ret@0.5.0: ++ resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + reusify@1.1.0: +@@ -453,15 +496,16 @@ + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + +- safe-regex2@3.1.0: +- resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} ++ safe-regex2@5.1.0: ++ resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} ++ hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + +- secure-json-parse@2.7.0: +- resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} ++ secure-json-parse@4.1.0: ++ resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} +@@ -474,6 +518,18 @@ + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + ++ shebang-command@2.0.0: ++ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} ++ engines: {node: '>=8'} ++ ++ shebang-regex@3.0.0: ++ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} ++ engines: {node: '>=8'} ++ ++ signal-exit@4.1.0: ++ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} ++ engines: {node: '>=14'} ++ + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + +@@ -485,8 +541,9 @@ + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + +- thread-stream@3.1.0: +- resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} ++ thread-stream@4.0.0: ++ resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} ++ engines: {node: '>=20'} + + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} +@@ -504,100 +561,122 @@ + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + +- wrappy@1.0.2: +- resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} +- +- yocto-queue@0.1.0: +- resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} +- engines: {node: '>=10'} ++ which@2.0.2: ++ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} ++ engines: {node: '>= 8'} ++ hasBin: true + + snapshots: + +- '@esbuild/android-arm64@0.17.19': ++ '@esbuild/aix-ppc64@0.25.12': ++ optional: true ++ ++ '@esbuild/android-arm64@0.25.12': ++ optional: true ++ ++ '@esbuild/android-arm@0.25.12': ++ optional: true ++ ++ '@esbuild/android-x64@0.25.12': + optional: true + +- '@esbuild/android-arm@0.17.19': ++ '@esbuild/darwin-arm64@0.25.12': + optional: true + +- '@esbuild/android-x64@0.17.19': ++ '@esbuild/darwin-x64@0.25.12': + optional: true + +- '@esbuild/darwin-arm64@0.17.19': ++ '@esbuild/freebsd-arm64@0.25.12': + optional: true + +- '@esbuild/darwin-x64@0.17.19': ++ '@esbuild/freebsd-x64@0.25.12': + optional: true + +- '@esbuild/freebsd-arm64@0.17.19': ++ '@esbuild/linux-arm64@0.25.12': + optional: true + +- '@esbuild/freebsd-x64@0.17.19': ++ '@esbuild/linux-arm@0.25.12': + optional: true + +- '@esbuild/linux-arm64@0.17.19': ++ '@esbuild/linux-ia32@0.25.12': + optional: true + +- '@esbuild/linux-arm@0.17.19': ++ '@esbuild/linux-loong64@0.25.12': + optional: true + +- '@esbuild/linux-ia32@0.17.19': ++ '@esbuild/linux-mips64el@0.25.12': + optional: true + +- '@esbuild/linux-loong64@0.17.19': ++ '@esbuild/linux-ppc64@0.25.12': + optional: true + +- '@esbuild/linux-mips64el@0.17.19': ++ '@esbuild/linux-riscv64@0.25.12': + optional: true + +- '@esbuild/linux-ppc64@0.17.19': ++ '@esbuild/linux-s390x@0.25.12': + optional: true + +- '@esbuild/linux-riscv64@0.17.19': ++ '@esbuild/linux-x64@0.25.12': + optional: true + +- '@esbuild/linux-s390x@0.17.19': ++ '@esbuild/netbsd-arm64@0.25.12': + optional: true + +- '@esbuild/linux-x64@0.17.19': ++ '@esbuild/netbsd-x64@0.25.12': + optional: true + +- '@esbuild/netbsd-x64@0.17.19': ++ '@esbuild/openbsd-arm64@0.25.12': + optional: true + +- '@esbuild/openbsd-x64@0.17.19': ++ '@esbuild/openbsd-x64@0.25.12': + optional: true + +- '@esbuild/sunos-x64@0.17.19': ++ '@esbuild/openharmony-arm64@0.25.12': + optional: true + +- '@esbuild/win32-arm64@0.17.19': ++ '@esbuild/sunos-x64@0.25.12': + optional: true + +- '@esbuild/win32-ia32@0.17.19': ++ '@esbuild/win32-arm64@0.25.12': + optional: true + +- '@esbuild/win32-x64@0.17.19': ++ '@esbuild/win32-ia32@0.25.12': + optional: true + +- '@fastify/accept-negotiator@1.1.0': {} ++ '@esbuild/win32-x64@0.25.12': ++ optional: true + +- '@fastify/ajv-compiler@3.6.0': ++ '@fastify/accept-negotiator@2.0.1': {} ++ ++ '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 +- ajv-formats: 2.1.1(ajv@8.17.1) +- fast-uri: 2.4.0 ++ ajv-formats: 3.0.1(ajv@8.17.1) ++ fast-uri: 3.0.6 ++ ++ '@fastify/cors@11.2.0': ++ dependencies: ++ fastify-plugin: 5.1.0 ++ toad-cache: 3.7.0 + +- '@fastify/error@3.4.1': {} ++ '@fastify/error@4.2.0': {} + +- '@fastify/fast-json-stringify-compiler@4.3.0': ++ '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: +- fast-json-stringify: 5.16.1 ++ fast-json-stringify: 6.3.0 + +- '@fastify/merge-json-schemas@0.1.1': ++ '@fastify/forwarded@3.0.1': {} ++ ++ '@fastify/merge-json-schemas@0.2.1': + dependencies: +- fast-deep-equal: 3.1.3 ++ dequal: 2.0.3 + +- '@fastify/send@2.1.0': ++ '@fastify/proxy-addr@5.1.0': ++ dependencies: ++ '@fastify/forwarded': 3.0.1 ++ ipaddr.js: 2.3.0 ++ ++ '@fastify/send@4.1.0': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 +@@ -605,19 +684,23 @@ + http-errors: 2.0.0 + mime: 3.0.0 + +- '@fastify/static@6.12.0': ++ '@fastify/static@8.3.0': + dependencies: +- '@fastify/accept-negotiator': 1.1.0 +- '@fastify/send': 2.1.0 ++ '@fastify/accept-negotiator': 2.0.1 ++ '@fastify/send': 4.1.0 + content-disposition: 0.5.4 +- fastify-plugin: 4.5.1 +- glob: 8.1.0 +- p-limit: 3.1.0 ++ fastify-plugin: 5.1.0 ++ fastq: 1.19.1 ++ glob: 11.1.0 ++ ++ '@isaacs/cliui@9.0.0': {} + + '@lukeed/ms@2.0.2': {} + + '@open-draft/deferred-promise@2.2.0': {} + ++ '@pinojs/redact@0.4.0': {} ++ + '@types/mime-db@1.43.5': {} + + '@types/mustache@4.2.5': {} +@@ -628,10 +711,6 @@ + + abstract-logging@2.0.1: {} + +- ajv-formats@2.1.1(ajv@8.17.1): +- optionalDependencies: +- ajv: 8.17.1 +- + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 +@@ -645,24 +724,30 @@ + + atomic-sleep@1.0.0: {} + +- avvio@8.4.0: ++ avvio@9.2.0: + dependencies: +- '@fastify/error': 3.4.1 ++ '@fastify/error': 4.2.0 + fastq: 1.19.1 + +- balanced-match@1.0.2: {} ++ balanced-match@4.0.4: {} + + base-x@4.0.1: {} + +- brace-expansion@2.0.1: ++ brace-expansion@5.0.5: + dependencies: +- balanced-match: 1.0.2 ++ balanced-match: 4.0.4 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + +- cookie@0.7.2: {} ++ cookie@1.1.1: {} ++ ++ cross-spawn@7.0.6: ++ dependencies: ++ path-key: 3.1.1 ++ shebang-command: 2.0.0 ++ which: 2.0.2 + + debug@4.4.0: + dependencies: +@@ -670,86 +755,84 @@ + + depd@2.0.0: {} + ++ dequal@2.0.3: {} ++ + dotenv@16.4.7: {} + +- esbuild-register@3.6.0(esbuild@0.17.19): ++ esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.0 +- esbuild: 0.17.19 ++ esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + +- esbuild@0.17.19: ++ esbuild@0.25.12: + optionalDependencies: +- '@esbuild/android-arm': 0.17.19 +- '@esbuild/android-arm64': 0.17.19 +- '@esbuild/android-x64': 0.17.19 +- '@esbuild/darwin-arm64': 0.17.19 +- '@esbuild/darwin-x64': 0.17.19 +- '@esbuild/freebsd-arm64': 0.17.19 +- '@esbuild/freebsd-x64': 0.17.19 +- '@esbuild/linux-arm': 0.17.19 +- '@esbuild/linux-arm64': 0.17.19 +- '@esbuild/linux-ia32': 0.17.19 +- '@esbuild/linux-loong64': 0.17.19 +- '@esbuild/linux-mips64el': 0.17.19 +- '@esbuild/linux-ppc64': 0.17.19 +- '@esbuild/linux-riscv64': 0.17.19 +- '@esbuild/linux-s390x': 0.17.19 +- '@esbuild/linux-x64': 0.17.19 +- '@esbuild/netbsd-x64': 0.17.19 +- '@esbuild/openbsd-x64': 0.17.19 +- '@esbuild/sunos-x64': 0.17.19 +- '@esbuild/win32-arm64': 0.17.19 +- '@esbuild/win32-ia32': 0.17.19 +- '@esbuild/win32-x64': 0.17.19 ++ '@esbuild/aix-ppc64': 0.25.12 ++ '@esbuild/android-arm': 0.25.12 ++ '@esbuild/android-arm64': 0.25.12 ++ '@esbuild/android-x64': 0.25.12 ++ '@esbuild/darwin-arm64': 0.25.12 ++ '@esbuild/darwin-x64': 0.25.12 ++ '@esbuild/freebsd-arm64': 0.25.12 ++ '@esbuild/freebsd-x64': 0.25.12 ++ '@esbuild/linux-arm': 0.25.12 ++ '@esbuild/linux-arm64': 0.25.12 ++ '@esbuild/linux-ia32': 0.25.12 ++ '@esbuild/linux-loong64': 0.25.12 ++ '@esbuild/linux-mips64el': 0.25.12 ++ '@esbuild/linux-ppc64': 0.25.12 ++ '@esbuild/linux-riscv64': 0.25.12 ++ '@esbuild/linux-s390x': 0.25.12 ++ '@esbuild/linux-x64': 0.25.12 ++ '@esbuild/netbsd-arm64': 0.25.12 ++ '@esbuild/netbsd-x64': 0.25.12 ++ '@esbuild/openbsd-arm64': 0.25.12 ++ '@esbuild/openbsd-x64': 0.25.12 ++ '@esbuild/openharmony-arm64': 0.25.12 ++ '@esbuild/sunos-x64': 0.25.12 ++ '@esbuild/win32-arm64': 0.25.12 ++ '@esbuild/win32-ia32': 0.25.12 ++ '@esbuild/win32-x64': 0.25.12 + + escape-html@1.0.3: {} + +- fast-content-type-parse@1.1.0: {} +- + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + +- fast-json-stringify@5.16.1: ++ fast-json-stringify@6.3.0: + dependencies: +- '@fastify/merge-json-schemas': 0.1.1 ++ '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) +- fast-deep-equal: 3.1.3 +- fast-uri: 2.4.0 +- json-schema-ref-resolver: 1.0.1 ++ fast-uri: 3.0.6 ++ json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + +- fast-redact@3.5.0: {} +- +- fast-uri@2.4.0: {} +- + fast-uri@3.0.6: {} + +- fastify-plugin@4.5.1: {} ++ fastify-plugin@5.1.0: {} + +- fastify@4.29.0: ++ fastify@5.8.4: + dependencies: +- '@fastify/ajv-compiler': 3.6.0 +- '@fastify/error': 3.4.1 +- '@fastify/fast-json-stringify-compiler': 4.3.0 ++ '@fastify/ajv-compiler': 4.0.5 ++ '@fastify/error': 4.2.0 ++ '@fastify/fast-json-stringify-compiler': 5.0.3 ++ '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 +- avvio: 8.4.0 +- fast-content-type-parse: 1.1.0 +- fast-json-stringify: 5.16.1 +- find-my-way: 8.2.2 +- light-my-request: 5.14.0 +- pino: 9.6.0 +- process-warning: 3.0.0 +- proxy-addr: 2.0.7 ++ avvio: 9.2.0 ++ fast-json-stringify: 6.3.0 ++ find-my-way: 9.5.0 ++ light-my-request: 6.6.0 ++ pino: 10.3.1 ++ process-warning: 5.0.0 + rfdc: 1.4.1 +- secure-json-parse: 2.7.0 ++ secure-json-parse: 4.1.0 + semver: 7.7.1 + toad-cache: 3.7.0 + +@@ -757,23 +840,25 @@ + dependencies: + reusify: 1.1.0 + +- find-my-way@8.2.2: ++ find-my-way@9.5.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 +- safe-regex2: 3.1.0 +- +- forwarded@0.2.0: {} ++ safe-regex2: 5.1.0 + +- fs.realpath@1.0.0: {} ++ foreground-child@3.3.1: ++ dependencies: ++ cross-spawn: 7.0.6 ++ signal-exit: 4.1.0 + +- glob@8.1.0: ++ glob@11.1.0: + dependencies: +- fs.realpath: 1.0.0 +- inflight: 1.0.6 +- inherits: 2.0.4 +- minimatch: 5.1.6 +- once: 1.4.0 ++ foreground-child: 3.3.1 ++ jackspeak: 4.2.3 ++ minimatch: 10.2.5 ++ minipass: 7.1.3 ++ package-json-from-dist: 1.0.1 ++ path-scurry: 2.0.2 + + http-errors@2.0.0: + dependencies: +@@ -783,34 +868,39 @@ + statuses: 2.0.1 + toidentifier: 1.0.1 + +- inflight@1.0.6: +- dependencies: +- once: 1.4.0 +- wrappy: 1.0.2 +- + inherits@2.0.4: {} + +- ipaddr.js@1.9.1: {} ++ ipaddr.js@2.3.0: {} + +- json-schema-ref-resolver@1.0.1: ++ isexe@2.0.0: {} ++ ++ jackspeak@4.2.3: + dependencies: +- fast-deep-equal: 3.1.3 ++ '@isaacs/cliui': 9.0.0 ++ ++ json-schema-ref-resolver@3.0.0: ++ dependencies: ++ dequal: 2.0.3 + + json-schema-traverse@1.0.0: {} + +- light-my-request@5.14.0: ++ light-my-request@6.6.0: + dependencies: +- cookie: 0.7.2 +- process-warning: 3.0.0 ++ cookie: 1.1.1 ++ process-warning: 4.0.1 + set-cookie-parser: 2.7.1 + ++ lru-cache@11.3.3: {} ++ + mime-db@1.54.0: {} + + mime@3.0.0: {} + +- minimatch@5.1.6: ++ minimatch@10.2.5: + dependencies: +- brace-expansion: 2.0.1 ++ brace-expansion: 5.0.5 ++ ++ minipass@7.1.3: {} + + ms@2.1.3: {} + +@@ -818,44 +908,40 @@ + + on-exit-leak-free@2.1.2: {} + +- once@1.4.0: +- dependencies: +- wrappy: 1.0.2 +- + outvariant@1.4.3: {} + +- p-limit@3.1.0: ++ package-json-from-dist@1.0.1: {} ++ ++ path-key@3.1.1: {} ++ ++ path-scurry@2.0.2: + dependencies: +- yocto-queue: 0.1.0 ++ lru-cache: 11.3.3 ++ minipass: 7.1.3 + +- pino-abstract-transport@2.0.0: ++ pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + +- pino@9.6.0: ++ pino@10.3.1: + dependencies: ++ '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 +- fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 +- pino-abstract-transport: 2.0.0 ++ pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.0.0 +- process-warning: 4.0.1 ++ process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 +- thread-stream: 3.1.0 +- +- process-warning@3.0.0: {} ++ thread-stream: 4.0.0 + + process-warning@4.0.1: {} + +- proxy-addr@2.0.7: +- dependencies: +- forwarded: 0.2.0 +- ipaddr.js: 1.9.1 ++ process-warning@5.0.0: {} + + quick-format-unescaped@4.0.4: {} + +@@ -863,7 +949,7 @@ + + require-from-string@2.0.2: {} + +- ret@0.4.3: {} ++ ret@0.5.0: {} + + reusify@1.1.0: {} + +@@ -871,13 +957,13 @@ + + safe-buffer@5.2.1: {} + +- safe-regex2@3.1.0: ++ safe-regex2@5.1.0: + dependencies: +- ret: 0.4.3 ++ ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + +- secure-json-parse@2.7.0: {} ++ secure-json-parse@4.1.0: {} + + semver@7.7.1: {} + +@@ -885,6 +971,14 @@ + + setprototypeof@1.2.0: {} + ++ shebang-command@2.0.0: ++ dependencies: ++ shebang-regex: 3.0.0 ++ ++ shebang-regex@3.0.0: {} ++ ++ signal-exit@4.1.0: {} ++ + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 +@@ -893,7 +987,7 @@ + + statuses@2.0.1: {} + +- thread-stream@3.1.0: ++ thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + +@@ -905,6 +999,6 @@ + + undici-types@5.26.5: {} + +- wrappy@1.0.2: {} +- +- yocto-queue@0.1.0: {} ++ which@2.0.2: ++ dependencies: ++ isexe: 2.0.0 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..1eb5684 --- /dev/null +++ b/system/machines/server/modules/webdav/default.nix @@ -0,0 +1,72 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.webdav; + domain = "ramos.codes"; + privateAccessRules = concatMapStringsSep "\n" (cidr: "allow ${cidr};") config.modules.system.nginx.privateAllowCidrs + "\ndeny all;"; + +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 = '' + ${privateAccessRules} + + 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/modules/wireguard/default.nix b/system/machines/server/modules/wireguard/default.nix new file mode 100644 index 0000000..04c6324 --- /dev/null +++ b/system/machines/server/modules/wireguard/default.nix @@ -0,0 +1,107 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.wireguard; +in +{ + options.modules.system.wireguard = { + enable = mkEnableOption "WireGuard VPN"; + + address = mkOption { + type = types.str; + default = "10.8.0.1/24"; + description = "WireGuard interface address with CIDR"; + }; + + subnet = mkOption { + type = types.str; + default = "10.8.0.0/24"; + description = "WireGuard subnet used for peer allocations"; + }; + + listenPort = mkOption { + type = types.port; + default = 51820; + description = "WireGuard UDP listen port"; + }; + + privateKeyFile = mkOption { + type = types.str; + default = "/var/lib/wireguard/server.key"; + description = "Path to WireGuard server private key"; + }; + + peers = mkOption { + type = types.listOf (types.submodule ({ ... }: { + options = { + publicKey = mkOption { + type = types.str; + description = "Peer public key"; + }; + + allowedIPs = mkOption { + type = types.listOf types.str; + description = "Allowed IPs for peer, usually a single /32"; + }; + + presharedKeyFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Optional preshared key file"; + }; + + persistentKeepalive = mkOption { + type = types.nullOr types.int; + default = 25; + description = "Persistent keepalive interval seconds"; + }; + }; + })); + default = [ ]; + description = "WireGuard peers"; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedUDPPorts = [ cfg.listenPort ]; + networking.firewall.trustedInterfaces = [ "wg0" ]; + networking.nat.internalInterfaces = mkAfter [ "wg0" ]; + + systemd.tmpfiles.rules = [ + "d /var/lib/wireguard 0700 root root -" + ]; + + systemd.services.wireguard-generate-key = { + description = "Generate WireGuard server key if missing"; + before = [ "wireguard-wg0.service" ]; + wantedBy = [ "wireguard-wg0.service" ]; + serviceConfig = { + Type = "oneshot"; + }; + path = with pkgs; [ wireguard-tools coreutils ]; + script = '' + set -euo pipefail + + if [ ! -s "${cfg.privateKeyFile}" ]; then + umask 077 + wg genkey | tee "${cfg.privateKeyFile}" | wg pubkey > /var/lib/wireguard/server.pub + elif [ ! -s /var/lib/wireguard/server.pub ]; then + umask 077 + wg pubkey < "${cfg.privateKeyFile}" > /var/lib/wireguard/server.pub + fi + ''; + }; + + networking.wireguard.interfaces.wg0 = { + ips = [ cfg.address ]; + listenPort = cfg.listenPort; + privateKeyFile = cfg.privateKeyFile; + peers = map (peer: { + inherit (peer) publicKey allowedIPs; + presharedKeyFile = peer.presharedKeyFile; + persistentKeepalive = peer.persistentKeepalive; + }) cfg.peers; + }; + }; +} diff --git a/system/machines/server/modules/wstunnel/default.nix b/system/machines/server/modules/wstunnel/default.nix new file mode 100644 index 0000000..035f65b --- /dev/null +++ b/system/machines/server/modules/wstunnel/default.nix @@ -0,0 +1,37 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.wstunnel; +in +{ + options.modules.system.wstunnel = { + enable = mkEnableOption "wstunnel WebSocket transport for WireGuard"; + + listenPort = mkOption { + type = types.port; + default = 8080; + description = "Local port wstunnel server listens on (nginx proxies to this)"; + }; + + wireguardPort = mkOption { + type = types.port; + default = 51820; + description = "Local WireGuard port to forward traffic to"; + }; + }; + + config = mkIf cfg.enable { + systemd.services.wstunnel = { + description = "wstunnel WebSocket server for WireGuard transport"; + after = [ "network.target" "wireguard-wg0.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${pkgs.wstunnel}/bin/wstunnel server ws://127.0.0.1:${toString cfg.listenPort} --restrict-to 127.0.0.1:${toString cfg.wireguardPort}"; + Restart = "on-failure"; + RestartSec = 5; + DynamicUser = true; + }; + }; + }; +} diff --git a/system/machines/server/system.nix b/system/machines/server/system.nix new file mode 100644 index 0000000..43b75f6 --- /dev/null +++ b/system/machines/server/system.nix @@ -0,0 +1,253 @@ +{ 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; }; + llama = { sopsFile = ../../../secrets/system/llama.yaml; }; + in { + "RTSP_USER" = cameras; + "RTSP_PASS" = cameras; + "LLAMA_API_KEY" = llama // { owner = config.user.name; }; + }; + + # API key auth for ai.ramos.codes — nginx validates Bearer token against sops secret + sops.templates."nginx-ai-auth.conf" = { + content = '' + if ($api_key != "${config.sops.placeholder."LLAMA_API_KEY"}") { + return 401 '{"error": "Invalid API key"}'; + } + ''; + owner = "nginx"; + }; + + # MCP endpoint auth — validates X-API-Key header + sops.templates."nginx-mcp-auth.conf" = { + content = '' + if ($http_x_api_key != "${config.sops.placeholder."LLAMA_API_KEY"}") { + return 401 '{"error": "Unauthorized"}'; + } + ''; + owner = "nginx"; + }; + + modules.system = { + nginx = { + enable = true; + }; + sandpack.enable = true; + forgejo.enable = true; + frigate.enable = true; + immich.enable = true; + webdav.enable = false; + wstunnel.enable = true; + wireguard = { + enable = true; + peers = [ + { + publicKey = "HRFsVXn3jeqKQLQIl0cB6KC/qia7M1gQf2lqG5HDxF8="; + allowedIPs = [ "10.8.0.2/32" ]; + } + { + publicKey = "eY2JTwuvzLLVnyhUTop0I+7qO2swFSjo12So4Yzkamk="; + allowedIPs = [ "10.8.0.3/32" ]; + } + ]; + }; + # 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; + efiInstallAsRemovable = true; # HP Z230 UEFI ignores custom boot entries + configurationLimit = 5; + splashImage = null; + }; + + efi = { + canTouchEfiVariables = false; # Not needed with efiInstallAsRemovable + efiSysMountPoint = "/boot"; + }; + }; + + environment.systemPackages = with pkgs; [ + wget + git + vim + htop + dmidecode + ]; + + 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"; + + boot.kernel.sysctl."net.ipv4.ip_forward" = 1; + + networking = { + hostName = "server"; + useDHCP = false; + nat = { + enable = true; + internalInterfaces = [ "enp2s0f1" ]; + externalInterface = "enp2s0f0"; + }; + 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" "wg0" ]; + 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" + ]; + }; + }; + + systemd.services.dnsmasq = { + after = [ "wireguard-wg0.service" ]; + wants = [ "wireguard-wg0.service" ]; + }; + + services.fail2ban = { + enable = true; + maxretry = 5; + bantime = "1h"; + ignoreIP = [ + "127.0.0.1/8" + "192.168.0.0/24" + "10.8.0.0/24" + ]; + }; + + 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/docker/default.nix b/system/modules/docker/default.nix new file mode 100644 index 0000000..31b537f --- /dev/null +++ b/system/modules/docker/default.nix @@ -0,0 +1,27 @@ +{ pkgs, lib, config, ... }: + +with lib; +let + cfg = config.modules.system.docker; + +in +{ + options.modules.system.docker = { enable = mkEnableOption "Enable Docker"; }; + config = mkIf cfg.enable { + virtualisation.docker = { + enable = true; + + # Explicit storage driver for ext4/xfs filesystems + storageDriver = "overlay2"; + }; + + # Add docker package to system packages + environment.systemPackages = with pkgs; [ + docker + docker-compose + ]; + + # Add user to docker group + users.users.${config.user.name}.extraGroups = [ "docker" ]; + }; +} diff --git a/system/modules/sops/default.nix b/system/modules/sops/default.nix new file mode 100644 index 0000000..e1b8610 --- /dev/null +++ b/system/modules/sops/default.nix @@ -0,0 +1,30 @@ +{ 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..559bc52 --- /dev/null +++ b/user/keys/age/yubikey.pub.key @@ -0,0 +1 @@ +age1yubikey1qfapxqnnkh92zkgayzzm9n0gtpkwaqcvrzy4d4xa4rxnjua8vjhy72hh9r9 diff --git a/user/keys/default.nix b/user/keys/default.nix new file mode 100644 index 0000000..e3f3aaf --- /dev/null +++ b/user/keys/default.nix @@ -0,0 +1,33 @@ +{ lib }: + +with builtins; +let + extractName = filename: + let + # Remove .key extension + noKey = lib.removeSuffix ".key" filename; + # Remove .pub/.priv/.public/.private markers + noMarkers = replaceStrings + [ ".pub" ".priv" ".public" ".private" ] + [ "" "" "" "" ] + noKey; + in noMarkers; + + constructKeys = dir: ( + listToAttrs ( + map (subdir: { + name = subdir; + value = listToAttrs ( + map (file: { + name = extractName file; + value = readFile "${dir}/${subdir}/${file}"; + }) (filter (file: + (readDir "${dir}/${subdir}").${file} == "regular" && + lib.hasSuffix ".key" file + ) (attrNames (readDir "${dir}/${subdir}"))) + ); + }) (filter (node: (readDir dir).${node} == "directory") (attrNames (readDir dir))) + ) + ); +in + constructKeys ./. diff --git a/user/keys/pgp/company.pub.key b/user/keys/pgp/company.pub.key new file mode 100755 index 0000000..3ddf45c --- /dev/null +++ b/user/keys/pgp/company.pub.key @@ -0,0 +1,53 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +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 +uWsnbjV0Rg== +=56r2 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/user/keys/pgp/work.pub.key b/user/keys/pgp/work.pub.key new file mode 100755 index 0000000..31e14fb --- /dev/null +++ b/user/keys/pgp/work.pub.key @@ -0,0 +1,121 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +Vv2c5XcFneMcDVBQ28VlwQ2fbIoDokz3Iw== +=my/o +-----END PGP PUBLIC KEY BLOCK----- diff --git a/user/keys/pgp/yubikey.pub.key b/user/keys/pgp/yubikey.pub.key new file mode 100644 index 0000000..a15a521 --- /dev/null +++ b/user/keys/pgp/yubikey.pub.key @@ -0,0 +1,109 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGP0BgMBEAC2v+n9plI0p+TqIrmvz7JHoYbtUK3NDkyNeIsgS+sE5nfLB1Ef +vQCR0HdysgpmAUywqEx+YK7Nkr4szoK8nDLpgpSfaDZNss+ePu6eYVTVktelBn2Q +5f5MKDILY9mkmDPgzvpDDhkFXGK3cpeUX+X5vY1W76yuRgm6zBDIux+yf027nw3U +phesn/WlWXRsmAXG2helt1nB6Foj6LjgwRG/aKMI8cQq0JS13cfUZO1nq2ifM0pm +4HqWgbZOKYWHsoOw4qNiuxWwVoL5E7UQW2MEyxZmLZaNohEpReYpI0N9FGB/AZYt +iKn9SO9SmG+afE+gxrExJYZSGVHHKlPc79dcIBVvYEA8dV/OJBjHc83EhUQVU4vQ +x1y386HYctlHVWZ64tc1XROlQe++NxfgQZz4jnvGzHMakr8/IZAV3LP3PGVAa7kx +iVrTE+WodK/kELm1PMLWlWmXT3GiumOngm4y1dWtUirqxni/Nl7BA4eHM3Q3OZiR +eEb80FkbXCoaP5REU1EdVlAW/ZGP+mTwiqekT5ThocaD/BgYSy9UlGf5YyOEnqOt +G+0JfS3mG0PysFjF0B5dMyBquikD4zVBo3+a7ppbrAage3EFhHiX0Les0q566I8p +0hlXS7nz0I4xAxxRLfydwJptndjZgeiq9o1XMRA0JUZQhzuk2VYQ6MSVhwARAQAB +tB9CcnlhbiBSYW1vcyA8YnJ5YW5AcmFtb3MuY29kZXM+iQJOBBMBCgA4FiEE8fNG +ZFhFKy3zUfHoZNErqVrOHy0FAmP0BgMCGwEFCwkIBwIGFQoJCAsCBBYCAwECHgEC +F4AACgkQZNErqVrOHy25SBAArl6JHrDm3fLXPhwtHf9WzxQvW6BmMgLQQ+bGGGba +A3e+eKb0ibSmXH9M22GOSxKqk2BePtoLFdyDKDFNwYDwzj0ioQ80Q9YR6aoSuwOf +HwXeiYsgK76IbsRciXSv6JgAsXO9UOGTlHlTgFsE3AMjnCgPrHbV3SZdkFt71XMo +fbRmYwC33HK6QNUXeq4O+gGO5vJI8Wx1mtmy6kq/3srzMpCGybg9M8C5AQoazo/u +WOjO57QkUdbAXO8HbHInexsstJJn+0o/FLfMoOy7v/cpzTLbbpONRzQbEq1/Utt1 +TaIc1FTWT1b4oWnIGv2stlCGzx9IgsseJocSBG+kGgkKwVBWIcCwq+cCdfkOReCk +VHTg1oRH8t078346KuxEaA7ofKaByirQosZUeF5WTyMuJUDf1mNxxZngRKjIHD3c +lmK8REnYjQ4b+RfznfV8qc8tH624EUTNlT123ufUIvba0fR8OryhdxPOOgdLjlNL +XdkfG5oENnBy3EzGn7xgR6sCRtlFSEcfKQFcec1fjqYMHxPAExajmSHLwr5107LT +4B+F5eOt9CBFKW/cxnVwG/3oW0mzLa231V0eYquiYkbYHVswLdhr02vyHpLXXVZk +JgiLSXIJ6yKwLA9W8HgHgDYCp899Jl+wqhFLxr7oUjXcLhuZO9Q3P3req0SJRfUu +GTO5Ag0EY/QGQAEQANsJBUpkk0ZW5swgzC/c7pxv4VGS8VZcr3Isol8NHAUUwHyo +jqAYNtqW8PQLgQ34uuuC5GCS2hxN57WdgmSkv/to8THl6IbE1V/YVaaGXX9yiJmH +72//kc9g2prXyrtObwVhgKiYQxPPegm9ubLkb1khCTLhozCJDM1wbQxmE5I2cICC +5lwCi1NDsAyvUtWANzb0EXPZh2iPv8sWMh3RStAGSsboHzHYdR9RZGRjKG/ET5zv +OBbFpRLFjvMJUL22M0V5FFPbuz+4Aut21wkYdueHtREpUgAcba68Doz75jQb0PEZ +52hjLKuXVf0/1sEPXUs/sL8kyl6QzIqFIXsrjbw6BrGSdhn6YoY95koCb6AXUrFC +oOXQC5BecTcP7V3GOWDEaDUbjN8mc2t1ujs7KYIqi0UCiHa9m5L2Q/9TyOSLyjSf +0VKHzib7Ov76GvphbYoQSXWX8R6ogcexQH6aQlXI31ir/HsHkatImYomySZiwNVV +5PQD/7lbWGjLB6LB9PsyVIVl3uq+sSX7xKeogZkEuTcerKVJjpknisKh6aR/uJRV +KJs2U3MolyVanDb/y6VBJrCOu8ZiCZuDtCntUg8MxeLNFO0MVdgAPiHMtJd8YrzK +bhbkHBufAgOLMbGTYq47bQNuRz/CjIz0xll0tLeS9LD1hcSWX/nMhFgfxDjxABEB +AAGJBGwEGAEKACAWIQTx80ZkWEUrLfNR8ehk0SupWs4fLQUCY/QGQAIbAgJACRBk +0SupWs4fLcF0IAQZAQoAHRYhBDgB5+1vnI0s1XHgHmq9zRRNZkPIBQJj9AZAAAoJ +EGq9zRRNZkPIMbUQAJaDnJHMMXTNmANva65XjY2eJpoYBCIvd8FodRfFCbAPkNad +MtsCgd2dXZPizTOUNqcOujACd7u3P/VazYT0cUgjx6mpWdvxYuGMCM71WLHKeCaq +bXzzKrNaREMDTsMBn0wrIr5ZEuRsLOi4ZVZ5vFvtMQYnzjNT6gON+fHpaD6sShnR +VWXWaYtQ2ttN2+6gwmKCaqiH2suA+QkI/gPjqdMOeXvu6sMUd5IjaCBJy3Ddyjif +/ZYkJUjDkxG7aC4B2XtGUf0lPG+kiCHGjgTsvIeYYSpi/TyevTF8QNfZWcp/NBcf +ZXhCoUoA62zzQ2SXpydZpryKn8klAYQLLA8mq6v/ljqcwFyLYtx0Cw49Thspo/4r +ba1jzsv5QdBveIKdGjzcuexTaIEFB6rQXIFuVVfn074tpZIO+KmHO/z62i73bbko +67tm+VDvbgsGUd4536lSKMekbdn0+5ODl76AJCD0M+Vzxkl9X/fg4zgz0vG2Ppiq +08LqBPidA9EQ+tEHm7OIXk9Z+wApDCb27zwsiygkV9uWXuEaNYjCjUZTEw9CYTuH +CdCPOdeJYBzKpfGXldJo6F6NbLLXywL4ej2Lt99tqFF2tQ3I6SKyYx+I2veYsjKs +7g29bF4WuU1IVi4Kn144NUzEHOJZKeyYOwEz5+chq9KuYBY8b1OHe1Q5pEFIbVIP +/1pdwhs6zV8tJZOgzLb9q+yLuXH1Fk4YE9wZDh/rK3hpD+KGyNRa+0J70wdYDOqk +4C9ybAaljvJPXO622Ai/RlFLQVK4KdJ2Ig9mwtIhwBvjnKkCmG502HGRUa3HVpDK +pb9WDrH9eJPxkRew1y7Kl6ua10mNh7vMIbEDzZY36Eovzc127ANy/EQR8OwnI8Vg +39rCq1wDVeULHmF4j63cm3pHo6LK1OGZjAkg9XjT/aDpuqigcdEmFjmx7RSBPZFC +RZTJ6kcafbnxQfKx7soI7+1AWVSrTt+/XePZPubnFeMlfXtGVXejTG2rCWJqRpGZ +sjwgGiOtcnzvF37TQ4XrWV5T45XeSmG4hsF+zShXqevGulOwGNPtJbmiINTaeKun +1KxjSVpwkniOQgrWNSFCD2RzSEuQRKSg0XMbgPLbmplVO4WAzhQ/Ry4DpNqjJwkp +2z5WQ8XhfsxecNBc10pbPGyDUbXk96bZSXc31s5tKIyUaCxMmUu87Z0q9KEaVrGc +Tp69o4LIX8dhEqAx8Mk1AKpk8TsT0Ebc75X+xbzVoiimblUuB/+OrDsK7R0hihIe +TU+1xOJ1gyppkuacOuHioV4k9k4NUwgk+YrSKTrhFEzbM6gcOngTB0VTFzQlEjxB +wxl2qN7f0lFD6F0rLJ0Rm06xIwTNIe/0MfMXAJBB45DFuQINBGP0BlIBEADAkdgW +M8SyGyde5Op/B9yMHNPfuSNRjK4/HHmLez1GTriNwuqor5FRrDCO8VPUbQX/x06O +2HZj8fJWa+6hc9+giUTXNbYtlMVpZOUVhGxzuy2Y6YE82maBaJ3EB/KBP7zdgvKT +bxmjv5hre9u/LaY6tloCzeaBUWPV9+e5Bxq72qC507V/z6lc+PgxWWfGkmWBuT+v +laHWFb6ZM5ldtcMSdscrLBcxLMnjNIRlIaWpj+tvuInMdV3HrTn/bdHCP/Ybrf95 +DYY+7p+KPGrdXJH121f8qZXRihTJerJOGvGbue6FIJ+wYSEr3nb9bNyym/w+Mk9Z +0wJZZVfjbqFNcGhTttZWlzdTJwerwj7cGsTtMcuIphhUdLhQns+dBTVKVrqvvHSu +p/w9IpnyDhcgqv8v23xfSCuKooWPn2E1/Pd4enLCHVzmFW1xQDtDunRuxBbHYpM4 +5gknVdIp8bY23y1fj0mottIfgZZEfiMR6FJxseFcWuG7VdC7VITdgbNl5YDXw4ts +xmg2qrRSNUTkFAKNwIekqwziay4DcnWkoikH+n3bHre5wQqFzHIV03Zo8YcgKvyT +0hwAvn2wGRoIynInFMi2/314xbAUBq10QhREGOPS3oUvBUZxhTkiBMKVYyKA97JQ +c2Xhrkx9cuZxh3y7j3DflRBW9XLJvbcLGDziTwARAQABiQI2BBgBCgAgFiEE8fNG +ZFhFKy3zUfHoZNErqVrOHy0FAmP0BlICGwwACgkQZNErqVrOHy0dOxAAlNRb0yBq +SLLU/pQHjnqRQsLpXFmokcAVfZcEoODTMmzPf3uKDExkHBsyRjbRrEazMLQZIwIb +78AXvPx6W+lwkmrZ1IXfTkURMi2RmSSOcjTJzipM4WKkOy6zSg29chnBz8edq8AF +rErYdY5IgGCn3RHtkGjtKRSV0m4cdoO/wqGHtZdxEhmfmAzs+Wwevqb1nzptG3my +ZdEJ5rkgGcnvUjkJo815FjR1fuo0KSuVZVelvWMp6JFYMWc4FUh2bYWymIQ6u8/f +2v8EnacG/oNHDkZG0edTPU4dClHCtXqejAxazHYUojJkFdWUMoEIJ7VYg23N4WAW +0qf78uBOuGBjl8g5sOmu/IQpMsO51NiDSw/lGLfPsKJKTIe7N6Jxs8PT66Jqvw2U +4moKEAcoLGxXkIfY7UMFGflaADzBQEebNiekRMw/SAxB3mRptuQ96QuCrpLE7kmI +KPs0vk3om0Lz59q3JoYmMEoEIMM3Z1j94mp07nyJzKvOREtQYY7WIKG/sgUHekjm +lrUfez8xHCG4G0r4KTiu3rGT/rvCehTxvkl4Gmimeo+XNb7vwcr1O0/DTH3ZCG8o ++mwGnah7T6ch60YFSWm0RkxNozNHWJf5Ee6gVv7nEyB1pbuqhXHliv3hhK+/4SWW +RMwhK4b5axJn9aHTu3rwDdaDpUkkApY4rhq5Ag0EY/QGZAEQAOXjz3loH0/mn+Wn +wermse6fhyW+HJNIcWLdTZ3o44GhbkWb5VxCdb/FuOYIGxeTkF2KjCwHFCHCfN1/ +P8okvsnlGhuiZQRpVHBv1TBPzx4m94unXgEbyPYndKN/KGsJf7iOQ/HRs9CTUcZy +5hj608Rd/Wr+mzzwOG7QIBEEjNhA5NhjpvWpbPGkOgVkYeMobyDmJjoUi7rnIoq+ +9XLV/wiBneXcinAFZVqbGCRNxhjRBhKubOjWftNfHCtZu96cCoGxDRwE+z6BVre4 +iv7VMmXQDPlISUFUa7cu9R2WTny2u09SPpNBHdhSSDtWOWXtYc52qG7HllA2GOQ6 +wd6t/RPDzp7pwTOB5O4htAchvQtyxS6fApy6Hb5q7tE7n31y8efT7FkTkxkHGWgM +NoncmyKWIzyTI8/9TcRGPTdxYtbsGptP6x+MA6XbVELOTSJDGTXC3/xWa0Kv0B2/ +sjKu1pi9/9vBE/6D72V2bMoa3wx1vrTm5XNnvQf8subXt/jRN75Adp7HlvL/qnpy +7AQRm2AiDndamCW7SsDpTGsF9AQcqX8m3cUt4TSacTJiSRHYycc23JZEhe26phkw +CbZRvWkUcfuNBXWAaINVPDprZ4jArbVr+Fe1GMVSkV3WcHWf4o18kETjNPfCbdR3 +uYrD/qtaehHKFhm8ZeQV2n6ISzj1ABEBAAGJAjYEGAEKACAWIQTx80ZkWEUrLfNR +8ehk0SupWs4fLQUCY/QGZAIbIAAKCRBk0SupWs4fLcubD/oDGub4+uep50VBUa0u +BZAUu/oS664+53sZyvogMzeIT32DT3vDaa3W2aqUNX/dZVzOcsV07HO4yk6+kiSk +1Db2FbRFODbFcs5mBYo/EFSxExhQMQFqgXaW3FrpvL5ljAwsjdoSN93DnMkLnC9K +XZUyUT+RDcJnk0xS+0ex77nc8vp13n2huHuXU6BbEGofrT9br7Kyezh84GV9nxls +C0PwTX0gBaesqeY/9rtAXq+p+kYBafbny/3zrL8CBwqHqRZWiNbkyGWx9WHvizZE +0VJJzGl0CTP7aE/N42t+LDGuaA76SJXkkqGs7GmJ3EHVA8N/2Lwhf0saaG3cBrKx +lXrJoSY7TxeoJ7rdt/KRJfKsU0bdXgVXDFrlf4ZvctCLZmQ0nno2cgYemTnELRYv +FzrS2itqqWP1ev2iPpCbKp099i/w6D13C3jBVAVYPBapD6aaD7YHWLhHIA5zH7bF +n8IgacgKBoJ8u3jo3eeT5CXfsrnwOYdrqposfMCUOriJHx41nGUqjNZDG2ByHxgS +mnUd3lrjRDWTUzXj8pRN2K7Uqbbs2Mz4Q64MgbCkkTichMlVux8kH+O/I/veAYto +OEpwdDwa67AtzYKG0ssOJI+po9TlbKYS4O4H8XnPhYSOEw8eObNPYCX7jyAjXloo +1hbflYLyMYo1BxGR6bPS9gJA2w== +=5uun +-----END PGP PUBLIC KEY BLOCK----- diff --git a/user/keys/ssh/graphone.pub.key b/user/keys/ssh/graphone.pub.key new file mode 100644 index 0000000..d07e510 --- /dev/null +++ b/user/keys/ssh/graphone.pub.key @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJM1HutPcWXdeTaAXY7ha8SlgeZFtLJGwNa3Kd/DL/R38fq5+fkh3iCoHgv+iiKcordtVTMhbOsHhz3H+Jm274c= diff --git a/user/keys/ssh/work.pub.key b/user/keys/ssh/work.pub.key new file mode 100644 index 0000000..c4b3a55 --- /dev/null +++ b/user/keys/ssh/work.pub.key @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDXYU5c7AUD5tQQdpzQ73yy3ti3R7dArZ+f/wETN7L2Z2Hw6zo6hDid9/Q4yxdgM/FlTj/Ok2DHBWqxJsEe3S4shwsT9l2qJatjdcUK6zH3/0nFPxGYIaByj87aZ+5dwMoWNGlioPWciUdKeovvau1PwvdBxPabHHap6nwC9yPaSIVbZi4GgYv/zEvOB4LVYLuxLqr0pPdMNz1ddjmjsQCq5alC33jSZWkABERw3GlF02dNHbUq6cZlFq9BudbNWBQ8zFgj/C8amK4DHUSeU8w+ckTmO5PjDjINOnFr8kytDap+/5AQ6kr618evJ2JCwnBj6txb3SVGhcvn3/DJjf2H7flVhZEWIMEMu7452SXfz9mxp3Vu3UMJkjHUj6Lxl302M318k9j+w1fa8EHO7OQHQZajNKrEP5/UK2CDfpP2KIybX5HnEqBcEqoSKhRt7ytNX6VGzURk3/mmk9L+An5z7ve+zqlgNOA8uaIoebB4476+n5pGiNIedO3FRjPofEidYjf5NTZ9YDpqFc5KbfbhduuP63G/kqmgTxXMuTsWINY2xKEc0BPnlEGfezMN+eQpwWINOUxW1ZEk3OYMvC91EndbVwxVbm3aze9894T3+wVTipJ88xARCQeQpu1eaWDSaNduD+8LAouQiTA4whM+jBEeQoWZe6Wf6W4tBeCZ6Q== diff --git a/user/keys/ssh/yubikey.pub.key b/user/keys/ssh/yubikey.pub.key new file mode 100644 index 0000000..a840349 --- /dev/null +++ b/user/keys/ssh/yubikey.pub.key @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDl4895aB9P5p/lp8Hq5rHun4clvhyTSHFi3U2d6OOBoW5Fm+VcQnW/xbjmCBsXk5BdiowsBxQhwnzdfz/KJL7J5RobomUEaVRwb9UwT88eJveLp14BG8j2J3SjfyhrCX+4jkPx0bPQk1HGcuYY+tPEXf1q/ps88Dhu0CARBIzYQOTYY6b1qWzxpDoFZGHjKG8g5iY6FIu65yKKvvVy1f8IgZ3l3IpwBWVamxgkTcYY0QYSrmzo1n7TXxwrWbvenAqBsQ0cBPs+gVa3uIr+1TJl0Az5SElBVGu3LvUdlk58trtPUj6TQR3YUkg7Vjll7WHOdqhux5ZQNhjkOsHerf0Tw86e6cEzgeTuIbQHIb0LcsUunwKcuh2+au7RO599cvHn0+xZE5MZBxloDDaJ3JsiliM8kyPP/U3ERj03cWLW7BqbT+sfjAOl21RCzk0iQxk1wt/8VmtCr9Adv7IyrtaYvf/bwRP+g+9ldmzKGt8Mdb605uVzZ70H/LLm17f40Te+QHaex5by/6p6cuwEEZtgIg53Wpglu0rA6UxrBfQEHKl/Jt3FLeE0mnEyYkkR2MnHNtyWRIXtuqYZMAm2Ub1pFHH7jQV1gGiDVTw6a2eIwK21a/hXtRjFUpFd1nB1n+KNfJBE4zT3wm3Ud7mKw/6rWnoRyhYZvGXkFdp+iEs49Q== diff --git a/user/modules/bash/bash/aliases b/user/modules/bash/bash/aliases new file mode 100644 index 0000000..ea7697b --- /dev/null +++ b/user/modules/bash/bash/aliases @@ -0,0 +1,25 @@ +# Navigation +alias cd='cd -L' + +# Colors +eval "$(dircolors -b)" +alias ls='ls --color=auto' + +# Search +alias grep='grep --color=auto' + +# Tree (uses eza if available) +if command -v eza >/dev/null 2>&1; then + alias tree='eza --tree --icons=never' +fi + +# Open (graphical environment only) +if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then + alias open='xdg-open' +fi + +if command -v nvim >/dev/null 2>&1; then + alias vim='nvim' +fi + +alias cdg='cd "$(git rev-parse --show-toplevel 2>/dev/null)"' diff --git a/user/modules/bash/bash/bashrc b/user/modules/bash/bash/bashrc new file mode 100644 index 0000000..4e08b82 --- /dev/null +++ b/user/modules/bash/bash/bashrc @@ -0,0 +1,21 @@ +# Use gpg-agent for SSH (YubiKey support) +if command -v gpgconf >/dev/null 2>&1; then + export SSH_AUTH_SOCK + SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" +fi + +# Source prompt and aliases +BASH_CONFIG_DIR="${BASH_SOURCE%/*}" +. "$BASH_CONFIG_DIR/prompt" +. "$BASH_CONFIG_DIR/aliases" + +# Vi mode +set -o vi + +# Completion +bind 'set completion-ignore-case on' +bind 'set completion-map-case on' + +if command -v direnv >/dev/null 2>&1; then + eval "$(direnv hook bash)" +fi diff --git a/user/modules/bash/bash/prompt b/user/modules/bash/bash/prompt new file mode 100644 index 0000000..9b7a494 --- /dev/null +++ b/user/modules/bash/bash/prompt @@ -0,0 +1,179 @@ +# Detect graphical environment once at source time +if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then + _gfx=1 + _py_sym=$'\ue73c' + _js_sym=$'\ue781' + _nix_sym=$'\ue843' + _proj_sym=$'\ueb45 ' + _branch_sym=$'\uf418' +else + _gfx="" + _py_sym="py" + _js_sym="js" + _nix_sym="nix" + _proj_sym="../" + _branch_sym="git" +fi + +# Pre-compute colored icons +_python_icon="\[\033[01;33m\]$_py_sym\[\033[00m\]" +_node_icon="\[\033[01;93m\]$_js_sym\[\033[00m\]" +_nix_icon="\[\033[01;34m\]$_nix_sym\[\033[00m\]" + +# SSH check once at source time +if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then + _ssh_PS1="\n\[\033[01;37m\]\u@\h:\[\033[00m\]\n" +else + _ssh_PS1="" +fi + +# Static prompt parts +_green_arrow="\[\033[01;32m\]>> " +_white_text="\[\033[00m\]" + +# Cache: dir -> "git_root|superproject|" or "-" for non-git +declare -A _dir_cache + +# Find git root by walking up (no git spawn) +_find_git_root() { + local dir="$PWD" + while [ -n "$dir" ]; do + if [ -e "$dir/.git" ]; then + _git_root="$dir" + return 0 + fi + dir="${dir%/*}" + done + return 1 +} + +# Find superproject by walking up from git root +_find_superproject() { + local dir="${_git_root%/*}" + _superproject="" + while [ -n "$dir" ]; do + if [ -e "$dir/.git" ]; then + _superproject="$dir" + return 0 + fi + dir="${dir%/*}" + done + return 1 +} + +# Read branch from .git/HEAD (no git spawn) +_read_branch() { + local git_dir="$_git_root/.git" + local head_file + + # Submodule: .git is a file with "gitdir: " + if [ -f "$git_dir" ]; then + read -r _ head_file < "$git_dir" + head_file="$head_file/HEAD" + else + head_file="$git_dir/HEAD" + fi + + [ -f "$head_file" ] || return 1 + + local head + read -r head < "$head_file" + + if [[ "$head" == "ref: refs/heads/"* ]]; then + _git_branch="${head#ref: refs/heads/}" + else + # Detached HEAD - short hash + _git_branch="${head:0:7}" + fi +} + +# Build venv icons (must run every prompt - env can change) +_build_venv_icons() { + venv_icons="" + [ -n "$IN_NIX_SHELL" ] && venv_icons+="$_nix_icon " + [ -n "$VIRTUAL_ENV" ] && venv_icons+="$_python_icon " + [ -d "$_git_root/node_modules" ] && venv_icons+="$_node_icon " +} + +# Main prompt logic +_set_prompt() { + local cached="${_dir_cache[$PWD]}" + + # Check cache + if [ -z "$cached" ]; then + if _find_git_root; then + _find_superproject + _dir_cache[$PWD]="$_git_root|$_superproject|" + else + _dir_cache[$PWD]="-" + cached="-" + fi + fi + + # Non-git directory + if [ "$cached" = "-" ] || [ "${_dir_cache[$PWD]}" = "-" ]; then + venv_icons="" + [ -n "$IN_NIX_SHELL" ] && venv_icons+="$_nix_icon " + [ -n "$VIRTUAL_ENV" ] && venv_icons+="$_python_icon " + PS1="$_ssh_PS1\n\[\033[01;34m\]\w\[\033[00m\]\n$venv_icons$_green_arrow$_white_text" + return + fi + + # Parse cache + [ -z "$cached" ] && cached="${_dir_cache[$PWD]}" + IFS='|' read -r _git_root _superproject _ <<< "$cached" + + # Get branch (can change without cd) - if fails, git root is gone + if ! _read_branch; then + unset "_dir_cache[$PWD]" + _set_prompt + return + fi + + # Build paths using bash string ops (no readlink spawn) + local git_curr_dir="${PWD#$_git_root}" + local git_root_dir="${_git_root##*/}" + + # Build working_dir + local working_dir + if [ -n "$_superproject" ]; then + local super_name="${_superproject##*/}" + working_dir="\[\033[01;34m\]$_proj_sym$super_name/$git_root_dir$git_curr_dir\[\033[00m\]" + elif [ -z "$git_curr_dir" ]; then + working_dir="\[\033[01;34m\]$_proj_sym$git_root_dir\[\033[00m\]" + else + working_dir="\[\033[01;34m\]$_proj_sym$git_root_dir$git_curr_dir\[\033[00m\]" + fi + + # Build branch PS1 + local git_branch_PS1 + if [ -n "$_gfx" ]; then + git_branch_PS1="\[\033[01;31m\]$_git_branch $_branch_sym:\[\033[00m\]" + else + git_branch_PS1="\[\033[01;31m\]$_git_branch:\[\033[00m\]" + fi + + # Build venv icons + _build_venv_icons + + PS1="$_ssh_PS1\n$working_dir\n$venv_icons$_green_arrow$git_branch_PS1$_white_text" +} + +# Invalidate cache for current directory +_prompt_cache_invalidate() { + unset "_dir_cache[$PWD]" +} + +# Wrap git to invalidate cache on repo-creating commands +git() { + command git "$@" + local ret=$? + [[ "$1" =~ ^(init|clone)$ ]] && _prompt_cache_invalidate + return $ret +} + +if [ -n "$PROMPT_COMMAND" ]; then + PROMPT_COMMAND="_set_prompt;$PROMPT_COMMAND" +else + PROMPT_COMMAND="_set_prompt" +fi 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/README.md b/user/modules/git/git/README.md new file mode 100644 index 0000000..86fba58 --- /dev/null +++ b/user/modules/git/git/README.md @@ -0,0 +1,9 @@ +# Git Config + +My global git configuration. + +## Install + +```bash +git clone git@github.com:itme-brain/git.git ~/.config/git +``` diff --git a/user/modules/git/git/config b/user/modules/git/git/config new file mode 100644 index 0000000..042a282 --- /dev/null +++ b/user/modules/git/git/config @@ -0,0 +1,28 @@ +[init] + defaultBranch = master + +# Use vimdiff for diffs and merge conflicts +[diff] + tool = vimdiff + +[merge] + tool = vimdiff + +[mergetool] + keepBackup = false + +[mergetool "vimdiff"] + trustExitCode = true + +[user] + name = Bryan Ramos + email = bryan@ramos.codes + signingKey = F1F3466458452B2DF351F1E864D12BA95ACE1F2D + +# Auto-set upstream on first push +[push] + autoSetupRemote = true + +# Enable per-project with: git config --local commit.gpgSign true +[commit] + gpgSign = false diff --git a/user/modules/git/git/ignore b/user/modules/git/git/ignore new file mode 100644 index 0000000..2831c75 --- /dev/null +++ b/user/modules/git/git/ignore @@ -0,0 +1,14 @@ +# JavaScript / Node +node_modules + +# Nix +.direnv +result + +# Haskell +dist-newstyle + +# Nuxt +.nuxt/ +.output/ +dist 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..a6069d0 --- /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/README.md b/user/modules/neovim/nvim/README.md new file mode 100644 index 0000000..144eb56 --- /dev/null +++ b/user/modules/neovim/nvim/README.md @@ -0,0 +1,73 @@ +# Neovim Configuration + +Portable Neovim configuration using lazy.nvim and native LSP (Neovim 0.11+, tested on 0.12.1). + +## Installation + +### Standalone (any system) +```bash +git clone git@github.com:itme-brain/nvim.git ~/.config/nvim +``` + +### As part of NixOS config +```bash +git clone --recurse-submodules git@github.com:itme-brain/nixos.git +``` + +## Features + +- **Native LSP** (`vim.lsp.config` / `vim.lsp.enable`) - no manual server list needed +- **Smart LSP picker** (`css`) - auto-detects installed servers for current filetype +- **Neovim 0.12 compatible** - uses built-in `:lsp` commands and keeps legacy `:Lsp*` aliases working +- **Portable** - works on NixOS (LSPs via extraPackages) or any system (LSPs via Mason) +- **Mason** - auto-disabled on NixOS, auto-installs essentials elsewhere + +## LSP Setup + +On NixOS, LSPs come from: +- `neovim.extraPackages` (global essentials) +- Project `devShell` (project-specific) + +On other systems, Mason auto-installs: +- `lua_ls` - Lua/Neovim +- `nil_ls` - Nix +- `bashls` - Bash/Shell +- `jsonls` - JSON +- `html` - HTML +- `cssls` - CSS +- `marksman` - Markdown +- `taplo` - TOML +- `yamlls` - YAML + +The smart picker (`css`) scans all lspconfig servers and shows only those with binaries installed. +On Neovim 0.12+, start/stop/restart uses the built-in `:lsp` commands under the hood. + +## Key Bindings + +| Key | Action | +|-----|--------| +| `css` | Start LSP (smart picker) | +| `csx` | Stop LSP | +| `csr` | Restart LSP | +| `cf` | Format code | +| `ca` | Code action | +| `cr` | Rename symbol | +| `gd` | Go to definition | +| `gr` | Find references | +| `e` | Toggle file explorer | +| `bd` | Delete buffer | +| `/` | Live grep from git root | +| `ff` | Find files from git root | + +## Plugins + +- **lazy.nvim** - plugin manager +- **nvim-lspconfig** - LSP configurations +- **nvim-cmp** - completion +- **telescope.nvim** - fuzzy finder +- **nvim-treesitter** - syntax highlighting +- **neo-tree.nvim** - file explorer +- **gitsigns.nvim** - git integration +- **lualine.nvim** - statusline +- **bufferline.nvim** - buffer tabs +- **which-key.nvim** - keybinding hints diff --git a/user/modules/neovim/nvim/init.lua b/user/modules/neovim/nvim/init.lua new file mode 100644 index 0000000..fad0bd2 --- /dev/null +++ b/user/modules/neovim/nvim/init.lua @@ -0,0 +1,13 @@ +local function load_config_directory(directory) + local config_path = vim.fn.stdpath("config") .. "/lua/" .. directory + local files = vim.fn.readdir(config_path, function(name) + return name:sub(-4) == ".lua" + end) + + for _, file in ipairs(files) do + local file_name = file:sub(1, -5) + require(directory .. "." .. file_name) + end +end + +load_config_directory("config") diff --git a/user/modules/neovim/nvim/lua/config/keymaps.lua b/user/modules/neovim/nvim/lua/config/keymaps.lua new file mode 100644 index 0000000..a12a224 --- /dev/null +++ b/user/modules/neovim/nvim/lua/config/keymaps.lua @@ -0,0 +1,23 @@ +-- Keep cursor centered while navigating document +vim.keymap.set("n", "", "zz", { silent = true }) +vim.keymap.set("n", "", "zz", { silent = true }) + +-- Remap Ctrl + J/K/H/L to navigate between windows +vim.keymap.set('n', '', 'j', { noremap = true, silent = true }) +vim.keymap.set('n', '', 'k', { noremap = true, silent = true }) +vim.keymap.set('n', '', 'h', { noremap = true, silent = true }) +vim.keymap.set('n', '', 'l', { noremap = true, silent = true }) + +vim.keymap.set('n', '', ':vertical resize +10', { noremap = true, silent = true }) +vim.keymap.set('n', '', ':vertical resize -10', { noremap = true, silent = true }) +vim.keymap.set('n', '', ':horizontal resize +10', { noremap = true, silent = true }) +vim.keymap.set('n', '', ':horizontal resize -10', { noremap = true, silent = true }) + +-- Remap Shift + H/L to switch between buffers +vim.keymap.set('n', '', ':bprevious', { noremap = true, silent = true }) +vim.keymap.set('n', '', ':bnext', { noremap = true, silent = true }) + +vim.keymap.set("v", "<", "", ">gv") + +vim.keymap.set("n", "", ':nohlsearchlet @/=""', { noremap = true, silent = true}) diff --git a/user/modules/neovim/nvim/lua/config/lazy.lua b/user/modules/neovim/nvim/lua/config/lazy.lua new file mode 100644 index 0000000..89c356d --- /dev/null +++ b/user/modules/neovim/nvim/lua/config/lazy.lua @@ -0,0 +1,35 @@ +-- Bootstrap lazy.nvim +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not (vim.uv or vim.loop).fs_stat(lazypath) then + local lazyrepo = "https://github.com/folke/lazy.nvim.git" + local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath }) + if vim.v.shell_error ~= 0 then + vim.api.nvim_echo({ + { "Failed to clone lazy.nvim:\n", "ErrorMsg" }, + { out, "WarningMsg" }, + { "\nPress any key to exit..." }, + }, true, {}) + vim.fn.getchar() + os.exit(1) + end +end +vim.opt.rtp:prepend(lazypath) + +-- Make sure to setup `mapleader` and `maplocalleader` before +-- loading lazy.nvim so that mappings are correct. +-- This is also a good place to setup other settings (vim.opt) +vim.g.mapleader = " " +vim.g.maplocalleader = "\\" + +-- Setup lazy.nvim +require("lazy").setup({ + spec = { + -- import your plugins + { import = "plugins" }, + }, + -- Configure any other settings here. See the documentation for more details. + -- colorscheme that will be used when installing plugins. + install = { colorscheme = { "onedark" } }, + -- automatically check for plugin updates + checker = { enabled = false }, +}) diff --git a/user/modules/neovim/nvim/lua/config/options.lua b/user/modules/neovim/nvim/lua/config/options.lua new file mode 100644 index 0000000..3bb30b8 --- /dev/null +++ b/user/modules/neovim/nvim/lua/config/options.lua @@ -0,0 +1,48 @@ +vim.o.clipboard = "unnamedplus" +vim.g.autoformat = false + +vim.opt.number = true +vim.opt.relativenumber = true +vim.opt.cursorline = true +-- Enable true color if terminal supports it (disabled in TTY/headless) +if vim.env.COLORTERM == "truecolor" or vim.env.COLORTERM == "24bit" then + vim.opt.termguicolors = true +end + +vim.opt.tabstop = 2 +vim.opt.shiftwidth = 2 +vim.opt.softtabstop = 2 +vim.opt.expandtab = true +vim.opt.smartindent = true +vim.opt.ignorecase = true +vim.opt.smartcase = true +vim.opt.incsearch = false + +vim.opt.swapfile = false +vim.opt.backup = false +vim.opt.undofile = true + +vim.opt.guicursor = "n-v-c:block,i:block,r:block" + +vim.opt.fillchars = { eob = " " } + +local options_group = vim.api.nvim_create_augroup("config_options", { clear = true }) + +vim.api.nvim_create_autocmd("FileType", { + group = options_group, + pattern = { "python", "haskell", "c", "cpp" }, + callback = function() + local opt = vim.opt_local + opt.tabstop = 4 + opt.shiftwidth = 4 + opt.softtabstop = 4 + end, +}) + +vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, { + group = options_group, + pattern = "*.purs", + callback = function(event) + vim.bo[event.buf].filetype = "purescript" + end, +}) diff --git a/user/modules/neovim/nvim/lua/plugins/bufferline.lua b/user/modules/neovim/nvim/lua/plugins/bufferline.lua new file mode 100644 index 0000000..8ddec03 --- /dev/null +++ b/user/modules/neovim/nvim/lua/plugins/bufferline.lua @@ -0,0 +1,40 @@ +return { + { + "akinsho/bufferline.nvim", + version = "*", + dependencies = "nvim-tree/nvim-web-devicons", + config = function() + require("bufferline").setup{ + options = { + separator_style = "thin", + show_buffer_close_buttons = false, + show_close_icon = false, + }, + highlights = { + -- Force all icon backgrounds to transparent + buffer_selected = { bg = "NONE" }, + buffer_visible = { bg = "NONE" }, + background = { bg = "NONE" }, + fill = { bg = "NONE" }, + separator = { bg = "NONE" }, + separator_selected = { bg = "NONE" }, + separator_visible = { bg = "NONE" }, + close_button = { bg = "NONE" }, + close_button_selected = { bg = "NONE" }, + close_button_visible = { bg = "NONE" }, + modified = { bg = "NONE" }, + modified_selected = { bg = "NONE" }, + modified_visible = { bg = "NONE" }, + duplicate = { bg = "NONE" }, + duplicate_selected = { bg = "NONE" }, + duplicate_visible = { bg = "NONE" }, + indicator_selected = { bg = "NONE" }, + indicator_visible = { bg = "NONE" }, + pick = { bg = "NONE" }, + pick_selected = { bg = "NONE" }, + pick_visible = { bg = "NONE" }, + }, + } + end + } +} diff --git a/user/modules/neovim/nvim/lua/plugins/colorscheme.lua b/user/modules/neovim/nvim/lua/plugins/colorscheme.lua new file mode 100644 index 0000000..a5ab11e --- /dev/null +++ b/user/modules/neovim/nvim/lua/plugins/colorscheme.lua @@ -0,0 +1,109 @@ +return { + { + "chriskempson/base16-vim", + config = function() + local color_group = vim.api.nvim_create_augroup("config_colorscheme", { clear = true }) + + local highlights = { + Normal = { bg = "NONE", fg = "#FFFFFF" }, + Visual = { bg = "Gray", fg = "Black" }, + NonText = { bg = "NONE" }, + LineNr = { bg = "NONE" }, + CursorLine = { bg = "NONE" }, + CursorLineNr = { bg = "NONE", fg = "#E5C07B", bold = true }, + Search = { bg = "#FFCC66", fg = "#000000" }, + Pmenu = { bg = "Black", fg = "White" }, + PmenuSel = { bg = "Green", fg = "Black" }, + PmenuThumb = { bg = "Green" }, + PmenuSbar = { bg = "Black" }, + WinSeparator = { bg = "NONE" }, + GitGutterChange = { bg = "NONE" }, + GitGutterAdd = { bg = "NONE" }, + GitGutterDelete = { bg = "NONE" }, + GitSignsAddNr = { bg = "NONE", fg = "#98c379" }, + GitSignsChangeNr = { bg = "NONE", fg = "#61afef" }, + GitSignsDeleteNr = { bg = "NONE", fg = "#e06c75" }, + SignColumn = { bg = "NONE" }, + NeoTreeGitAdded = { bg = "NONE", fg = "#98c379" }, + NeoTreeGitModified = { bg = "NONE", fg = "#e5c07b" }, + NeoTreeGitDeleted = { bg = "NONE", fg = "#e06c75" }, + NeoTreeGitConflict = { bg = "NONE", fg = "#e06c75" }, + NeoTreeGitUntracked = { bg = "NONE", fg = "#61afef" }, + TelescopeSelection = { bg = "Gray", fg = "Green", bold = true }, + TelescopePreviewMatch = { bg = "Yellow", fg = "Black" }, + TreesitterContext = { bg = "NONE" }, + LazyH1 = { bg = "Black", fg = "Green" }, + IblScope = { bg = "NONE", fg = "Yellow" }, + ConflictMarker = { fg = "red" }, + DiffAdd = { bg = "NONE" }, + DiffChange = { bg = "NONE" }, + DiffDelete = { bg = "NONE" }, + DiffText = { bg = "NONE" }, + BufferLineFill = { bg = "NONE" }, + BufferLineBackground = { bg = "NONE", fg = "#5c6370" }, + BufferLineBuffer = { bg = "NONE", fg = "#5c6370" }, + BufferLineBufferSelected = { bg = "NONE", fg = "#FFFFFF", bold = true }, + BufferLineBufferVisible = { bg = "NONE", fg = "#abb2bf" }, + BufferLineCloseButton = { bg = "NONE", fg = "#5c6370" }, + BufferLineCloseButtonSelected = { bg = "NONE", fg = "#e06c75" }, + BufferLineCloseButtonVisible = { bg = "NONE", fg = "#5c6370" }, + BufferLineModified = { bg = "NONE", fg = "#e5c07b" }, + BufferLineModifiedSelected = { bg = "NONE", fg = "#e5c07b" }, + BufferLineModifiedVisible = { bg = "NONE", fg = "#e5c07b" }, + BufferLineSeparator = { bg = "NONE", fg = "#3e4452" }, + BufferLineSeparatorSelected = { bg = "NONE", fg = "#3e4452" }, + BufferLineSeparatorVisible = { bg = "NONE", fg = "#3e4452" }, + BufferLineIndicatorSelected = { bg = "NONE", fg = "#61afef" }, + YankHighlight = { bg = "yellow", fg = "black" }, + } + + local function apply_highlights() + for group, spec in pairs(highlights) do + vim.api.nvim_set_hl(0, group, spec) + end + end + + local conflict_pattern = [[<<<<<<< HEAD\|=======\|>>>>>>> .\+]] + local function apply_conflict_match(win) + if vim.w[win].conflict_marker_match_id then + pcall(vim.fn.matchdelete, vim.w[win].conflict_marker_match_id, win) + end + vim.w[win].conflict_marker_match_id = vim.fn.matchadd("ConflictMarker", conflict_pattern, 10, -1, { + window = win, + }) + end + + vim.cmd.colorscheme("base16-onedark") + apply_highlights() + + vim.api.nvim_create_autocmd("ColorScheme", { + group = color_group, + callback = apply_highlights, + }) + + vim.api.nvim_create_autocmd({ "BufWinEnter", "WinEnter" }, { + group = color_group, + callback = function(event) + apply_conflict_match(vim.api.nvim_get_current_win()) + end, + }) + + vim.api.nvim_create_autocmd("TextYankPost", { + group = color_group, + callback = function() + vim.highlight.on_yank({ higroup = "YankHighlight", timeout = 150 }) + end, + }) + end, + }, + + { + "folke/todo-comments.nvim", + dependencies = { "nvim-lua/plenary.nvim" }, + }, + + { + "fei6409/log-highlight.nvim" + } + +} diff --git a/user/modules/neovim/nvim/lua/plugins/disabled.lua b/user/modules/neovim/nvim/lua/plugins/disabled.lua new file mode 100644 index 0000000..64e75ec --- /dev/null +++ b/user/modules/neovim/nvim/lua/plugins/disabled.lua @@ -0,0 +1,5 @@ +return { + { "williamboman/mason.nvim", enabled = false }, + { "williamboman/mason-lspconfig.nvim", enabled = false }, + { "jay-babu/mason-nvim-dap.nvim", enabled = false }, +} diff --git a/user/modules/neovim/nvim/lua/plugins/gitsigns.lua b/user/modules/neovim/nvim/lua/plugins/gitsigns.lua new file mode 100644 index 0000000..b34952f --- /dev/null +++ b/user/modules/neovim/nvim/lua/plugins/gitsigns.lua @@ -0,0 +1,59 @@ +return { + { + "lewis6991/gitsigns.nvim", + config = function() + require('gitsigns').setup { + signs = { + add = { text = '+' }, + change = { text = '~' }, + delete = { text = '-' }, + topdelete = { text = '‾' }, + changedelete = { text = '~' }, + untracked = { text = '┆' }, + }, + signs_staged = { + add = { text = '+' }, + change = { text = '~' }, + delete = { text = '-' }, + topdelete = { text = '‾' }, + changedelete = { text = '~' }, + untracked = { text = '┆' }, + }, + signs_staged_enable = true, + signcolumn = false, -- Toggle with `:Gitsigns toggle_signs` + numhl = true, -- Toggle with `:Gitsigns toggle_numhl` + linehl = false, -- Toggle with `:Gitsigns toggle_linehl` + word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff` + watch_gitdir = { + follow_files = true + }, + auto_attach = true, + attach_to_untracked = false, + current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame` + current_line_blame_opts = { + virt_text = true, + virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align' + delay = 0, + ignore_whitespace = false, + virt_text_priority = 100, + }, + current_line_blame_formatter = ', - ', + sign_priority = 6, + update_debounce = 100, + status_formatter = nil, -- Use default + max_file_length = 40000, -- Disable if file is longer than this (in lines) + preview_config = { + -- Options passed to nvim_open_win + border = 'single', + style = 'minimal', + relative = 'cursor', + row = 0, + col = 1 + }, + } + require("which-key").add({ + { "Gb", ":Gitsigns toggle_current_line_blame", desc = "Git blame" } + }) + end + } +} diff --git a/user/modules/neovim/nvim/lua/plugins/indent-blankline.lua b/user/modules/neovim/nvim/lua/plugins/indent-blankline.lua new file mode 100644 index 0000000..27fae5a --- /dev/null +++ b/user/modules/neovim/nvim/lua/plugins/indent-blankline.lua @@ -0,0 +1,22 @@ +return { + { + "lukas-reineke/indent-blankline.nvim", + config = function() + local hooks = require("ibl.hooks") + + hooks.register(hooks.type.HIGHLIGHT_SETUP, function() + vim.api.nvim_set_hl(0, "IblIndent", { link = "Comment" }) + end) + + require("ibl").setup({ + indent = { + char = "|", + highlight = "IblIndent", + }, + scope = { + enabled = false + }, + }) + end, + } +} diff --git a/user/modules/neovim/nvim/lua/plugins/lsp.lua b/user/modules/neovim/nvim/lua/plugins/lsp.lua new file mode 100644 index 0000000..c31339b --- /dev/null +++ b/user/modules/neovim/nvim/lua/plugins/lsp.lua @@ -0,0 +1,310 @@ +-- Neovim 0.11+ LSP configuration +-- Uses nvim-lspconfig for server definitions + vim.lsp.enable() API + +-- Detect NixOS (Mason doesn't work on NixOS due to FHS issues) +local is_nixos = vim.fn.filereadable("/etc/NIXOS") == 1 + +-- Servers to ensure are installed via Mason (non-NixOS only) +-- On NixOS, install these via extraPackages or per-project devShells +local mason_ensure_installed = { + "lua_ls", -- Neovim config + "nil_ls", -- Nix (nixd not available in Mason) + "bashls", -- Shell scripts + "jsonls", -- JSON configs + "html", -- HTML + "cssls", -- CSS + "marksman", -- Markdown + "taplo", -- TOML + "yamlls", -- YAML +} + +return { + { + "nvim-treesitter/nvim-treesitter", + build = ":TSUpdate", + config = function() + require('nvim-treesitter.configs').setup({ + ensure_installed = { + "lua", + "c", + "cpp", + "python", + "nix", + "rust", + "bash", + "markdown", + "html", + "javascript", + "css", + + "vim", + + "git_config", + "git_rebase", + "gitattributes", + "gitcommit", + "gitignore" + }, + auto_install = true, + sync_install = true, + highlight = { + enable = true, + } + }) + end + }, + + { + "m4xshen/autoclose.nvim", + config = function() + require("autoclose").setup() + end + }, + + { + "hrsh7th/nvim-cmp", + dependencies = { + { + "L3MON4D3/LuaSnip", + version = "v2.*", + build = "make install_jsregexp", + }, + "saadparwaiz1/cmp_luasnip", + "hrsh7th/cmp-nvim-lsp" + }, + + config = function() + local cmp = require("cmp") + cmp.setup({ + enabled = function() + local context = require("cmp.config.context") + if vim.api.nvim_get_mode().mode == "c" then + return true + else + return not context.in_treesitter_capture("comment") and not context.in_syntax_group("comment") + end + end, + + snippet = { + expand = function(args) + require('luasnip').lsp_expand(args.body) + end + }, + + mapping = cmp.mapping.preset.insert({ + [""] = cmp.mapping.select_prev_item(), + [""] = cmp.mapping.select_next_item(), + [""] = cmp.mapping.scroll_docs(-4), + [""] = cmp.mapping.scroll_docs(4), + [""] = cmp.mapping.abort(), + [""] = cmp.mapping.confirm(), + [""] = cmp.mapping(function(fallback) + fallback() + end, { "i", "s" }), + }), + + sources = cmp.config.sources({ + { name = 'nvim_lsp' }, + { name = 'luasnip' }, + }, { + { name = 'buffer' } + }), + }) + end + }, + + -- Mason: portable LSP installer (disabled on NixOS where it doesn't work) + { + "williamboman/mason.nvim", + enabled = not is_nixos, + config = function() + require("mason").setup() + end + }, + { + "williamboman/mason-lspconfig.nvim", + enabled = not is_nixos, + dependencies = { "williamboman/mason.nvim" }, + config = function() + require("mason-lspconfig").setup({ + ensure_installed = mason_ensure_installed, + automatic_installation = false, -- Only install what's in ensure_installed + }) + end + }, + + { + "neovim/nvim-lspconfig", + dependencies = { + "hrsh7th/cmp-nvim-lsp", + }, + config = function() + local lspconfig = require('lspconfig') + + -- Neovim 0.12 exposes built-in :lsp commands and skips lspconfig's legacy + -- :Lsp* aliases. Recreate the old names so existing mappings keep working. + if vim.fn.exists(':lsp') == 2 and vim.fn.exists(':LspStart') == 0 then + vim.api.nvim_create_user_command('LspStart', function(info) + vim.cmd('lsp enable ' .. table.concat(info.fargs, ' ')) + end, { nargs = '*' }) + + vim.api.nvim_create_user_command('LspRestart', function(info) + vim.cmd('lsp restart ' .. table.concat(info.fargs, ' ')) + end, { nargs = '*', bang = true }) + + vim.api.nvim_create_user_command('LspStop', function(info) + local suffix = info.bang and '!' or '' + vim.cmd('lsp stop' .. suffix .. ' ' .. table.concat(info.fargs, ' ')) + end, { nargs = '*', bang = true }) + end + + -- Diagnostic display configuration + vim.diagnostic.config({ + virtual_text = { + prefix = '●', + spacing = 2, + current_line = true; + }, + float = { + border = 'rounded', + source = true, + }, + signs = { + text = { + [vim.diagnostic.severity.ERROR] = '', + [vim.diagnostic.severity.WARN] = '', + [vim.diagnostic.severity.INFO] = '', + [vim.diagnostic.severity.HINT] = '', + }, + }, + underline = true, + update_in_insert = false, + severity_sort = true, + }) + + -- Add border to hover and signature help windows. + local hover_handler = vim.lsp.handlers.hover + vim.lsp.handlers['textDocument/hover'] = function(err, result, ctx, config) + return hover_handler(err, result, ctx, vim.tbl_extend('force', config or {}, { + border = 'rounded', + })) + end + + local signature_help_handler = vim.lsp.handlers.signature_help + vim.lsp.handlers['textDocument/signatureHelp'] = function(err, result, ctx, config) + return signature_help_handler(err, result, ctx, vim.tbl_extend('force', config or {}, { + border = 'rounded', + })) + end + + -- Get all known server names by scanning lspconfig's lsp directory + local function get_all_servers() + local servers = {} + local lsp_path = vim.fn.stdpath('data') .. '/lazy/nvim-lspconfig/lsp' + local files = vim.fn.readdir(lsp_path, function(name) + return name:sub(-4) == '.lua' + end) + for _, file in ipairs(files) do + local server = file:sub(1, -5) + table.insert(servers, server) + end + return servers + end + + local all_servers = get_all_servers() + + -- local navic = require('nvim-navic') + local capabilities = require('cmp_nvim_lsp').default_capabilities() + + -- Global config applied to all servers + vim.lsp.config('*', { + autostart = false, -- Don't auto-attach, use css to start manually + capabilities = capabilities, + -- on_attach = function(client, bufnr) + -- if client.server_capabilities.documentSymbolProvider then + -- navic.attach(client, bufnr) + -- end + -- end, + }) + + -- Server-specific settings (merged with lspconfig defaults) + vim.lsp.config.lua_ls = { + settings = { + Lua = { + diagnostics = { + globals = { 'vim' } + } + } + } + } + + -- Check if server binary is available + local function is_server_installed(config) + if config.default_config and config.default_config.cmd then + local cmd = config.default_config.cmd[1] + return vim.fn.executable(cmd) == 1 + end + return false + end + + -- Find and start LSP server(s) for current filetype + local function lsp_start_smart() + local ft = vim.bo.filetype + if ft == '' then + vim.notify("No filetype detected", vim.log.levels.WARN) + return + end + + -- Find all matching servers (filetype match + binary installed) + local matching = {} + for _, server in ipairs(all_servers) do + local ok, config = pcall(require, 'lspconfig.configs.' .. server) + if ok and config.default_config and config.default_config.filetypes then + if vim.tbl_contains(config.default_config.filetypes, ft) and is_server_installed(config) then + table.insert(matching, server) + end + end + end + + -- Sort for consistent ordering + table.sort(matching) + + local function start_server(server) + vim.lsp.enable(server) + end + + if #matching == 0 then + vim.notify("No LSP server installed for filetype: " .. ft, vim.log.levels.WARN) + elseif #matching == 1 then + start_server(matching[1]) + else + vim.ui.select(matching, { + prompt = "Select LSP server:", + }, function(choice) + if choice then + start_server(choice) + end + end) + end + end + + -- LSP keybindings + require("which-key").add({ + { "cs", group = "LSP Commands" }, + { "cf", function() vim.lsp.buf.format() end, desc = "Code Format" }, + { "csi", ":checkhealth vim.lsp", desc = "LSP Info" }, + { "csr", ":lsp restart", desc = "LSP Restart" }, + { "css", lsp_start_smart, desc = "LSP Start" }, + { "csx", ":lsp stop", desc = "LSP Stop" }, + }) + end + }, + + { + "taproot-wizards/bitcoin-script-hints.nvim", + dependencies = { "nvim-treesitter/nvim-treesitter" }, + config = function() + require("bitcoin-script-hints").setup() + end + } +} diff --git a/user/modules/neovim/nvim/lua/plugins/lualine.lua b/user/modules/neovim/nvim/lua/plugins/lualine.lua new file mode 100644 index 0000000..1c09d05 --- /dev/null +++ b/user/modules/neovim/nvim/lua/plugins/lualine.lua @@ -0,0 +1,60 @@ +return { + { + "nvim-lualine/lualine.nvim", + dependencies = { + "nvim-tree/nvim-web-devicons", + "SmiteshP/nvim-navic" + }, + config = function() + require("lualine").setup ({ + options = { + icons_enabled = true, + theme = 'material', + component_separators = { left = '>', right = '|'}, + section_separators = { left = '', right = ''}, + disabled_filetypes = { + statusline = {}, + winbar = {}, + }, + ignore_focus = {}, + always_divide_middle = true, + globalstatus = true, + refresh = { + statusline = 1000, + tabline = 1000, + winbar = 1000, + } + }, + sections = { + lualine_a = {'mode'}, + lualine_b = {'branch', 'diff', 'diagnostics'}, + lualine_c = { + {'filename'}, + { function() return require("nvim-navic").get_location() end, cond = function() + return require("nvim-navic").is_available() + end, + }, + }, + lualine_x = {'filetype'}, + lualine_y = {'progress'}, + lualine_z = {'location'} + }, + inactive_sections = { + lualine_a = {}, + lualine_b = {}, + lualine_c = {}, + lualine_x = {}, + lualine_y = {}, + lualine_z = {} + }, + tabline = {}, + winbar = {}, + inactive_winbar = {}, + extensions = { + 'lazy', + 'neo-tree', + } + }) + end + } +} diff --git a/user/modules/neovim/nvim/lua/plugins/neotree.lua b/user/modules/neovim/nvim/lua/plugins/neotree.lua new file mode 100644 index 0000000..c7de716 --- /dev/null +++ b/user/modules/neovim/nvim/lua/plugins/neotree.lua @@ -0,0 +1,102 @@ +return { + { + "nvim-neo-tree/neo-tree.nvim", + dependencies = { + "nvim-lua/plenary.nvim", + "nvim-tree/nvim-web-devicons", -- not strictly required, but recommended + "MunifTanjim/nui.nvim", + }, + config = function() + require("neo-tree").setup({ + enable_diagnostics = false, + default_component_configs = { + git_status = { + symbols = { + added = "+", + modified = "~", + deleted = "-", + renamed = ">", + untracked = "?", + ignored = "!", + unstaged = "U", + staged = "S", + conflict = "C", + }, + }, + }, + window = { + position = "left", + width = 20, + }, + event_handlers = { + { + event = "neo_tree_window_after_open", + handler = function() + local win = vim.api.nvim_get_current_win() + vim.wo[win].winfixwidth = true + vim.wo[win].winfixbuf = true + vim.wo[win].cursorline = true + end + }, + }, + }) + + -- Keep the selected entry readable without a solid row background. + vim.api.nvim_set_hl(0, "NeoTreeCursorLine", { bg = "NONE", fg = "#a6e3a1" }) + + -- Apply highlight and re-apply on colorscheme change + vim.api.nvim_create_autocmd({ "FileType", "ColorScheme" }, { + pattern = { "neo-tree", "*" }, + callback = function(ev) + if ev.event == "ColorScheme" then + vim.api.nvim_set_hl(0, "NeoTreeCursorLine", { bg = "NONE", fg = "#a6e3a1" }) + end + local win = vim.api.nvim_get_current_win() + local buf = vim.api.nvim_win_get_buf(win) + if vim.bo[buf].filetype == "neo-tree" then + vim.wo[win].winhighlight = "CursorLine:NeoTreeCursorLine" + end + end, + }) + + -- Lock cursor to leftmost column in neo-tree + vim.api.nvim_create_autocmd("CursorMoved", { + pattern = "neo-tree*", + callback = function() + local col = vim.api.nvim_win_get_cursor(0)[2] + if col ~= 0 then + vim.api.nvim_win_set_cursor(0, { vim.api.nvim_win_get_cursor(0)[1], 0 }) + end + end, + }) + + local function toggle_neotree() + local api = vim.api + local bufs = api.nvim_list_bufs() + + for _, buf in ipairs(bufs) do + local name = api.nvim_buf_get_name(buf) + if name:match("neo%-tree filesystem") then + vim.cmd("Neotree close") + return + end + end + + vim.cmd("Neotree") + end + + require("which-key").add({ + { "e", toggle_neotree, desc = "File Explorer" } + }) + + --vim.fn.sign_define("DiagnosticSignError", + -- {text = " ", texthl = "DiagnosticSignError"}) + --vim.fn.sign_define("DiagnosticSignWarn", + -- {text = " ", texthl = "DiagnosticSignWarn"}) + --vim.fn.sign_define("DiagnosticSignInfo", + -- {text = " ", texthl = "DiagnosticSignInfo"}) + --vim.fn.sign_define("DiagnosticSignHint", + -- {text = "󰌵", texthl = "DiagnosticSignHint"}) + end, + }, +} diff --git a/user/modules/neovim/nvim/lua/plugins/telescope.lua b/user/modules/neovim/nvim/lua/plugins/telescope.lua new file mode 100644 index 0000000..06d1e0c --- /dev/null +++ b/user/modules/neovim/nvim/lua/plugins/telescope.lua @@ -0,0 +1,62 @@ +local function get_root() + local result = vim.system({ "git", "rev-parse", "--show-toplevel" }, { text = true }):wait() + if result.code == 0 and result.stdout then + local git_dir = vim.trim(result.stdout) + if git_dir ~= "" then + return git_dir + end + end + + return vim.fn.getcwd() +end + +return { + { + "nvim-telescope/telescope.nvim", + branch = '0.1.x', + dependencies = { + { 'nvim-lua/plenary.nvim' }, + { 'nvim-tree/nvim-web-devicons' } + }, + config = function() + -- Custom Telescope command to grep from Git root + require("which-key").add({ + { "/", function() + require('telescope.builtin').live_grep({ cwd = get_root() }) + end, + desc = "grep" }, + { "ff", function() + require('telescope.builtin').find_files({ cwd = get_root() }) + end, + desc = "Search for Files" }, + { "fp", ":Telescope oldfiles", desc = "Oldfiles" }, + { "?", ":Telescope command_history", desc = "Command History" }, + { "cm", ":Telescope man_pages", desc = "Manpages" }, + + -- Code + { "gd", + function() + local attached = vim.lsp.get_clients({ bufnr = 0 }) + if next(attached) ~= nil then + require('telescope.builtin').lsp_definitions() + else + vim.api.nvim_feedkeys("gd", "n", false) + end + end, + mode = "n", + desc = "Go to Definition" + }, + { "gd", ":Telescope lsp_definitions", desc = "Go to Definition" }, + { "gr", ":Telescope lsp_references", desc = "Goto References" }, + { "gi", ":Telescope lsp_implementations", desc = "Go to Implementations" }, + { "gt", ":Telescope lsp_type_definitions", desc = "Go to Type Definition" }, + { "cv", ":Telescope treesitter", desc = "Function Names & Variables" }, + { "cd", ":Telescope diagnostics", desc = "Code Diagnostics" }, + + -- Git + { "Gt", ":Telescope git_branches", desc = "Git Branches" }, + { "Gc", ":Telescope git_commits", desc = "Git Commits" }, + }) + end + } +} diff --git a/user/modules/neovim/nvim/lua/plugins/which-key.lua b/user/modules/neovim/nvim/lua/plugins/which-key.lua new file mode 100644 index 0000000..94aac0a --- /dev/null +++ b/user/modules/neovim/nvim/lua/plugins/which-key.lua @@ -0,0 +1,89 @@ +return { + { + "folke/which-key.nvim", + event = "VeryLazy", + opts = { + spec = { + { "l", ":Lazy", desc = "Lazy" }, + { "t", + function() + vim.cmd.botright("new") + vim.opt_local.number = false + vim.opt_local.relativenumber = false + vim.cmd.resize(10) + vim.cmd.terminal() + vim.cmd.startinsert() + end, + mode = "n", + desc = "Open Terminal" + }, + + --{ "wd", "execute 'bd' | execute 'close'", desc = "Delete window & buffer" }, + -- Window & Buffer Management + { "w", group = "Windows"}, + { "wc", ":close", desc = "Close Window" }, + { "ws", ":split", desc = "Horizontal Window Split" }, + { "wv", ":vsplit", desc = "Vertial Window Split" }, + { "wm", "_", desc = "Maximize Window" }, + + { "b", group = "Buffers"}, + { "bd", function() + local function is_neotree(bufnr) + return vim.bo[bufnr].filetype == "neo-tree" + end + + local current_buf = vim.api.nvim_get_current_buf() + + -- Skip if in neo-tree + if is_neotree(current_buf) then + vim.notify("Cannot delete buffer from neo-tree", vim.log.levels.WARN) + return + end + local buflisted = vim.fn.getbufinfo({ buflisted = 1 }) + -- Prevent deleting last buffer + if #buflisted <= 1 then + vim.notify("Cannot delete last buffer", vim.log.levels.WARN) + return + end + vim.cmd.bprevious() + vim.cmd.bdelete({ args = { tostring(current_buf) } }) + -- If we ended up in neo-tree, move back to a regular window + local new_buf = vim.api.nvim_get_current_buf() + if is_neotree(new_buf) then + vim.cmd.wincmd("l") + end + end, desc = "Delete Buffer" }, + { "bD", function() + local current_buf = vim.api.nvim_get_current_buf() + local current_win = vim.api.nvim_get_current_win() + + if vim.bo[current_buf].filetype == "neo-tree" then + vim.notify("Cannot delete neo-tree buffer", vim.log.levels.WARN) + return + end + + local wins = vim.fn.win_findbuf(current_buf) + if #wins > 1 then + vim.api.nvim_win_close(current_win, false) + end + + if vim.api.nvim_buf_is_valid(current_buf) then + vim.cmd('bdelete! ' .. current_buf) + end + end, desc = "Delete Window & Buffer" }, + + { "ca", vim.lsp.buf.code_action, desc = "Code Action" }, + { "cr", vim.lsp.buf.rename, desc = "Rename Variable" }, + { "ch", vim.lsp.buf.hover, desc = "Hover Info" }, + { "ce", vim.diagnostic.open_float, desc = "Show Diagnostic" }, + { "]d", vim.diagnostic.goto_next, desc = "Next Diagnostic" }, + { "[d", vim.diagnostic.goto_prev, desc = "Prev Diagnostic" }, + + { "G", group = "Git"}, + { "f", group = "Files"}, + { "c", group = "Code"}, + { "g", group = "Goto"}, + }, + }, + } +} 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..991524b --- /dev/null +++ b/user/modules/utils/dev/default.nix @@ -0,0 +1,57 @@ +{ 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; [ + unstable.claude-code + unstable.codex + unstable.opencode + + bubblewrap + + nix-init + nix-prefetch-git + nurl + + pkg-config + qrencode + + # Network/system tools + fping + wireguard-tools + pciutils + lshw + ] ++ optionals (osConfig.virtualisation.libvirtd.enable) [ + virt-manager + ]; + + programs = { + bash = { + initExtra = "export LLAMA_API_KEY=$(cat /run/secrets/LLAMA_API_KEY)"; + }; + 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/.gitignore b/user/modules/vim/vim/.gitignore new file mode 100644 index 0000000..b6a61f0 --- /dev/null +++ b/user/modules/vim/vim/.gitignore @@ -0,0 +1,3 @@ +autoload +plugged +.netrwhist diff --git a/user/modules/vim/vim/README.md b/user/modules/vim/vim/README.md new file mode 100644 index 0000000..4d23289 --- /dev/null +++ b/user/modules/vim/vim/README.md @@ -0,0 +1,74 @@ +# Vim Config + +Lightweight Vim config that mirrors the core editing feel of the Neovim setup without the IDE stack. + +## Install + +```bash +git clone git@github.com:itme-brain/vim.git ~/.vim +vim # plugins auto-install on first run +``` + +Requires `curl` and `git` for vim-plug bootstrap. On NixOS this is managed via home-manager instead. + +## How it works + +- Vim auto-loads `~/.vim/vimrc` — no symlinks needed +- [vim-plug](https://github.com/junegunn/vim-plug) auto-downloads itself via curl on first run if missing +- Missing plugins are installed with `PlugInstall --sync` on `VimEnter`, then the vimrc is re-sourced +- `silent! colorscheme` suppresses errors if the colorscheme hasn't been fetched yet (e.g. offline first run) +- Undo history persists to `~/.vim/undodir` across sessions (`undofile`) + +## Plugins + +| Plugin | What it does | +|--------|-------------| +| base16-vim | Colorscheme (onedark) | +| vim-surround | Surround text objects (`cs"'`, `ysiw]`) | +| auto-pairs | Auto-close brackets/quotes | +| fzf + fzf.vim | Fuzzy finder (files, buffers, grep) | +| vim-log-highlighting | Syntax highlighting for log files | +| vim-highlightedyank | Flash feedback on yank | +| lightline.vim | Statusline | +| vim-anzu | Search match count in statusline | + +## Keybinds + +Leader is `Space`. + +### File explorer & search +| Key | Action | +|-----|--------| +| `e` | Toggle netrw sidebar | +| `/` | Ripgrep search from git root | +| `ff` | Find files from git root (fzf) | +| `fp` | Recent files (fzf) | +| `fb` | Open buffers (fzf) | +| `?` | Command history (fzf) | + +### Buffers +| Key | Action | +|-----|--------| +| `H` / `L` | Previous / next buffer | +| `bd` | Delete buffer safely | + +### Windows +| Key | Action | +|-----|--------| +| `` | Navigate windows (skips netrw) | +| `` | Resize windows | +| `wc` | Close window | +| `ws` | Horizontal split | +| `wv` | Vertical split | +| `wm` | Maximize window | + +### Other +| Key | Action | +|-----|--------| +| `` | Clear search highlight | +| `` / `` | Scroll half-page (centered) | +| `<` / `>` (visual) | Indent and keep selection | +| `t` | Terminal (bottom split) | +| `ts` | Insert timestamp | +| `pu` | PlugUpdate | +| `pi` | PlugInstall | diff --git a/user/modules/vim/vim/vimrc b/user/modules/vim/vim/vimrc new file mode 100644 index 0000000..7a526d5 --- /dev/null +++ b/user/modules/vim/vim/vimrc @@ -0,0 +1,193 @@ +let data_dir = has('nvim') ? stdpath('data') . '/site' : '~/.vim' +if empty(glob(data_dir . '/autoload/plug.vim')) + silent execute '!curl -fLo '.data_dir.'/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim' +endif + +autocmd VimEnter * if len(filter(values(g:plugs), '!isdirectory(v:val.dir)')) + \| PlugInstall --sync | source $MYVIMRC + \| endif + +call plug#begin('~/.vim/plugged') + Plug 'chriskempson/base16-vim' + Plug 'tpope/vim-surround' + Plug 'jiangmiao/auto-pairs' + Plug 'junegunn/fzf', { 'do': { -> fzf#install() } } + Plug 'junegunn/fzf.vim' + Plug 'mtdl9/vim-log-highlighting' + Plug 'machakann/vim-highlightedyank' + Plug 'itchyny/lightline.vim' + Plug 'osyo-manga/vim-anzu' +call plug#end() + +let mapleader = "\" +set background=dark +if has('termguicolors') + set termguicolors +endif +silent! colorscheme base16-onedark + +highlight Normal ctermbg=NONE guibg=NONE ctermfg=White guifg=#FFFFFF +highlight NonText ctermbg=NONE guibg=NONE +highlight CursorLine ctermbg=NONE guibg=NONE +highlight CursorLineNr ctermfg=Yellow guifg=#E5C07B ctermbg=NONE guibg=NONE cterm=bold gui=bold +highlight HighlightedyankRegion ctermfg=Black guifg=#000000 ctermbg=Yellow guibg=yellow +highlight NormalNC ctermbg=NONE guibg=NONE +highlight Search ctermfg=Black guifg=#000000 ctermbg=Yellow guibg=#FFCC66 +highlight LineNr ctermbg=NONE guibg=NONE +highlight Visual ctermbg=Gray guibg=Gray ctermfg=Black guifg=Black + +let g:highlightedyank_highlight_duration = 140 +let g:lightline = { 'colorscheme': 'deus', } + +let $FZF_DEFAULT_OPTS = '--bind=tab:up,shift-tab:down' +let g:fzf_layout = { 'window': 'enew' } + +" netrw settings +let g:netrw_banner = 0 +let g:netrw_winsize = 20 +let g:netrw_liststyle = 3 +let g:netrw_browse_split = 4 +let g:netrw_altv = 1 + +autocmd FileType netrw nnoremap :wincmd l +autocmd FileType netrw nnoremap :wincmd h +autocmd FileType netrw nnoremap :wincmd j +autocmd FileType netrw nnoremap :wincmd k + +set laststatus=2 + +set number +set relativenumber +set cursorline +set scrolloff=8 + +set noincsearch +set ignorecase +set smartcase + +set clipboard=unnamedplus +set noswapfile +set nobackup +set undofile +set undodir=~/.vim/undodir + +set hidden + +set tabstop=2 +set shiftwidth=2 +set softtabstop=2 +set expandtab +set smartindent +set fillchars=eob:\ + +set statusline=%{exists('*anzu#search_status')?anzu#search_status():''} + +" --- Netrw toggle (like neo-tree) --- +function! NetrwToggle() + for i in range(1, winnr('$')) + if getbufvar(winbufnr(i), '&filetype') ==# 'netrw' + execute i . 'wincmd w' + close + return + endif + endfor + let g:netrw_return_win = winnr() + Lexplore +endfunction + +function! SafeWincmd(dir) + let target = winnr(a:dir) + if target == winnr() + return + endif + if getbufvar(winbufnr(target), '&filetype') ==# 'netrw' + return + endif + execute 'wincmd ' . a:dir +endfunction + +function! GitRoot() + let l:root = systemlist('git rev-parse --show-toplevel') + if v:shell_error == 0 && !empty(l:root) && !empty(l:root[0]) + return l:root[0] + endif + return getcwd() +endfunction + +function! SafeBdelete() + if &filetype ==# 'netrw' + echohl WarningMsg | echom 'Cannot delete buffer from netrw' | echohl None + return + endif + + let l:buflisted = getbufinfo({'buflisted': 1}) + if len(l:buflisted) <= 1 + echohl WarningMsg | echom 'Cannot delete last buffer' | echohl None + return + endif + + let l:buf = bufnr('%') + bprevious + execute 'bdelete ' . l:buf +endfunction + +" --- Plugin management --- +nnoremap pu :PlugUpdate +nnoremap pd :PlugUpgrade +nnoremap ps :PlugStatus +nnoremap pi :PlugInstall + +" --- Search (anzu) --- +nmap n (anzu-n-with-echo) +nmap N (anzu-N-with-echo) +nmap * (anzu-star-with-echo) +nmap # (anzu-sharp-with-echo) +nmap (anzu-clear-search-status) +nnoremap :nohlet @/="" + +" --- Visual indentation --- +vnoremap < >gv + +" --- Scrolling (centered) --- +nnoremap zz +nnoremap zz + +" --- Window navigation (matches nvim , skips netrw) --- +nnoremap :call SafeWincmd('h') +nnoremap :call SafeWincmd('j') +nnoremap :call SafeWincmd('k') +nnoremap :call SafeWincmd('l') + +" --- Window resize (matches nvim ) --- +nnoremap :vertical resize +10 +nnoremap :vertical resize -10 +nnoremap :resize +10 +nnoremap :resize -10 + +" --- Window management --- +nnoremap wc :close +nnoremap ws :split +nnoremap wv :vsplit +nnoremap wm _ +nnoremap ww :wincmd w +nnoremap wW :wincmd W + +" --- File explorer & search --- +nnoremap e :call NetrwToggle() +nnoremap / :execute 'lcd ' . fnameescape(GitRoot()) Rg +nnoremap ff :execute 'lcd ' . fnameescape(GitRoot()) Files +nnoremap fp :History +nnoremap fb :Buffers +nnoremap ? :History: + +" --- Buffers --- +nnoremap bd :call SafeBdelete() +nnoremap H :bprevious +nnoremap L :bnext + +" --- Terminal --- +nnoremap t :below terminal ++rows=10 + +" --- Misc --- +nnoremap ts :execute "normal! a" . strftime('[%b %d %H:%M:%S - BR]')