Compare commits

...

37 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
daniil-berg 031bad3238 feat(waybar): add window title module and styled clock calendar
Center a `hyprland/window` title module and move the clock/date to the far
right of the bar. Give the clock a month-calendar tooltip with One Dark
Pango styling (yellow header, purple weekdays, blue underlined today, dim
ISO week-number gutter), 3-per-row year view via right-click.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 08:03:48 +02:00
daniil-berg dc676b6f8c refactor(i18n): make defaultLocale overridable via mkDefault
Template hard-set `en_US.UTF-8`, so a consumer setting `i18n.defaultLocale`
in the private inventory hit a conflicting-definition eval error. Wrap the
template default in `lib.mkDefault` per the overrides-are-options contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 08:03:43 +02:00
daniil-berg f64d68ac05 fix(mc): discard xdg-open stdout/stderr so app logs don't clobber mc pane 2026-07-13 07:03:03 +02:00
daniil-berg 4be1c4fb67 feat(mc): open files with their configured app via xdg-open
Ship `mc.ext.ini` as the `local.mc.extIni` option (like `local.mc.menu`):
Enter browses archives in-place as before and delegates every other type to
`xdg-open`. Add public `mime.nix` with `xdg.mimeApps` defaults (pdf ->
zathura, images -> imv, audio/video -> mpv, office -> libreoffice) -- public
because those handlers are; personal browser/mail defaults stay private.

