No description
  • Nix 91.9%
  • CSS 4.1%
  • Lua 3.8%
  • Shell 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-24 12:38:54 -03:00
.opencode/skills feat(desktop): add declarative autostart registry 2026-08-29 17:26:04 -03:00
modules feat(tmux): scope popup to parent session 2026-09-24 12:38:54 -03:00
secrets feat(desklab): add applications and integrations 2026-09-02 12:05:31 -03:00
.gitignore chore: stop tracking nix build result 2026-08-17 02:02:27 -03:00
.sops.yaml feat(secrets): provision desklab identity 2026-09-02 00:34:46 -03:00
AGENTS.md feat(desktop): add declarative autostart registry 2026-08-29 17:26:04 -03:00
flake.lock feat(hermes-agent): enable desktop client 2026-09-19 16:13:26 -03:00
flake.nix feat(hermes-agent): enable desktop client 2026-09-19 16:13:26 -03:00
README.md feat(desktop): add declarative autostart registry 2026-08-29 17:26:04 -03:00

dotfiles

A flake-based NixOS + Home Manager configuration for user s8p, built on flake-parts, import-tree, and a dendritic module registry.

Files in this repo represent features, not configuration layers. There is no nixos/ vs home/ split — a feature such as Hyprland or Stylix owns its NixOS side, its Home Manager side, and any runtime scripts or Lua it needs, all in one place.


Table of contents


Concepts

Three ideas carry the whole configuration.

1. flake.nix is thin. It declares inputs and hands ./modules to import-tree. Nothing else:

outputs = inputs:
  inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules);

2. Every feature registers itself into a shared registry. A module doesn't get imported by a parent — it publishes itself under a name:

{ ... }:
{
  flake.modules.nixos.pipewire = { ... }: {
    security.rtkit.enable = true;
    services.pipewire = { enable = true; /* ... */ };
  };
}

Everything under modules/ is auto-discovered by import-tree and evaluated as a flake-parts module. The registry then holds flake.modules.nixos.<feature> and flake.modules.homeManager.<feature> attribute sets.

3. Hosts compose explicitly. A host picks the features it wants by name. There are no hidden transitive imports:

flake.nixosConfigurations.vmtest = inputs.nixpkgs.lib.nixosSystem {
  system = "x86_64-linux";
  modules = with config.flake.modules.nixos; [
    nix nixpkgs networkmanager locale
    home-manager nh sops
    containers git zsh s8p
    pipewire bluetooth keyboard
    hyprland greetd stylix extra-fonts
    vmtest vmtest-secrets
  ];
};

A feature is free to expose only one module class. Not everything needs both a NixOS and a Home Manager branch.

Underscore-prefixed paths

import-tree treats every discovered .nix file as a top-level flake-parts module. Files that are not flake-parts modules — ordinary NixOS/HM fragments, helper functions, data tables — live under a _-prefixed directory so the scanner skips them:

modules/shell/tmux/
├── default.nix          # public: registers flake.modules.homeManager.tmux
├── _parts/              # private: plain HM fragments, imported by default.nix
├── _scripts/            # private: writeShellApplication helpers
├── _plugins/            # private: plugin set
└── layouts/             # data: tmuxifier session layouts

Same rule for _lua/, _extensions/, _boot.nix, _hardware.nix.


Layout

flake.nix                 # inputs + import-tree entry point
.sops.yaml                # age recipients and per-file key groups
secrets/                  # SOPS-encrypted YAML (shared.yaml, vmtest.yaml, …)
AGENTS.md                 # agent working rules for this repo
.opencode/skills/         # project-specific procedures (dendritic, sops, hyprland, …)

