Compare commits

..
17 Commits
Author SHA1 Message Date
daniil-berg 4a6830c24e feat(home): add git-filter-repo 2026-09-20 17:20:08 +02:00
daniil-berg e0912f568c feat(gpg): raise agent cache TTLs and make them overridable
Idle cache 30 min -> 1 h, absolute cap 2 h -> 8 h. Both `lib.mkDefault` so an
inventory can set `services.gpg-agent.defaultCacheTtl` without `mkForce`.
2026-09-18 14:39:15 +02:00
daniil-berg cb01efd267 feat(dictionaries): add hunspell dictionary module 2026-09-16 11:14:32 +02:00
daniil-bergandClaude Opus 5 0f7f1dade2 docs(nvim): record why this is hand-rolled, not a distro
State the reason for nixvim over LazyVim/NvChad — lazy.nvim fetches and updates
plugins at runtime, putting the editor outside the flake — along with what that
costs in ergonomics, and that borrowing a distro's keymaps and defaults into
this file remains open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015D4NsszUG7hy9ghKZ4nfhm
2026-09-15 13:31:06 +02:00
daniil-bergandClaude Opus 5 bbb468c432 fix(nvim): keep window layout when closing a buffer
`:bdelete` closes the window when no other listed buffer is left for it to
show, so closing the last file with neo-tree open left a full-width tree.
Point the window at another listed buffer, or a fresh empty one, before
deleting. `confirm` matches `<leader>Q`, prompting instead of failing on a
modified buffer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015D4NsszUG7hy9ghKZ4nfhm
2026-09-15 13:27:06 +02:00
daniil-bergandClaude Opus 5 24b940810e feat(nvim): prompt to save on <leader>Q instead of aborting
`:qa` fails with E37 when any buffer is modified, leaving the quit dead-ended.
`:confirm qa` asks Save/Discard/Cancel per modified buffer, which neither
strands the user nor writes unintended edits the way `:wqa` would.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015D4NsszUG7hy9ghKZ4nfhm
2026-09-15 12:56:55 +02:00
daniil-bergandClaude Opus 5 d4f326bfd9 feat(nvim): highlight mustache templates
nvim detects `.mustache` but ships no syntax for it, leaving Moodle templates
as plain text. `vim-mustache-handlebars` supplies syntax, indent and ftdetect,
covering the constructs the Handlebars tree-sitter grammar rejects: bare
sections, `{{{unescaped}}}`, partials and `{{< parent }}` inheritance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015D4NsszUG7hy9ghKZ4nfhm
2026-09-15 12:53:54 +02:00
daniil-bergandClaude Opus 5 93a4a934eb feat(nvim): accept completion with <CR>, indent 4
blink.cmp uses the `enter` preset with `completion.list.selection.preselect =
false`, so an item is taken only once selected with `<Down>`/`<C-n>` and `<CR>`
otherwise inserts a newline. `<C-y>` stays bound as vim's own confirm key.

`<Esc>` and `<C-e>` both run `cancel` rather than the preset's `hide`: hiding
leaves the auto_insert preview in the buffer without the `additionalTextEdits`
and snippet expansion a real accept applies, and builtin ins-completion `<C-e>`
likewise restores the typed text.