Also document, at the Hyprland swallow config, how swallowing differs from an
app opening fullscreen/maximized (LibreOffice restores its own maximized state
and looks swallowed but isn't).
2026-07-12 22:09:01 +02:00
daniil-berg 7205841c4d fix(ssh): spawn askpass pinentry off-tree so push doesn't swallow terminal 2026-07-12 16:45:18 +02:00
daniil-berg 0851515de0 fix(hyprland): exempt pinentry dialog from terminal swallowing 2026-07-12 16:22:59 +02:00
daniil-berg de6c3993cc feat(systemd): add shutdown/boot hang diagnostics module
Shutdown stalls are hard to debug because journald is torn down mid-way,
so late hangs never reach the journal. `modules/shutdown-debug.nix`
exposes `local.shutdownDebug.*`: verbose systemd logging routed to kmsg
(recoverable from pstore after a forced power-off), an opt-in tty9 debug
shell, and a shortened `DefaultTimeoutStopSec` preset (system + user) so
a hung unit is killed and logged in seconds instead of spinning at 90s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 14:47:30 +02:00
daniil-berg 98eb0f87ba feat(waybar): highlight active workspace and flag urgent ones
Style the `hyprland/workspaces` buttons so the focused workspace is
distinguishable when several are occupied: white text, darker
background, blue underline; urgent workspaces turn red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 08:01:41 +02:00
daniil-berg d8d86aadcb fix(hyprland): repair master-width, fullscreen, and brightness binds
- `$mod+h`/`l` used dwindle's `splitratio`, a no-op in the master layout;
  switch to `mfact` so they actually resize the master area.
- `$mod+f` ran `fullscreen 1` (maximize, bar/gaps kept); swap so `$mod+f` is
  true fullscreen and `$mod SHIFT+f` maximizes.
- add `$mod+space` `swapwithmaster` (dwm-style zoom).
- the `XF86MonBrightness` binds called `brightnessctl`, which was never
  installed; add the package. It writes via the active logind session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 07:56:59 +02:00
daniil-berg c375a15561 feat(hyprland): swallow GUI windows spawned from foot into the terminal
A viewer opened from a foot shell (zathura/imv, incl. from mc) now hides
the terminal and takes its place, restoring it on close. Requires
standalone foot: server/client mode shares one process across all
windows, so PID-based swallow cannot tell them apart. `$mod+Return` runs
`foot`, not `footclient`, and the `foot --server` exec-once is dropped.
2026-07-11 14:01:04 +02:00
daniil-berg 08a14d52f2 feat(mc): make the F2 menu overridable via local.mc.menu
Expose the user menu as a `types.lines` option instead of a baked source
file. The shipped menu is a `mkBefore` definition (not a `default`, which a
consumer definition would drop), so `local.mc.menu = "…"` appends entries
after the baseline and `lib.mkForce "…"` replaces it wholesale.

`ini` stays a baked source file: it is section/key=value, so append-to-extend
risks duplicate keys — not a safe override path.
2026-07-10 23:07:16 +02:00
daniil-berg eab578e699 feat(mc): add midnight commander with declarative config
Public baseline module: installs `mc` plus `ouch` (backs the F2 pack/unpack
entries, archive format inferred from the extension), and ships `ini`, `menu`,
and a generated `panels.ini` into `$XDG_CONFIG_HOME/mc`.

`local.mc.otherDir` exposes the other-panel start dir as an option defaulting
to `config.home.homeDirectory`, so the one personal value isn't baked in. The
F2 user menu is trimmed to the entries that match this config (imv/zathura/mpv
openers, ouch pack/unpack, man view); the stock rcp/uudecode/news/latex cruft
is dropped. `auto_save_setup=false` keeps mc from rewriting the read-only store
symlinks on exit.
2026-07-10 22:46:32 +02:00
daniil-berg 8f9eff1640 feat(librewolf): expose package as an overridable option
Move librewolf out of the bare package list into its own module declaring
`local.librewolf.package` (default pkgs.librewolf), mirroring texlive. Lets a
consumer swap the wrapped package — extraPolicies/extraPrefs — to share
extensions and an about:config baseline across profiles without editing the
module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:19:40 +02:00
daniil-berg 3e093b02d1 docs: explain how consumers override public module defaults
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:00:50 +02:00
daniil-berg b907a39c41 feat(home): add pandoc with texlive for PDF output
texlive scheme is the `local.texlive.package` option (default scheme-medium)
so consumers can swap schemes without editing the module; TEXMF* vars keep
its cache/config out of $HOME. pandoc PDF defaults to xelatex — pdflatex
fails on combining Unicode (e.g. U+0308 in decomposed ä).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:00:33 +02:00
daniil-berg c1ec7401d1 feat(home): add pv to packages 2026-07-09 07:43:42 +02:00
daniil-berg 80c90c03ae feat(python): manage python with uv via nix-ld 2026-07-08 14:32:05 +02:00
daniil-berg 53f079c242 feat(home): add jq to packages 2026-07-08 11:25:15 +02:00
daniil-berg e6d96725ad feat(home): put ~/.local/bin on PATH 2026-07-08 10:49:45 +02:00
daniil-berg f9c3dad584 fix(desktop): suppress hyprland default wallpaper flash on login
awww starts after the compositor, so hyprland painted its bundled
splash until the real wallpaper landed. Disable it via `misc`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:40:10 +02:00
daniil-berg 270ffa8208 fix(ssh): give ssh-agent askpass env for verify-required sk keys
The agent, not the client, collects the FIDO PIN when signing with an
agent-held verify-required `sk-ssh-ed25519` key. Its systemd user service
started with a minimal env lacking `SSH_ASKPASS`, so those signatures failed
as "incorrect passphrase supplied to decrypt private key". Set `SSH_ASKPASS`
and `SSH_ASKPASS_REQUIRE=force` on the unit so the agent can pop pinentry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:21:14 +02:00
daniil-berg b6f9a9beec chore(git): drop github:/gh: remote rewrite rules
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:37:18 +02:00
daniil-berg 0c2ae04cdf feat(home): enable ssh-agent user service
gnome-keyring 50 dropped its ssh component and gpg's ssh support is off,
so there was no agent at all — SSH_AUTH_SOCK unset, `ssh-add` failed.
Keys stay added-on-demand (`AddKeysToAgent = "no"` in the inventory).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 07:58:29 +02:00
daniil-berg eb8693e853 fix(desktop): dark ssh-askpass pinentry via hyprland env
QT_QPA_PLATFORMTHEME=gtk3 (what makes Qt apps read Adwaita-dark) reaches
the systemd-user env — so gpg-agent's signing pinentry is dark — but not
Hyprland's children, so the shell-spawned ssh askpass pinentry launched
without it and came up light. Export it in the compositor env so every
session child inherits it. Keeps pinentry-qt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:02:02 +02:00
daniil-berg 7b44fe8bf1 feat(desktop): dark fuzzel, curated launcher, dmenu power menu
Fuzzel ignores the portal color-scheme, so set its palette explicitly to
match waybar. Hide non-launcher `.desktop` entries (foot variants, mpv/
umpv, zathura, nvim) via `noDisplay` shadows. Replace the per-action
app-list power entries with a `$mod+Backspace` `fuzzel --dmenu` power
menu (Log Out / Reboot / Shut Down), colored Papirus icons.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:36:52 +02:00
daniil-berg b2fabbc764 feat(desktop): dark-mode Qt/GTK apps via portal color-scheme
Qt apps (keepassxc, pinentry-qt) rendered light while the GTK side was
Adwaita-dark. keepassxc's `auto` theme reads dark/light only from the
xdg-desktop-portal `color-scheme` value, which the gtk portal serves from
dconf `org/gnome/desktop/interface/color-scheme` — unset, so it reported
"no preference" and fell back to light. Set it to `prefer-dark`.

Also route Qt through the `gtk3` platform theme (`libqgtk3` ships in
qtbase, unlike an external `adwaita-qt` style plugin which nixpkgs' baked
`QT_PLUGIN_PATH` makes unreachable), and set the GTK prefer-dark flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 16:26:38 +02:00
daniil-berg 5172ca4d18 feat(desktop): Print-key screenshots via grim/slurp
Bare Print grabs the whole screen to a timestamped PNG under
~/Pictures/Screenshots; $mod selects a region (slurp), SHIFT sends to the
clipboard instead. A mako toast confirms each capture. Adds grim/slurp and
enables xdg.userDirs for the Pictures target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 14:09:51 +02:00
18 changed files with 1528 additions and 40 deletions
+14
View File
@@ -15,6 +15,20 @@ one as a flake input. Deploy happens from the private repo, not here.
- secrets (keys, tokens, passwords) → **neither** repo; runtime files or a secrets manager
decrypting to a path. See the SSH section.
## Overrides = options (the consumer contract)
Private imports these modules **read-only** (flake input) — it can't patch a package list
or a hard-coded value in place. So anything a consumer might reasonably want to change is
exposed as an **option with a default**, never baked into config. That is what makes this
repo a *baseline others extend* rather than a fork-and-patch.
Pattern (see `modules/texlive.nix`): option-bearing config lives in its own sub-module
under the `local.*` namespace, `imports`-ed by `home.nix`. A module that declares `options`
must use the split `{ options = …; config = …; }` form, so isolating it per file keeps
`home.nix` bare. Override is then one line, e.g.
`local.texlive.package = pkgs.texlive.withPackages (ps: [ ps.scheme-full ]);`. A plain value
with nothing to vary goes straight in the bare package list — no option, don't over-abstract.
## Maxim: keep $HOME clean (XDG)
Standing principle for every addition: minimise dotfiles/dirs in `$HOME`. Route a tool's
+79 -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:
@@ -71,3 +139,7 @@ hosts/example/ # the template host (placeholder hardware-configuration.nix
```
Shared `*.nix` / `*.lock` live at the top level; per-host files under `hosts/<host>/`.
Chasing a shutdown/boot hang? `modules/shutdown-debug.nix` exposes `local.shutdownDebug.*`
(verbose systemd logging to kmsg, a tty9 debug shell, a shortened stop-timeout) — see its
header comment for how to read the evidence.
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
+13
View File
@@ -14,4 +14,17 @@
name = "Example User";
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"
# ];
}
+160
View File
@@ -0,0 +1,160 @@
[Midnight-Commander]
verbose=true
shell_patterns=true
auto_save_setup=false
preallocate_space=false
auto_menu=false
use_internal_view=true
use_internal_edit=false
clear_before_exec=true
confirm_delete=true
confirm_overwrite=true
confirm_execute=false
confirm_history_cleanup=true
confirm_exit=false
confirm_directory_hotlist_delete=false
confirm_view_dir=false
safe_delete=false
safe_overwrite=false
use_8th_bit_as_meta=false
mouse_move_pages_viewer=true
mouse_close_dialog=false
fast_refresh=false
drop_menus=false
wrap_mode=false
old_esc_mode=true
cd_symlinks=true
show_all_if_ambiguous=true
use_file_to_guess_type=true
alternate_plus_minus=false
only_leading_plus_minus=true
show_output_starts_shell=false
xtree_mode=false
file_op_compute_totals=true
classic_progressbar=true
use_netrc=true
ftpfs_always_use_proxy=false
ftpfs_use_passive_connections=true
ftpfs_use_passive_connections_over_proxy=false
ftpfs_use_unix_list_options=true
ftpfs_first_cd_then_ls=true
ignore_ftp_chattr_errors=true
editor_fill_tabs_with_spaces=false
editor_return_does_auto_indent=true
editor_backspace_through_tabs=false
editor_fake_half_tabs=true
editor_option_save_position=true
editor_option_auto_para_formatting=false
editor_option_typewriter_wrap=false
editor_edit_confirm_save=true
editor_syntax_highlighting=true
editor_persistent_selections=true
editor_drop_selection_on_copy=true
editor_cursor_beyond_eol=false
editor_cursor_after_inserted_block=false
editor_visible_tabs=true
editor_visible_spaces=true
editor_line_state=false
editor_simple_statusbar=false
editor_check_new_line=false
editor_show_right_margin=false
editor_group_undo=false
editor_state_full_filename=false
editor_ask_filename_before_edit=false
nice_rotating_dash=true
mcview_remember_file_position=false
auto_fill_mkdir_name=true
copymove_persistent_attr=true
pause_after_run=1
mouse_repeat_rate=100
double_click_speed=250
old_esc_mode_timeout=1000000
max_dirt_limit=10
num_history_items_recorded=60
vfs_timeout=60
ftpfs_directory_timeout=900
ftpfs_retry_seconds=30
fish_directory_timeout=900
editor_tab_spacing=8
editor_word_wrap_line_length=72
editor_option_save_mode=0
editor_backup_extension=~
editor_filesize_threshold=64M
editor_stop_format_chars=-+*\\,.;:&>
mcview_eof=
skin=yadt256-defbg
shadows=true
[Layout]
output_lines=0
left_panel_size=115
top_panel_size=0
message_visible=true
keybar_visible=true
xterm_title=true
command_prompt=true
menubar_visible=true
free_space=true
horizontal_split=false
vertical_equal=true
horizontal_equal=true
[Misc]
timeformat_recent=%d. %b %H:%M
timeformat_old=%d. %b %Y
ftp_proxy_host=gate
ftpfs_password=anonymous@
display_codepage=UTF-8
source_codepage=Other_8_bit
autodetect_codeset=
clipboard_store=
clipboard_paste=
[Colors]
base_color=
st-256color=
color_terminals=
[Panels]
show_mini_info=true
kilobyte_si=true
mix_all_files=false
show_backups=true
show_dot_files=true
fast_reload=false
fast_reload_msg_shown=false
mark_moves_down=true
reverse_files_only=true
auto_save_setup_panels=false
navigate_with_arrows=true
panel_scroll_pages=true
panel_scroll_center=false
mouse_move_pages=true
filetype_mode=true
permission_mode=false
torben_fj_mode=false
quick_search_mode=2
select_flags=6
simple_swap=false
[FindFile]
file_case_sens=true
file_shell_pattern=true
file_find_recurs=true
file_skip_hidden=false
file_all_charsets=false
content_case_sens=true
content_regexp=false
content_first_hit=false
content_whole_words=false
content_all_charsets=false
ignore_dirs_enable=true
ignore_dirs=
[Panelize]
Geänderte Git-Dateien=git ls-files --modified
Nach dem Patchen nach Ablehnungen suchen=find . -name \\*.rej -print
Suche SUID- und SGID-Programme=find . \\( \\( -perm -04000 -a -perm /011 \\) -o \\( -perm -02000 -a -perm /01 \\) \\) -print
Suche nach *.orig nach dem Patchen=find . -name \\*.orig -print
+86
View File
@@ -0,0 +1,86 @@
# Midnight Commander file associations (public baseline).
#
# mc reads a SINGLE mc.ext.ini; this user copy fully replaces the system one
# (they are not merged), so it must also carry the archive entries we keep.
#
# Policy: Enter (Open) on an archive mounts it as a virtual filesystem (mc
# built-in, no external helper). Every other file falls through to [Default],
# which runs `xdg-open` -- so the app is whatever the XDG mimeapps default
# resolves to (set those in the private inventory's xdg.mimeApps). One source of
# truth, shared with every other launcher; nothing app-specific baked in here.
#
# Note: office documents (.docx/.odt/.xlsx/...) are ZIP containers, so they are
# deliberately absent from the [zip] entry below -- otherwise Enter would mount
# them as archives instead of handing them to LibreOffice via xdg-open.
#
# Override contract (this is a types.lines option shipped as mkBefore): append a
# same-named section to override one key -- mc merges same-named sections and
# the last key wins -- e.g. add a [Default] with a different Open to change the
# fallback, or add [pdf] Type=^PDF / Open=mupdf %f & to force one type to a
# specific app. lib.mkForce "..." replaces this baseline wholesale.
[mc.ext.ini]
Version=4.0
### Archives: Enter mounts them as a virtual filesystem (F3/F4 unaffected) ###
[tar]
Regex=\\.(tar(\\.(gz|bz2|xz|zst|lz|lz4|lzo|Z))?|t(gz|az|bz2?|xz|zst|lz))$
Open=%cd %p/utar://
[zip]
Regex=\\.(zip|jar|xpi|whl|egg|apk|war|ear)$
Open=%cd %p/uzip://
[7z]
Shell=.7z
Open=%cd %p/u7z://
[rar]
Regex=\\.r(ar|[0-9][0-9])$
Open=%cd %p/urar://
[arj]
Regex=\\.a(rj|[0-9][0-9])$
Open=%cd %p/uarj://
[cab]
Shell=.cab
Open=%cd %p/ucab://
[cpio]
Regex=\\.cpio(\\.(gz|xz|bz2|zst|lz|lz4|lzo|Z))?$
Open=%cd %p/ucpio://
[deb]
Regex=\\.u?deb$
Open=%cd %p/deb://
[rpm]
Regex=\\.(rpm|src\\.rpm|spm)$
Open=%cd %p/rpm://
[iso]
Shell=.iso
Open=%cd %p/iso9660://
[patch]
Regex=\\.(diff|patch)(\\.(gz|bz2|xz|zst|Z))?$
Open=%cd %p/patchfs://
### Everything else: hand off to the desktop's configured default app ###
# `&` backgrounds xdg-open so mc's shell returns and the launched GUI is orphaned
# to init. Hyprland only swallows the terminal when the new window's parent-PID
# chain still has a foot ancestor (getSwallower in Hyprland's Window.cpp), so an
# orphaned app opens as its own tile instead of swallowing mc (zathura, imv, ...).
# %f MUST stay unquoted: mc already backslash-escapes the path, so quoting turns
# those backslashes literal and breaks filenames with spaces.
# Redirect stdout/stderr to /dev/null: `&` backgrounds but leaves the app's fds on
# mc's terminal, so a chatty app paints over the mc pane and looks like a broken
# swallow. Tradeoff: this also discards any real diagnostics when an app fails to
# open a file. To keep a breadcrumb, swap /dev/null for a log file, e.g.
# `>>$XDG_STATE_HOME/mc-open.log 2>&1`.
[Default]
Open=xdg-open %f >/dev/null 2>&1 &
+30
View File
@@ -0,0 +1,30 @@
shell_patterns=0
# F2 user menu. Format macros: %f current file · %d current dir · %s tagged
# files else current · %t tagged files · %D other panel's dir. Full list: mc(1).
c Pack selected into an archive (ouch — format from the name's extension)
NAME=%{Archive name, e.g. stuff.tar.zst}
ouch compress %s "$NAME"
x Unpack archive here (ouch)
ouch decompress %f
i Open file with image viewer (imv)
imv %f
I Open current directory with image viewer (imv)
imv %d
p Open file with document viewer (zathura)
zathura %f
v Play file with mpv
mpv %f
V Play current directory with mpv
mpv %d
m View a man page
MAN=%{Enter manual name}
%view{ascii,nroff} MANROFFOPT='-c -Tlatin1' MAN_KEEP_FORMATTING=1 man -P cat "$MAN"
+370 -24
View File
@@ -3,7 +3,7 @@
# user.name/email), host topology (SSH hosts, Syncthing peers) and personal app
# choices (grayjay, Claude Code, …) live in the inventory, not in this module.
{ config, pkgs, ... }:
{ config, pkgs, lib, ... }:
let
terminal = "foot";
@@ -18,16 +18,115 @@ let
# confirm/none informational, e.g. "Confirm user presence" (touch request)
# A FIDO touch can't be faked by software, so those messages need no dialog:
# succeed silently instead of popping a spurious PIN box (the token blinks).
# pinentry runs under `systemd-run --user`, not directly, so it is reparented
# onto the user service manager instead of this askpass shell. ssh spawns the
# askpass from inside the foot terminal, and Hyprland's window swallowing walks
# a new window's parent-PID chain for a `foot` ancestor (swallow_regex) — a
# direct pinentry would match and hide the terminal behind the PIN dialog.
# Detaching breaks that chain (parent becomes systemd), exactly like the
# commit-signing pinentry that gpg-agent already spawns off-tree. --pipe wires
# stdin/stdout through so the Assuan exchange still works; --wait propagates
# the exit status; --collect reaps the transient unit.
sshAskpass = pkgs.writeShellScript "ssh-askpass-pinentry" ''
[ -n "''${SSH_ASKPASS_PROMPT:-}" ] && exit 0
desc=$(printf '%s' "''${1:-Passphrase:}" | sed 's/%/%25/g')
printf 'SETTITLE ssh\nSETPROMPT PIN:\nSETDESC %s\nGETPIN\nBYE\n' "$desc" \
| ${pkgs.pinentry-qt}/bin/pinentry-qt \
| ${pkgs.systemd}/bin/systemd-run --user --pipe --wait --collect --quiet \
${pkgs.pinentry-qt}/bin/pinentry-qt \
| sed -n 's/^D //p'
'';
# Screenshot helper for the Print-key binds below. Two axes, chosen by args:
# scope ($1 = full | region, the latter an interactive slurp selection) and
# destination ($2 = file | clip). Files are timestamped PNGs under
# ~/Pictures/Screenshots; clip copies to the Wayland clipboard. A mako toast
# confirms each grab. slurp exits non-zero on Escape, which aborts here.
screenshot = pkgs.writeShellScript "screenshot" ''
set -eu
if [ "$1" = region ]; then
geom=$(${pkgs.slurp}/bin/slurp) || exit 1
grab() { ${pkgs.grim}/bin/grim -g "$geom" "$@"; }
else
grab() { ${pkgs.grim}/bin/grim "$@"; }
fi
if [ "$2" = clip ]; then
grab - | ${pkgs.wl-clipboard}/bin/wl-copy
${pkgs.libnotify}/bin/notify-send "Screenshot" "Copied to clipboard"
else
dir="${config.xdg.userDirs.pictures}/Screenshots"
mkdir -p "$dir"
file="$dir/$(date +%Y-%m-%d-%H%M%S).png"
grab "$file"
${pkgs.libnotify}/bin/notify-send "Screenshot" "Saved $file"
fi
'';
# Session/power menu: a fuzzel --dmenu picker (not app-list entries), bound to
# $mod+Backspace. --index returns the chosen row number, so dispatch matches on
# position, not label text. Icons use the Papirus names via fuzzel's dmenu
# icon protocol (NUL + 0x1f after each label). Escape/no pick → fuzzel exits
# non-zero, command substitution is empty, case falls through → no-op.
#
# Icon names deliberately picked from Papirus's apps/ (colored) set that have
# NO actions/ variant: system-{reboot,shutdown,log-out} exist in both, and at
# dmenu line-height fuzzel resolves the monochrome actions/ copy. gnome-logout
# / system-restart / gnome-session-halt are apps-only, so they stay colored.
powerMenu = pkgs.writeShellScript "power-menu" ''
case $(printf 'Log Out\0icon\x1fgnome-logout\nReboot\0icon\x1fsystem-restart\nShut Down\0icon\x1fgnome-session-halt\n' \
| ${pkgs.fuzzel}/bin/fuzzel --dmenu --index --prompt 'power ') in
0) ${config.wayland.windowManager.hyprland.finalPackage}/bin/hyprctl dispatch exit ;;
1) ${pkgs.systemd}/bin/systemctl reboot ;;
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 ];
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.
home.sessionPath = [ "${config.home.homeDirectory}/.local/bin" ];
home.sessionVariables = {
TERMINAL = terminal;
@@ -41,6 +140,10 @@ in
PYTHONIOENCODING = "UTF-8";
PYTHONSTARTUP = "${config.xdg.configHome}/python/startup";
PYTHON_HISTFILE = "${config.xdg.cacheHome}/python/history";
# Default interpreter uv picks for `uv run`/`uv venv` without an explicit
# -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.
UV_PYTHON = "3.14";
# psql / wget / readline configs, XDG-routed (the rc files are declared
# via xdg.configFile below).
PSQLRC = "${config.xdg.configHome}/psql/rc";
@@ -67,20 +170,54 @@ in
xdg.enable = true;
home.packages = with pkgs; [
librewolf
# librewolf is installed by ./librewolf.nix (via local.librewolf.package) so
# the wrapped package stays overridable — do NOT also list it here.
keepassxc # config in ~/.config/keepassxc
papirus-icon-theme # fuzzel icon-theme; ships non-symbolic power icons
# (Adwaita only has them as symbolic, which fuzzel skips)
# claude-code is installed by programs.claude-code below (its wrapped
# finalPackage) — do NOT also list it here, or buildEnv conflicts on the
# wrapper binary.
nodejs
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
grim # screenshot capture (also used by the Print-key binds)
slurp # interactive region selector for grim
brightnessctl # backlight control for the XF86MonBrightness binds; writes
# via the active logind session, so no udev rule needed
];
# Canonical XDG user dirs. Only Pictures is actually used (screenshot target,
# file-picker default). createDirectories stays off so the rest aren't
# littered into $HOME — they're merely declared in ~/.config/user-dirs.dirs,
# created lazily by whatever app wants them.
xdg.userDirs = {
enable = true;
createDirectories = false;
pictures = "${config.home.homeDirectory}/Pictures";
};
# ---- Per-tool config files, routed into XDG dirs ---------------------
xdg.configFile = {
"python/startup".source = ./assets/python-startup.py;
# uv global config. "managed" = always use uv's own CPython, never a
# system/Nix python — keeps behaviour reproducible regardless of what else
# is on PATH. Default version comes from UV_PYTHON above.
"uv/uv.toml".text = ''
python-preference = "managed"
'';
# psql: unlimited, per-database history file (kept out of $HOME).
"psql/rc".text = ''
\set HISTFILE ${config.xdg.cacheHome}/psql/history-:DBNAME
@@ -108,6 +245,14 @@ in
programs.home-manager.enable = true;
# Document converter (md/html/docx/…). PDF output shells out to a LaTeX engine
# (texlive.nix): default it to xelatex, which is Unicode-native — pdflatex
# (pandoc's own default) fails on combining marks, e.g. U+0308 in decomposed ä.
programs.pandoc = {
enable = true;
defaults.pdf-engine = "xelatex";
};
programs.git = {
enable = true;
settings = {
@@ -125,10 +270,7 @@ in
merge.log = true;
push = { default = "simple"; followTags = true; };
submodule.recurse = true;
# github: / gh: / gist: remote shorthands.
url."git@github.com:".insteadOf = "gh:";
url."git://github.com/".insteadOf = "github:";
fetch.recurseSubmodules = "on-demand";
alias = {
l = "log --pretty=oneline -n 20 --graph --abbrev-commit";
@@ -168,7 +310,56 @@ in
# so their stderr goes to the journal, not whatever terminal you're in.
programs.fuzzel = {
enable = true;
settings.main.font = "JetBrainsMono Nerd Font:size=11";
settings = {
main.font = "JetBrainsMono Nerd Font:size=11";
# Papirus for colored power icons in the $mod+Backspace dmenu (Adwaita
# ships those only as symbolic, and fuzzel skips symbolic dirs). It
# inherits hicolor, so app icons still resolve (recolored to Papirus).
main.icon-theme = "Papirus";
# Fuzzel ignores the portal color-scheme, and its built-in palette is
# light — dark has to be set here. Matches waybar (#222/#bbb + One Dark
# accents); flat, thin border, no rounding per the minimal look.
border = { width = 1; radius = 0; };
colors = {
background = "222222f2";
text = "bbbbbbff";
match = "e5c07bff";
selection = "333333ff";
selection-text = "ffffffff";
selection-match = "e5c07bff";
border = "444444ff";
};
};
};
# Curate what fuzzel's app-list mode shows. Fuzzel just enumerates XDG
# `.desktop` files, so we shape that set here. Shadow a package's entry by
# redefining its id with noDisplay = true: our ~/.local/share/applications
# copy wins over the profile's. noDisplay only hides from launchers/menus —
# the app stays wired up for xdg-open and "open with", so opening a file
# *with* mpv/zathura is unaffected. The attrset merges with anything a private
# host adds. noDisplay is mkDefault so a host can un-hide one (e.g. re-expose
# mpv) with a plain override, no conflict. (Power/session actions are not
# entries here — see the $mod+Backspace dmenu power menu above.)
xdg.desktopEntries = let
hide = exec: {
inherit exec;
name = "hidden";
noDisplay = lib.mkDefault true;
};
in {
# foot's three entries: launched via $mod+Return, never from a menu.
foot = hide "foot";
foot-server = hide "foot --server";
footclient = hide "footclient";
# mpv/zathura are file handlers, not things to launch bare from a menu.
mpv = hide "mpv %U";
umpv = hide "umpv %U";
"org.pwmt.zathura" = hide "zathura %U";
"org.pwmt.zathura-djvu" = hide "zathura %U";
"org.pwmt.zathura-cb" = hide "zathura %U";
# nvim is $EDITOR / opened from a terminal; no place in a GUI launcher.
nvim = hide "nvim %F";
};
programs.fzf.enable = true; # provides the ^f "cd into fzf dir" binding
@@ -213,10 +404,11 @@ in
grep = "grep --color=auto";
diff = "diff --color=auto";
ccat = "highlight --out-format=ansi";
ka = "killall";
g = "git";
e = editor;
v = editor;
# Throwaway REPL/scripts as if `python` were global; resolves to UV_PYTHON.
python = "uv run python";
};
# Interactive bits with no first-class HM option: prompt, autocd,
@@ -286,6 +478,31 @@ in
# Keep the GTK2 rc out of $HOME (default ~/.gtkrc-2.0). Setting this also
# makes home-manager export GTK2_RC_FILES so GTK2 apps still find it.
gtk2.configLocation = "${config.xdg.configHome}/gtk-2.0/gtkrc";
# The theme *name* alone isn't enough: the qgtk3 Qt bridge and apps' dark
# detection (e.g. keepassxc's "auto" theme) read this flag, not the name.
# Set it so both GTK apps and the Qt-via-gtk3 apps resolve to dark.
gtk3.extraConfig.gtk-application-prefer-dark-theme = true;
gtk4.extraConfig.gtk-application-prefer-dark-theme = true;
};
# The canonical dark/light signal on Wayland: apps query the xdg-desktop-portal
# Settings interface for org.freedesktop.appearance/color-scheme over D-Bus, and
# the gtk portal serves that from this dconf key. keepassxc's "auto" theme reads
# it (NixUtils::isDarkMode) and ignores the GTK theme name / Qt style entirely,
# so without this the portal reports 0 ("no preference") and keepass stays light.
# prefer-dark makes the portal report dark to every color-scheme-aware app.
dconf.settings."org/gnome/desktop/interface".color-scheme = "prefer-dark";
# Match the Qt apps (keepassxc, pinentry-qt, ssh askpass) to the GTK
# Adwaita-dark above, else they render the default light Fusion style. Use the
# gtk3 platform theme, not an adwaita *style* override: nixpkgs wraps Qt apps
# with a baked QT_PLUGIN_PATH pointing only at their build deps, so an external
# style plugin (adwaita-qt) is unreachable and QT_STYLE_OVERRIDE is silently
# ignored. The gtk3 plugin (libqgtk3) ships inside qtbase itself, so it's
# always on that path, and it reads the GTK theme (Adwaita-dark) directly.
qt = {
enable = true;
platformTheme.name = "gtk3";
};
# The home-manager Hyprland module below enables xdg.portal and pins the
@@ -309,7 +526,19 @@ 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
# env. gpg-agent (a user service) inherits it, so commit-signing pinentry
# is dark — but Hyprland doesn't source that file, so its children (foot →
# shell → ssh askpass → pinentry) launch without it and came up light.
# Verified the single var flips pinentry-qt dark; export it here so every
# session child gets it too.
env = [ "QT_QPA_PLATFORMTHEME,gtk3" ];
general = {
layout = "master";
@@ -328,9 +557,47 @@ in
enabled = false;
};
# Suppress Hyprland's built-in default wallpaper. The wallpaper here is
# driven by a userspace daemon (awww, see modules/media.nix) that only
# starts — and pushes the chosen image — once the compositor is up. Until
# then Hyprland paints its bundled splash, so on login it briefly shows
# before the real wallpaper replaces it. Disabling it leaves the plain
# background colour in that gap instead of a flash of unrelated art.
misc = {
force_default_wallpaper = 0;
disable_hyprland_logo = true;
# Window swallowing: a GUI spawned from a foot shell (e.g. zathura/imv
# from mc) hides the terminal and takes its place, restoring it on close.
# Matches on the *parent* terminal's class. Needs standalone foot: in
# server/client mode all windows share one process, so PID ancestry can't
# tell them apart and swallowing breaks — that's why $mod+Return runs
# `foot`, not `footclient`. Swallow is decided by getSwallower() walking
# the new window's parent-PID chain (up to 25 hops) for a window whose
# class matches swallow_regex; swallow_exception_regex, if set, matches the
# *terminal's title*, not the new window's class (a common misreading).
#
# Do NOT confuse swallowing with an app opening fullscreen/maximized — they
# look identical (the launching terminal vanishes) but are different things:
# - real swallow removes the terminal from the layout and restores it only
# when the swallower closes; you cannot un-swallow a live window.
# - a fullscreen/maximized app merely covers the workspace; the launcher
# is still a tile underneath and $mod+f / $mod SHIFT f toggles it back.
# `hyprctl clients` is decisive: a real swallow shows the terminal
# hidden=true and the new window swallowing!=0x0. LibreOffice trips this —
# it restores its own maximized state on open (from mc or a shell), so a
# document appears to "swallow" mc when it is only fullscreen. And being
# single-instance (one soffice.bin owns every window via IPC handoff), when
# it genuinely does swallow, that is fixed by whatever launched its daemon,
# not per-open — so it can't do the tile-from-mc / swallow-from-shell split
# that single-process handlers like zathura/imv get for free.
enable_swallow = true;
swallow_regex = "^(foot)$";
};
input = {
kb_layout = "de";
follow_mouse = 1;
numlock_by_default = true;
};
"$mod" = "SUPER";
@@ -344,19 +611,30 @@ in
];
bind = [
"$mod, Return, exec, footclient"
"$mod, Return, exec, foot"
"$mod, q, killactive"
"$mod SHIFT, q, exit"
"$mod, f, fullscreen, 1"
"$mod SHIFT, f, fullscreen, 0"
# fullscreen arg: 0 = true fullscreen, 1 = maximize (keeps bar/gaps).
"$mod, f, fullscreen, 0"
"$mod SHIFT, f, fullscreen, 1"
"$mod, w, exec, ${browser}"
"$mod, d, exec, fuzzel"
"$mod, Backspace, exec, ${powerMenu}"
"$mod, j, layoutmsg, cyclenext"
"$mod SHIFT, j, layoutmsg, swapnext"
"$mod, k, layoutmsg, cycleprev"
"$mod SHIFT, k, layoutmsg, swapprev"
"$mod, h, layoutmsg, splitratio -0.05"
"$mod, l, layoutmsg, splitratio +0.05"
# swap focused window with the master (dwm-style "zoom").
"$mod, space, layoutmsg, swapwithmaster"
# master area width. Master layout uses `mfact`, not dwindle's `splitratio`.
"$mod, h, 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.
# References physical key codes for robustness with the SHIFT key, e.g. `code:10` instead of `1`.
@@ -374,20 +652,23 @@ in
", XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
", XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 3%+"
", XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 3%-"
# Screenshots. $mod = pick a region (slurp), SHIFT = to clipboard;
# bare Print grabs the whole screen to a file (the common case).
", Print, exec, ${screenshot} full file"
"$mod, Print, exec, ${screenshot} region file"
"SHIFT, Print, exec, ${screenshot} full clip"
"$mod SHIFT, Print, exec, ${screenshot} region clip"
];
exec-once = [
"waybar"
# foot in server/daemon mode: $mod+Return runs `footclient`, which
# attaches to this one process → faster new-window startup, shared RAM.
# Uses foot's default socket, which footclient also defaults to.
# Tradeoff: if the server dies, all foot windows die with it.
"foot --server"
# wallpaper (see modules/media.nix): start the awww daemon, then restore
# the persisted choice. awww keeps no state across restarts, so re-push
# the ~/.local/share/bg symlink if it exists.
"awww-daemon"
"sh -c '[ -e \"$HOME/.local/share/bg\" ] && awww img \"$HOME/.local/share/bg\"'"
# re-push the wallpaper onto monitors connected after login (media.nix)
"wallpaper-hotplug"
];
};
};
@@ -399,6 +680,23 @@ in
# session has none otherwise; apps using libsecret/qtkeychain need it.
services.gnome-keyring.enable = true;
# ssh-agent user service; sets SSH_AUTH_SOCK so `ssh-add` works in any shell.
# gnome-keyring above no longer ships an ssh component (dropped upstream), and
# gpg's ssh support is off, so this is the only agent. Keys are added on
# demand — nothing here presumes a particular ssh config.
services.ssh-agent.enable = true;
# The agent, not the ssh client, collects the FIDO PIN when signing with a
# verify-required sk-ed25519 key it holds — so the *agent's* environment needs
# the askpass helper. The systemd user service starts with a minimal env that
# lacks it, so such signatures fail with the misleading "incorrect passphrase
# supplied to decrypt private key". REQUIRE=force drives the prompt with no
# DISPLAY (a daemon has none); sshAskpass exits 0 for the touch step, so only
# the PIN pops a dialog.
systemd.user.services.ssh-agent.Service.Environment = [
"SSH_ASKPASS=${sshAskpass}"
"SSH_ASKPASS_REQUIRE=force"
];
services.mako = {
enable = true;
settings = {
@@ -416,15 +714,37 @@ in
position = "top";
height = 28;
modules-left = [ "hyprland/workspaces" ];
modules-center = [ "clock" ];
modules-right = [ "pulseaudio" "network" "battery" ];
modules-center = [ "hyprland/window" ];
modules-right = [ "pulseaudio" "network" "battery" "clock" ];
"hyprland/workspaces" = {
format = "{id}";
};
"hyprland/window" = {
format = "{title}";
max-length = 100;
separate-outputs = true;
};
clock = {
format = "{:%H:%M %a %d %b}";
tooltip-format = "<tt>{calendar}</tt>";
calendar = {
mode = "month";
mode-mon-col = 3;
weeks-pos = "left";
format = {
months = "<span color='#e5c07b'><b>{}</b></span>";
weekdays = "<span color='#c678dd'><b>{}</b></span>";
days = "<span color='#bbbbbb'>{}</span>";
today = "<span color='#61afef'><b><u>{}</u></b></span>";
weeks = "<span color='#5c6370'>W{:%V}</span>";
};
};
actions = {
on-click-right = "mode";
};
};
pulseaudio = {
@@ -464,9 +784,23 @@ in
background: #222222;
color: #bbbbbb;
}
#pulseaudio, #network, #battery {
#pulseaudio, #network, #battery, #clock {
padding: 0 12px;
}
#window {
color: #ffffff;
}
tooltip {
background: #222222;
border: 1px solid #333333;
}
tooltip label {
font-size: 15px;
padding: 4px;
}
window#waybar.empty #window {
background: transparent;
}
#battery.warning {
color: #e5c07b;
}
@@ -476,6 +810,18 @@ in
#battery.charging {
color: #98c379;
}
#workspaces button {
padding: 0 8px;
color: #bbbbbb;
}
#workspaces button.active {
color: #ffffff;
background: #333333;
box-shadow: inset 0 -2px #61afef;
}
#workspaces button.urgent {
color: #e06c75;
}
'';
};
}
+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'";
};
}
+35
View File
@@ -0,0 +1,35 @@
# LibreWolf browser. The package is an option so a consumer can swap the wrapped
# package without editing this module: the base is plain pkgs.librewolf, but the
# wrapper (wrapFirefox) accepts `extraPolicies` and `extraPrefs`, and overriding
# those is the clean way to manage LibreWolf declaratively.
#
# The useful case is sharing state across LibreWolf profiles that otherwise stay
# separate (each `-P <name>` has its own history/logins/cookies). Enterprise
# policy `ExtensionSettings` force- or normal-installs the same add-ons into
# every profile, and `extraPrefs` (autoconfig `defaultPref`/`lockPref`) seeds a
# shared about:config baseline — both applied per-launch, neither touching
# per-profile user data. Override, e.g.:
#
# local.librewolf.package = pkgs.librewolf.override {
# extraPolicies.ExtensionSettings."uBlock0@raymondhill.net" = {
# installation_mode = "normal_installed";
# install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi";
# };
# extraPrefs = ''defaultPref("browser.startup.page", 3);'';
# };
{ config, lib, pkgs, ... }:
{
options.local.librewolf.package = lib.mkOption {
type = lib.types.package;
default = pkgs.librewolf;
defaultText = lib.literalExpression "pkgs.librewolf";
description = ''
LibreWolf package. Override with
`pkgs.librewolf.override { extraPolicies = ; extraPrefs = ; }`
to share extensions/settings across profiles.
'';
};
config.home.packages = [ config.local.librewolf.package ];
}
+108
View File
@@ -0,0 +1,108 @@
# Midnight Commander: TUI file manager. Its config (ini, panels, F2 user menu,
# mc.ext.ini file associations) is opinionated but generic, so it lives in the
# public baseline alongside the other desktop tooling. The only personal value
# is the right-panel start dir, exposed as an option defaulting to the
# consumer's own home. mc.ext.ini keeps archive browsing but routes every other
# type to xdg-open, so the app choices live in xdg.mimeApps (baseline: mime.nix).
#
# ouch backs the F2 pack/unpack entries (one tool, archive format inferred from
# the extension), so it ships with mc to keep the menu self-contained.
#
# mc honours XDG natively: config in $XDG_CONFIG_HOME/mc (the files below),
# mutable runtime state (history, cursor positions) in $XDG_DATA_HOME/mc — no
# env vars needed. `ini` sets auto_save_setup=false so mc never rewrites these
# read-only store symlinks on exit.
#
# The F2 menu is a `types.lines` option, not a baked source file, so consumers
# can extend or replace it. The shipped menu is a `mkBefore` definition (stays
# first, ahead of appended entries), NOT a `default` — a `default` is dropped
# the moment a consumer adds a definition, which would lose the base. As a real
# definition it merges: `local.mc.menu = "…";` appends after the base entries;
# `lib.mkForce "…"` replaces it wholesale.
{ config, lib, pkgs, ... }:
{
options.local.mc = {
otherDir = lib.mkOption {
type = lib.types.str;
default = config.home.homeDirectory;
defaultText = lib.literalExpression "config.home.homeDirectory";
description = "Startup directory for mc's other (right) panel.";
};
menu = lib.mkOption {
type = lib.types.lines;
description = ''
Contents of mc's F2 user menu. The shipped baseline is a mkBefore
definition, so a plain assignment appends entries after it; use
lib.mkForce to replace it entirely.
'';
};
extIni = lib.mkOption {
type = lib.types.lines;
description = ''
Contents of mc's mc.ext.ini (file-type associations driving Enter/F3/F4).
The shipped baseline keeps mc's archive browsing and delegates every
other type to xdg-open, so the actual app is the XDG mimeapps default.
Like `menu`, it is a mkBefore definition: append a same-named section to
override one key (mc merges same-named sections, last key wins), or use
lib.mkForce to replace it entirely.
'';
};
};
config = {
home.packages = [ pkgs.mc pkgs.ouch ];
local.mc.menu = lib.mkBefore (builtins.readFile ./assets/mc/menu);
local.mc.extIni = lib.mkBefore (builtins.readFile ./assets/mc/mc.ext.ini);
xdg.configFile = {
"mc/ini".source = ./assets/mc/ini;
"mc/menu".text = config.local.mc.menu;
"mc/mc.ext.ini".text = config.local.mc.extIni;
# Generated (not a static asset) so other_dir tracks the option instead of
# baking in a personal path.
"mc/panels.ini".text = ''
[New Left Panel]
display=listing
reverse=false
case_sensitive=true
exec_first=false
sort_order=name
list_mode=full
brief_cols=2
user_format=half type name | size | perm | mtime
user_status0=half type name | size | perm
user_status1=half type name | size | perm
user_status2=half type name | size | perm
user_status3=half type name | owner | group | ctime
user_mini_status=true
list_format=user
[New Right Panel]
display=listing
reverse=false
case_sensitive=true
exec_first=false
sort_order=name
list_mode=full
brief_cols=2
user_format=half type name | size | perm | mtime
user_status0=half type name | size | perm
user_status1=half type name | size | perm
user_status2=half type name | size | perm
user_status3=half type name | owner | group | ctime
user_mini_status=true
list_format=user
[Dirs]
current_is_left=true
other_dir=${config.local.mc.otherDir}
'';
};
};
}
+17
View File
@@ -17,6 +17,22 @@ let
[ -n "$1" ] && ln -sf "$(readlink -f "$1")" "$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
{
programs.mpv = {
@@ -50,6 +66,7 @@ in
home.packages = with pkgs; [
setbg
wallpaperHotplug # re-push wallpaper to a hotplugged monitor (see comment above)
awww # wayland wallpaper daemon (setbg backend)
wl-clipboard # wl-copy, for the yank binds
imagemagick # `magick`: rotate/flip binds
+75
View File
@@ -0,0 +1,75 @@
# Default applications per file type -- the single source of truth for "open
# with" across the desktop: xdg-open, file pickers, and Enter in mc (whose
# mc.ext.ini delegates every non-archive type to xdg-open; see mc.nix).
#
# These belong in the public baseline because the handlers do too: zathura, imv
# and mpv come from media.nix, libreoffice from home.nix. So "PDF opens in
# zathura", "images in imv", etc. are the natural defaults of this baseline, not
# personal choices -- every entry is mkDefault, so a consumer overrides one type
# by setting the same key (their definition wins), or flips enable off entirely.
#
# Personal handlers -- web browser, mail -- are NOT here; they map to a specific
# browser profile / mail client and live in the private inventory, merging into
# the same defaultApplications attrset.
{ lib, ... }:
let
# Map every mime in the list to one .desktop id, at mkDefault priority.
forAll = desktop: mimes: lib.genAttrs mimes (_: lib.mkDefault desktop);
zathura = "org.pwmt.zathura.desktop";
imv = "imv.desktop";
mpv = "mpv.desktop";
writer = "writer.desktop";
calc = "calc.desktop";
impress = "impress.desktop";
in {
xdg.mimeApps.enable = lib.mkDefault true;
xdg.mimeApps.defaultApplications =
(forAll zathura [
"application/pdf"
"application/epub+zip"
"application/oxps"
"application/postscript"
])
// (forAll imv [
"image/png"
"image/jpeg"
"image/gif"
"image/webp"
"image/bmp"
"image/tiff"
])
// (forAll mpv [
"video/mp4"
"video/x-matroska"
"video/webm"
"video/x-msvideo"
"video/quicktime"
"video/mpeg"
"audio/mpeg"
"audio/flac"
"audio/ogg"
"audio/x-wav"
"audio/mp4"
"audio/x-opus+ogg"
])
// (forAll writer [
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
"application/msword"
"application/vnd.oasis.opendocument.text"
"application/rtf"
])
// (forAll calc [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
"application/vnd.ms-excel"
"application/vnd.oasis.opendocument.spreadsheet"
])
// (forAll impress [
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
"application/vnd.ms-powerpoint"
"application/vnd.oasis.opendocument.presentation"
]);
}
+26 -7
View File
@@ -5,6 +5,8 @@
{ config, lib, pkgs, ... }:
{
imports = [ ./shutdown-debug.nix ];
nix.settings.experimental-features = [ "nix-command" "flakes" ];
# Deduplicate identical store paths via hardlinks.
nix.settings.auto-optimise-store = true;
@@ -39,14 +41,14 @@
];
};
# Time zone. Template default — override per host/region.
time.timeZone = "Europe/Berlin";
# Internationalisation properties. Template defaults — override per region.
i18n.defaultLocale = "en_US.UTF-8";
# 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.
@@ -54,6 +56,23 @@
programs.hyprland.enable = true;
programs.dconf.enable = true;
# nix-ld: install a stub dynamic linker at the standard FHS path
# (/lib64/ld-linux-*.so), which NixOS otherwise lacks. Lets unpatched
# prebuilt binaries run — notably the standalone CPython builds uv downloads
# and pip wheels with compiled extensions (numpy, pydantic-core, ...). The
# libraries list is exposed as NIX_LD_LIBRARY_PATH so those binaries find
# common shared libs; extend it when an import fails with "libFOO.so.N: cannot
# open shared object file".
programs.nix-ld = {
enable = true;
libraries = with pkgs; [
stdenv.cc.cc.lib # libstdc++, libgcc_s, libgomp
zlib # libz
zstd # libzstd (pyarrow, polars)
openssl # libssl, libcrypto
];
};
# Packages installed in system profile.
environment.systemPackages = with pkgs; [
curl
+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'';
}
];
};
};
}
+92
View File
@@ -0,0 +1,92 @@
# Shutdown/boot hang diagnostics.
#
# Shutdown stalls are awkward to debug because journald is torn down mid-way,
# so anything that wedges *after* it dies never reaches /var/log/journal — the
# tail just stops with no explanation. These knobs make the next hang leave a
# trail instead. All are opt-in except the shortened stop-timeout, which is a
# baseline preset (overridable / disable with null).
#
# Reading the evidence after a hang:
# - On screen while it hangs: watch for "A stop job is running for <unit>
# (Xs / 1min 30s)". That line names the culprit; note whether its counter
# advances (named unit = easy) or the screen is frozen with no counter
# (late systemd-shutdown phase = needs pstore, below).
# - After a *clean* next boot: `journalctl -b -1 -e` shows the previous
# boot's shutdown tail (NixOS journald is persistent by default).
# - After a *forced* power-off: journald never flushed, so check the kernel
# ring buffer preserved across reboot in `/sys/fs/pstore/` (needs pstore
# backing — ACPI ERST on most laptops, else ramoops). `verboseLogging`
# below routes systemd's own messages to kmsg so they land there too.
# - With `debugShell` on: switch to tty9 (Ctrl+Alt+F9) while hung and run
# `systemctl list-jobs` to see which job is still pending.
{ config, lib, pkgs, ... }:
let
cfg = config.local.shutdownDebug;
in
{
options.local.shutdownDebug = {
stopTimeout = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "20s";
example = null;
description = ''
DefaultTimeoutStopSec for both the system and per-user managers. The
default shortens systemd's 90s so a hung unit is killed and logged as
`timed out` in seconds rather than spinning at a black screen. Set to
null to leave systemd's default untouched. Any systemd time span, e.g.
"20s", "45s", "1min".
'';
};
verboseLogging = lib.mkEnableOption ''
verbose systemd shutdown logging. Adds the kernel params
`systemd.log_level=debug systemd.log_target=kmsg`: bumps systemd to debug
verbosity and routes its messages into the kernel ring buffer, which
outlives journald and can be recovered from /sys/fs/pstore after a forced
power-off. Noisy on every boot and mildly slows it enable only while
actively hunting a hang'';
debugShell = lib.mkEnableOption ''
an unauthenticated root shell on tty9 (Ctrl+Alt+F9), available through
shutdown, for running `systemctl list-jobs` while the system is wedged.
SECURITY: anyone with physical console access gets root leave off
except while investigating'';
};
config = lib.mkMerge [
(lib.mkIf (cfg.stopTimeout != null) {
systemd.settings.Manager.DefaultTimeoutStopSec = cfg.stopTimeout;
systemd.user.extraConfig = "DefaultTimeoutStopSec=${cfg.stopTimeout}";
})
(lib.mkIf cfg.verboseLogging {
boot.kernelParams = [ "systemd.log_level=debug" "systemd.log_target=kmsg" ];
})
# Uniquely named rather than enabling the upstream (static) debug-shell.service,
# whose enablement via systemd.services would generate a shadowing unit.
(lib.mkIf cfg.debugShell {
systemd.services.shutdown-debug-shell = {
description = "Root shell on tty9 for boot/shutdown debugging";
unitConfig = {
DefaultDependencies = false;
ConditionPathExists = "/dev/tty9";
};
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Environment = "TERM=linux";
ExecStart = "${pkgs.bashInteractive}/bin/bash";
Restart = "always";
RestartSec = 0;
StandardInput = "tty";
TTYPath = "/dev/tty9";
TTYReset = true;
TTYVHangup = true;
KillMode = "process";
};
};
})
];
}
+32
View File
@@ -0,0 +1,32 @@
# TeX Live for pandoc PDF output: pandoc shells out to a LaTeX engine, so PDF
# conversion needs a TeX distribution (other formats — html, docx, md — do not).
# The package set is an option so a consumer can swap the scheme without editing
# this module: scheme-medium covers pandoc's default LaTeX template; override
# for a larger set (missing .sty files) or a smaller one.
#
# TeX Live's font cache and config trees default to ~/.texlive<year> and ~/texmf
# (written on the first PDF build). The TEXMF* vars route them to XDG dirs.
{ config, lib, pkgs, ... }:
{
options.local.texlive.package = lib.mkOption {
type = lib.types.package;
default = pkgs.texlive.withPackages (ps: [ ps.scheme-medium ]);
defaultText = lib.literalExpression
"pkgs.texlive.withPackages (ps: [ ps.scheme-medium ])";
description = "TeX Live package set. Override for a larger/smaller scheme.";
};
config = {
home.packages = [ config.local.texlive.package ];
# texmf-var is regenerable font/format cache → cacheHome; the other two hold
# user config/data trees.
home.sessionVariables = {
TEXMFHOME = "${config.xdg.dataHome}/texmf";
TEXMFVAR = "${config.xdg.cacheHome}/texlive/texmf-var";
TEXMFCONFIG = "${config.xdg.configHome}/texlive/texmf-config";
};
};
}