modules/
├── core/                 # flake-parts wiring, nix settings, nixpkgs, locale, nh, HM integration
├── users/                # s8p — user account + the HM feature list they get
├── hosts/                # per-machine: host.nix, secrets.nix, _boot.nix, _hardware.nix
├── desktop/              # hyprland, waybar, stylix, greetd, mako, fuzzel, ghostty,
│                         # wallpaper, screenshots, cliphist, nwg-displays, pcmanfm, fonts
├── shell/                # zsh, tmux, zoxide
├── cli/                  # bat, cli-programs, cli-utilities
├── development/          # git, neovim, direnv, tools, secretspec
├── applications/         # zen-browser, obsidian, vesktop
├── virtualisation/       # containers (docker/podman), container-tools, container-nvidia, qemu-guest
├── networking/           # networkmanager, ssh
├── audio/                # pipewire
├── hardware/             # bluetooth
├── input/                # keyboard
└── security/             # sops

Hosts

Host Role stateVersion Notes
vmtest Test VM 26.05 Full desktop. Preferred target for static build checks.
devlab Physical dev machine 25.05 Headless-ish; core + shell + git + user only.

A host module is three things: local hardware/boot imports, host policy, and the nixosConfigurations entry. vmtest is the fully worked example — it sets keyboardConfig, containerRuntime, theme.colorScheme, and its own Home Manager branch:

flake.modules.nixos.vmtest = { ... }: {
  imports = [ ./_hardware.nix ./_boot.nix ];

  networking.hostName = "vmtest";
  system.stateVersion = "26.05";

  keyboardConfig = {
    consoleKeyMap = "br-abnt2";
    xkb = { layout = "br"; variant = ""; options = [ "altwin:swap_alt_win" ]; };
  };

  containerRuntime = { enable = true; engine = "docker"; users = [ "s8p" ]; };

  theme.colorScheme = "ayu-dark";

  home-manager.users.s8p.imports = [ config.flake.modules.homeManager.vmtest ];
};

Host-specific user behavior goes in a matching flake.modules.homeManager.<host> branch — for vmtest, that's the wallpaper directory.

Boot config, _hardware.nix, system.stateVersion, and the secrets identity are per-host and must be preserved when touching a host.


Feature registry

Names available for host composition.

NixOS (flake.modules.nixos.*)

Group Features
Core nix, nixpkgs, locale, nh, home-manager
Security sops, <host>-secrets
Desktop hyprland, stylix, greetd, extra-fonts
System pipewire, bluetooth, networkmanager, keyboard, zsh, git
Virt containers, container-nvidia, qemu-guest
User/Host s8p, vmtest, devlab

Home Manager (flake.modules.homeManager.*)

Group Features
Base s8p, stylix, keyboard
Shell zsh, tmux, zoxide, bat, cli-programs, cli-utilities
Dev git, neovim, direnv, development-tools, secretspec, ssh
Desktop hyprland, waybar, fuzzel, mako, ghostty, pcmanfm, cliphist, nwg-displays, screenshots, wallpaper, hyprpolkitagent
Apps zen-browser, obsidian, vesktop
Containers container-tools
Host branches vmtest

The user module (modules/users/s8p.nix) is where the Home Manager feature list is assembled — it defines the NixOS account and imports the HM features by name:

home-manager.users.s8p.imports = with config.flake.modules.homeManager; [
  stylix s8p secretspec
  cli-programs cli-utilities development-tools container-tools
  bat git hyprland fuzzel mako pcmanfm ghostty nwg-displays
  cliphist screenshots wallpaper waybar direnv tmux zsh zoxide
  keyboard neovim obsidian vesktop zen-browser ssh hyprpolkitagent
];

Package bundles are kept deliberately small and named: cli-utilities is curl file jq p7zip pv ripgrep unzip wget; development-tools is lazygit nixfmt statix deadnix shellcheck shfmt yq-go just; container-tools is lazydocker skopeo dive.


Custom options

Features define their own options rather than exposing raw upstream config. Hosts set these; the feature translates them.

theme.colorScheme

theme.colorScheme = "ayu-dark";   # base16-schemes name, no .yaml

Defined in both module classes. The NixOS side propagates the host's choice into every Home Manager user via home-manager.sharedModules.

keyboardConfig