`shiftwidth`/`tabstop` are 4, matching PHP's PSR-12. PHP's `indentkeys` carry
`*<Return>`, which reindents the just-finished line on every newline; at width 2
that silently re-indented 4-space files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015D4NsszUG7hy9ghKZ4nfhm
2026-09-15 12:47:21 +02:00
daniil-bergandClaude Opus 4.8 61300bd9b9 docs(readme): add day-to-day upgrade section
Explain that a single `nix flake update` in the consumer's own flake
bumps every package version — including packages this repo's modules
install, since they build against the consumer's nixpkgs via `follows`.
Cover per-input bumps and the release-jump caveat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 09:37:00 +02:00
daniil-bergandClaude Opus 4.8 38556c8839 fix(home): match wrapped waybar comm when killing bar on monitor change
`pkill -x waybar` matched only comm `waybar`, but the home-manager
wrapper launches the bar with comm `.waybar-wrapped` (arg[0] still
`waybar`), so the kill missed and left a stale bar drawing on the old
geometry after an unplug. Match both comms via an anchored ERE in both
the `monitor-hotplug` handler and `swapMonitors`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 19:16:44 +02:00
daniil-bergandClaude Opus 4.8 102fb3c3c4 fix(home): relaunch waybar on monitor hotplug instead of SIGUSR2 reload
A real DP/HDMI unplug makes Hyprland tear down the layer-shell surfaces on
the surviving monitor and not recreate them, so waybar and the wallpaper
vanish there. A SIGUSR2 reload rereads waybar's config but does not
recreate a bar that was already destroyed, so it left the laptop bar gone.
Kill and restart waybar instead — a fresh process enumerates the current
outputs and draws a bar on each — and add a settle delay before re-pushing
the wallpaper so it lands on the settled geometry. Verified against a real
DP-1 unplug: the LVDS-1 bar and wallpaper survive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-09 11:37:03 +02:00
daniil-bergandClaude Opus 4.8 752ee84430 fix(home): keep waybar + wallpaper on surviving monitor after unplug
Hyprland drops the layer-shell surfaces on the remaining output when a
monitor is removed and does not recreate them, so waybar and the awww
wallpaper vanish on unplug. The socket2 watcher only reacted to
`monitoradded`, which masked it — reconnecting re-fired the add and
restored both. Handle `monitorremoved` too, and on either event re-push
the wallpaper and reload waybar (SIGUSR2 rebuilds its per-output bars).
Renamed `wallpaper-hotplug` to `monitor-hotplug` since it now does both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-09 10:48:16 +02:00
daniil-bergandClaude Opus 4.8 fbf68fa4a4 refactor(home): move psql config out to the inventory
The postgresql client is installed from the private inventory, not this
baseline, so its dotfile config belongs there too. Drops `PSQLRC` and the
`psql/rc` history file; wget/readline stay (their tools are ubiquitous).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-08 09:41:56 +02:00
daniil-berg b9e8641bbf feat(nixos): add nixos-diff rebuild diff + reboot/re-login helper 2026-08-06 14:55:06 +02:00
daniil-bergandClaude Opus 4.8 5d0a33d09f chore(flake): update nixpkgs, home-manager, rust-overlay
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 11:26:52 +02:00
daniil-bergandClaude Opus 4.8 cc7945b28b refactor(home): default hyprland kb_layout to neutral us
the baseline hard-coded a german layout, leaking a personal value into a
module meant to be region-neutral. ship `mkDefault "us"` like `console.keyMap`
so a consumer overrides it from their inventory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 09:25:07 +02:00
daniil-bergandClaude Opus 4.8 ac4fd32e2b docs(readme): advertise decoupled consumption, rename input to baseline
default the consumer example to owning its own `nixpkgs` (with the baseline
following it) so a consumer's update cadence stays theirs; keep the coupled
`follows` form as a quickstart alternative. rename the `pub` input to
`baseline`, and note that `local.*` options double as the override for
deliberately-frozen pins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 09:13:29 +02:00
10 changed files with 325 additions and 54 deletions
+2 -1
View File
@@ -48,7 +48,8 @@ declaratively in `modules/home.nix` (`wayland.windowManager.hyprland`, `programs
`programs.foot`). Font is JetBrainsMono Nerd Font; Waybar/foot use Nerd Font glyphs `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. (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"`). Hyprland uses the **master** layout; `kb_layout` ships neutral (`mkDefault "us"`),
overridden per-inventory like `console.keyMap`.
Look deliberately minimal: animations, blur, rounding, and shadows are all turned Look deliberately minimal: animations, blur, rounding, and shadows are all turned
**off** on purpose — don't reintroduce them when adding desktop tweaks. **off** on purpose — don't reintroduce them when adding desktop tweaks.
+46 -8
View File
@@ -33,7 +33,7 @@ keeps it.
``` ```
your-inventory/ your-inventory/
├── flake.nix # inputs (pub + follows) + nixosConfigurations.<host> ├── flake.nix # inputs (baseline + your nixpkgs) + nixosConfigurations.<host>
├── flake.lock ├── flake.lock
├── system/ # system-level modules ├── system/ # system-level modules
│ ├── regional.nix # time zone / keymap / locale (see Overriding) │ ├── regional.nix # time zone / keymap / locale (see Overriding)
@@ -55,17 +55,22 @@ The `flake.nix` that wires it together:
```nix ```nix
{ {
inputs = { inputs = {
pub.url = "github:<owner>/<repo>"; # You own nixpkgs, so your update cadence is yours — the baseline modules
# Reuse the public flake's pins so versions never drift between the two. # build against the versions you pin here.
nixpkgs.follows = "pub/nixpkgs"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager.follows = "pub/home-manager"; home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
baseline.url = "github:<owner>/<repo>";
baseline.inputs.nixpkgs.follows = "nixpkgs"; # baseline builds against YOUR nixpkgs
baseline.inputs.home-manager.follows = "home-manager";
}; };
outputs = inputs@{ self, nixpkgs, pub, home-manager, ... }: { outputs = inputs@{ self, nixpkgs, baseline, home-manager, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; system = "x86_64-linux";
modules = [ modules = [
pub.nixosModules.default # generic system baseline baseline.nixosModules.default # generic system baseline
./system/regional.nix # your regional values (see below) ./system/regional.nix # your regional values (see below)
./hosts/myhost # hardware + hostname ./hosts/myhost # hardware + hostname
home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager
@@ -75,7 +80,7 @@ The `flake.nix` that wires it together:
# Inject your own inputs into your home modules; the public ones ignore them. # Inject your own inputs into your home modules; the public ones ignore them.
home-manager.extraSpecialArgs = { inherit inputs; }; home-manager.extraSpecialArgs = { inherit inputs; };
home-manager.users.me.imports = [ home-manager.users.me.imports = [
pub.homeModules.default # generic home baseline baseline.homeModules.default # generic home baseline
./home/identity.nix # your git identity, name/email, … ./home/identity.nix # your git identity, name/email, …
./home/ssh.nix # your SSH hosts (topology stays private) ./home/ssh.nix # your SSH hosts (topology stays private)
# … more personal home modules # … more personal home modules
@@ -87,9 +92,38 @@ The `flake.nix` that wires it together:
} }
``` ```
This repo ships *modules*, not pinned packages: they build against the `nixpkgs` **you**
declare above, so you decide when packages move. *Quickstart alternative:* drop your own
`nixpkgs` and write `nixpkgs.follows = "baseline/nixpkgs"` to pin to this repo's tested
versions instead — one less thing to think about, but then your update cadence is whatever
this repo pins, not the reverse.
Your machine list, hostnames, and any private specifics stay in *your* flake — you pull Your machine list, hostnames, and any private specifics stay in *your* flake — you pull
only the generic modules from here. only the generic modules from here.
## Updating (day-to-day)
**Every** package version comes from the `nixpkgs` **you** declare — including the packages
this repo's own modules install, since they build against your `nixpkgs` via `follows`, not
against anything this repo pins. So the whole upgrade happens in your flake; you never touch
this repo to move packages, even the ones it declares:
```sh
nix flake update # bump every input to newest
sudo nixos-rebuild switch --flake .#myhost # build + activate
```
`nix flake update` advances `nixpkgs` to the newest commit on the branch you pinned (e.g.
`nixos-unstable` or `nixos-25.11`) — that is what upgrades your packages. `home-manager` and
`baseline` **follow** your `nixpkgs`, so they move in lockstep with no version skew. To move
one input at a time, name it: `nix flake update nixpkgs` bumps only packages;
`nix flake update baseline` pulls the latest **modules** from this repo (new features/fixes)
without changing any package version. These are independent knobs — routine upgrades need
only the `nixpkgs` bump.
A **release jump** (e.g. `nixos-25.11``nixos-26.05`) is not a plain update: edit the
branch in the `nixpkgs` and `home-manager` URLs in your `flake.nix`, then update and rebuild.
## Overriding the baseline ## Overriding the baseline
The public modules are a baseline you extend without forking. Three ways to shape them The public modules are a baseline you extend without forking. Three ways to shape them
@@ -111,6 +145,10 @@ from your private flake, in rough order of how often you reach for them:
local.texlive.package = pkgs.texlive.withPackages (ps: [ ps.scheme-full ]); local.texlive.package = pkgs.texlive.withPackages (ps: [ ps.scheme-full ]);
``` ```
A few `local.*` options exist precisely because a module pins a package to a nixpkgs
*other* than the one you declare (a deliberate freeze that would otherwise reach you
regardless of your own pin); the option is how you pull it back onto your nixpkgs.
3. **Extra modules.** Anything with no public counterpart is just another module in the 3. **Extra modules.** Anything with no public counterpart is just another module in the
import lists above — personal apps, SSH/Syncthing topology, secrets wiring. import lists above — personal apps, SSH/Syncthing topology, secrets wiring.
Generated
+9 -9
View File
@@ -28,11 +28,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1782704057, "lastModified": 1785119570,
"narHash": "sha256-G1I1gd32F7mp9LAe1DaZ4ZL7NX5gyiKwdCMwro1Vrck=", "narHash": "sha256-Rgs2xKnGLFWQscxUaXX07oyZeuMDOHEbqDOsgliLFGM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "868d0a692de703c2de98fab61968e4e310b7c28e", "rev": "d4fd24667c8cbef124bb70a20380cab75ec8474d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -44,11 +44,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1782691344, "lastModified": 1785858998,
"narHash": "sha256-i5nw9BYYsMDAaOC4J+JmTof6b2GhlyH076awYRNrTV8=", "narHash": "sha256-fKCq5jphd6l/Ms6gc3dptkw/TcKLYub9lQE5g6rbbkc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "1f01958ffb5b3545c96d9ef2f4e24c5e5e1eb846", "rev": "04607e1165ac22c5fde6dcc54c9e0b3c0487c555",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -96,11 +96,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1782875958, "lastModified": 1785993740,
"narHash": "sha256-5eqDcnBjb1424HRQdnhuhNOBZguq1Z2tqSa2OMF/m2c=", "narHash": "sha256-naSJ+p7zkIT2hHNSJkhxiPZlmVRgE3UdODinMM7rpD8=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "13139aefa973f3d96c60c0fbab801de058ae25ca", "rev": "3d1b34a12a26be073f7ae67165a6b61229ca6025",
"type": "github" "type": "github"
}, },
"original": { "original": {
+53
View File
@@ -0,0 +1,53 @@
# Hunspell dictionaries, joined into one directory so anything needing a
# dictionary *path* can be pointed at a single store path.
#
# Two consumers, two mechanisms:
# - LibreOffice needs nothing beyond the package being installed: its wrapper
# scans `share/hunspell` in every NIX_PROFILES entry.
# - Gecko apps (LibreWolf, Thunderbird) need `spellchecker.dictionary_path`
# set explicitly — there is no /usr/share/hunspell on NixOS and wrapFirefox
# wires up nothing itself. Point that pref at `local.dictionaries.path`.
#
# Chromium-based browsers are NOT served by this: they ignore hunspell and
# fetch their own dictionaries, selected by enterprise policy.
#
# Which languages is a personal choice, so the default is empty; a consumer sets
# `local.dictionaries.packages = [ pkgs.hunspellDicts.de_DE ];`. en-US ships
# inside Firefox/Thunderbird already and does not need listing for those.
{ config, lib, pkgs, ... }:
let
cfg = config.local.dictionaries;
joined = pkgs.symlinkJoin {
name = "hunspell-dicts";
paths = cfg.packages;
};
in
{
options.local.dictionaries = {
packages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
example = lib.literalExpression "[ pkgs.hunspellDicts.de_DE ]";
description = ''
Hunspell dictionary packages to install and expose via
`local.dictionaries.path`.
'';
};
path = lib.mkOption {
type = lib.types.str;
readOnly = true;
description = ''
Directory holding every `packages` dictionary. Feed this to
`spellchecker.dictionary_path` in Gecko apps.
'';
};
};
config = {
local.dictionaries.path = "${joined}/share/hunspell";
home.packages = lib.optional (cfg.packages != [ ]) joined;
};
}
+8 -3
View File
@@ -2,7 +2,7 @@
# signing identity — those are personal (→ inventory). Enabling this alone is # signing identity — those are personal (→ inventory). Enabling this alone is
# harmless: a user with no key just has an idle agent. # harmless: a user with no key just has an idle agent.
{ config, pkgs, ... }: { config, lib, pkgs, ... }:
{ {
programs.gpg = { programs.gpg = {
@@ -26,7 +26,12 @@
# sk-based ssh keys use the normal ssh-agent; don't let gpg-agent hijack # sk-based ssh keys use the normal ssh-agent; don't let gpg-agent hijack
# SSH_AUTH_SOCK. # SSH_AUTH_SOCK.
enableSshSupport = false; enableSshSupport = false;
defaultCacheTtl = 1800; # 30 min # Idle timer, reset on every use. The hard cap runs from the unlock and
maxCacheTtl = 7200; # 2 h # never resets, so a machine in continuous use still re-asks once a workday —
# otherwise an unattended unlocked session signs forever.
# mkDefault so an inventory can dial these per machine with a plain
# `services.gpg-agent.defaultCacheTtl = …;` and no mkForce.
defaultCacheTtl = lib.mkDefault 3600; # 1 h idle
maxCacheTtl = lib.mkDefault 28800; # 8 h absolute
}; };
} }
+15 -15
View File
@@ -115,14 +115,17 @@ let
| "keyword monitor \($l.name),preferred,0x0,\($l.scale)" | "keyword monitor \($l.name),preferred,0x0,\($l.scale)"
+ " ; keyword monitor \($r.name),preferred,\($off)x0,\($r.scale)"') + " ; keyword monitor \($r.name),preferred,\($off)x0,\($r.scale)"')
"$hc" --batch "$cmd" "$hc" --batch "$cmd"
# re-anchor the layer-shell clients onto the new geometry (see comment above) # re-anchor the layer-shell clients onto the new geometry (see comment above).
${pkgs.procps}/bin/pkill -USR2 -x .waybar-wrapped || true # Anchored ERE covers both comms: the wrapper-launched bar (comm
# `.waybar-wrapped`) and one the monitor-hotplug handler relaunched via
# `hyprctl dispatch exec waybar` (comm `waybar`).
${pkgs.procps}/bin/pkill -USR2 -x '\.waybar-wrapped|waybar' || true
bg="$HOME/.local/share/bg" bg="$HOME/.local/share/bg"
[ -e "$bg" ] && ${pkgs.awww}/bin/awww img "$bg" || true [ -e "$bg" ] && ${pkgs.awww}/bin/awww img "$bg" || true
''; '';
in in
{ {
imports = [ ./gpg.nix ./media.nix ./texlive.nix ./librewolf.nix ./mc.nix ./mime.nix ./hyprland-workspaces.nix ./nvim.nix ]; imports = [ ./gpg.nix ./media.nix ./texlive.nix ./librewolf.nix ./mc.nix ./mime.nix ./hyprland-workspaces.nix ./nvim.nix ./dictionaries.nix ];
# Put ~/.local/bin (the XDG user-bin dir) on PATH so hand-dropped scripts run # Put ~/.local/bin (the XDG user-bin dir) on PATH so hand-dropped scripts run
# by name without wiring each through nix. # by name without wiring each through nix.
@@ -144,9 +147,8 @@ in
# -p. Pins throwaway REPLs and new projects to one version across machines # -p. Pins throwaway REPLs and new projects to one version across machines
# (further steered by uv.toml below). Fetched on demand by uv, not Nix. # (further steered by uv.toml below). Fetched on demand by uv, not Nix.
UV_PYTHON = "3.14"; UV_PYTHON = "3.14";
# psql / wget / readline configs, XDG-routed (the rc files are declared # wget / readline configs, XDG-routed (the rc files are declared via
# via xdg.configFile below). # xdg.configFile below).
PSQLRC = "${config.xdg.configHome}/psql/rc";
WGETRC = "${config.xdg.configHome}/wget/wgetrc"; WGETRC = "${config.xdg.configHome}/wget/wgetrc";
INPUTRC = "${config.xdg.configHome}/readline/inputrc"; INPUTRC = "${config.xdg.configHome}/readline/inputrc";
# Rust/cargo: keep registry cache, git checkouts and `cargo install` bins # Rust/cargo: keep registry cache, git checkouts and `cargo install` bins
@@ -190,6 +192,9 @@ in
unzip # extract .zip (tools that shell out to it expect the binary) unzip # extract .zip (tools that shell out to it expect the binary)
jq # JSON on the command line (scripts, ad-hoc inspection) jq # JSON on the command line (scripts, ad-hoc inspection)
pv # pipe progress meter (throughput, ETA on streamed data) pv # pipe progress meter (throughput, ETA on streamed data)
git-filter-repo # `git filter-repo`: history rewriting (upstream's replacement
# for `git filter-branch`); a standalone script git picks
# up as a subcommand, so programs.git needs no config
libreoffice libreoffice
grim # screenshot capture (also used by the Print-key binds) grim # screenshot capture (also used by the Print-key binds)
slurp # interactive region selector for grim slurp # interactive region selector for grim
@@ -218,12 +223,6 @@ in
python-preference = "managed" python-preference = "managed"
''; '';
# 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: keep its HSTS db in the cache dir, not ~/.
"wget/wgetrc".text = '' "wget/wgetrc".text = ''
hsts-file=${config.xdg.cacheHome}/wget/hsts hsts-file=${config.xdg.cacheHome}/wget/hsts
@@ -595,7 +594,8 @@ in
}; };
input = { input = {
kb_layout = "de"; # Neutral default; set the real layout in your inventory (like console.keyMap).
kb_layout = lib.mkDefault "us";
follow_mouse = 1; follow_mouse = 1;
numlock_by_default = true; numlock_by_default = true;
}; };
@@ -667,8 +667,8 @@ in
# the ~/.local/share/bg symlink if it exists. # the ~/.local/share/bg symlink if it exists.
"awww-daemon" "awww-daemon"
"sh -c '[ -e \"$HOME/.local/share/bg\" ] && awww img \"$HOME/.local/share/bg\"'" "sh -c '[ -e \"$HOME/.local/share/bg\" ] && awww img \"$HOME/.local/share/bg\"'"
# re-push the wallpaper onto monitors connected after login (media.nix) # re-push wallpaper + relaunch waybar on monitor add/remove (media.nix)
"wallpaper-hotplug" "monitor-hotplug"
]; ];
}; };
}; };
+28 -8
View File
@@ -7,7 +7,7 @@
# awww-daemon and the boot-time restore from that symlink run as Hyprland # awww-daemon and the boot-time restore from that symlink run as Hyprland
# `exec-once` (see home.nix). # `exec-once` (see home.nix).
{ pkgs, ... }: { pkgs, config, ... }:
let let
# setbg: persist the choice as an XDG-data symlink, then set it live. Startup # setbg: persist the choice as an XDG-data symlink, then set it live. Startup
@@ -21,15 +21,35 @@ let
# awww-daemon paints only the outputs present when `awww img` last ran and does # awww-daemon paints only the outputs present when `awww img` last ran and does
# not repaint a monitor hotplugged afterwards — it comes up on the fill color # not repaint a monitor hotplugged afterwards — it comes up on the fill color
# (black), and a transparent foot on it then shows that black, not the # (black), and a transparent foot on it then shows that black, not the
# wallpaper. Watch Hyprland's event socket and re-push the wallpaper (setbg # wallpaper. Symmetric problem on real DP/HDMI unplug: Hyprland drops the
# with no arg → all current outputs) on each monitor-add. Hyprland emits both # layer-shell surfaces on the surviving monitor and does not recreate them, so
# `monitoradded` and `monitoraddedv2`; match only the former so setbg fires # its waybar and wallpaper vanish. Watch Hyprland's event socket and, on either
# once. Runs as a Hyprland exec-once (see home.nix). # add or remove, re-push the wallpaper (setbg with no arg → all current
wallpaperHotplug = pkgs.writeShellScriptBin "wallpaper-hotplug" '' # outputs) and fully relaunch waybar. A SIGUSR2 reload is not enough — it
# rereads config but does not recreate a bar Hyprland already tore down — so
# kill and restart, which enumerates the current outputs and draws a bar on
# each. The event flaps (several add/remove lines per plug); the loop is
# sequential so each pass converges to one waybar. Hyprland emits both
# `monitoradded` and `monitoraddedv2`; match only the non-v2 forms so each side
# fires once. Runs as a Hyprland exec-once (see home.nix).
hyprctl = "${config.wayland.windowManager.hyprland.finalPackage}/bin/hyprctl";
monitorHotplug = pkgs.writeShellScriptBin "monitor-hotplug" ''
sock="''${XDG_RUNTIME_DIR}/hypr/''${HYPRLAND_INSTANCE_SIGNATURE}/.socket2.sock" sock="''${XDG_RUNTIME_DIR}/hypr/''${HYPRLAND_INSTANCE_SIGNATURE}/.socket2.sock"
${pkgs.socat}/bin/socat -u UNIX-CONNECT:"$sock" - | while IFS= read -r line; do ${pkgs.socat}/bin/socat -u UNIX-CONNECT:"$sock" - | while IFS= read -r line; do
case "$line" in case "$line" in
"monitoradded>>"*) setbg ;; "monitoradded>>"* | "monitorremoved>>"*)
sleep 1 # let Hyprland settle the new geometry
setbg # re-push wallpaper to all current outputs
# Anchored ERE matching `comm` (max 15 chars): the bar launched via the
# home-manager wrapper reports comm `.waybar-wrapped` (arg[0] is still
# `waybar`), so a bare `-x waybar` never matches it and leaves a stale bar.
wb='\.waybar-wrapped|waybar'
${pkgs.procps}/bin/pkill -x "$wb" || true # relaunch waybar fresh (see comment)
i=0
while [ $i -lt 6 ] && ${pkgs.procps}/bin/pgrep -x "$wb" >/dev/null; do sleep 0.3; i=$((i+1)); done
${pkgs.procps}/bin/pkill -9 -x "$wb" || true
${hyprctl} dispatch exec waybar
;;
esac esac
done done
''; '';
@@ -66,7 +86,7 @@ in
home.packages = with pkgs; [ home.packages = with pkgs; [
setbg setbg
wallpaperHotplug # re-push wallpaper to a hotplugged monitor (see comment above) monitorHotplug # re-push wallpaper + reload waybar on monitor add/remove (see comment above)
awww # wayland wallpaper daemon (setbg backend) awww # wayland wallpaper daemon (setbg backend)
wl-clipboard # wl-copy, for the yank binds wl-clipboard # wl-copy, for the yank binds
imagemagick # `magick`: rotate/flip binds imagemagick # `magick`: rotate/flip binds
+1 -1
View File
@@ -5,7 +5,7 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
{ {
imports = [ ./shutdown-debug.nix ./printing-scanning.nix ]; imports = [ ./shutdown-debug.nix ./printing-scanning.nix ./rebuild-diff.nix ];
nix.settings.experimental-features = [ "nix-command" "flakes" ]; nix.settings.experimental-features = [ "nix-command" "flakes" ];
# Deduplicate identical store paths via hardlinks. # Deduplicate identical store paths via hardlinks.
+80 -9
View File
@@ -1,8 +1,26 @@
# Neovim, configured declaratively via nixvim. The editor — plugins, LSP, # Neovim, configured declaratively via nixvim. The editor — plugins, LSP,
# completion, keymaps — is defined here and pinned by the flake lock; nothing is # completion, keymaps — is defined here and pinned by the flake lock; nothing is
# downloaded at runtime, unlike an imperative distro (NvChad/LazyVim). The nixvim # downloaded at runtime. The nixvim home-manager module that provides
# home-manager module that provides `programs.nixvim` is imported for consumers by # `programs.nixvim` is imported for consumers by `homeModules.default` in
# `homeModules.default` in flake.nix, so this file only sets options. # flake.nix, so this file only sets options.
#
# Why this rather than a distro (LazyVim, NvChad, AstroNvim): those drive
# lazy.nvim as a package manager, which git-clones plugins into
# ~/.local/share/nvim on first launch and updates them in place. The editor's
# actual contents would then live outside the flake — unpinned, unreproducible,
# and invisible to a rollback.
#
# That choice has a price, and it is paid in ergonomics. A distro ships hundreds
# of defaults other people already sanded smooth; this config does not. Rough
# edges surface one at a time, mid-task, and each gets fixed by hand here (see
# the `<leader>bd` and `<leader>Q` keymaps for two such fixes). Anyone extending
# this should expect that cost rather than be surprised by it.
#
# The middle path stays open: the objection is to lazy.nvim as a *runtime*
# package manager, not to a distro's ergonomics. Individual keymaps, option
# sets, and plugin configurations can be lifted from LazyVim & co. into this
# file, and nixvim installs any plugin set declaratively. Borrow freely — just
# keep the installation declarative.
# #
# Consumer knobs (the option-with-default contract): `local.nvim.enable` to opt # Consumer knobs (the option-with-default contract): `local.nvim.enable` to opt
# out wholesale, `local.nvim.colorscheme` to swap theme in one line, and # out wholesale, `local.nvim.colorscheme` to swap theme in one line, and
@@ -86,8 +104,8 @@ in
scrolloff = 8; # keep 8 lines of context around the cursor scrolloff = 8; # keep 8 lines of context around the cursor
expandtab = true; # spaces, not tabs expandtab = true; # spaces, not tabs
shiftwidth = 2; shiftwidth = 4;
tabstop = 2; tabstop = 4;
smartindent = true; smartindent = true;
ignorecase = true; # case-insensitive search… ignorecase = true; # case-insensitive search…
@@ -162,10 +180,31 @@ in
blink-cmp = { blink-cmp = {
enable = true; enable = true;
settings = { settings = {
keymap.preset = "default"; # C-space open, C-y confirm, C-n/C-p move # "enter" preset: <CR> accepts, Up/Down and C-n/C-p move, Tab is
# left to snippet jumping. Custom keys below merge over the preset.
keymap = {
preset = "enter";
# Vim's own ins-completion confirm key, kept alongside <CR>.
"<C-y>" = [ "select_and_accept" "fallback" ];
# Dismiss the menu and undo the auto_insert preview, staying in
# insert mode. `cancel` rather than the preset's `hide`, which
# would leave the previewed text in the buffer without the
# additionalTextEdits (imports) and snippet expansion that a real
# accept applies — a degraded accept nobody wants. This also
# matches builtin ins-completion, where C-e restores the typed
# text. With no menu open both keys fall back to their native
# meaning (Esc leaves insert mode).
"<C-e>" = [ "cancel" "fallback" ];
"<Esc>" = [ "cancel" "fallback" ];
};
appearance.nerd_font_variant = "mono"; appearance.nerd_font_variant = "mono";
sources.default = [ "lsp" "path" "snippets" "buffer" ]; sources.default = [ "lsp" "path" "snippets" "buffer" ];
completion.documentation.auto_show = true; completion = {
documentation.auto_show = true;
# No item is selected until one is picked with Down/C-n, so <CR>
# inserts a newline unless a completion was deliberately chosen.
list.selection.preselect = false;
};
}; };
}; };
@@ -227,11 +266,24 @@ in
}; };
}; };
# Moodle/Mustache templates. nvim detects `.mustache` as filetype mustache
# but ships no syntax file for it, so the buffer renders as plain text.
# Tree-sitter is not an option here: the Handlebars grammar (glimmer)
# rejects mustache's argument-less sections, `{{{unescaped}}}`, partials
# and Moodle's `{{< parent }}` inheritance, and a single error node drops
# highlighting for the rest of the file. This plugin carries the syntax,
# indent and ftdetect scripts. nixpkgs marks it unfree only because
# upstream ships no license metadata; `allowUnfree` in modules/nixos.nix
# already covers it.
extraPlugins = [ pkgs.vimPlugins.vim-mustache-handlebars ];
# Global (non-LSP) keymaps. Deliberately small — a starting point to grow. # Global (non-LSP) keymaps. Deliberately small — a starting point to grow.
keymaps = [ keymaps = [
{ mode = "n"; key = "<leader>w"; action = "<cmd>w<cr>"; options.desc = "Save"; } { mode = "n"; key = "<leader>w"; action = "<cmd>w<cr>"; options.desc = "Save"; }
{ mode = "n"; key = "<leader>q"; action = "<cmd>q<cr>"; options.desc = "Quit window"; } { mode = "n"; key = "<leader>q"; action = "<cmd>q<cr>"; options.desc = "Quit window"; }
{ mode = "n"; key = "<leader>Q"; action = "<cmd>qa<cr>"; options.desc = "Quit all (exit nvim)"; } # `confirm` prompts Save/Discard/Cancel per modified buffer rather than
# aborting with E37, so quitting never dead-ends and never writes silently.
{ mode = "n"; key = "<leader>Q"; action = "<cmd>confirm qa<cr>"; options.desc = "Quit all (prompt to save)"; }
# Restore the saved session for the current directory (persistence.nvim). # Restore the saved session for the current directory (persistence.nvim).
{ mode = "n"; key = "<leader>S"; action.__raw = "function() require('persistence').load() end"; options.desc = "Restore session"; } { mode = "n"; key = "<leader>S"; action.__raw = "function() require('persistence').load() end"; options.desc = "Restore session"; }
{ mode = "n"; key = "<leader>e"; action = "<cmd>Neotree toggle<cr>"; options.desc = "File tree"; } { mode = "n"; key = "<leader>e"; action = "<cmd>Neotree toggle<cr>"; options.desc = "File tree"; }
@@ -251,7 +303,26 @@ in
# Buffer navigation (no bufferline: cycle with Shift-h/l, list via <leader>fb) # Buffer navigation (no bufferline: cycle with Shift-h/l, list via <leader>fb)
{ mode = "n"; key = "<S-l>"; action = "<cmd>bnext<cr>"; options.desc = "Next buffer"; } { mode = "n"; key = "<S-l>"; action = "<cmd>bnext<cr>"; options.desc = "Next buffer"; }
{ mode = "n"; key = "<S-h>"; action = "<cmd>bprevious<cr>"; options.desc = "Prev buffer"; } { mode = "n"; key = "<S-h>"; action = "<cmd>bprevious<cr>"; options.desc = "Prev buffer"; }
{ mode = "n"; key = "<leader>bd"; action = "<cmd>bdelete<cr>"; options.desc = "Close buffer"; } # Plain `:bdelete` closes the window when no other listed buffer is left
# for it to show, which with neo-tree open leaves a full-width tree.
# Point the window at another buffer (or a fresh empty one) first, so the
# layout survives; `confirm` prompts rather than failing when modified.
{
mode = "n";
key = "<leader>bd";
action.__raw = ''
function()
local cur = vim.api.nvim_get_current_buf()
local listed = vim.tbl_filter(
function(b) return vim.bo[b].buflisted end,
vim.api.nvim_list_bufs()
)
if #listed > 1 then vim.cmd('bprevious') else vim.cmd('enew') end
vim.cmd('confirm bdelete ' .. cur)
end
'';
options.desc = "Close buffer";
}
# Terminal: toggle a horizontal or vertical split (toggleterm). Same key # Terminal: toggle a horizontal or vertical split (toggleterm). Same key
# hides it again. From inside the terminal, <C-w> drops to normal mode and # hides it again. From inside the terminal, <C-w> drops to normal mode and
+83
View File
@@ -0,0 +1,83 @@
# `nixos-diff`: after a rebuild, report (1) what packages changed and (2) whether the
# change needs a reboot, a re-login, or nothing. NixOS keeps two markers —
# /run/booted-system (what you booted) and /run/current-system (what the last `switch`
# activated). A switch applies userland live, but:
# - the kernel, initrd, and kernel modules are frozen until the next BOOT — detected
# exactly, by comparing those store paths between the two systems;
# - session-mapped components (the Wayland compositor, GL stack, portals) a running
# graphical session keeps until you log out and back in — detected HEURISTICALLY,
# by matching changed package names against `local.rebuildDiff.reloginPackages`.
# Generic and topology-free, so it rides in the baseline; disable with
# `local.rebuildDiff.enable = false`.
{ config, lib, pkgs, ... }:
let
cfg = config.local.rebuildDiff;
reloginPattern = lib.concatStringsSep "|" cfg.reloginPackages;
nixos-diff = pkgs.writeShellApplication {
name = "nixos-diff";
runtimeInputs = [ pkgs.nvd pkgs.coreutils pkgs.gnugrep ];
text = ''
booted=/run/booted-system
current=/run/current-system
# Package-level diff of the running system vs the activated one. Force colour
# (nvd auto-disables it when captured); only version parts are coloured, so the
# package-name grep below is unaffected.
diff=$(nvd --color always diff "$booted" "$current")
printf '%s\n' "$diff"
# Reboot (exact): kernel/initrd/modules only take effect on the next boot.
reboot=0
for c in kernel initrd kernel-modules; do
[ "$(readlink -f "$booted/$c" 2>/dev/null)" = "$(readlink -f "$current/$c" 2>/dev/null)" ] \
|| reboot=1
done
# Re-login (heuristic): session-mapped components a running Wayland session
# keeps until you log out and back in.
relogin_hits=""
${lib.optionalString (reloginPattern != "") ''
relogin_hits=$(printf '%s\n' "$diff" | grep -Ew "${reloginPattern}" || true)
''}
echo
if [ "$reboot" -eq 1 ]; then
printf '>>> REBOOT needed (kernel/initrd/modules changed).\n'
elif [ -n "$relogin_hits" ]; then
printf '>>> RE-LOGIN recommended session components changed:\n'
printf '%s\n' "$relogin_hits"
else
printf '>>> No reboot/re-login needed.\n'
fi
'';
};
in
{
options.local.rebuildDiff = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Install `nixos-diff`: after a `nixos-rebuild switch`, print an `nvd` closure
diff of the running vs activated system plus a reboot / re-login / live verdict.
'';
};
reloginPackages = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "mesa" "wayland" "hyprland" "xdg-desktop-portal" ];
description = ''
Whole-word package-name patterns matched against the diff; a match means a
running Wayland session should be restarted (re-login) to pick the change up
the compositor, GL stack, and portals. Extend per host (e.g. a specific GPU
driver). Empty disables the re-login heuristic.
'';
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ nixos-diff pkgs.nvd ];
};
}