# 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 ./media.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"; # GUI helper for ssh passphrase/PIN prompts. Not forced: with a tty ssh still # prompts inline; the helper is only used when a caller sets # SSH_ASKPASS_REQUIRE (e.g. a headless/agent context). SSH_ASKPASS = "${pkgs.ksshaskpass}/bin/ksshaskpass"; }; # 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 libreoffice ksshaskpass # Qt GUI prompt for ssh passphrase/PIN (see SSH_ASKPASS above) ]; # ---- 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"; logs = "log --show-signature"; logo = "log --pretty=format:\"%C(auto)%h%x09%ad%x09%<(16,trunc)%an%x09%s%x09%d\""; 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"; # Drag to move ($mod+LMB) / resize ($mod+RMB) any window. Needed to reach # oversized floating dialogs (e.g. GTK save/upload pickers) whose confirm # button lands off-screen on short displays. bindm = [ "$mod, mouse:272, movewindow" "$mod, mouse:273, resizewindow" ]; 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" # wallpaper (see modules/media.nix): start the awww daemon, then restore # the persisted choice. awww keeps no state across restarts, so re-push # the ~/.local/share/bg symlink if it exists. "awww-daemon" "sh -c '[ -e \"$HOME/.local/share/bg\" ] && awww img \"$HOME/.local/share/bg\"'" ]; }; }; # Notification daemon. notify-send clients (e.g. imv's info/delete toasts in # modules/media.nix, gpg-agent errors) need a running server on the bus. # Minimal to match the desktop: Nerd Font, no rounding, square border. # Secret Service provider (freedesktop org.freedesktop.secrets). Bare Wayland # session has none otherwise; apps using libsecret/qtkeychain need it. services.gnome-keyring.enable = true; services.mako = { enable = true; settings = { font = "JetBrainsMono Nerd Font 10"; border-radius = 0; default-timeout = 5000; }; }; 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; } ''; }; }