keyboardConfig = {
  consoleKeyMap = "br-abnt2";
  xkb = { layout = "br"; variant = ""; options = [ "altwin:swap_alt_win" ]; };
};

Drives console.keyMap and services.xserver.xkb, and propagates xkb into Home Manager (where Hyprland consumes it) — but only when Home Manager is actually present on that machine.

containerRuntime

containerRuntime = {
  enable = true;
  engine = "docker";          # or "podman"
  users = [ "s8p" ];          # rootless/group access
  dockerCompat = true;        # podman only
  autoPrune = { enable = true; dates = "weekly"; };
};

container-nvidia asserts this is enabled before wiring hardware.nvidia-container-toolkit (pinned to device-name-strategy = "index", so devices are nvidia.com/gpu=0, =1, =all).

desktop.wallpaper

desktop.wallpaper = {
  enable = true;
  directory = "/home/s8p/Images/Wallpapers";
  prefixes = { horizontal = "h_"; vertical = "v_"; };
  resize = "crop";            # crop | fit | stretch | no
};

Nix owns the mechanism, not the images. A wallpaperctl helper detects monitor orientation at runtime and picks a matching image by filename prefix; the collection stays mutable on disk and out of the Nix store. Runs awww as a systemd user service.

desktop.screenshots

desktop.screenshots = {
  enable = true;
  directory = "${config.home.homeDirectory}/Images/screenshots";
};

Installs grim/slurp plus three writeShellApplication commands — screenshotRegion, screenshotEdit, screenshotScreen — bound to the Print key family in Hyprland.

greeter

greeter = {
  sessionCommand = "start-hyprland";
  numlock = true;             # setleds +num before tuigreet, never fatal
};

Theming (Stylix)

Current policy:

  • Base16 scheme: Tokyo Night Storm (default; vmtest overrides to ayu-dark)
  • Icons: Colloid
  • Cursor: oreo_white_cursors, size 5 (intentionally small)
  • Fonts: JetBrainsMono Nerd Font + Noto Sans/Serif/Color Emoji

Both module classes share one mkTheme function, so NixOS and Home Manager can never drift. Two settings matter and are load-bearing:

# NixOS side — we import the HM Stylix module ourselves
stylix.homeManagerIntegration.autoImport = false;

# HM side — Home Manager uses the NixOS pkgs instance (useGlobalPkgs)
stylix.overlays.enable = false;

Target-specific overrides belong with the feature, not with Stylix. Waybar, for example, disables Stylix's stock CSS in its own module because it ships custom CSS:

stylix.targets.waybar = { addCss = false; colors.enable = true; fonts.enable = true; };

Hyprland

Uses Hyprland's native Lua configuration (configType = "lua"), not legacy Hyprlang.

modules/desktop/hyprland/
├── default.nix
└── _lua/
    ├── animations.lua
    ├── binds.lua
    └── rules.lua

Nix generates a lib.vars Lua module carrying Nix-store-resolved executable paths; the static Lua consumes it with require("lib.vars"). No hardcoded /nix/store/... in the Lua files.

Long-running desktop programs register themselves through desktop.autostart.programs. Features choose defaults with lib.mkDefault; user or host modules can override enable and startAt. The central autostart feature maps merged declarations to supervised systemd user units.

Structural rules:

  • Ordinary config (input, general, decoration, misc) goes under HM settings.config → becomes hl.config({...}).
  • Top-level generated settings like monitor and bind map to native hl.* APIs. Do not invent settings.input — it generates an invalid hl.input(...) call.
  • Long-running processes (Mako, hyprpolkitagent, awww, cliphist, Waybar) are systemd user services, not Hyprland startup hooks.
  • nwg-displays owns ~/.config/hypr/monitors.lua and workspaces.lua. These are mutable and loaded defensively with pcall(require, ...) — never replaced by Home Manager symlinks.

Keybindings

SUPER is mainMod.

