Files
nixos-config/modules/home.nix
T
daniil-berg 238528f961 feat(desktop): provide Secret Service via gnome-keyring
Bare Wayland session ships no `org.freedesktop.secrets` provider, so
libsecret/qtkeychain apps can't persist credentials. Run gnome-keyring
and unlock it at the greetd login via PAM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:18:59 +02:00

440 lines
14 KiB
Nix

# 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";
};
# 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
];
# ---- 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;
}
'';
};
}