style: align comments with the project conventions
Apply a codebase-wide comment audit against the comment conventions: split banned semicolons and multi-sentence lines into one sentence per line, cut cross-file and history narration, trim file-top headers to a single purpose line, and drop verbosity that did not earn its place. Prose docs (CLAUDE.md, install.md) get the same one-sentence-per-line and no-semicolon treatment.
This commit is contained in:
45
CLAUDE.md
45
CLAUDE.md
@@ -1,13 +1,15 @@
|
||||
# dotfiles-nixos
|
||||
|
||||
One flake that builds every machine the user owns.
|
||||
The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overlays) lives in `.claude/CONTEXT.md`; the current deliverable's spec is `.claude/spec/laptop-mvi.md`.
|
||||
The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overlays) lives in `.claude/CONTEXT.md`.
|
||||
The current deliverable's spec is `.claude/spec/laptop-mvi.md`.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Comments posted to Gitea (pull requests, issues, reviews) go out under the operator's account, so sign every one to make clear the author is the agent, not the operator.
|
||||
End the comment with a `— Claude` sign-off.
|
||||
(A dedicated bot account may replace this later; until then, the sign-off is the only marker.)
|
||||
(A dedicated bot account may replace this later.
|
||||
Until then, the sign-off is the only marker.)
|
||||
- Commit messages follow Conventional Commits, specified in `docs/conventional-commits.md`.
|
||||
Scope is the module or host the change belongs to (`fish`, `nvim`, `neogaia`), omitted for repo-wide changes.
|
||||
Keep messages free of Gitea-specific references: this repository is mirrored to GitHub, where issue and pull-request numbers resolve to unrelated things.
|
||||
@@ -21,7 +23,8 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
Flakes and the chaotic substituter come from this flake's own `nix.settings`, so no `NIX_CONFIG` export or per-command `--extra-experimental-features` is needed, and building a toplevel with `boot.kernelPackages = linuxPackages_cachyos` fetches the kernel from `nyx-cache` rather than compiling it.
|
||||
Both were true only while the machine still ran CachyOS against a distro Nix daemon.
|
||||
- The substituters a `nix build` fetches from are the **daemon's** (`/etc/nix/nix.conf`), *not* the `nix.settings` of the config being built — those only govern the built system.
|
||||
The two coincide here because the dev host runs this flake; they diverge on any machine that does not.
|
||||
The two coincide here because the dev host runs this flake.
|
||||
They diverge on any machine that does not.
|
||||
- Git identity is declared in the flake by `modules/git.nix`, which writes `alexion <contact@alexion.dev>` — the identity all history uses — on any host enabling `modules.git`.
|
||||
Every new host has to enable it, so that a host reads as a full checklist of what it carries.
|
||||
It is deployed on `neogaia` and verified: a commit in a repository outside this checkout is authored `alexion <contact@alexion.dev>` with no override.
|
||||
@@ -32,12 +35,15 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
With both global files present it prints only `~/.gitconfig`, which reads as proof that `~/.config/git/config` is being ignored entirely.
|
||||
It is not: drop `--global` and both files appear, each key resolving to the last file that sets it.
|
||||
A `git config --global <key> <value>` write also lands in `~/.gitconfig`, the file that outranks the flake-managed one.
|
||||
- The primary build/verify seam for any Host is `nix flake check`, which builds `checks.x86_64-linux.<host>` (the system toplevel); cheap targeted checks use `nix eval .#nixosConfigurations.<host>.config...`.
|
||||
- The primary build/verify seam for any Host is `nix flake check`, which builds `checks.x86_64-linux.<host>` (the system toplevel).
|
||||
Cheap targeted checks use `nix eval .#nixosConfigurations.<host>.config...`.
|
||||
- A flake only sees **git-tracked** files, so a new file that has not been `git add`ed is invisible to evaluation even though it exists on disk.
|
||||
The failure names the path and reads as if the file were missing: `error: Path 'secrets/shared.yaml' does not exist in Git repository`.
|
||||
Staging is enough; the file need not be committed.
|
||||
Staging is enough.
|
||||
The file need not be committed.
|
||||
- chaotic-nyx must **not** follow our `nixpkgs`, and its packages are built against chaotic's own pinned nixpkgs (its overlay defaults to `onTopOf = "flake-nixpkgs"`, the cache-friendly path).
|
||||
That is what lets the `nyx-cache.chaotic.cx` binary cache hit instead of compiling the CachyOS kernel from source; the tradeoff is that chaotic packages do not see our `unstable`/`stable` overlays.
|
||||
That is what lets the `nyx-cache.chaotic.cx` binary cache hit instead of compiling the CachyOS kernel from source.
|
||||
The tradeoff is that chaotic packages do not see our `unstable`/`stable` overlays.
|
||||
- The remote is self-hosted Gitea (`git.alexion.dev`), and the forge CLI is `gitea-axi` rather than `tea`.
|
||||
`gitea-axi` resolves the repository from the `origin` remote and discovers credentials from a `tea` login whose host matches the remote, so both are implicit inside a checkout.
|
||||
It is installed on `neogaia` by `modules.agents.tools.gitea-axi`, and verified: `gitea-axi` run from this checkout renders the `alexion/dotfiles` dashboard authenticated, so the claude-code `SessionStart` hook that runs it now resolves to a real binary rather than a missing one.
|
||||
@@ -54,7 +60,8 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
On this machine the two are provably distinct: the daemon presents `SHA256:2ysuBX0+Z6GbdCTujz5JHX6rqnJzIyWhYNrxdhhGwEM`, while pushes to `git.alexion.dev` authenticate with `SHA256:nEhHwtHDnLlsuFxyfp+cETgHUZ8xDMxaPVmYM5vuCkA`.
|
||||
Renaming host keys after user keys, or vice versa, is therefore always wrong.
|
||||
- `~/.claude/skills` is generated by home-manager with `recursive = true`, so the directories are real and writable but every leaf file is a read-only symlink into the store.
|
||||
Editing a skill in place fails; its source is `modules/agents/claude-code/skills/<name>/` here, applied by a rebuild.
|
||||
Editing a skill in place fails.
|
||||
Its source is `modules/agents/claude-code/skills/<name>/` here, applied by a rebuild.
|
||||
Creating a new file under `~/.claude/skills/` succeeds silently and is the trap — it stays outside the repo and reaches no other machine.
|
||||
Copying out of that tree needs `cp -rL` plus `chmod -R u+w`: a plain `cp -r` copies the symlinks, putting store paths into the destination, and dereferenced files keep the store's read-only mode.
|
||||
- `home-manager.users.<user>.home.file` is keyed by whatever path string the **defining module wrote**, absolute or relative, not by one canonical form.
|
||||
@@ -62,9 +69,14 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
The other form fails with "does not provide attribute", so overriding an entry (e.g. setting `.force = true` on it) requires matching the writer's exact key.
|
||||
List the real keys with `nix eval --json .#nixosConfigurations.<host>.config.home-manager.users.<user>.home.file --apply builtins.attrNames` rather than guessing one.
|
||||
A key's `.source` is the input file, whose store path differs from the deployed symlink's target (home-manager copies it to a `hm_`-prefixed path) even though the contents match.
|
||||
- nixpkgs `vimPlugins.nord-nvim` is `shaunsingh/nord.nvim` (no `require("nord").setup()`); the config wants `gbprod/nord.nvim`, which is packaged as `vimPlugins.gbprod-nord`.
|
||||
- nixpkgs `vimPlugins.nvim-treesitter` tracks the rewritten `main` branch: there is no `require("nvim-treesitter.configs").setup{ensure_installed,highlight,indent}`. Under nixvim, use `plugins.treesitter` with `highlight.enable`/`indent.enable` and `grammarPackages = with config.programs.nixvim.plugins.treesitter.package.builtGrammars; [ ... ]` — the module's own `package.builtGrammars`, **not** `pkgs.vimPlugins.nvim-treesitter.*` (whose query files can mismatch). The module targets the main branch and enables features via neovim-native APIs (`vim.treesitter.start()`, `require'nvim-treesitter'.indentexpr()`).
|
||||
- Neovim is configured via **nixvim** (flake input `nixvim`, consumed as `inputs.nixvim.homeModules.nixvim` added to `home-manager.sharedModules`, config under `home-manager.users.<user>.programs.nixvim`). `nixvim.inputs.nixpkgs.follows = "nixpkgs"` is set; nixvim then emits a benign eval warning that its pinned nixpkgs differs from the followed one — builds and runs fine, do not "fix" it by dropping the follows.
|
||||
- nixpkgs `vimPlugins.nord-nvim` is `shaunsingh/nord.nvim` (no `require("nord").setup()`).
|
||||
The config wants `gbprod/nord.nvim`, which is packaged as `vimPlugins.gbprod-nord`.
|
||||
- nixpkgs `vimPlugins.nvim-treesitter` tracks the rewritten `main` branch: there is no `require("nvim-treesitter.configs").setup{ensure_installed,highlight,indent}`.
|
||||
Under nixvim, use `plugins.treesitter` with `highlight.enable`/`indent.enable` and `grammarPackages = with config.programs.nixvim.plugins.treesitter.package.builtGrammars; [ ... ]` — the module's own `package.builtGrammars`, **not** `pkgs.vimPlugins.nvim-treesitter.*` (whose query files can mismatch).
|
||||
The module targets the main branch and enables features via neovim-native APIs (`vim.treesitter.start()`, `require'nvim-treesitter'.indentexpr()`).
|
||||
- Neovim is configured via **nixvim** (flake input `nixvim`, consumed as `inputs.nixvim.homeModules.nixvim` added to `home-manager.sharedModules`, config under `home-manager.users.<user>.programs.nixvim`).
|
||||
`nixvim.inputs.nixpkgs.follows = "nixpkgs"` is set.
|
||||
nixvim then emits a benign eval warning that its pinned nixpkgs differs from the followed one — builds and runs fine, do not "fix" it by dropping the follows.
|
||||
- To reference the nixvim-built package's own attrs (e.g. treesitter `builtGrammars`) inside our NixOS module, give `home-manager.users.<user>` the module-function form (`hm: { programs.nixvim = { ... hm.config.programs.nixvim... }; }`), since the outer `config` is the NixOS config, not the home-manager one.
|
||||
- The agent's Bash sandbox blocks `sudo` and swallows it into a bare exit 1 with **no stderr**, which looks identical to the command itself failing.
|
||||
Re-run with the sandbox disabled to see the real error (`sudo: a password is required`) before diagnosing anything else.
|
||||
@@ -72,7 +84,8 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
- Sudo's credential cache is keyed per user rather than per terminal (`timestamp_type=global`, 60-minute window, declared by the claude-code module), so an authentication made in one terminal counts for commands the agent runs.
|
||||
Warming it with `sudo -v` through the agent's own shell — including the `!` prefix — never works: that shell has no controlling terminal, and sudo reports `a terminal is required to read the password`.
|
||||
It has to be a separate terminal.
|
||||
A `PreToolUse` hook refuses privileged commands while the cache is cold, so a cold cache announces itself instead of stalling; a failure *without* that message is the sandbox, not the cache.
|
||||
A `PreToolUse` hook refuses privileged commands while the cache is cold, so a cold cache announces itself instead of stalling.
|
||||
A failure *without* that message is the sandbox, not the cache.
|
||||
- An `mkOption` of a list or attribute-set type is **not** mandatory the way a scalar one is.
|
||||
Those types carry an `emptyValue`, so an option declared with no `default` and never set evaluates to `[ ]` or `{ }` instead of failing with "option used but not defined".
|
||||
A declaration that is genuinely required cannot be expressed by omitting the default — it needs an assertion, or a default chosen so that the silent case is the safe one.
|
||||
@@ -80,7 +93,9 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
- `home-manager.users.<user>` cannot be assigned twice at the same level in one module: `home-manager.users.${user}.home.packages` alongside `home-manager.users.${user}.programs.x` fails with `error: dynamic attribute 'alexion' already defined`.
|
||||
The interpolated key makes it a dynamic attribute, which nix will not merge the way it merges static paths.
|
||||
Nest both under a single `home-manager.users.${user} = { ... }`.
|
||||
- **Verifying a nixvim change headless:** `programs.nixvim.build.package`'s wrapper has **no `-u`**, so running `$OUT/bin/nvim` loads the caller's `~/.config/nvim` (the dev host's real config), *not* the built config — silently. To exercise the built config, launch with `-u "$(nix build --no-link --print-out-paths .#…programs.nixvim.build.initFile)"` and a scratch `HOME`/`XDG_CONFIG_HOME`. `conceallevel` is window-local: set it with `opt_local`/`vim.wo`, never `vim.bo[buf]` (which errors).
|
||||
- **Verifying a nixvim change headless:** `programs.nixvim.build.package`'s wrapper has **no `-u`**, so running `$OUT/bin/nvim` loads the caller's `~/.config/nvim` (the dev host's real config), *not* the built config — silently.
|
||||
To exercise the built config, launch with `-u "$(nix build --no-link --print-out-paths .#…programs.nixvim.build.initFile)"` and a scratch `HOME`/`XDG_CONFIG_HOME`.
|
||||
`conceallevel` is window-local: set it with `opt_local`/`vim.wo`, never `vim.bo[buf]` (which errors).
|
||||
- Host GPUs: `neogaia` is Intel and `zeus` (the desktop) is **AMD**.
|
||||
`raichu`, a server with no desktop, is the only Nvidia machine.
|
||||
`laptop-mvi.md`'s out-of-scope line calls zeus Nvidia, but that is stale and the document is kept historical and unchanged, so do not infer any host's GPU from it.
|
||||
@@ -110,7 +125,9 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
Engines are referenced by their current id, which the module maps from the old display names, so the default is `default = "ddg"`, not `"DuckDuckGo"` (the latter only warns and migrates).
|
||||
Prove the result by decoding the built file: `mozlz4a -d <search.json.mozlz4>` shows the `_metaData.hidden` flags and `defaultEngineId`.
|
||||
- `home.sessionVariables` do **not** reach the Hyprland session, so anything the compositor reads from its environment (cursor theme, toolkit hints) has to be declared elsewhere.
|
||||
home-manager writes those variables to `hm-session-vars.sh`, which only a login shell sources; UWSM starts the graphical session without it, so the compositor's environment never gains them.
|
||||
home-manager writes those variables to `hm-session-vars.sh`, which only a login shell sources.
|
||||
UWSM starts the graphical session without it, so the compositor's environment never gains them.
|
||||
This is why a Stylix cursor (`XCURSOR_THEME`/`XCURSOR_SIZE` via `home.pointerCursor`) silently fails to apply and Hyprland draws its built-in cursor: the variables exist in `sessionVariables` but not in the running session (`tr '\0' '\n' < /proc/$(pgrep -x Hyprland)/environ` shows them absent).
|
||||
The fix is Hyprland's own `env = KEY,VALUE` directive in `settings`, read at compositor startup regardless of the shell profile; `modules/desktop/hyprland/hyprland.nix` sets the cursor that way, sourced from `config.stylix.cursor`.
|
||||
The fix is Hyprland's own `env = KEY,VALUE` directive in `settings`, read at compositor startup regardless of the shell profile.
|
||||
`modules/desktop/hyprland/hyprland.nix` sets the cursor that way, sourced from `config.stylix.cursor`.
|
||||
Bibata ships XCursor format only (no `hyprcursor/` dir), which Hyprland renders through its XCursor fallback, so both `XCURSOR_*` and `HYPRCURSOR_*` naming the same theme are safe.
|
||||
|
||||
@@ -103,7 +103,8 @@ $ cd dotfiles
|
||||
Do **not** point `disko-install` straight at the Gitea flake URL.
|
||||
Gitea serves HTTPS with a self-signed certificate and expects authentication, and Nix's flake fetcher has no easy way to skip certificate verification or supply those credentials mid-install.
|
||||
A plain `git clone` sidesteps that entirely — over SSH there is no TLS, and over HTTPS git takes the `sslVerify=false` above that the flake fetcher won't — and then `disko-install` consumes the flake from a local path, where no fetch of our repo happens during the build.
|
||||
(Every other flake input is public and still fetched from GitHub over ordinary, valid TLS; only our own repo is the problem the local clone solves.)
|
||||
(Every other flake input is public and still fetched from GitHub over ordinary, valid TLS.
|
||||
Only our own repo is the problem the local clone solves.)
|
||||
|
||||
### 3. Generate the host identity
|
||||
|
||||
@@ -201,17 +202,20 @@ $ sudo nix --extra-experimental-features 'nix-command flakes' run \
|
||||
What each part does:
|
||||
|
||||
- `--flake .#neogaia` installs the `neogaia` `Host` from the local clone.
|
||||
- `--disk main /dev/nvme0n1` maps disko's `main` disk to the NVMe device; it matches the device declared in `hosts/neogaia/disk.nix` and is stated explicitly so there is no doubt about the target.
|
||||
- `--disk main /dev/nvme0n1` maps disko's `main` disk to the NVMe device.
|
||||
It matches the device declared in `hosts/neogaia/disk.nix` and is stated explicitly so there is no doubt about the target.
|
||||
- `--write-efi-boot-entries` writes the systemd-boot entry into this machine's NVRAM, because the disk stays in the machine it was installed from.
|
||||
- The two `--option` lines are the important part: they hand the **chaotic binary cache** to the install-time Nix daemon on the live ISO.
|
||||
|
||||
The chaotic substituter must be passed here explicitly.
|
||||
The `nix.settings` in the flake configure the substituters of the *installed* system, not the live ISO's daemon that runs this build; the ISO's daemon has no `substituters` beyond `cache.nixos.org`.
|
||||
The `nix.settings` in the flake configure the substituters of the *installed* system, not the live ISO's daemon that runs this build.
|
||||
The ISO's daemon has no `substituters` beyond `cache.nixos.org`.
|
||||
Without these two `--option` flags, the build cannot fetch the prebuilt CachyOS kernel and **compiles `linuxPackages_cachyos` (and its toolchain) from source on the USB stick** — a very long detour that the cache avoids.
|
||||
Because the install runs as root, and root is a trusted Nix user, the daemon honours these client-supplied substituter settings.
|
||||
|
||||
Partway through, disko formats the LUKS container and **prompts for a disk-encryption passphrase**.
|
||||
This is the passphrase you will type at every boot to unlock the disk; choose it deliberately.
|
||||
This is the passphrase you will type at every boot to unlock the disk.
|
||||
Choose it deliberately.
|
||||
|
||||
When it finishes it prints `disko-install succeeded`.
|
||||
`disko-install` unmounts the target filesystem on exit, so nothing is mounted at this point — step 6 remounts it.
|
||||
|
||||
16
flake.nix
16
flake.nix
@@ -22,13 +22,15 @@
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Declarative disk partitioning; each host declares its own layout.
|
||||
# Declarative disk partitioning.
|
||||
# Each host declares its own layout.
|
||||
disko = {
|
||||
url = "github:nix-community/disko";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Upstream per-machine hardware profiles; each host imports its own.
|
||||
# Upstream per-machine hardware profiles.
|
||||
# Each host imports its own.
|
||||
nixos-hardware = {
|
||||
url = "github:NixOS/nixos-hardware";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
@@ -47,22 +49,20 @@
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Agent-ergonomic CLI for Gitea, with its home-manager module wiring in the
|
||||
# Claude Code context where that harness is present.
|
||||
# Agent-ergonomic CLI for Gitea, with a home-manager module for the agent context.
|
||||
gitea-axi = {
|
||||
url = "git+https://git.alexion.dev/alexion/gitea-axi";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Personal agent skills, packaged as per-skill derivations with a
|
||||
# home-manager module that places them under Claude Code's skills directory.
|
||||
# Personal agent skills, packaged as per-skill derivations with a home-manager module.
|
||||
skills = {
|
||||
url = "git+https://git.alexion.dev/alexion/skills";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# CachyOS kernel and binary cache. Pins its own nixpkgs so its cache stays
|
||||
# usable and the kernel is fetched from it.
|
||||
# CachyOS kernel and binary cache.
|
||||
# Pins its own nixpkgs so its cache stays usable and the kernel is fetched from it.
|
||||
chaotic.url = "github:chaotic-cx/nyx/nyxpkgs-unstable";
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
...
|
||||
}:
|
||||
# neogaia — Dell XPS 13 9380 laptop.
|
||||
# Disk layout is in ./disk.nix; `fileSystems` are derived from it, none declared here.
|
||||
# Disk layout is in ./disk.nix.
|
||||
# `fileSystems` are derived from it, none declared here.
|
||||
{
|
||||
imports = [
|
||||
inputs.nixos-hardware.nixosModules.dell-xps-13-9380
|
||||
@@ -22,10 +23,10 @@
|
||||
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
||||
|
||||
# Redistributable firmware for the QCA6174 wifi (ath10k blobs).
|
||||
# Intel microcode updates follow from this; none declared here.
|
||||
# Intel microcode updates follow from this, so none is declared here.
|
||||
hardware.enableRedistributableFirmware = true;
|
||||
|
||||
# RAM-backed swap; no on-disk swap partition.
|
||||
# RAM-backed swap, no on-disk swap partition.
|
||||
zramSwap.enable = true;
|
||||
|
||||
# So wifi can be joined from the console.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{ ... }:
|
||||
# neogaia's disk layout for disko: one NVMe disk, GPT, with an EFI system
|
||||
# partition and a LUKS container holding btrfs subvolumes. No swap partition;
|
||||
# swap is zram. disko derives `fileSystems` and `boot.initrd.luks.devices` from this.
|
||||
# partition and a LUKS container holding btrfs subvolumes.
|
||||
# No swap partition, since swap is zram.
|
||||
# disko derives `fileSystems` and `boot.initrd.luks.devices` from this.
|
||||
{
|
||||
disko.devices.disk.main = {
|
||||
type = "disk";
|
||||
@@ -10,8 +11,8 @@
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
ESP = {
|
||||
# Each generation stores a kernel and initrd here and the CachyOS
|
||||
# kernel is large; an exhausted partition fails bootloader installs.
|
||||
# Each generation stores a kernel and initrd here and the CachyOS kernel is large.
|
||||
# An exhausted partition fails bootloader installs.
|
||||
size = "2G";
|
||||
type = "EF00";
|
||||
content = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{ lib, modulesPath, ... }:
|
||||
# Hardware detected by nixos-generate-config on this machine.
|
||||
# disko derives `fileSystems` and the LUKS device; none declared here.
|
||||
# disko derives `fileSystems` and the LUKS device, none declared here.
|
||||
{
|
||||
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
...
|
||||
}:
|
||||
# Claude Code for the primary user, configured through home-manager, which ships
|
||||
# the package and manages ~/.claude. Login credentials are left unmanaged so they
|
||||
# survive rebuilds.
|
||||
# the package and manages ~/.claude.
|
||||
# Login credentials are left unmanaged so they survive rebuilds.
|
||||
let
|
||||
cfg = config.modules.agents.claude-code;
|
||||
user = config.user.name;
|
||||
@@ -22,10 +22,8 @@ in
|
||||
cached credential while it lasts. Suitable for a single-user machine'';
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# Keying sudo's credential cache per user rather than per terminal lets one
|
||||
# authentication cover commands issued by processes holding no terminal of
|
||||
# their own. Any process running as this user can spend that credential
|
||||
# until it lapses, so this suits a single-user machine.
|
||||
# Key the credential cache per user rather than per terminal, so one
|
||||
# authentication covers the agent's terminal-less commands.
|
||||
security.sudo.extraConfig = ''
|
||||
Defaults timestamp_type=global
|
||||
Defaults timestamp_timeout=60
|
||||
@@ -38,7 +36,7 @@ in
|
||||
programs.claude-code = {
|
||||
enable = true;
|
||||
|
||||
# Global agent instructions, rendered to ~/.claude/CLAUDE.md.
|
||||
# The global agent-instructions file.
|
||||
context = ./CLAUDE.md;
|
||||
|
||||
# One directory per skill, symlinked under ~/.claude/skills.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/bin/sh
|
||||
# agent-sudo-guard.sh — refuse a privileged command while sudo's credential
|
||||
# cache is cold, naming the command that warms it.
|
||||
# Refuse a privileged command while sudo's credential cache is cold, naming the
|
||||
# command that warms it.
|
||||
#
|
||||
# Commands arrive here from subprocesses holding no terminal, so an uncached
|
||||
# sudo fails with a bare non-zero exit and no output, reading as an unexplained
|
||||
# stall. The probe below reads a cache keyed per user rather than per terminal,
|
||||
# sudo fails with a bare non-zero exit and no output, reading as an unexplained stall.
|
||||
# The probe below reads a cache keyed per user rather than per terminal,
|
||||
# so an authentication made in the operator's own terminal counts.
|
||||
|
||||
input=$(cat)
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
{ config, inputs, ... }:
|
||||
# Global agent skills from the skills flake, placed under the agent harness's
|
||||
# skills directory so they are active in every project. The flake's home-manager
|
||||
# module self-gates on the harness being enabled and installs nothing for an
|
||||
# empty selection, so a host without one carries no skills either way.
|
||||
#
|
||||
# Unlike every other module, this one declares no `enable` flag and wires
|
||||
# unconditionally, by design.
|
||||
# The flake's self-gating above already makes it inert where the harness is
|
||||
# absent, so a gate would guard nothing.
|
||||
# Global agent skills, placed under the skills directory so they are active in
|
||||
# every project.
|
||||
let
|
||||
user = config.user.name;
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
...
|
||||
}:
|
||||
# gitea-axi for the primary user, installed through its own home-manager module.
|
||||
# That module also declares the Claude Code context when that harness is
|
||||
# enabled on the host; enabling this alone installs the CLI and nothing else.
|
||||
let
|
||||
cfg = config.modules.agents.tools.gitea-axi;
|
||||
user = config.user.name;
|
||||
|
||||
@@ -35,8 +35,7 @@ in
|
||||
Snippets = false;
|
||||
};
|
||||
|
||||
# An ad and content blocker, the operator's password manager, and a
|
||||
# video sponsor-skipper. All three are self-contained web extensions.
|
||||
# An ad blocker, a password manager, and a video sponsor-skipper.
|
||||
ExtensionSettings = {
|
||||
"uBlock0@raymondhill.net" = forceInstalled "ublock-origin";
|
||||
"78272b6fa58f4a1abaac99321d503a20@proton.me" = forceInstalled "proton-pass";
|
||||
@@ -65,16 +64,15 @@ in
|
||||
};
|
||||
|
||||
settings = {
|
||||
# Scale the UI and page by a fixed factor. Left at auto (-1),
|
||||
# Firefox reads the panel's 1.5x and inflates its whole chrome on
|
||||
# this display while point-sized apps stay put; a shade under that
|
||||
# brings it into line without dropping to true 1:1, which reads too
|
||||
# small at this DPI.
|
||||
# Scale the UI and page by a fixed factor.
|
||||
# Left at auto (-1), Firefox reads the panel's 1.5x and inflates its
|
||||
# whole chrome while point-sized apps stay put.
|
||||
# A shade under that brings it into line without dropping to true
|
||||
# 1:1, which reads too small at this DPI.
|
||||
"layout.css.devPixelsPerPx" = "1.25";
|
||||
|
||||
# Auto-enable the sideloaded Firefox Color add-on that carries the
|
||||
# Nord chrome theme, which Firefox otherwise leaves disabled.
|
||||
# The functional extensions come through policy and are unaffected.
|
||||
# Auto-enable the sideloaded Firefox Color add-on carrying the Nord
|
||||
# chrome theme, which Firefox otherwise leaves disabled.
|
||||
"extensions.autoDisableScopes" = 0;
|
||||
|
||||
# Sponsored surfaces the policies above do not reach.
|
||||
@@ -103,9 +101,8 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
# Nord chrome from the shared Stylix scheme, against the one profile.
|
||||
# colorTheme recolours the toolbar and tabs, which the target does not do
|
||||
# on its own, through the Stylix-managed Firefox Color add-on.
|
||||
# Nord chrome for the one profile, applied through the Stylix-managed
|
||||
# Firefox Color add-on that colorTheme enables.
|
||||
stylix.targets.firefox = {
|
||||
enable = true;
|
||||
profileNames = [ "default" ];
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# Idle management: hypridle locks on idle, powers the displays off, and locks
|
||||
# before every suspend, so an unattended session always lands at hyprlock.
|
||||
# hypridle: idle-triggered locking and display power management.
|
||||
let
|
||||
cfg = config.modules.desktop.hyprland.hypridle;
|
||||
user = config.user.name;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# The Hyprland compositor, sourced from nixpkgs.
|
||||
# The Hyprland compositor.
|
||||
let
|
||||
cfg = config.modules.desktop.hyprland;
|
||||
user = config.user.name;
|
||||
@@ -63,12 +63,13 @@ in
|
||||
"$mod" = "SUPER";
|
||||
"$terminal" = "alacritty";
|
||||
|
||||
# Hand the cursor theme to the compositor directly. UWSM launches the
|
||||
# session without the shell profile that carries the pointer-cursor
|
||||
# variables, so without this Hyprland never sees a theme and falls back
|
||||
# to its built-in cursor. Bibata ships XCursor only; the hyprcursor
|
||||
# variables name the same theme, which Hyprland resolves through its
|
||||
# XCursor fallback.
|
||||
# Hand the cursor theme to the compositor directly.
|
||||
# UWSM launches the session without the shell profile that carries the
|
||||
# pointer-cursor variables, so without this Hyprland never sees a theme
|
||||
# and falls back to its built-in cursor.
|
||||
# Bibata ships XCursor only.
|
||||
# The hyprcursor variables name the same theme, which Hyprland resolves
|
||||
# through its XCursor fallback.
|
||||
env = lib.optionals (cursor != null) [
|
||||
"XCURSOR_THEME,${cursor.name}"
|
||||
"XCURSOR_SIZE,${toString cursor.size}"
|
||||
@@ -81,7 +82,7 @@ in
|
||||
# Caps is a second Escape.
|
||||
# Shift+Caps still toggles a real CapsLock.
|
||||
kb_options = "caps:escape_shifted_capslock";
|
||||
# Snappy: a short delay before repeat begins, then a fast repeat rate.
|
||||
# Snappy key repeat.
|
||||
repeat_delay = 250;
|
||||
repeat_rate = 45;
|
||||
accel_profile = "flat";
|
||||
@@ -156,8 +157,9 @@ in
|
||||
++ workspaceBinds;
|
||||
|
||||
# Volume and brightness keys repeat while held, each raising a popup
|
||||
# through the OSD client. Volume is capped at 100 percent; the client
|
||||
# floors brightness so a full hold cannot black the screen out.
|
||||
# through the OSD client.
|
||||
# Volume is capped at 100 percent.
|
||||
# The client floors brightness so a full hold cannot black the screen out.
|
||||
binde = [
|
||||
", XF86AudioRaiseVolume, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume raise --max-volume 100"
|
||||
", XF86AudioLowerVolume, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume lower"
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# The lock screen: hyprlock, a session-lock client whose surface the compositor owns, so it survives a crash of the locker rather than exposing the session.
|
||||
# The lock screen: a hyprlock session-lock surface the compositor owns.
|
||||
let
|
||||
cfg = config.modules.desktop.hyprland.hyprlock;
|
||||
user = config.user.name;
|
||||
|
||||
# The hyprlock this module installs, so the keybind and the idle daemon lock
|
||||
# with one package and never split versions.
|
||||
# The hyprlock this module installs, used by the lock keybind below.
|
||||
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||
in
|
||||
{
|
||||
@@ -30,7 +29,8 @@ in
|
||||
disable_loading_bar = true;
|
||||
};
|
||||
|
||||
# A centered password field; its colors are the Stylix target's.
|
||||
# A centered password field.
|
||||
# Its colors come from the Stylix hyprlock target.
|
||||
input-field = {
|
||||
size = "260, 52";
|
||||
rounding = 8;
|
||||
|
||||
@@ -16,14 +16,13 @@ in
|
||||
environment.systemPackages = [ pkgs.swayosd ];
|
||||
|
||||
# The udev rule chgrps each backlight's brightness node to `video` and adds
|
||||
# group write, so the server dims the panel without root. Membership below
|
||||
# grants the running session that access.
|
||||
# group write, so the server dims the panel without root.
|
||||
# Membership below grants the running session that access.
|
||||
services.udev.packages = [ pkgs.swayosd ];
|
||||
users.users.${user}.extraGroups = [ "video" ];
|
||||
|
||||
# The server draws the popups, so it runs for the whole graphical session.
|
||||
# Bound to the target uwsm activates, like the bar, rather than an
|
||||
# exec-once in the compositor config.
|
||||
# It binds to the target uwsm activates.
|
||||
home-manager.users.${user}.systemd.user.services.swayosd = {
|
||||
Unit = {
|
||||
Description = "SwayOSD on-screen display server";
|
||||
|
||||
@@ -10,8 +10,9 @@ in
|
||||
config = lib.mkIf cfg.enable {
|
||||
# The backend packages arrive with the compositor, so only the routing is set
|
||||
# here.
|
||||
# GTK is the default backend, and the three compositor-native requests go to
|
||||
# Hyprland.
|
||||
# Screen sharing, screenshots, and global shortcuts need the compositor's
|
||||
# own backend.
|
||||
# Everything else uses GTK.
|
||||
xdg.portal.config.common = {
|
||||
default = [ "gtk" ];
|
||||
"org.freedesktop.impl.portal.ScreenCast" = [ "hyprland" ];
|
||||
|
||||
@@ -15,9 +15,8 @@ let
|
||||
wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
|
||||
xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir";
|
||||
|
||||
# satty is the annotation step, and its copy action is set to save as well,
|
||||
# so a single keystroke through it lands the shot in both the clipboard and a
|
||||
# file.
|
||||
# satty's copy action also saves, so one keystroke lands the shot in both the
|
||||
# clipboard and a file.
|
||||
capture =
|
||||
target:
|
||||
pkgs.writeShellScript "screenshot-${target}" ''
|
||||
@@ -46,8 +45,8 @@ in
|
||||
];
|
||||
|
||||
# Print with plain/Shift/Ctrl for region/window/full.
|
||||
# Super+L, the spec's chosen key, is already the hjkl focus and movement
|
||||
# bind, so screenshots take the Print key instead.
|
||||
# Super+L is already the hjkl focus and movement bind, so screenshots take
|
||||
# the Print key instead.
|
||||
wayland.windowManager.hyprland.settings.bind = [
|
||||
", Print, exec, ${capture "area"}"
|
||||
"SHIFT, Print, exec, ${capture "active"}"
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# Alacritty as the desktop terminal, a lightweight GPU renderer that opens
|
||||
# fast on the laptop's integrated graphics.
|
||||
# Alacritty as the desktop terminal.
|
||||
let
|
||||
cfg = config.modules.desktop.terminal;
|
||||
user = config.user.name;
|
||||
|
||||
@@ -9,8 +9,7 @@ in
|
||||
options.modules.desktop.userdirs.enable = lib.mkEnableOption "XDG user directories";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# enable writes ~/.config/user-dirs.dirs from the option defaults, which
|
||||
# xdg-user-dir then reads.
|
||||
# Write ~/.config/user-dirs.dirs from the option defaults.
|
||||
home-manager.users.${user}.xdg.userDirs.enable = true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ in
|
||||
};
|
||||
|
||||
functions = {
|
||||
# Pick the logo that fits the terminal so the greeting never wraps:
|
||||
# the full logo needs about 118 columns beside the info block, the
|
||||
# compact one about 89, and the info block alone about 71.
|
||||
# Pick the logo that fits the terminal so the greeting never wraps.
|
||||
# The full logo needs about 118 columns beside the info block, the
|
||||
# compact one about 89, the info block alone about 71.
|
||||
fish_greeting = ''
|
||||
if test "$COLUMNS" -ge 118
|
||||
fastfetch
|
||||
@@ -97,10 +97,9 @@ in
|
||||
end
|
||||
'';
|
||||
|
||||
# vi insert mode omits the emacs ctrl-f and ctrl-r bindings, so
|
||||
# restore both.
|
||||
# vi insert mode omits the emacs ctrl-f and ctrl-r bindings, so restore both.
|
||||
# ctrl-f accepts an autosuggestion and ctrl-r opens the history pager.
|
||||
# Only insert mode is bound: vi normal mode keeps ctrl-r as redo.
|
||||
# vi normal mode keeps its ctrl-r redo, since only insert mode is bound.
|
||||
fish_user_key_bindings = ''
|
||||
bind -M insert ctrl-f forward-char
|
||||
bind -M insert ctrl-r history-pager
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
-- Imperative configuration that has no typed nixvim option: the colorscheme
|
||||
-- call and two autocmds. Everything expressible as Nix lives in ./nvim.nix.
|
||||
-- Imperative configuration that has no typed nixvim option: the colorscheme call and autocmds.
|
||||
|
||||
-- gbprod/nord.nvim, provided as an extra plugin from nixpkgs.
|
||||
-- gbprod/nord.nvim.
|
||||
require("nord").setup({
|
||||
transparent = true,
|
||||
})
|
||||
vim.cmd.colorscheme("nord")
|
||||
|
||||
-- Conceal markdown syntax in markdown buffers (previously an after/ftplugin).
|
||||
-- Conceal markdown syntax in markdown buffers.
|
||||
-- conceallevel is window-local, so it is set with opt_local when the filetype
|
||||
-- is applied to the buffer's window.
|
||||
vim.api.nvim_create_autocmd("FileType", {
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
# Neovim for the primary user, configured declaratively through nixvim. The
|
||||
# imperative remainder (colorscheme, Neogit blame autocmd) lives in ./config.lua.
|
||||
# Neovim for the primary user, configured declaratively through nixvim.
|
||||
let
|
||||
cfg = config.modules.nvim;
|
||||
user = config.user.name;
|
||||
@@ -176,8 +175,8 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
# gbprod/nord.nvim (nixvim's colorschemes.nord is a different plugin);
|
||||
# set up in ./config.lua.
|
||||
# gbprod/nord.nvim, set up in ./config.lua.
|
||||
# nixvim's colorschemes.nord is a different plugin.
|
||||
extraPlugins = [ pkgs.vimPlugins.gbprod-nord ];
|
||||
|
||||
extraConfigLua = builtins.readFile ./config.lua;
|
||||
|
||||
@@ -6,8 +6,8 @@ bind -T copy-mode-vi v send -X begin-selection
|
||||
bind -T copy-mode-vi y send -X copy-selection-and-cancel
|
||||
bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-selection-and-cancel
|
||||
|
||||
# \ splits the pane side-by-side, - splits it stacked; both open the new pane
|
||||
# in the current pane's directory, as does c for a new window.
|
||||
# \ splits the pane side-by-side, - splits it stacked.
|
||||
# Both open the new pane in the current pane's directory, as does c for a new window.
|
||||
unbind %
|
||||
unbind '"'
|
||||
bind \\ split-window -h -c "#{pane_current_path}"
|
||||
@@ -27,13 +27,14 @@ bind r source-file ~/.config/tmux/tmux.conf \; display-message "tmux.conf reload
|
||||
set -ag terminal-overrides ",*:RGB"
|
||||
|
||||
# monitor-bell flags a background window in the status bar when it rings the
|
||||
# terminal bell while unfocused. bell-action=none suppresses the actual BEL
|
||||
# (no beep or flash); the per-window status highlight is independent of it.
|
||||
# terminal bell while unfocused.
|
||||
# bell-action=none suppresses the actual BEL (no beep or flash).
|
||||
# The per-window status highlight is independent of it.
|
||||
setw -g monitor-bell on
|
||||
set -g bell-action none
|
||||
|
||||
# Minimal status bar: session name plus window list only.
|
||||
# Colours are left to Stylix, which themes the status and pane styles.
|
||||
# Colours are left to Stylix.
|
||||
set -g status-position bottom
|
||||
set -g status-left " #S "
|
||||
set -g status-left-length 20
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
# tmux for the primary user, configured through home-manager. Settings without a
|
||||
# home-manager option are read from ./extra.conf.
|
||||
# tmux for the primary user, configured through home-manager.
|
||||
# Settings without a home-manager option are read from ./extra.conf.
|
||||
let
|
||||
cfg = config.modules.tmux;
|
||||
user = config.user.name;
|
||||
|
||||
10
system.nix
10
system.nix
@@ -52,8 +52,8 @@ in
|
||||
"flakes"
|
||||
];
|
||||
|
||||
# chaotic's binary cache, so the CachyOS kernel is fetched rather than
|
||||
# compiled. The `extra-` prefix keeps cache.nixos.org alongside it.
|
||||
# chaotic's binary cache, so the CachyOS kernel is fetched rather than compiled.
|
||||
# The `extra-` prefix keeps cache.nixos.org alongside it.
|
||||
nix.settings.extra-substituters = [ "https://nyx-cache.chaotic.cx/" ];
|
||||
nix.settings.extra-trusted-public-keys = [
|
||||
"nyx-cache.chaotic.cx:dJxTrgMC3V3cFfyIiBQDQorG6k1LsqurH/srpMSq7qk="
|
||||
@@ -80,7 +80,8 @@ in
|
||||
|
||||
environment.systemPackages = [ pkgs.git ];
|
||||
|
||||
# Caps Lock is a second Escape; Shift+Caps Lock still toggles Caps Lock.
|
||||
# Caps Lock is a second Escape.
|
||||
# Shift+Caps Lock still toggles Caps Lock.
|
||||
services.xserver.xkb.layout = "us";
|
||||
services.xserver.xkb.options = "caps:escape_shifted_capslock";
|
||||
|
||||
@@ -108,7 +109,8 @@ in
|
||||
# That is early enough to precede the account that reads it.
|
||||
sops.secrets.${passwordSecret}.neededForUsers = true;
|
||||
|
||||
# Primary user, in the wheel group.
|
||||
# Primary user.
|
||||
# The wheel group is the way in, since root is locked.
|
||||
users.users.${user.name} = {
|
||||
isNormalUser = true;
|
||||
description = user.description;
|
||||
|
||||
Reference in New Issue
Block a user