Bind Action
mod + Return Terminal
mod + D Launcher (fuzzel)
mod + E File manager
mod + Q Close window
mod + H/J/K/L Focus left/down/up/right
mod + Shift + H/J/K/L Swap window
mod + Space Toggle float
mod + F Toggle maximized
mod + T Toggle split
mod + P Toggle pseudo
mod + Tab Cycle window, raise to top
mod + S / mod + Shift + S Special workspace magic — toggle / move to
mod + 1‥9 / mod + Shift + 1‥9 Focus / move to workspace
mod + V Clipboard history (cliphist → fuzzel → wl-copy)
mod + B Color picker (hyprpicker)
mod + N Toggle wayscriber daemon
mod + ' Keyboard layout picker
Print / Shift + Print / mod + Print Screenshot region / edit / full screen
mod + LMB / mod + RMB Drag / resize window

Secrets (SOPS)

sops-nix, with a per-machine age identity at /var/lib/sops-nix/key.txt. Keys are never generated by the configuration:

sops.age = { keyFile = "/var/lib/sops-nix/key.txt"; generateKey = false; };

.sops.yaml maps public recipients to files: secrets/<host>.yaml is readable by s8p + that host; secrets/shared.yaml by s8p + all hosts.

A host's secrets module declares only its file and the exposed secret names:

flake.modules.nixos.vmtest-secrets = { ... }: {
  sops = {
    defaultSopsFile = ../../../secrets/vmtest.yaml;
    secrets.test_secret.owner = "s8p";
    secrets.litellm_api_key = {
      sopsFile = ../../../secrets/shared.yaml;
      owner = "s8p";
    };
  };
};

sops-key

A writeShellApplication helper shipped by the sops feature:

sops-key init      # create this machine's age identity (mutates machine state)
sops-key public    # print this machine's public age recipient
sops-key status    # identity path + public recipient

Onboarding a new host: sops-key init → add the printed age1... to .sops.yaml → sops updatekeys secrets/<host>.yaml.

Never commit: /var/lib/sops-nix/key.txt, personal age private keys, plaintext secrets, or API tokens. Public age1... recipients in .sops.yaml are safe.

Consumers should read runtime secret paths — never interpolate plaintext into a Nix derivation.

secretspec is for developer/application credentials and project-level API key declarations. It is not a substitute for boot/service secrets.


Zen Browser

The most involved application feature. default.nix wires inputs.zen-browser.homeModules.beta and assembles everything from _parts/:

modules/applications/zen-browser/
├── default.nix
├── _parts/
│   ├── settings.nix       # about:config prefs (compact mode, telemetry off, …)
│   ├── policies.nix       # enterprise policies + Redirector rule injection
│   ├── extensions.nix     # addon package list
│   ├── search.nix         # search engines (default: Brave; + MyNixOS, …)
│   ├── containers.nix     # Personal / Work / Shopping
│   └── redirects.nix      # YouTube → Invidious rewrite rules
└── _extensions/reddit-invidious/   # locally-built MV3 extension

Extensions come from firefox-addons (LanguageTool, Bitwarden, uBlock Origin, SponsorBlock, Dark Reader, Violentmonkey, Temporary Containers, Vimium C, React DevTools, SingleFile, Karakeep, Stylus), plus two declarative ones: redirector-declarative — whose rules are generated in redirects.nix and injected through browser policies — and reddit-invidious, an MV3 extension built in-tree that rewrites Reddit's YouTube embeds into Invidious links. The Invidious instance URL is a single variable in default.nix.

Containers and search are declared with force/containersForce, so they are authoritative over profile state.


Shell

Zsh — public module plus _parts/ for completion, prompt, keybindings, exports. Syntax highlighting, autosuggestions, dedup'd appended history, and history substring search on ^P/^N.

Tmux — prefix C-f, vi keys, mouse on, baseIndex = 1, tmux-256color, custom pane navigation. Split with | and -. Helper commands live in _scripts/ as writeShellApplication derivations (container status, service status, popup, tmuxifier picker) so runtime deps are explicit. Session layouts under layouts/.

Zoxide — its own feature (not buried in Zsh), aliased over cd via --cmd cd. Standalone tools own their shell integration.

