Compare commits

...

8 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
6 changed files with 486 additions and 18 deletions
+75 -7
View File
@@ -24,19 +24,63 @@ you can tweak.
## 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
{
inputs.config.url = "github:<owner>/<repo>";
# …
outputs = { nixpkgs, config, home-manager, ... }: {
inputs = {
pub.url = "github:<owner>/<repo>";
# 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 {
system = "x86_64-linux";
modules = [
config.nixosModules.default
./hosts/myhost # your hardware + hostname
pub.nixosModules.default # generic system baseline
./system/regional.nix # your regional values (see below)
./hosts/myhost # hardware + hostname
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
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
Kept apart on purpose. When adding config, decide which layer it belongs to:
Generated
+60
View File
@@ -1,5 +1,26 @@
{
"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": {
"inputs": {
"nixpkgs": [
@@ -37,10 +58,34 @@
"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": {
"inputs": {
"home-manager": "home-manager",
"nixpkgs": "nixpkgs",
"nixvim": "nixvim",
"rust-overlay": "rust-overlay"
}
},
@@ -63,6 +108,21 @@
"repo": "rust-overlay",
"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",
+17 -2
View File
@@ -17,9 +17,20 @@
url = "github:oxalica/rust-overlay";
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
system = "x86_64-linux";
in {
@@ -28,7 +39,11 @@
# imports = [ config.nixosModules.default ];
# home-manager.users.<you>.imports = [ config.homeModules.default ];
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 -----------------------------------
# Real and buildable; dogfoods the modules above. Clone, replace
+56 -3
View File
@@ -79,9 +79,50 @@ let
2) ${pkgs.systemd}/bin/systemctl poweroff ;;
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
{
imports = [ ./gpg.nix ./media.nix ./texlive.nix ./librewolf.nix ./mc.nix ./mime.nix ./hyprland-workspaces.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
# by name without wiring each through nix.
@@ -140,7 +181,13 @@ in
nodejs
uv # Python: versions, venvs, deps, lockfile. Runs uv-managed
# standalone CPython via nix-ld (see modules/nixos.nix).
htop # process viewer; config in ~/.config/htop (XDG-native)
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)
pv # pipe progress meter (throughput, ETA on streamed data)
libreoffice
@@ -223,6 +270,7 @@ in
merge.log = true;
push = { default = "simple"; followTags = true; };
submodule.recurse = true;
fetch.recurseSubmodules = "on-demand";
alias = {
l = "log --pretty=oneline -n 20 --graph --abbrev-commit";
@@ -356,7 +404,6 @@ in
grep = "grep --color=auto";
diff = "diff --color=auto";
ccat = "highlight --out-format=ansi";
ka = "killall";
g = "git";
e = editor;
v = editor;
@@ -479,7 +526,10 @@ in
configType = "hyprlang";
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
# Adwaita-dark theme), but only into hm-session-vars.sh + the systemd user
@@ -547,6 +597,7 @@ in
input = {
kb_layout = "de";
follow_mouse = 1;
numlock_by_default = true;
};
"$mod" = "SUPER";
@@ -582,6 +633,8 @@ in
# 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.
# References physical key codes for robustness with the SHIFT key, e.g. `code:10` instead of `1`.
+6 -6
View File
@@ -41,14 +41,14 @@
];
};
# Time zone. Template default — override per host/region.
time.timeZone = "Europe/Berlin";
# Internationalisation properties. Template defaults — override per region.
# Neutral template defaults for regional/localized settings. All are
# mkDefault: set the real values (time zone, keymap, locale categories) in your
# own config so this public baseline stays region-agnostic.
time.timeZone = lib.mkDefault "UTC";
i18n.defaultLocale = lib.mkDefault "en_US.UTF-8";
console = {
font = "Lat2-Terminus16";
keyMap = "de";
font = lib.mkDefault "Lat2-Terminus16";
keyMap = lib.mkDefault "us";
};
# 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'';
}
];
};
};
}