Compare commits

...

10 Commits

Author SHA1 Message Date
daniil-berg 5e1b9332a4 docs(readme): show a realistic private-inventory layout and overrides
The consumer section had only a bare-bones flake snippet. Add an example
private layout (`system/` + `home/` split, `hosts/<host>/`), a fuller
`flake.nix` matching it, and an "Overriding the baseline" section covering
the three mechanisms (neutral default → real value, `local.*` options,
extra modules). Note that structurally only `flake.nix` is required.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:42:53 +02:00
daniil-berg c9b976f0ae refactor(nixos): default regional settings to neutral values
The public baseline hard-coded a German keymap and Europe/Berlin time
zone — personal values a consumer would have to override. Default them to
region-agnostic values instead (`us` keymap, `UTC`, `en_US` locale), all
`mkDefault`, so a private inventory sets the real regional values on top
without needing `mkForce`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:25:05 +02:00
daniil-berg 07a78b6012 feat(home): add declarative Neovim via nixvim
New `nixvim` flake input (release branch, follows nixpkgs) drives the whole
editor from `modules/nvim.nix`, wired into `homeModules.default`. Minimal LSP
baseline (`nixd`, `bashls`) exposed as `local.nvim.lspServers`; blink-cmp,
Telescope, neo-tree, lualine, treesitter, colorizer, gitsigns, toggleterm, and a
catppuccin theme. which-key groups plus `<leader>?`/`<leader>sk` give an
in-editor keybind cheatsheet; per-directory session persistence with
`sessionoptions` + a `PersistenceSavePre` neo-tree close for clean layouts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:51:58 +02:00
daniil-berg 94411c0abe feat(home): add file, ripgrep, fd, tree, unzip; remove dead ka alias 2026-07-22 12:39:01 +02:00
daniil-berg 7415ea0b54 feat(git): fetch submodules on-demand 2026-07-21 14:35:11 +02:00
daniil-berg 184a2f5545 feat(home): add htop 2026-07-21 13:56:11 +02:00
daniil-berg b413c8af81 feat(hyprland): enable numlock by default for any keyboard 2026-07-21 10:41:15 +02:00
daniil-berg 5c4045f4a6 feat(hyprland): add runtime monitor left/right swap bind
`$mod CTRL o` reflows the two connected monitors adjacent in reversed
order (gap-free regardless of differing widths) and re-inits the
layer-shell clients, which Hyprland doesn't reliably re-notify on a
runtime output move: waybar rebuilds on SIGUSR2, wallpaper is re-pushed.
`monitor` default becomes `mkDefault` so a host can pin its own layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 10:38:16 +02:00
daniil-berg e4a8b07117 fix(wallpaper): repaint monitors hotplugged after login
awww-daemon paints only the outputs present when `awww img` last ran and does
not repaint a monitor connected later, so it comes up on the daemon's black
fill — which a transparent foot then shows instead of the wallpaper. Add
`wallpaper-hotplug`: it watches Hyprland's event socket and re-runs `setbg`
(which pushes to all outputs) on each `monitoradded`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 09:33:15 +02:00
daniil-berg f533bbeb05 feat(hyprland): add per-monitor workspace homing and monitor nav binds
New `local.hyprland.workspaceRules` option binds workspaces to monitor
connectors so each has a home screen; a consumer sets it from its own host
config (empty by default). Soft binding — an absent monitor's workspaces fall
back to an active one, so a laptop-only session still works.

