5172ca4d18
Bare Print grabs the whole screen to a timestamped PNG under ~/Pictures/Screenshots; $mod selects a region (slurp), SHIFT sends to the clipboard instead. A mako toast confirms each capture. Adds grim/slurp and enables xdg.userDirs for the Pictures target. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
525 lines
19 KiB
Nix
525 lines
19 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";
|
|
|
|
# SSH_ASKPASS helper that drives the GPG pinentry-qt: ssh passphrase/PIN
|
|
# prompts reuse the same Qt dialog as commit signing, adding no extra GUI
|
|
# toolkit (a packaged askpass would drag in KDE/GTK — hundreds of MB).
|
|
# ssh signals the prompt kind via $SSH_ASKPASS_PROMPT (OpenSSH ≥ 8.4):
|
|
# empty real passphrase/PIN entry — collect input, echo to stdout
|
|
# confirm/none informational, e.g. "Confirm user presence" (touch request)
|
|
# A FIDO touch can't be faked by software, so those messages need no dialog:
|
|
# succeed silently instead of popping a spurious PIN box (the token blinks).
|
|
sshAskpass = pkgs.writeShellScript "ssh-askpass-pinentry" ''
|
|
[ -n "''${SSH_ASKPASS_PROMPT:-}" ] && exit 0
|
|
desc=$(printf '%s' "''${1:-Passphrase:}" | sed 's/%/%25/g')
|
|
printf 'SETTITLE ssh\nSETPROMPT PIN:\nSETDESC %s\nGETPIN\nBYE\n' "$desc" \
|
|
| ${pkgs.pinentry-qt}/bin/pinentry-qt \
|
|
| sed -n 's/^D //p'
|
|
'';
|
|
|
|
# Screenshot helper for the Print-key binds below. Two axes, chosen by args:
|
|
# scope ($1 = full | region, the latter an interactive slurp selection) and
|
|
# destination ($2 = file | clip). Files are timestamped PNGs under
|
|
# ~/Pictures/Screenshots; clip copies to the Wayland clipboard. A mako toast
|
|
# confirms each grab. slurp exits non-zero on Escape, which aborts here.
|
|
screenshot = pkgs.writeShellScript "screenshot" ''
|
|
set -eu
|
|
if [ "$1" = region ]; then
|
|
geom=$(${pkgs.slurp}/bin/slurp) || exit 1
|
|
grab() { ${pkgs.grim}/bin/grim -g "$geom" "$@"; }
|
|
else
|
|
grab() { ${pkgs.grim}/bin/grim "$@"; }
|
|
fi
|
|
if [ "$2" = clip ]; then
|
|
grab - | ${pkgs.wl-clipboard}/bin/wl-copy
|
|
${pkgs.libnotify}/bin/notify-send "Screenshot" "Copied to clipboard"
|
|
else
|
|
dir="${config.xdg.userDirs.pictures}/Screenshots"
|
|
mkdir -p "$dir"
|
|
file="$dir/$(date +%Y-%m-%d-%H%M%S).png"
|
|
grab "$file"
|
|
${pkgs.libnotify}/bin/notify-send "Screenshot" "Saved $file"
|
|
fi
|
|
'';
|
|
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";
|
|
# Run Chromium/Electron apps (brave, signal, grayjay's webview) natively on
|
|
# Wayland instead of XWayland: nixpkgs' chromium/electron wrappers read this
|
|
# and inject the Ozone Wayland flags. This is a Wayland-only (Hyprland)
|
|
# session, so XWayland is pure downside (blurry fractional scaling, extra
|
|
# hop). Override per-app with --ozone-platform=x11 if one misbehaves.
|
|
NIXOS_OZONE_WL = "1";
|
|
# GUI helper for ssh passphrase/PIN prompts (see sshAskpass above). Not
|
|
# forced: with a tty ssh still prompts inline; the helper is used only when a
|
|
# caller sets SSH_ASKPASS_REQUIRE (e.g. a headless/agent context).
|
|
SSH_ASKPASS = "${sshAskpass}";
|
|
};
|
|
# 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
|
|
grim # screenshot capture (also used by the Print-key binds)
|
|
slurp # interactive region selector for grim
|
|
];
|
|
|
|
# Canonical XDG user dirs. Only Pictures is actually used (screenshot target,
|
|
# file-picker default). createDirectories stays off so the rest aren't
|
|
# littered into $HOME — they're merely declared in ~/.config/user-dirs.dirs,
|
|
# created lazily by whatever app wants them.
|
|
xdg.userDirs = {
|
|
enable = true;
|
|
createDirectories = false;
|
|
pictures = "${config.home.homeDirectory}/Pictures";
|
|
};
|
|
|
|
# ---- 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";
|
|
};
|
|
|
|
# The home-manager Hyprland module below enables xdg.portal and pins the
|
|
# portal search dir (NIX_XDG_DESKTOP_PORTAL_DIR) to this user profile, adding
|
|
# only the Hyprland backend — which implements Screenshot/ScreenCast/
|
|
# GlobalShortcuts but not FileChooser. Add the GTK portal here (system-level
|
|
# extraPortals aren't in the pinned dir, so they're never seen) and route
|
|
# FileChooser to it, else file dialogs in Chromium/Electron apps (signal,
|
|
# brave) fail with a DBus "No such interface
|
|
# org.freedesktop.portal.FileChooser" error.
|
|
xdg.portal = {
|
|
extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
|
|
config.common = {
|
|
default = [ "hyprland" "gtk" ];
|
|
"org.freedesktop.impl.portal.FileChooser" = [ "gtk" ];
|
|
};
|
|
};
|
|
|
|
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%-"
|
|
# Screenshots. $mod = pick a region (slurp), SHIFT = to clipboard;
|
|
# bare Print grabs the whole screen to a file (the common case).
|
|
", Print, exec, ${screenshot} full file"
|
|
"$mod, Print, exec, ${screenshot} region file"
|
|
"SHIFT, Print, exec, ${screenshot} full clip"
|
|
"$mod SHIFT, Print, exec, ${screenshot} region clip"
|
|
];
|
|
|
|
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;
|
|
}
|
|
'';
|
|
};
|
|
}
|