Aliases — cat → bat --paging=never. EDITOR/VISUAL/SUDO_EDITOR → nvim, from a private flake input.


Working on this repo

Validate

nix flake show
nix flake check

nix flake show may warn that the generic modules output is unknown — expected, since flake.modules is a custom registry.

Full host build without activating:

nix build --no-link .#nixosConfigurations.vmtest.config.system.build.toplevel

Targeted evaluation when debugging module wiring:

nix eval .#modules.nixos --apply builtins.attrNames
nix eval .#modules.homeManager --apply builtins.attrNames
nix eval .#nixosConfigurations.vmtest.config.<option>

Untracked files: git-backed flakes ignore untracked files. Either git add (without committing) the files for the task at hand, or validate against a path flake:

nix flake check "path:$PWD"

Apply

Applying a generation is a manual, human step:

nh os test     # try it, no bootloader entry
nh os switch   # commit it

programs.nh.flake defaults to /home/s8p/.config/dotfiles.

Note for automated agents: see AGENTS.md. Agents may edit files and run non-activating evaluation/build checks, but must never activate — no nh os test/switch, nixos-rebuild test/switch, home-manager switch, switch-to-configuration, systemctl start|stop|restart|enable|disable, reboot, or sops-key init. Read-only inspection (systemctl status, journalctl, nix eval) is fine. Activation and runtime testing belong to the user.


Adding things

A new feature

Create one file named for the feature and register it:

# modules/desktop/swaylock.nix
{ ... }:
{
  flake.modules.homeManager.swaylock = { pkgs, ... }: {
    programs.swaylock.enable = true;
  };
}

Then wire it: a Home Manager feature into modules/users/s8p.nix, a NixOS feature into the relevant host's nixosConfigurations module list. Nothing is active until a host or user asks for it.

If the feature grows implementation files, move to a directory with a public default.nix and _parts/.

A new host

modules/hosts/<name>/
├── host.nix        # flake.modules.nixos.<name> + flake.nixosConfigurations.<name>
├── secrets.nix     # flake.modules.nixos.<name>-secrets
├── _boot.nix
└── _hardware.nix
  • Generate its own _hardware.nix; keep its existing boot config.
  • Preserve its existing system.stateVersion.
  • Compose shared features explicitly in the module list.
  • Give it its own age identity (sops-key init) and secrets file, add the recipient to .sops.yaml, then sops updatekeys.

Conventions

  1. Organize by feature/concept, never by nixos/ vs home/.
  2. A feature may expose one or several module classes — don't force both.
  3. Keep host hardware and boot local to the host.
  4. Prefer explicit host composition over hidden transitive imports.
  5. No growing catch-all base.nix. Create focused features.
  6. Avoid lib.mkForce unless there's a demonstrated merge conflict that can't be solved more narrowly. lib.mkDefault is used liberally so hosts can override.
  7. No Home Manager-side nixpkgs.overlays or nixpkgs.config while useGlobalPkgs = true.
  8. Use pkgs.writeShellApplication for standalone helper commands so runtime dependencies are explicit.
  9. Change the narrowest responsible layer. Separate structural moves from behavioral changes; get the move passing first.
  10. Don't introduce new frameworks or abstraction layers just to reduce repetition. Abstract after repeated real use shows the boundary.

Unfree packages are allowlisted individually, not globally:

nixpkgs.config.allowUnfreePredicate = pkg:
  builtins.elem (lib.getName pkg) [ "obsidian" "languagetool" ];

Inputs

nixpkgs (unstable), flake-parts, import-tree, home-manager, sops-nix, stylix, zen-browser, firefox-addons (rycee NUR), firefox-extensions-declarative, and nvim — a private Neovim flake hosted on git.tn.rsbp.dev. Everything follows the root nixpkgs.

Project-specific agent procedures live in .opencode/skills/: dendritic-nix, static-nixos-validation, sops-host-secrets, hyprland-lua, home-manager-feature.