From aee4d0aa1525f3f1282eb08b84b22a9b7d51ba09 Mon Sep 17 00:00:00 2001 From: Daniel Fainberg Date: Sun, 5 Jul 2026 15:40:35 +0200 Subject: [PATCH] chore: initial commit --- .gitignore | 9 + CLAUDE.md | 47 +++ README.md | 73 ++++ flake.lock | 70 ++++ flake.nix | 98 ++++++ hosts/example/default.nix | 24 ++ hosts/example/hardware-configuration.nix | 37 +++ hosts/example/home.nix | 17 + modules/assets/python-startup.py | 26 ++ modules/gpg.nix | 32 ++ modules/home.nix | 407 +++++++++++++++++++++++ modules/nixos.nix | 100 ++++++ 12 files changed, 940 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 hosts/example/default.nix create mode 100644 hosts/example/hardware-configuration.nix create mode 100644 hosts/example/home.nix create mode 100644 modules/assets/python-startup.py create mode 100644 modules/gpg.nix create mode 100644 modules/home.nix create mode 100644 modules/nixos.nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f66b94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# nix build symlinks +result +result-* + +# direnv +.direnv/ + +# local, machine-specific Claude Code settings (permissions etc.) +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..15f45ad --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,47 @@ +# Flake-based NixOS + home-manager config (public half) + +Usage, quick-start, and how to consume the modules are in `README.md` — read it first. + +This is the **public** half of a two-repo split: **reusable modules** + an `example` +template host. The matching **private** repo holds the real inventory +(`nixosConfigurations.`, `hosts//`, SSH/Syncthing topology) and imports this +one as a flake input. Deploy happens from the private repo, not here. + +**Editing invariant — pick the layer before adding config:** +- generic/reusable → a module here (`modules/*.nix`, `nixosModules.*` / `homeModules.*`); + make machine/user specifics **options with defaults**, never hard-code personal values. +- machine list / hostnames / topology (SSH hosts, Syncthing peers, IPs) → the private + inventory repo, never here. The `example` host carries only placeholders. +- secrets (keys, tokens, passwords) → **neither** repo; runtime files or a secrets manager + decrypting to a path. See the SSH section. + +## Maxim: keep $HOME clean (XDG) + +Standing principle for every addition: minimise dotfiles/dirs in `$HOME`. Route a tool's +config → `~/.config`, state/history → `~/.local/state`, cache → `~/.cache`, data → +`~/.local/share`, normally by setting env vars in `home.sessionVariables` +(use `config.xdg.{config,state,cache,data}Home`). Prefer this to letting a tool litter `~`. + +Cannot be relocated, leave alone: `~/.zshenv` (bootstraps `ZDOTDIR`), `~/.nix-profile` +and `~/.nix-defexpr` (nix), `~/.icons` (cursor theme — spec mandates `~/.icons`), +`~/.pki/nssdb` (NSS cert/key DB — path hardcoded in NSS, ignores XDG; created by any +Chromium/NSS/CEF app, e.g. grayjay's embedded webview). + +## Desktop environment + +Wayland. Compositor **Hyprland**, bar **Waybar**, terminal **foot** — all configured +declaratively in `modules/home.nix` (`wayland.windowManager.hyprland`, `programs.waybar`, +`programs.foot`). Font is JetBrainsMono Nerd Font; Waybar/foot use Nerd Font glyphs +(nf-md set) for icons, so new glyphs should come from that set for visual consistency. + +Hyprland uses the **master** layout and a **German** keyboard (`kb_layout = "de"`). +Look deliberately minimal: animations, blur, rounding, and shadows are all turned +**off** on purpose — don't reintroduce them when adding desktop tweaks. + +## Rust dev environments + +Before setting up Rust for a project, read the `devShells` comment in `flake.nix`. +Short version: default is the shared `rust` devShell (`use flake ~/.nixos-config#rust` +via direnv); give a project its own `flake.nix` only when it needs its own pinned Rust +version or native C deps. The comment explains the tradeoff. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fd8f91 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# NixOS + home-manager flake + +A flake-based NixOS and home-manager configuration: a set of **reusable modules** plus a +**ready-to-run example host** that assembles them. Use it either way: + +- as a **starting template** — clone, swap in your hardware, rebuild; or +- as **modules** — import them into your own (private) flake. + +## Quick start (template) + +```sh +nix flake init -t github:/ # or: git clone … +# generate hardware config for THIS machine (the one file you must replace): +sudo nixos-generate-config --show-hardware-config > hosts/example/hardware-configuration.nix +# set your hostName + username in hosts/example/default.nix, then: +sudo nixos-rebuild switch --flake .#example +``` + +`nixosConfigurations.example` is a real, buildable machine assembled from the modules in +`modules/`. It doubles as living documentation: it shows exactly how the modules wire +together, so there's nothing to reverse-engineer. The only genuinely machine-specific file +is `hardware-configuration.nix` (NixOS generates it); everything else works from defaults +you can tweak. + +## Import the modules into your own flake + +Add this repo as a flake input and import its modules from your own configuration: + +```nix +{ + inputs.config.url = "github:/"; + # … + outputs = { nixpkgs, config, home-manager, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + modules = [ + config.nixosModules.default + ./hosts/myhost # your hardware + hostname + home-manager.nixosModules.home-manager + { home-manager.users.me.imports = [ config.homeModules.default ]; } + ]; + }; + }; +} +``` + +Your machine list, hostnames, and any private specifics stay in *your* flake — you pull +only the generic modules from here. + +## Architecture: three layers + +Kept apart on purpose. When adding config, decide which layer it belongs to: + +1. **Modules — generic, public (this repo).** Reusable building blocks under `modules/`, + exposed as `nixosModules.*` / `homeModules.*`. Anything machine- or user-specific is a + module **option with a sane default** — never hard-code personal values into a module. +2. **Inventory — topology, private (your flake).** The actual machines: + `nixosConfigurations.`, per-host `hosts//` (hardware, hostname), and + topology-revealing config (SSH hosts, Syncthing peers, internal IPs). This is what you + deploy from; it imports the modules above. Publish the modules, keep the inventory in a + private repo. The `example` host here is a placeholder-filled stand-in for it. +3. **Secrets — neither repo.** Private keys, tokens, passwords never enter *any* repo. + Provide them as runtime files (`0600`, out-of-band) or via a secrets manager that + decrypts to a runtime path; nix references only the path. + +## Layout + +``` +flake.nix # inputs + outputs (nixosModules, homeModules, example host, devShells) +modules/ # the reusable library +hosts/example/ # the template host (placeholder hardware-configuration.nix) +``` + +Shared `*.nix` / `*.lock` live at the top level; per-host files under `hosts//`. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8a9b682 --- /dev/null +++ b/flake.lock @@ -0,0 +1,70 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782704057, + "narHash": "sha256-G1I1gd32F7mp9LAe1DaZ4ZL7NX5gyiKwdCMwro1Vrck=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "868d0a692de703c2de98fab61968e4e310b7c28e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-26.05", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1782691344, + "narHash": "sha256-i5nw9BYYsMDAaOC4J+JmTof6b2GhlyH076awYRNrTV8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1f01958ffb5b3545c96d9ef2f4e24c5e5e1eb846", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-26.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782875958, + "narHash": "sha256-5eqDcnBjb1424HRQdnhuhNOBZguq1Z2tqSa2OMF/m2c=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "13139aefa973f3d96c60c0fbab801de058ae25ca", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2c5b8ec --- /dev/null +++ b/flake.nix @@ -0,0 +1,98 @@ +{ + description = "Reusable NixOS + home-manager modules, with an example host"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; + + home-manager = { + url = "github:nix-community/home-manager/release-26.05"; + inputs.nixpkgs.follows = "nixpkgs"; # Use our nixpkgs, not any pinned by home-manager. + }; + + # rust-overlay: precise, NixOS-patched Rust toolchains (any stable/nightly, + # any component/target) exposed as pkgs.rust-bin. Used by the `rust` devShell + # below. `follows` so it shares our nixpkgs. Bump with + # `nix flake update rust-overlay`. + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs@{ self, nixpkgs, home-manager, rust-overlay, ... }: + let + system = "x86_64-linux"; + in { + # ---- Reusable modules ---------------------------------------------- + # Consume these from your own (private) inventory flake: + # imports = [ config.nixosModules.default ]; + # home-manager.users..imports = [ config.homeModules.default ]; + nixosModules.default = import ./modules/nixos.nix; + homeModules.default = import ./modules/home.nix; + + # ---- The example / template host ----------------------------------- + # Real and buildable; dogfoods the modules above. Clone, replace + # hosts/example/hardware-configuration.nix, set hostName + user, rebuild. + nixosConfigurations.example = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + self.nixosModules.default + ./hosts/example + home-manager.nixosModules.home-manager + { + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + home-manager.users.user.imports = [ + self.homeModules.default + ./hosts/example/home.nix + ]; + } + ]; + }; + + # ---- Template: `nix flake init -t github:/` ----------- + templates.default = { + path = ./.; + description = "NixOS + home-manager flake: module library + example host"; + }; + + # ---- Rust dev shells ------------------------------------------------- + # DEFAULT for Rust work: one shared toolchain, used from any project + # WITHOUT giving that project its own flake. In the project dir: + # + # echo 'use flake #rust' > .envrc && direnv allow + # + # (or one-off, no direnv: `nix develop #rust`). Then + # cargo/rustc/clippy/rustfmt/rust-analyzer appear on entry and vanish on + # exit. direnv itself is enabled in modules/home.nix (programs.direnv). The + # Rust version pin rides on THIS flake's flake.lock; bump it with + # `nix flake update rust-overlay`. + # + # LIMITATION (the catch): a flake can only read files inside its own tree, + # so this shell CANNOT read a project's local rust-toolchain.toml — the + # `./` resolves to this flake, not the project dir. Every consumer + # therefore gets this ONE toolchain. Want a few fixed alternates? Add more + # named shells here (e.g. rust-nightly) and `use flake ...#name`. + # + # Give a project its OWN flake instead when it needs its own Rust version + # driven by ITS OWN rust-toolchain.toml, OR native (C) deps (openssl, + # pkg-config, protobuf, libclang...). That repo gets its own flake.nix + + # committed flake.lock; there `rust-bin.fromRustupToolchainFile + # ./rust-toolchain.toml` works (now `./` is that repo) and `buildInputs` + # holds the C libs — per-repo and reproducible for anyone who clones it. + devShells.${system} = + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + }; + in { + rust = pkgs.mkShell { + packages = [ rustToolchain ]; + }; + }; + }; +} diff --git a/hosts/example/default.nix b/hosts/example/default.nix new file mode 100644 index 0000000..bf27395 --- /dev/null +++ b/hosts/example/default.nix @@ -0,0 +1,24 @@ +# Example host — the template's "bin". Real, buildable, assembled from the +# public modules; filled with placeholders where your inventory would go. +# To adopt: replace hardware-configuration.nix (see its header), set hostName + +# the user account below, then `nixos-rebuild switch --flake .#example`. + +{ pkgs, ... }: + +{ + imports = [ ./hardware-configuration.nix ]; + + networking.hostName = "example"; + + # First NixOS version installed on this machine. Do not bump casually — see + # `man configuration.nix` (system.stateVersion). + system.stateVersion = "26.05"; + + # The login account. Rename to taste; the home-manager side is wired in + # flake.nix (home-manager.users.user) and hosts/example/home.nix. + users.users.user = { + isNormalUser = true; + extraGroups = [ "wheel" "networkmanager" "video" "input" ]; + shell = pkgs.zsh; + }; +} diff --git a/hosts/example/hardware-configuration.nix b/hosts/example/hardware-configuration.nix new file mode 100644 index 0000000..e95288d --- /dev/null +++ b/hosts/example/hardware-configuration.nix @@ -0,0 +1,37 @@ +# ┌───────────────────────────────────────────────────────────────────────┐ +# │ PLACEHOLDER — REPLACE THIS FILE. │ +# │ │ +# │ Generate the real one for YOUR machine: │ +# │ sudo nixos-generate-config --show-hardware-config \ │ +# │ > hosts/example/hardware-configuration.nix │ +# │ │ +# │ The values below are dummies. They let the flake EVALUATE and BUILD │ +# │ (UUIDs are just strings until activation), so `nix flake check` stays │ +# │ green — but this config will NOT boot on real hardware until replaced. │ +# └───────────────────────────────────────────────────────────────────────┘ + +{ lib, modulesPath, ... }: + +{ + imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; + + boot.initrd.availableKernelModules = [ "ahci" "xhci_pci" "sd_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-uuid/0000-0000"; + fsType = "vfat"; + options = [ "fmask=0022" "dmask=0022" ]; + }; + + swapDevices = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/hosts/example/home.nix b/hosts/example/home.nix new file mode 100644 index 0000000..da8d856 --- /dev/null +++ b/hosts/example/home.nix @@ -0,0 +1,17 @@ +# Example home identity — the per-user placeholders that a real inventory fills +# in. Imported alongside the generic homeModules.default (see flake.nix). + +{ ... }: + +{ + home.username = "user"; + home.homeDirectory = "/home/user"; + home.stateVersion = "26.05"; + + # Git identity. Set to your own; leaks into commit authorship anyway, so it + # belongs with the person, not the shared module. + programs.git.settings.user = { + name = "Example User"; + email = "user@example.com"; + }; +} diff --git a/modules/assets/python-startup.py b/modules/assets/python-startup.py new file mode 100644 index 0000000..607993a --- /dev/null +++ b/modules/assets/python-startup.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# Interactive Python REPL startup. Wired via $PYTHONSTARTUP (see home.nix). +# Persists REPL history to $PYTHON_HISTFILE (XDG cache), creating the dir. + +from pathlib import Path +import os +import atexit +import readline + + +history = os.getenv('PYTHON_HISTFILE') + +if history: + try: + Path(history).parent.mkdir(parents=True, exist_ok=True) + readline.read_history_file(history) + except OSError: + pass + + def write_history(): + try: + readline.write_history_file(history) + except OSError: + pass + + atexit.register(write_history) diff --git a/modules/gpg.nix b/modules/gpg.nix new file mode 100644 index 0000000..9debde5 --- /dev/null +++ b/modules/gpg.nix @@ -0,0 +1,32 @@ +# Generic GPG tooling: agent + pinentry + XDG-relocated homedir. No keys and no +# signing identity — those are personal (→ inventory). Enabling this alone is +# harmless: a user with no key just has an idle agent. + +{ config, pkgs, ... }: + +{ + programs.gpg = { + enable = true; + # Keep GnuPG out of ~/.gnupg (XDG maxim). Secret keys are imported into this + # homedir out-of-band — mutable, NEVER in the nix store. + homedir = "${config.xdg.dataHome}/gnupg"; + settings = { + no-comments = true; + keyid-format = "0xlong"; + with-fingerprint = true; + }; + }; + + services.gpg-agent = { + enable = true; + # GUI pinentry → pops on the Wayland session even when signing is triggered + # non-interactively (e.g. an agent-run `git commit`). Qt is already pulled in + # by keepassxc, so this is cheap. + pinentry.package = pkgs.pinentry-qt; + # sk-based ssh keys use the normal ssh-agent; don't let gpg-agent hijack + # SSH_AUTH_SOCK. + enableSshSupport = false; + defaultCacheTtl = 1800; # 30 min + maxCacheTtl = 7200; # 2 h + }; +} diff --git a/modules/home.nix b/modules/home.nix new file mode 100644 index 0000000..61824d5 --- /dev/null +++ b/modules/home.nix @@ -0,0 +1,407 @@ +# Generic user-level (home-manager) config. Reusable across users — nothing +# user-specific here. Identity (home.username/homeDirectory/stateVersion, git +# user.name/email), host topology (SSH hosts, Syncthing peers) and personal app +# choices (grayjay, Claude Code, …) live in the inventory, not in this module. + +{ config, pkgs, ... }: + +let + terminal = "foot"; + browser = "librewolf"; + editor = "nvim"; +in +{ + imports = [ ./gpg.nix ]; + + home.sessionVariables = { + TERMINAL = terminal; + BROWSER = browser; + EDITOR = editor; + # Keep $HOME clean: route node/npm out of ~ into XDG dirs. + NODE_REPL_HISTORY = "${config.xdg.stateHome}/node_repl_history"; + NPM_CONFIG_CACHE = "${config.xdg.cacheHome}/npm"; + NPM_CONFIG_USERCONFIG = "${config.xdg.configHome}/npm/npmrc"; + # Python REPL: UTF-8 I/O, custom startup file, XDG-routed history. + PYTHONIOENCODING = "UTF-8"; + PYTHONSTARTUP = "${config.xdg.configHome}/python/startup"; + PYTHON_HISTFILE = "${config.xdg.cacheHome}/python/history"; + # psql / wget / readline configs, XDG-routed (the rc files are declared + # via xdg.configFile below). + PSQLRC = "${config.xdg.configHome}/psql/rc"; + WGETRC = "${config.xdg.configHome}/wget/wgetrc"; + INPUTRC = "${config.xdg.configHome}/readline/inputrc"; + # Rust/cargo: keep registry cache, git checkouts and `cargo install` bins + # out of ~/.cargo (the toolchain itself comes from the flake's rust devShell). + CARGO_HOME = "${config.xdg.dataHome}/cargo"; + # Mute the harmless "Wayland does not support QWindow::requestActivate()" + # spam Qt apps (keepassxc) print; scoped to that one log category only. + QT_LOGGING_RULES = "qt.qpa.wayland=false"; + }; + # Sets XDG_{CONFIG,DATA,CACHE}_HOME and enables xdg.configFile management. + xdg.enable = true; + + home.packages = with pkgs; [ + librewolf + keepassxc # config in ~/.config/keepassxc + # claude-code is installed by programs.claude-code below (its wrapped + # finalPackage) — do NOT also list it here, or buildEnv conflicts on the + # wrapper binary. + nodejs + highlight # `ccat` alias: syntax-highlighted cat + ]; + + # ---- Per-tool config files, routed into XDG dirs --------------------- + xdg.configFile = { + "python/startup".source = ./assets/python-startup.py; + + # psql: unlimited, per-database history file (kept out of $HOME). + "psql/rc".text = '' + \set HISTFILE ${config.xdg.cacheHome}/psql/history-:DBNAME + \set HISTSIZE -1 + ''; + + # wget: keep its HSTS db in the cache dir, not ~/. + "wget/wgetrc".text = '' + hsts-file=${config.xdg.cacheHome}/wget/hsts + ''; + + # readline: affects any libreadline program (python, psql, bash...). + # zsh ignores it — it uses ZLE (see programs.zsh below). Binds Up/Down to + # prefix history search with the cursor jumped to end of line. + "readline/inputrc".text = '' + $include /etc/inputrc + "\C-x\C-p": history-search-backward + "\C-x\C-n": history-search-forward + "\e[A": "\C-x\C-p\C-e" + "\e[B": "\C-x\C-n\C-e" + "\eOA": "\C-x\C-p\C-e" + "\eOB": "\C-x\C-n\C-e" + ''; + }; + + programs.home-manager.enable = true; + + programs.git = { + enable = true; + settings = { + pull.rebase = true; + init.defaultBranch = "master"; + + # Detect whitespace errors; speed up untracked-file scans. + apply.whitespace = "fix"; + core.whitespace = "space-before-tab,-indent-with-non-tab,trailing-space"; + core.untrackedCache = true; + branch.sort = "-committerdate"; + color.ui = "auto"; + diff.renames = "copies"; + help.autocorrect = 1; + merge.log = true; + push = { default = "simple"; followTags = true; }; + submodule.recurse = true; + + # github: / gh: / gist: remote shorthands. + url."git@github.com:".insteadOf = "gh:"; + url."git://github.com/".insteadOf = "github:"; + + alias = { + l = "log --pretty=oneline -n 20 --graph --abbrev-commit"; + s = "status -s"; + d = "!\"git diff-index --quiet HEAD -- || clear; git --no-pager diff --patch-with-stat\""; + p = "pull --recurse-submodules"; + c = "clone --recursive"; + ca = "!git add -A && git commit -av"; + go = "!f() { git checkout -b \"$1\" 2> /dev/null || git checkout \"$1\"; }; f"; + amend = "commit --amend --reuse-message=HEAD"; + reb = "!r() { git rebase -i HEAD~$1; }; r"; + aliases = "config --get-regexp alias"; + contributors = "shortlog --summary --numbered"; + whoami = "config user.email"; + # Delete local branches already merged into the current branch. + dm = "!git branch --merged | grep -v '\\*' | xargs -n 1 git branch -d"; + }; + }; + }; + + programs.foot = { + enable = true; + settings = { + main = { + font = "JetBrainsMono Nerd Font:size=11"; + pad = "8x8"; + }; + colors-dark = { + alpha = 0.9; + }; + }; + }; + + # App launcher ($mod+d in Hyprland). Wayland-native, launches apps detached + # so their stderr goes to the journal, not whatever terminal you're in. + programs.fuzzel = { + enable = true; + settings.main.font = "JetBrainsMono Nerd Font:size=11"; + }; + + programs.fzf.enable = true; # provides the ^f "cd into fzf dir" binding + + # direnv + nix-direnv: a project's `.envrc` with `use flake` auto-loads any + # devShell on cd-in (and caches the eval) — general per-directory environments, + # not tied to one language. First consumer is the shared Rust dev shell (see + # the `devShells` comment in flake.nix). Zsh integration is on by default. + programs.direnv = { + enable = true; + nix-direnv.enable = true; + }; + + programs.zsh = { + enable = true; + autosuggestion.enable = true; + syntaxHighlighting.enable = true; + enableCompletion = true; + # Keep the completion dump in the cache dir, not ~/.zcompdump. + completionInit = '' + _zcd="${config.xdg.cacheHome}/zsh"; [ -d "$_zcd" ] || mkdir -p "$_zcd" + autoload -U compinit && compinit -d "$_zcd/zcompdump" + ''; + defaultKeymap = "viins"; # vi keybindings + history = { + path = "${config.xdg.cacheHome}/zsh/history"; + size = 1000000; + save = 1000000; + ignoreAllDups = true; # drop older duplicate of a re-run command + ignoreSpace = true; # don't record space-prefixed commands + share = true; + }; + + shellAliases = { + vim = "nvim"; + vimdiff = "nvim -d"; + cp = "cp -iv"; + mv = "mv -iv"; + rm = "rm -vI"; + mkd = "mkdir -pv"; + ls = "ls -hN --color=auto --group-directories-first"; + grep = "grep --color=auto"; + diff = "diff --color=auto"; + ccat = "highlight --out-format=ansi"; + ka = "killall"; + g = "git"; + e = editor; + v = editor; + }; + + # Interactive bits with no first-class HM option: prompt, autocd, + # completion-menu styling, vi cursor shaping, and a few keybinds. + initContent = '' + # Colored prompt: [user@host ~]$ + autoload -U colors && colors + PS1="%B%{$fg[red]%}[%{$fg[yellow]%}%n%{$fg[green]%}@%{$fg[blue]%}%M %{$fg[magenta]%}%~%{$fg[red]%}]%{$reset_color%}$%b " + + setopt autocd interactive_comments + stty stop undef # free up ctrl-s + export KEYTIMEOUT=1 + + # Completion menu: arrow-select, include dotfiles, vi keys in the menu. + zstyle ':completion:*' menu select + zmodload zsh/complist + _comp_options+=(globdots) + bindkey -M menuselect 'h' vi-backward-char + bindkey -M menuselect 'k' vi-up-line-or-history + bindkey -M menuselect 'l' vi-forward-char + bindkey -M menuselect 'j' vi-down-line-or-history + bindkey -v '^?' backward-delete-char + bindkey '^[[P' delete-char + + # Beam cursor in insert mode, block in normal mode. + function zle-keymap-select () { + case $KEYMAP in + vicmd) echo -ne '\e[1 q';; + viins|main) echo -ne '\e[5 q';; + esac + } + zle -N zle-keymap-select + zle-line-init() { zle -K viins; echo -ne "\e[5 q"; } + zle -N zle-line-init + echo -ne '\e[5 q' + preexec() { echo -ne '\e[5 q'; } + + # Edit current line in $EDITOR with ctrl-e; quick bc and fzf-cd binds. + autoload edit-command-line; zle -N edit-command-line + bindkey '^e' edit-command-line + bindkey -s '^a' 'bc -lq\n' + bindkey -s '^f' 'cd "$(dirname "$(fzf)")"\n' + + # Up/Down: search history by the text already typed before the cursor, + # then place the cursor at the end of the matched line. + autoload -U history-search-end + zle -N history-beginning-search-backward-end history-search-end + zle -N history-beginning-search-forward-end history-search-end + bindkey "^[[A" history-beginning-search-backward-end + bindkey "^[[B" history-beginning-search-forward-end + ''; + }; + + home.pointerCursor = { + name = "Adwaita"; + package = pkgs.adwaita-icon-theme; + size = 24; + gtk.enable = true; + }; + + gtk = { + enable = true; + theme = { + name = "Adwaita-dark"; + package = pkgs.gnome-themes-extra; + }; + # Keep the GTK2 rc out of $HOME (default ~/.gtkrc-2.0). Setting this also + # makes home-manager export GTK2_RC_FILES so GTK2 apps still find it. + gtk2.configLocation = "${config.xdg.configHome}/gtk-2.0/gtkrc"; + }; + + wayland.windowManager.hyprland = { + enable = true; + configType = "hyprlang"; + + settings = { + monitor = ",preferred,auto,1"; + + general = { + layout = "master"; + gaps_in = 5; + gaps_out = 10; + border_size = 2; + }; + + decoration = { + rounding = 0; + shadow.enabled = false; + blur.enabled = false; + }; + + animations = { + enabled = false; + }; + + input = { + kb_layout = "de"; + follow_mouse = 1; + }; + + "$mod" = "SUPER"; + + bind = [ + "$mod, Return, exec, footclient" + "$mod, q, killactive" + "$mod SHIFT, q, exit" + "$mod, f, fullscreen, 1" + "$mod SHIFT, f, fullscreen, 0" + "$mod, w, exec, ${browser}" + "$mod, d, exec, fuzzel" + "$mod, j, layoutmsg, cyclenext" + "$mod SHIFT, j, layoutmsg, swapnext" + "$mod, k, layoutmsg, cycleprev" + "$mod SHIFT, k, layoutmsg, swapprev" + "$mod, h, layoutmsg, splitratio -0.05" + "$mod, l, layoutmsg, splitratio +0.05" + ] ++ ( + # Dynamic definitions for viewing workspaces 1 to 9 and moving windows between them. + # References physical key codes for robustness with the SHIFT key, e.g. `code:10` instead of `1`. + builtins.concatLists ( + builtins.genList (i: + let target = i + 1; in [ + "$mod, code:1${toString i}, workspace, ${toString target}" + "$mod SHIFT, code:1${toString i}, movetoworkspace, ${toString target}" + ] + ) 9 + ) + ) ++ [ + ", XF86MonBrightnessUp, exec, brightnessctl set 15%+" + ", XF86MonBrightnessDown, exec, brightnessctl set 15%-" + ", XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle" + ", XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 3%+" + ", XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 3%-" + ]; + + exec-once = [ + "waybar" + # foot in server/daemon mode: $mod+Return runs `footclient`, which + # attaches to this one process → faster new-window startup, shared RAM. + # Uses foot's default socket, which footclient also defaults to. + # Tradeoff: if the server dies, all foot windows die with it. + "foot --server" + ]; + }; + }; + + programs.waybar = { + enable = true; + settings = { + mainBar = { + layer = "top"; + position = "top"; + height = 28; + modules-left = [ "hyprland/workspaces" ]; + modules-center = [ "clock" ]; + modules-right = [ "pulseaudio" "network" "battery" ]; + + "hyprland/workspaces" = { + format = "{id}"; + }; + + clock = { + format = "{:%H:%M %a %d %b}"; + }; + + pulseaudio = { + format = "{volume}% {icon}"; + format-muted = "󰝟"; + format-icons = { + default = [ "󰕿" "󰖀" "󰕾" ]; + headphone = "󰋋"; + }; + }; + + network = { + format-wifi = "{essid} 󰖩"; + format-ethernet = "eth 󰈀"; + format-disconnected = "󰖪"; + }; + + battery = { + states = { + warning = 30; + critical = 15; + }; + format = "{capacity}% {icon}"; + format-charging = "{capacity}% 󰂄"; + format-plugged = "{capacity}% 󰚥"; + format-icons = [ "󰁺" "󰁻" "󰁼" "󰁽" "󰁾" "󰁿" "󰂀" "󰂁" "󰂂" "󰁹" ]; + }; + }; + }; + + style = '' + * { + font-family: "JetBrainsMono Nerd Font"; + font-size: 13px; + } + window#waybar { + background: #222222; + color: #bbbbbb; + } + #pulseaudio, #network, #battery { + padding: 0 12px; + } + #battery.warning { + color: #e5c07b; + } + #battery.critical { + color: #e06c75; + } + #battery.charging { + color: #98c379; + } + ''; + }; +} diff --git a/modules/nixos.nix b/modules/nixos.nix new file mode 100644 index 0000000..e36db55 --- /dev/null +++ b/modules/nixos.nix @@ -0,0 +1,100 @@ +# Generic system-level config. Reusable across hosts — nothing machine- or +# user-specific here. The user account, hostname and system.stateVersion live in +# the per-host inventory (hosts//), not in this module. + +{ config, lib, pkgs, ... }: + +{ + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + # Deduplicate identical store paths via hardlinks. + nix.settings.auto-optimise-store = true; + # Weekly GC of generations older than 30 days (frees disk + prunes stale boot + # entries). Flat age cutoff — Nix has no snapper-style tiered retention. + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + + # Bootloader systemd-boot much simpler than GRUB. + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + # Show only the newest 5 generations in the boot menu (display cap only; GC + # above does the actual deletion). + boot.loader.systemd-boot.configurationLimit = 5; + + # Use latest kernel. + boot.kernelPackages = pkgs.linuxPackages_latest; + + # Configure network connections interactively with nmcli or nmtui. + networking.networkmanager.enable = true; + + networking.firewall = { + allowedTCPPorts = [ + 22000 # syncthing sync transport (TCP) + ]; + allowedUDPPorts = [ + 22000 # syncthing sync transport (QUIC) + 21027 # syncthing LAN discovery + ]; + }; + + # Time zone. Template default — override per host/region. + time.timeZone = "Europe/Berlin"; + + # Internationalisation properties. Template defaults — override per region. + i18n.defaultLocale = "en_US.UTF-8"; + console = { + font = "Lat2-Terminus16"; + keyMap = "de"; + }; + + # Programs. + programs.zsh.enable = true; + programs.hyprland.enable = true; + programs.dconf.enable = true; + + # Packages installed in system profile. + environment.systemPackages = with pkgs; [ + curl + git + neovim + wget + ]; + nixpkgs.config.allowUnfree = true; + + # Install nice font. + fonts.packages = with pkgs; [ + nerd-fonts.jetbrains-mono + ]; + + # Enable sound. + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + }; + security.rtkit.enable = true; + + # Cache sudo credentials for 30 min per terminal (default 5). Tradeoff: an + # unlocked, unattended terminal can sudo without re-auth for that window. + security.sudo.extraConfig = "Defaults timestamp_timeout=30"; + + # Login manager. + services.greetd = { + enable = true; + settings = { + default_session = { + command = "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd start-hyprland"; + user = "greeter"; + }; + }; + }; + + # XDG portals (screen sharing, file pickers, and so on). + xdg.portal = { + enable = true; + extraPortals = [ pkgs.xdg-desktop-portal-hyprland ]; + }; +}