`$mod+o` / `$mod SHIFT+o` focus and move the current workspace to the other
monitor. When rules are set, an `exec-once` nudges a single-monitor session to
workspace 1 rather than a support monitor's high default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 09:32:59 +02:00
9 changed files with 564 additions and 18 deletions
+75 -7
View File
@@ -24,19 +24,63 @@ you can tweak.
## Import the modules into your own flake ## Import the modules into your own flake
Add this repo as a flake input and import its modules from your own configuration: Add this repo as an input and assemble your machines in a private flake. Below is a
**realistic example layout**. Structurally only `flake.nix` is required — every other path
is just one way to organize your config; rename, merge, inline, or drop them freely. The one
bit of *content* a bootable machine can't skip is its `hardware-configuration.nix`
(`nixos-generate-config` output); the `hosts/<host>/` directory is only where this example
keeps it.
```
your-inventory/
├── flake.nix # inputs (pub + follows) + nixosConfigurations.<host>
├── flake.lock
├── system/ # system-level modules
│ ├── regional.nix # time zone / keymap / locale (see Overriding)
│ ├── packages.nix # extra system packages
│ └── tailscale.nix # VPN / network topology
├── home/ # home-manager modules, one file per concern
│ ├── identity.nix # git identity, name/email
│ ├── ssh.nix # SSH hosts (topology — stays private)
│ ├── syncthing.nix # Syncthing peers
│ └── … # personal apps: browser, editor, chat, …
└── hosts/
└── myhost/
├── default.nix # hostname, user account, host-specific system bits
└── hardware-configuration.nix # `nixos-generate-config` output
```
The `flake.nix` that wires it together:
```nix ```nix
{ {
inputs.config.url = "github:<owner>/<repo>"; inputs = {
# … pub.url = "github:<owner>/<repo>";
outputs = { nixpkgs, config, home-manager, ... }: { # Reuse the public flake's pins so versions never drift between the two.
nixpkgs.follows = "pub/nixpkgs";
home-manager.follows = "pub/home-manager";
};
outputs = inputs@{ self, nixpkgs, pub, home-manager, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ modules = [
config.nixosModules.default pub.nixosModules.default # generic system baseline
./hosts/myhost # your hardware + hostname ./system/regional.nix # your regional values (see below)
./hosts/myhost # hardware + hostname
home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager
{ home-manager.users.me.imports = [ config.homeModules.default ]; } {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
# Inject your own inputs into your home modules; the public ones ignore them.
home-manager.extraSpecialArgs = { inherit inputs; };
home-manager.users.me.imports = [
pub.homeModules.default # generic home baseline
./home/identity.nix # your git identity, name/email, …
./home/ssh.nix # your SSH hosts (topology stays private)
# … more personal home modules
];
}
]; ];
}; };
}; };
@@ -46,6 +90,30 @@ Add this repo as a flake input and import its modules from your own configuratio
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.
## Overriding the baseline
The public modules are a baseline you extend without forking. Three ways to shape them
from your private flake, in rough order of how often you reach for them:
1. **Neutral default → your real value.** Region-agnostic settings ship as `mkDefault`
placeholders (`console.keyMap = "us"`, `time.timeZone = "UTC"`, `en_US` locale). Set the
real ones in a private module — no `mkForce` needed:
```nix
# system/regional.nix
{ time.timeZone = "Europe/Berlin"; console.keyMap = "de"; }
```
2. **`local.*` options.** A module that exposes a knob puts it under the `local.*`
namespace with a default. Override in one line, e.g. a fuller TeX Live:
```nix
local.texlive.package = pkgs.texlive.withPackages (ps: [ ps.scheme-full ]);
```
3. **Extra modules.** Anything with no public counterpart is just another module in the
import lists above — personal apps, SSH/Syncthing topology, secrets wiring.
## Architecture: three layers ## Architecture: three layers
Kept apart on purpose. When adding config, decide which layer it belongs to: Kept apart on purpose. When adding config, decide which layer it belongs to:
Generated
+60
View File
@@ -1,5 +1,26 @@
{ {
"nodes": { "nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"home-manager": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -37,10 +58,34 @@
"type": "github" "type": "github"
} }
}, },
"nixvim": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems"
},
"locked": {
"lastModified": 1782919967,
"narHash": "sha256-pRwjfB5HQJ3m8J8bOR43pPHtHI7VUJSqwLA3P06cOY0=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "667c8471f4a0fb24d702d1a61af8609f1a5f1ba6",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "nixos-26.05",
"repo": "nixvim",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"home-manager": "home-manager", "home-manager": "home-manager",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"nixvim": "nixvim",
"rust-overlay": "rust-overlay" "rust-overlay": "rust-overlay"
} }
}, },
@@ -63,6 +108,21 @@
"repo": "rust-overlay", "repo": "rust-overlay",
"type": "github" "type": "github"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",
+17 -2
View File
@@ -17,9 +17,20 @@
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
# nixvim: declarative Neovim as home-manager options — the whole editor
# (plugins, LSP, keymaps) lives in modules/nvim.nix, pinned by this flake's
# lock. Track the release branch matching nixpkgs (a mismatched branch fails
# with `vimPlugins.<name> not found`). `follows` keeps one nixpkgs: both are
# on the same stable channel, so the version skew nixvim warns about (only an
# issue against unstable) does not arise here.
nixvim = {
url = "github:nix-community/nixvim/nixos-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = inputs@{ self, nixpkgs, home-manager, rust-overlay, ... }: outputs = inputs@{ self, nixpkgs, home-manager, rust-overlay, nixvim, ... }:
let let
system = "x86_64-linux"; system = "x86_64-linux";
in { in {
@@ -28,7 +39,11 @@
# imports = [ config.nixosModules.default ]; # imports = [ config.nixosModules.default ];
# home-manager.users.<you>.imports = [ config.homeModules.default ]; # home-manager.users.<you>.imports = [ config.homeModules.default ];
nixosModules.default = import ./modules/nixos.nix; nixosModules.default = import ./modules/nixos.nix;
homeModules.default = import ./modules/home.nix; # nixvim's home-manager module rides along so `modules/nvim.nix` can set
# `programs.nixvim.*`; consumers get the editor by importing this one module.
homeModules.default = {
imports = [ nixvim.homeModules.nixvim ./modules/home.nix ];
};
# ---- The example / template host ----------------------------------- # ---- The example / template host -----------------------------------
# Real and buildable; dogfoods the modules above. Clone, replace # Real and buildable; dogfoods the modules above. Clone, replace
+13
View File
@@ -14,4 +14,17 @@
name = "Example User"; name = "Example User";
email = "user@example.com"; email = "user@example.com";
}; };
# Optional: give workspaces a home monitor (see modules/hyprland-workspaces.nix).
# Bind by connector name (`hyprctl monitors`), not panel identity, so any
# monitor on that port inherits the rules. `default:true` sets what a screen
# shows on connect. Absent monitors fall back, so a single-screen session still
# works. Left unset here — uncomment and adjust to your outputs.
#
# local.hyprland.workspaceRules = [
# "1, monitor:HDMI-A-1, default:true" # external, workspaces 1-5
# "2, monitor:HDMI-A-1"
# "6, monitor:LVDS-1, default:true" # laptop, workspaces 6-9
# "7, monitor:LVDS-1"
# ];
} }
+62 -3
View File
@@ -79,9 +79,50 @@ let
2) ${pkgs.systemd}/bin/systemctl poweroff ;; 2) ${pkgs.systemd}/bin/systemctl poweroff ;;
esac esac
''; '';
# Swap the physical left/right placement of a two-monitor setup at runtime.
# Reflows the pair adjacent from x=0 (left one at origin, right one butted
# against its logical width) after reversing their order — so it stays
# gap-free regardless of differing widths, unlike a naive x-origin exchange
# (equal widths only). Machine-agnostic: reads whichever two connectors are
# live from `hyprctl -j`, no connector names. Ephemeral (hyprctl keyword) —
# the per-host default layout is restored on the next reload/relogin. No-op
# unless exactly two monitors are connected.
#
# Hyprland doesn't reliably re-notify layer-shell clients when an output is
# reconfigured at runtime (a known limitation — same class as the mirror/unplug
# cases where waybar needs a restart). Observed here: after the move, waybar and
# the wallpaper keep drawing at the pre-swap offsets, so the moved monitor's
# bar/bg land off-screen. Re-init both against the new geometry: waybar rebuilds
# its bars on SIGUSR2, and awww only paints outputs present when `awww img` last
# ran, so re-push the wallpaper symlink. Both are idempotent — harmless when a
# surface happened not to lag (which surface stales seems to race).
swapMonitors = pkgs.writeShellScript "hypr-swap-monitors" ''
set -eu
hc=${config.wayland.windowManager.hyprland.finalPackage}/bin/hyprctl
jq=${pkgs.jq}/bin/jq
data=$("$hc" monitors -j)
n=$(printf '%s' "$data" | "$jq" length)
if [ "$n" -ne 2 ]; then
${pkgs.libnotify}/bin/notify-send "Monitor swap" "need exactly 2 monitors ($n connected)"
exit 0
fi
# sort by current x [left, right]; the new left is the old right.
cmd=$(printf '%s' "$data" | "$jq" -r '
sort_by(.x)
| .[1] as $l | .[0] as $r
| (($l.width / $l.scale) | floor) as $off
| "keyword monitor \($l.name),preferred,0x0,\($l.scale)"
+ " ; keyword monitor \($r.name),preferred,\($off)x0,\($r.scale)"')
"$hc" --batch "$cmd"
# re-anchor the layer-shell clients onto the new geometry (see comment above)
${pkgs.procps}/bin/pkill -USR2 -x .waybar-wrapped || true
bg="$HOME/.local/share/bg"
[ -e "$bg" ] && ${pkgs.awww}/bin/awww img "$bg" || true
'';
in in
{ {
imports = [ ./gpg.nix ./media.nix ./texlive.nix ./librewolf.nix ./mc.nix ./mime.nix ]; imports = [ ./gpg.nix ./media.nix ./texlive.nix ./librewolf.nix ./mc.nix ./mime.nix ./hyprland-workspaces.nix ./nvim.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.
@@ -140,7 +181,13 @@ in
nodejs nodejs
uv # Python: versions, venvs, deps, lockfile. Runs uv-managed uv # Python: versions, venvs, deps, lockfile. Runs uv-managed
# standalone CPython via nix-ld (see modules/nixos.nix). # standalone CPython via nix-ld (see modules/nixos.nix).
htop # process viewer; config in ~/.config/htop (XDG-native)
highlight # `ccat` alias: syntax-highlighted cat highlight # `ccat` alias: syntax-highlighted cat
file # identify file type by content
ripgrep # `rg`: fast recursive grep
fd # fast, ergonomic find
tree # recursive directory listing
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)
libreoffice libreoffice
@@ -223,6 +270,7 @@ in
merge.log = true; merge.log = true;
push = { default = "simple"; followTags = true; }; push = { default = "simple"; followTags = true; };
submodule.recurse = true; submodule.recurse = true;
fetch.recurseSubmodules = "on-demand";
alias = { alias = {
l = "log --pretty=oneline -n 20 --graph --abbrev-commit"; l = "log --pretty=oneline -n 20 --graph --abbrev-commit";
@@ -356,7 +404,6 @@ in
grep = "grep --color=auto"; grep = "grep --color=auto";
diff = "diff --color=auto"; diff = "diff --color=auto";
ccat = "highlight --out-format=ansi"; ccat = "highlight --out-format=ansi";
ka = "killall";
g = "git"; g = "git";
e = editor; e = editor;
v = editor; v = editor;
@@ -479,7 +526,10 @@ in
configType = "hyprlang"; configType = "hyprlang";
settings = { settings = {
monitor = ",preferred,auto,1"; # Fallback for hosts that don't pin a layout; a two-monitor host overrides
# this with explicit positions (see the private inventory). mkDefault so
# that override is a plain value, not a mkForce fight.
monitor = lib.mkDefault ",preferred,auto,1";
# The qt module sets QT_QPA_PLATFORMTHEME=gtk3 (→ Qt apps read the GTK # The qt module sets QT_QPA_PLATFORMTHEME=gtk3 (→ Qt apps read the GTK
# Adwaita-dark theme), but only into hm-session-vars.sh + the systemd user # Adwaita-dark theme), but only into hm-session-vars.sh + the systemd user
@@ -547,6 +597,7 @@ in
input = { input = {
kb_layout = "de"; kb_layout = "de";
follow_mouse = 1; follow_mouse = 1;
numlock_by_default = true;
}; };
"$mod" = "SUPER"; "$mod" = "SUPER";
@@ -578,6 +629,12 @@ in
# master area width. Master layout uses `mfact`, not dwindle's `splitratio`. # master area width. Master layout uses `mfact`, not dwindle's `splitratio`.
"$mod, h, layoutmsg, mfact -0.05" "$mod, h, layoutmsg, mfact -0.05"
"$mod, l, layoutmsg, mfact +0.05" "$mod, l, layoutmsg, mfact +0.05"
# Move focus / the current workspace to the other monitor ("o" = other).
# With two monitors the relative +1 just toggles between them.
"$mod, o, focusmonitor, +1"
"$mod SHIFT, o, movecurrentworkspacetomonitor, +1"
# Swap the two monitors' left/right placement (see swapMonitors above).
"$mod CTRL, o, exec, ${swapMonitors}"
] ++ ( ] ++ (
# Dynamic definitions for viewing workspaces 1 to 9 and moving windows between them. # 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`. # References physical key codes for robustness with the SHIFT key, e.g. `code:10` instead of `1`.
@@ -610,6 +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)
"wallpaper-hotplug"
]; ];
}; };
}; };
+42
View File
@@ -0,0 +1,42 @@
# Per-monitor "home" for workspaces. Each rule pins a workspace to a monitor
# connector, so it opens there and Hyprland moves it back when that monitor
# reappears after an unplug. The connector names are host hardware, so the rule
# list is an option a consumer sets from its own host config (see the example in
# `hosts/example/home.nix`), not baked in here; empty by default, so the example
# host binds nothing.
#
# Soft binding: when a bound monitor is absent the workspace just falls back to
# an active one — every workspace stays reachable, so a laptop-only (no external)
# session works unchanged. The `default` keyword marks the workspace a monitor
# shows on connect.
{ config, lib, pkgs, ... }:
{
options.local.hyprland.workspaceRules = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"1, monitor:HDMI-A-1, default:true"
"6, monitor:LVDS-1, default:true"
];
description = ''
Hyprland `workspace` rules binding workspaces to monitor connectors, so
each workspace has a home screen. Connector names are host-specific set
this from the inventory rather than here.
'';
};
config.wayland.windowManager.hyprland.settings = {
workspace = config.local.hyprland.workspaceRules;
# With a support monitor whose `default` workspace is high (e.g. a laptop
# holding 6-9), a solo session with that monitor as the only screen opens on
# that high workspace instead of 1. When rules are in effect, nudge a
# single-monitor startup back to workspace 1. No-op with no rules (1 is
# already the default). jq is pinned by store path so a consumer needn't
# have it installed; hyprctl is bare (a Hyprland session always has it, like
# the wallpaper exec-once assumes awww).
exec-once = lib.optional (config.local.hyprland.workspaceRules != [ ])
"sh -c '[ \"$(hyprctl monitors -j | ${pkgs.jq}/bin/jq length)\" -eq 1 ] && hyprctl dispatch workspace 1'";
};
}
+17
View File
@@ -17,6 +17,22 @@ let
[ -n "$1" ] && ln -sf "$(readlink -f "$1")" "$bgloc" [ -n "$1" ] && ln -sf "$(readlink -f "$1")" "$bgloc"
${pkgs.awww}/bin/awww img "$bgloc" ${pkgs.awww}/bin/awww img "$bgloc"
''; '';
# 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
# (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
# with no arg → all current outputs) on each monitor-add. Hyprland emits both
# `monitoradded` and `monitoraddedv2`; match only the former so setbg fires
# once. Runs as a Hyprland exec-once (see home.nix).
wallpaperHotplug = pkgs.writeShellScriptBin "wallpaper-hotplug" ''
sock="''${XDG_RUNTIME_DIR}/hypr/''${HYPRLAND_INSTANCE_SIGNATURE}/.socket2.sock"
${pkgs.socat}/bin/socat -u UNIX-CONNECT:"$sock" - | while IFS= read -r line; do
case "$line" in
"monitoradded>>"*) setbg ;;
esac
done
'';
in in
{ {
programs.mpv = { programs.mpv = {
@@ -50,6 +66,7 @@ in
home.packages = with pkgs; [ home.packages = with pkgs; [
setbg setbg
wallpaperHotplug # re-push wallpaper to a hotplugged monitor (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
+6 -6
View File
@@ -41,14 +41,14 @@
]; ];
}; };
# Time zone. Template default — override per host/region. # Neutral template defaults for regional/localized settings. All are
time.timeZone = "Europe/Berlin"; # mkDefault: set the real values (time zone, keymap, locale categories) in your
# own config so this public baseline stays region-agnostic.
# Internationalisation properties. Template defaults — override per region. time.timeZone = lib.mkDefault "UTC";
i18n.defaultLocale = lib.mkDefault "en_US.UTF-8"; i18n.defaultLocale = lib.mkDefault "en_US.UTF-8";
console = { console = {
font = "Lat2-Terminus16"; font = lib.mkDefault "Lat2-Terminus16";
keyMap = "de"; keyMap = lib.mkDefault "us";
}; };
# Programs. # Programs.
+272
View File
@@ -0,0 +1,272 @@
# Neovim, configured declaratively via nixvim. The editor — plugins, LSP,
# completion, keymaps — is defined here and pinned by the flake lock; nothing is
# downloaded at runtime, unlike an imperative distro (NvChad/LazyVim). The nixvim
# home-manager module that provides `programs.nixvim` is imported for consumers by
# `homeModules.default` in flake.nix, so this file only sets options.
#
# Consumer knobs (the option-with-default contract): `local.nvim.enable` to opt
# out wholesale, `local.nvim.colorscheme` to swap theme in one line, and
# `local.nvim.lspServers` to reshape the language set. Everything else is
# opinionated — but since nixvim exposes every plugin as an option, a consumer can
# still override any `programs.nixvim.plugins.*` directly from their own config.
{ config, lib, pkgs, ... }:
let
cfg = config.local.nvim;
in
{
options.local.nvim = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to enable the nixvim-based Neovim setup.";
};
colorscheme = lib.mkOption {
type = lib.types.str;
default = "catppuccin";
example = "tokyonight";
description = ''
nixvim colorscheme module to enable (the attr under `colorschemes.*`,
e.g. "catppuccin", "tokyonight", "gruvbox"). Catppuccin is tuned below
(mocha flavour); for another scheme set its `colorschemes.<name>.settings`
from your own config if you want to tweak it.
'';
};
lspServers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"nixd" # Nix — this repo's own language, so a sensible public baseline
"bashls" # Shell — likewise generic enough to default on
];
example = [ "nixd" "bashls" "rust_analyzer" "basedpyright" ];
description = ''
lspconfig server names to enable under `plugins.lsp.servers.<name>`.
Each must exist in nixvim's server list and its binary in nixpkgs. The
public default is deliberately minimal (Nix + shell); a consumer sets its
own language set here and any per-server tuning a pick needs (e.g.
rust_analyzer's `installRustc`) via `programs.nixvim.plugins.lsp.servers`.
Note: standalone Lua wants "lua_ls", but Lua embedded in Nix strings is
invisible to any LSP, so it is not needed just to edit this config.
'';
};
};
config = lib.mkIf cfg.enable {
programs.nixvim = {
enable = true;
# Point nixvim at our (followed) nixpkgs instead of the rev it pins. Both
# track nixos-26.05, so the version skew nixvim warns about does not arise,
# and this keeps a single nixpkgs in the closure. Setting it explicitly is
# also what silences that warning.
nixpkgs.source = pkgs.path;
# System clipboard on Wayland: yanks go to / puts read from wl-clipboard,
# so copy/paste crosses the nvim boundary. Enabling the provider pulls the
# wl-clipboard binaries in as a dependency.
clipboard = {
register = "unnamedplus";
providers.wl-copy.enable = true;
};
globals = {
mapleader = " ";
maplocalleader = " ";
};
opts = {
number = true;
relativenumber = true; # relative line numbers for quick j/k motions
signcolumn = "yes"; # always show the gutter so text doesn't jump
cursorline = true;
termguicolors = true; # 24-bit colour, required by catppuccin & co.
scrolloff = 8; # keep 8 lines of context around the cursor
expandtab = true; # spaces, not tabs
shiftwidth = 2;
tabstop = 2;
smartindent = true;
ignorecase = true; # case-insensitive search…
smartcase = true; # …unless the query has a capital
undofile = true; # persistent undo across sessions
splitright = true;
splitbelow = true;
# What :mksession stores (persistence.nvim). Drops "blank" and "terminal"
# from nvim's default so empty/plugin windows (neo-tree, toggleterm) aren't
# serialised — they restore as broken empty splits otherwise. Adds
# "globals", which some plugins rely on. Pairs with the PersistenceSavePre
# neo-tree close below.
sessionoptions = "buffers,curdir,folds,globals,tabpages,winsize";
};
# Theme. Swap via local.nvim.colorscheme; catppuccin gets the mocha flavour.
# mkMerge keeps the computed `${cfg.colorscheme}` key from colliding with the
# literal `catppuccin` one when they happen to be the same attr.
colorschemes = lib.mkMerge [
{ ${cfg.colorscheme}.enable = true; }
(lib.mkIf (cfg.colorscheme == "catppuccin") {
catppuccin.settings.flavour = "mocha";
})
];
plugins = {
# Shared icon set for the tree, statusline, completion menu.
web-devicons.enable = true;
# Syntax via tree-sitter. The default grammar set is broad, so most
# languages get highlighting without an LSP or any per-language wiring.
treesitter = {
enable = true;
settings = {
highlight.enable = true;
indent.enable = true;
};
};
# LSP. Servers come from local.nvim.lspServers; blink-cmp feeds its
# completion capabilities in automatically (setupLspCapabilities).
lsp = {
enable = true;
servers = lib.genAttrs cfg.lspServers (_: { enable = true; });
keymaps = {
silent = true;
lspBuf = {
gd = "definition";
gD = "declaration";
K = "hover";
"<leader>rn" = "rename";
"<leader>ca" = "code_action";
};
# gr/gi (references, implementations) are wired to Telescope pickers in
# the global keymaps below instead — fuzzy + preview browsing beats the
# quickfix list that vim.lsp.buf.references would populate. gd stays a
# direct jump here (usually one target; Ctrl-o returns).
diagnostic = {
# Idiomatic vim bracket pair for stepping a list, kept as the public
# default. A consumer whose layout makes brackets awkward (they need
# AltGr on QWERTZ) can add comfortable aliases from their own config:
# this option is attrsOf, so extra keys merge in without conflict.
"[d" = "goto_prev";
"]d" = "goto_next";
"<leader>ds" = "open_float"; # show the diagnostic under the cursor
};
};
};
# Completion. blink.cmp — faster and lighter to configure than nvim-cmp.
blink-cmp = {
enable = true;
settings = {
keymap.preset = "default"; # C-space open, C-y confirm, C-n/C-p move
appearance.nerd_font_variant = "mono";
sources.default = [ "lsp" "path" "snippets" "buffer" ];
completion.documentation.auto_show = true;
};
};
# Fuzzy finder — files, live grep, open buffers (buffer switching lives
# here rather than a bufferline; see the S-h/S-l binds below too).
telescope.enable = true;
# File tree.
neo-tree.enable = true;
# Statusline, NvChad-ish blocks: powerline separators, one global bar,
# theme following the colorscheme.
lualine = {
enable = true;
settings.options = {
theme = "auto";
globalstatus = true;
section_separators = { left = ""; right = ""; };
component_separators = { left = ""; right = ""; };
};
};
# Inline colour swatches for #rrggbb / colour names.
colorizer.enable = true;
# Git signs in the gutter.
gitsigns.enable = true;
# Session persistence: auto-saves a session per project directory into XDG
# state, restorable later. <leader>S restores the current dir's session; a
# consumer wanting auto-restore on a bare `nvim` adds a VimEnter autocmd.
persistence.enable = true;
# Floating terminal toggled with Ctrl-\ (shares nvim's cwd). Try it; if
# a tiled foot next to nvim serves you better, drop this block.
toggleterm = {
enable = true;
settings = {
open_mapping = { __raw = "[[<c-\\>]]"; };
direction = "float";
};
};
# Popup of the pending keybinds after leader — a live, always-accurate
# cheatsheet. The group labels name each leader subtree so the popup reads
# by category; <leader>? opens the full list on demand (keymaps below).
which-key = {
enable = true;
settings.spec = [
{ __unkeyed-1 = "<leader>f"; group = "Find (Telescope)"; }
{ __unkeyed-1 = "<leader>d"; group = "Diagnostics"; }
{ __unkeyed-1 = "<leader>b"; group = "Buffer"; }
{ __unkeyed-1 = "<leader>c"; group = "Code"; }
{ __unkeyed-1 = "<leader>r"; group = "Refactor"; }
{ __unkeyed-1 = "<leader>s"; group = "Search"; }
];
};
};
# Global (non-LSP) keymaps. Deliberately small — a starting point to grow.
keymaps = [
{ 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>qa<cr>"; options.desc = "Quit all (exit 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>e"; action = "<cmd>Neotree toggle<cr>"; options.desc = "File tree"; }
{ mode = "n"; key = "<Esc>"; action = "<cmd>nohlsearch<cr>"; }
# Telescope
{ mode = "n"; key = "<leader>ff"; action = "<cmd>Telescope find_files<cr>"; options.desc = "Find files"; }
{ mode = "n"; key = "<leader>fg"; action = "<cmd>Telescope live_grep<cr>"; options.desc = "Live grep"; }
{ mode = "n"; key = "<leader>fb"; action = "<cmd>Telescope buffers<cr>"; options.desc = "Buffers"; }
{ mode = "n"; key = "<leader>fh"; action = "<cmd>Telescope help_tags<cr>"; options.desc = "Help tags"; }
# LSP navigation via Telescope pickers (fuzzy + preview) for the
# many-result lookups; gd/gD stay direct jumps (plugins.lsp.keymaps above).
{ mode = "n"; key = "gr"; action = "<cmd>Telescope lsp_references<cr>"; options.desc = "References"; }
{ mode = "n"; key = "gi"; action = "<cmd>Telescope lsp_implementations<cr>"; options.desc = "Implementations"; }
# 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-h>"; action = "<cmd>bprevious<cr>"; options.desc = "Prev buffer"; }
{ mode = "n"; key = "<leader>bd"; action = "<cmd>bdelete<cr>"; options.desc = "Close buffer"; }
# Cheatsheet: <leader>? pops the full which-key list; <leader>sk fuzzy-
# searches every active mapping via Telescope (catches non-leader binds too).
{ mode = "n"; key = "<leader>?"; action = "<cmd>WhichKey<cr>"; options.desc = "All keybinds (which-key)"; }
{ mode = "n"; key = "<leader>sk"; action = "<cmd>Telescope keymaps<cr>"; options.desc = "Search keymaps"; }
];
# persistence saves the window layout via :mksession, but neo-tree's special
# buffer doesn't serialise — a saved tree window restores as a blank split.
# Close neo-tree just before a session is written so the layout stays clean.
autoCmd = [
{
event = "User";
pattern = "PersistenceSavePre";
callback.__raw = ''function() pcall(vim.cmd, "Neotree close") end'';
}
];
};
};
}