My literate configurations with Org and Nix

Table of Contents

1. About

This repository manages system configurations for multiple machines using Nix and Org mode. All configuration is written as literate programs—Org documents where prose explains the reasoning behind each decision, and Nix code blocks are tangled into the actual configuration files.

1.1. Documentation

The full configuration document is published at:

1.2. Nix

Nix is a purely functional package manager and build system. This repository uses several Nix ecosystem tools:

  • Flakes for reproducible dependency management
  • NixOS for declarative Linux system configuration
  • nix-darwin for declarative macOS system configuration
  • home-manager for user environment management
  • nix-on-droid for Android (Termux) environment

1.3. Machines

Name Platform Device Role
kilimanjaro NixOS (x86_64) i5-12400F / RTX 3080 Main desktop
tarangire NixOS (x86_64) Ryzen 9 9950X Build server
manyara NixOS (x86_64) Beelink Mini S12 Home server
arusha NixOS (x86_64) WSL2 WSL environment
serengeti NixOS (aarch64) OCI A1 Flex Build server
katavi macOS (aarch64) M1 MacBook Air Main laptop
work macOS (aarch64) M4 MacBook Pro Work laptop
mikumi macOS (aarch64) M1 Mac mini Build server
android nix-on-droid Galaxy S24 FE Phone

2. Philosophy

2.1. Literate Configuration

Nix is declarative. Reading a Nix expression reveals what the system should become, and Nix itself handles how to get there. But neither the code nor the build system captures why a particular configuration exists, or why alternatives were rejected.

Why was fish chosen over zsh or bash? Why does the desktop profile enable this specific set of services? Why was a particular package pinned to an older version? The code shows the decision, but not the reasoning behind it. Without this context, future changes risk undoing intentional tradeoffs or repeating previously rejected approaches.

This repository uses literate programming to preserve intent. Configuration lives in Org mode documents where prose surrounds code. Each decision—from high-level architecture to individual package overrides—is accompanied by its rationale: why this approach was chosen, and why alternatives were not.

Configurations are documented with the problem that motivated them, the alternatives considered, and the reasoning behind the final choice. For temporary workarounds, the conditions for removal are also noted.

3. Configuration

3.1. Flake

This flake manages NixOS, nix-darwin, and home-manager configurations for multiple machines across different platforms (macOS, Linux, Android). It uses flake-parts for modular organization and includes tooling for development, code formatting, and pre-commit hooks.

{
  description = "dotfiles";

  inputs = {
    # Core
    <<nixpkgs>>
    <<nixpkgs-stable>>
    # Flake Infrastructure
    <<flake-parts>>
    # Transitive Dependencies
    <<flake-utils>>
    # System Configuration
    <<darwin>>
    <<home-manager>>
    <<nixos-wsl>>
    <<nix-on-droid>>
    <<disko>>
    <<impermanence>>
    <<lanzaboote>>
    <<nixos-facter-modules>>
    # Infrastructure
    <<comin>>
    <<sops-nix>>
    <<tsnsrv>>
    # Development Tools
    <<git-hooks>>
    <<treefmt-nix>>
    # Desktop & Theming
    <<nix-colors>>
    <<nix-wallpaper>>
    # Applications
    <<brew-nix>>
    <<claude-desktop>>
    <<edgepkgs>>
    <<emacs-overlay>>
    <<firefox-addons>>
    <<mcp-servers>>
    <<niri-flake>>
    <<nur-packages>>
    <<simple-wol-manager>>
    <<zen-browser>>
  };

  nixConfig = {
    <<nix-config>>
  };

  outputs =
    { self, flake-parts, ... }@inputs:
    flake-parts.lib.mkFlake { inherit inputs; } {
      <<outputs>>
    };
}

3.1.1. Inputs

External flake dependencies.

To minimize evaluation time caused by dependency graph bloat, almost all flakes are configured to follow the same nixpkgs and other common inputs wherever possible.

  1. Core
    1. nixpkgs

      https://github.com/NixOS/nixpkgs

      Nix Packages collection & NixOS

      The primary package set. Using nixos-unstable-small instead of nixos-unstable for faster channel updates. The -small variant skips some less critical CI tests, allowing new package versions to propagate faster while maintaining stability for core packages.

      For channel selection guidance, see https://nix.dev/concepts/faq.html#which-channel-branch-should-i-use

      Channel status can be checked at https://status.nixos.org/

      Using git+https:// with shallow=1 instead of github: for slightly faster file extraction on large repositories like nixpkgs.

      nixpkgs.url = "git+https://github.com/nixos/nixpkgs?shallow=1&ref=nixos-unstable-small";
      
    2. nixpkgs-stable

      Provides stable packages when unstable has build failures or regressions. Mainly used by the stable overlay (see overlays/configuration.org) for packages that are broken in unstable.

      nixpkgs-stable.url = "git+https://github.com/nixos/nixpkgs?shallow=1&ref=nixos-25.05";
      
  2. Flake Infrastructure
    1. flake-parts

      https://github.com/hercules-ci/flake-parts

      Simplify Nix Flakes with the module system

      Framework for organizing flake outputs. Provides module system for flakes, making complex configurations more maintainable through separation of concerns.

      flake-parts = {
        url = "github:hercules-ci/flake-parts";
        inputs.nixpkgs-lib.follows = "nixpkgs";
      };
      
  3. Transitive Dependencies
    1. flake-utils

      https://github.com/numtide/flake-utils

      Pure Nix flake utility functions

      Common flake utilities. Only used transitively via follows to unify the flake-utils version across inputs that depend on it.

      flake-utils.url = "github:numtide/flake-utils";
      
  4. System Configuration
    1. darwin

      https://github.com/nix-darwin/nix-darwin

      Manage your macOS using Nix

      nix-darwin provides NixOS-style system configuration for macOS. Essential for managing macOS system settings, launchd services, and Homebrew declaratively.

      darwin = {
        url = "github:nix-darwin/nix-darwin";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    2. home-manager

      https://github.com/nix-community/home-manager

      Manage a user environment using Nix

      User environment management. Manages dotfiles, user services, and per-user packages declaratively. The backbone of user-level configuration in this repository.

      home-manager = {
        url = "github:nix-community/home-manager";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    3. nixos-wsl

      https://github.com/nix-community/nixos-wsl

      NixOS on WSL

      NixOS on Windows Subsystem for Linux. Provides NixOS experience within WSL2, useful for Windows machines that need Linux development environments.

      nixos-wsl = {
        url = "github:nix-community/nixos-wsl";
        inputs.flake-compat.follows = "";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    4. nix-on-droid

      https://github.com/nix-community/nix-on-droid

      Nix-enabled environment for your Android device.

      Nix environment for Android via Termux. Enables the same declarative configuration approach on mobile devices.

      nix-on-droid = {
        url = "github:nix-community/nix-on-droid";
        inputs.home-manager.follows = "home-manager";
        inputs.nix-formatter-pack.follows = "";
        inputs.nixpkgs-docs.follows = "nixpkgs";
        inputs.nixpkgs-for-bootstrap.follows = "nixpkgs";
        inputs.nixpkgs.follows = "nixpkgs";
        inputs.nmd.follows = "";
      };
      
    5. disko

      https://github.com/nix-community/disko

      Declarative disk partitioning and formatting using nix

      Used for reproducible NixOS installations with automated partition layout, filesystem creation, and encryption setup.

      disko = {
        url = "github:nix-community/disko";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    6. impermanence

      https://github.com/nix-community/impermanence

      Modules to help you handle persistent state on systems with ephemeral root storage

      Manages stateful paths on systems with ephemeral root filesystems. Used with btrfs snapshots to ensure only explicitly declared state persists across reboots.

      impermanence.url = "github:nix-community/impermanence";
      
    7. lanzaboote

      https://github.com/nix-community/lanzaboote

      Secure Boot for NixOS

      Signs boot components with custom keys, enabling Secure Boot on NixOS machines.

      lanzaboote = {
        url = "github:nix-community/lanzaboote";
        inputs.nixpkgs.follows = "nixpkgs";
        inputs.pre-commit.follows = "git-hooks";
      };
      
    8. nixos-facter-modules

      https://github.com/numtide/nixos-facter-modules

      A series of NixOS modules to be used in conjunction with nixos-facter

      Hardware detection for NixOS. Automatically generates hardware configuration based on detected hardware, simplifying initial system setup.

      nixos-facter-modules.url = "github:numtide/nixos-facter-modules";
      
  5. Infrastructure
    1. comin

      https://github.com/nlewo/comin

      GitOps For NixOS Machines

      Automatically deploys configuration changes when pushed to the repository, enabling continuous deployment for servers without manual nixos-rebuild switch.

      comin = {
        url = "github:nlewo/comin";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    2. sops-nix

      https://github.com/Mic92/sops-nix

      Atomic secret provisioning for NixOS based on sops

      Secrets management using Mozilla SOPS. Encrypts secrets in the repository that are decrypted at activation time using age keys.

      sops-nix = {
        url = "github:Mic92/sops-nix";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    3. tsnsrv

      https://github.com/boinkor-net/tsnsrv

      A reverse proxy that exposes services on your tailnet (as their own tailscale participants)

      Tailscale service proxy. Exposes local services to the Tailscale network with automatic HTTPS certificates.

      tsnsrv = {
        url = "github:boinkor-net/tsnsrv";
        inputs.flake-parts.follows = "flake-parts";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
  6. Development Tools
    1. git-hooks

      https://github.com/cachix/git-hooks.nix

      Seamless integration of pre-commit.com git hooks with Nix.

      Pre-commit hooks as Nix derivations. Ensures code quality checks run consistently across all development environments without requiring global tool installation.

      Inputs that don't affect this flake are removed to prevent lockfile bloat.

      git-hooks = {
        url = "github:cachix/git-hooks.nix";
        inputs.flake-compat.follows = "";
        inputs.gitignore.follows = "";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    2. treefmt-nix

      https://github.com/numtide/treefmt-nix

      treefmt nix configuration

      Unified code formatter configuration. Runs multiple formatters (nixfmt, shfmt, etc.) through a single interface, ensuring consistent formatting across the repository.

      treefmt-nix = {
        url = "github:numtide/treefmt-nix";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
  7. Desktop & Theming
    1. nix-colors

      https://github.com/misterio77/nix-colors

      Modules and schemes to make theming with Nix awesome.

      Base16 color scheme framework for Nix. Provides consistent theming across applications through a single color scheme definition.

      nix-colors = {
        url = "github:misterio77/nix-colors";
        inputs.nixpkgs-lib.follows = "nixpkgs";
      };
      
    2. nix-wallpaper

      https://github.com/natsukium/nix-wallpaper

      A configurable wallpaper for nix systems

      Generates Nix logo wallpapers. Using a custom branch (custom-logo) that supports additional logo variants.

      nix-wallpaper = {
        url = "github:natsukium/nix-wallpaper/custom-logo";
        inputs.flake-utils.follows = "flake-utils";
        inputs.nixpkgs.follows = "nixpkgs";
        inputs.pre-commit-hooks.follows = "git-hooks";
      };
      
  8. Applications
    1. brew-nix

      https://github.com/BatteredBunny/brew-nix

      Experimental nix expression to package all MacOS casks from homebrew automatically

      Provides Homebrew casks as Nix packages for darwin. Useful for proprietary macOS applications not available in nixpkgs.

      The brew-api input is marked as non-flake because it's just a data source (JSON API dump from Homebrew's API).

      brew-api = {
        url = "github:BatteredBunny/brew-api";
        flake = false;
      };
      brew-nix = {
        url = "github:BatteredBunny/brew-nix";
        inputs.brew-api.follows = "brew-api";
        inputs.nix-darwin.follows = "darwin";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    2. claude-desktop

      https://github.com/k3d3/claude-desktop-linux-flake

      Nix Flake for Claude Desktop on Linux

      Provides Claude Desktop for Linux. Using a community flake since Anthropic doesn't officially support Linux yet.

      claude-desktop = {
        url = "github:k3d3/claude-desktop-linux-flake";
        inputs.flake-utils.follows = "flake-utils";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    3. edgepkgs

      https://github.com/natsukium/edgepkgs

      Personal repository for bleeding-edge packages. Contains packages not yet in nixpkgs, those requiring modifications, or packages that wouldn't be accepted upstream (e.g., niche or experimental software).

      edgepkgs = {
        url = "github:natsukium/edgepkgs";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    4. emacs-overlay

      https://github.com/nix-community/emacs-overlay

      Bleeding edge emacs overlay

      Provides latest Emacs builds including native compilation and pure GTK variants. Also includes MELPA packages updated more frequently than nixpkgs. Mainly used for the utility that parses org files and automatically configures dependency packages.

      emacs-overlay = {
        url = "github:nix-community/emacs-overlay";
        inputs.nixpkgs-stable.follows = "nixpkgs-stable";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    5. firefox-addons

      https://gitlab.com/rycee/nur-expressions

      A few Nix expressions suitable for inclusion in Nix User Repository

      Firefox/browser extensions packaged for Nix. Allows declarative browser extension management through home-manager.

      firefox-addons = {
        url = "gitlab:rycee/nur-expressions?dir=pkgs/firefox-addons";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    6. mcp-servers

      https://github.com/natsukium/mcp-servers-nix

      A Nix-based configuration framework for Model Control Protocol (MCP) servers with ready-to-use packages.

      Provides both a configuration framework and packaged MCP servers for Nix. Used with Claude Code and other MCP-compatible AI assistants. See MCP Servers for configuration details.

      mcp-servers = {
        url = "github:natsukium/mcp-servers-nix";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    7. niri-flake

      https://github.com/sodiboo/niri-flake

      Nix-native configuration for niri

      Niri is a scrollable-tiling Wayland compositor. This flake provides the compositor and related modules for NixOS/home-manager integration.

      niri-flake = {
        url = "github:sodiboo/niri-flake";
        inputs.niri-stable.follows = "";
        inputs.niri-unstable.follows = "";
        inputs.nixpkgs-stable.follows = "nixpkgs-stable";
        inputs.nixpkgs.follows = "nixpkgs";
        inputs.xwayland-satellite-stable.follows = "";
        inputs.xwayland-satellite-unstable.follows = "";
      };
      
    8. nur-packages

      https://github.com/natsukium/nur-packages

      Personal NUR (Nix User Repository). Contains packages maintained personally that are either too niche for nixpkgs or require customizations.

      nur-packages = {
        url = "github:natsukium/nur-packages";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    9. simple-wol-manager

      https://git.natsukium.com/natsukium/simple-wol-manager

      A web-based application for managing Wake-on-LAN (WoL) devices

      Personal Wake-on-LAN management tool. Provides a web interface for sending WoL packets and managing device configurations.

      simple-wol-manager = {
        url = "git+https://git.natsukium.com/natsukium/simple-wol-manager";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
    10. zen-browser

      https://github.com/0xc000022070/zen-browser-flake

      Community-driven Nix Flake for the Zen browser

      Firefox-based browser focused on privacy. Community flake providing Nix packaging with home-manager integration.

      zen-browser = {
        url = "github:0xc000022070/zen-browser-flake";
        inputs.nixpkgs.follows = "nixpkgs";
        inputs.home-manager.follows = "home-manager";
      };
      

3.1.2. Nix Config

Nix settings used by this flake. While these settings are already configured on all managed machines, documenting them here helps with initial setup and allows others to use this flake.

For detailed documentation on each setting, see https://nix.dev/manual/nix/latest/command-ref/conf-file.html

Note: flake.nix uses a restricted subset of the Nix language that prevents code reuse (see https://github.com/NixOS/nix/issues/4945). The values below are currently hardcoded in this section. Once machine configurations are migrated to org-mode, noweb references will allow sharing these values across both the flake and machine-specific settings.

  1. Binary Caches

    Binary cache (substituter) configuration for building this flake. While optional, configuring these caches significantly reduces build times by downloading pre-built binaries instead of compiling from source.

    Using extra-substituters and extra-trusted-public-keys instead of substituters and trusted-public-keys ensures this flake's cache configuration is additive rather than replacing the user's existing settings. This respects any caches the user has already configured in their nix.conf or system configuration.

    extra-substituters = [
      "https://natsukium.cachix.org"
      "https://nix-community.cachix.org"
    ];
    
    extra-trusted-public-keys = [
      "natsukium.cachix.org-1:STD7ru7/5+KJX21m2yuDlgV6PnZP/v5VZWAJ8DZdMlI="
      "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
    ];
    
    1. nix-community.cachix.org

      Community binary cache, primarily used for CUDA-related packages. Since November 2024, CUDA binaries are distributed under the nix-community namespace. Build status can be monitored at https://hydra.nix-community.org/project/nixpkgs

      For more details, see https://discourse.nixos.org/t/cuda-cache-for-nix-community/56038

      Alternatively, Flox's binary cache can be used for CUDA packages. As of September 2025, Flox has partnered with NVIDIA to obtain redistribution rights. See https://discourse.nixos.org/t/nix-flox-nvidia-opening-up-cuda-redistribution-on-nix/69189

    2. natsukium.cachix.org

      Personal binary cache containing outputs from this flake. Packages not available in the official cache.nixos.org or nix-community.cachix.org are built on GitHub Actions and pushed here.

3.1.3. MCP Servers

Configuration for mcp-servers-nix, enabled in the development shell. The flavors.claude-code preset generates a .mcp.json file compatible with Claude Code's expected format.

Enabled servers:

  • nixos: NixOS package/option search and Home Manager documentation
  • terraform: Terraform registry lookup for providers, modules, and policies
  • grafana: Query dashboards, datasources, and metrics from the home server's Grafana instance

The passwordCommand option retrieves secrets at runtime using rbw (Bitwarden CLI), avoiding plaintext credentials in the repository.

mcp-servers = {
  flavors.claude-code.enable = true;
  programs = {
    nixos.enable = true;
    terraform.enable = true;
    grafana = {
      enable = true;
      env = {
        GRAFANA_URL = "http://manyara:3001";
        GRAFANA_USERNAME = "admin";
      };
      passwordCommand = {
        GRAFANA_PASSWORD = [
          "rbw"
          "get"
          "grafana"
        ];
      };
    };
  };
};

3.1.4. Hosts

Machine definitions for all managed systems.

hosts = {
  <<host-katavi>>
  <<host-mikumi>>
  <<host-work>>
  <<host-kilimanjaro>>
  <<host-arusha>>
  <<host-manyara>>
  <<host-serengeti>>
  <<host-tarangire>>
  <<host-android>>
};
  1. katavi

    Main laptop (M1 MacBook Air).

    katavi = {
      system = "aarch64-darwin";
    };
    
  2. mikumi

    Build server (M1 Mac mini).

    mikumi = {
      system = "aarch64-darwin";
    };
    
  3. work

    Laptop for work (M4 MacBook Pro).

    work = {
      system = "aarch64-darwin";
    };
    
  4. kilimanjaro

    Main desktop (Intel Core i5-12400F).

    Wake-on-LAN (WoL) is enabled via the following BIOS setting: Advanced > APM Configuration > Power On By PCI-E > Enabled

    kilimanjaro = {
      system = "x86_64-linux";
    };
    
  5. arusha

    WSL (dual boot with kilimanjaro).

    arusha = {
      system = "x86_64-linux";
    };
    
  6. manyara

    A mini PC with Intel N100, serving as a lightweight home server. https://www.bee-link.com/products/beelink-mini-s12-pro-n100

    BIOS is configured with the following setting to automatically power on after an outage: Chipset > PCH-IO Configuration > State After G3 > S0 State

    manyara = {
      system = "x86_64-linux";
    };
    
  7. serengeti

    Build server (OCI A1 Flex).

    serengeti = {
      system = "aarch64-linux";
    };
    
  8. tarangire

    Build server (Ryzen 9 9950X).

    Wake-on-LAN (WoL) is enabled via the following BIOS setting: Advanced > APM Configuration > Power On By PCI-E > Enabled

    tarangire = {
      system = "x86_64-linux";
    };
    
  9. android

    Phone (Pixel 7a).

    android = {
      system = "aarch64-linux";
      platform = "android";
    };
    

3.1.5. Outputs

systems = [
  "x86_64-linux"
  "aarch64-linux"
  "aarch64-darwin"
];

imports = [
  ./flake-module.nix
  inputs.git-hooks.flakeModule
  inputs.mcp-servers.flakeModule
  inputs.treefmt-nix.flakeModule
];

<<hosts>>

flake = {
  overlays = import ./overlays { inherit inputs; };
};

perSystem =
  {
    self',
    config,
    pkgs,
    system,
    ...
  }:
  {
    _module.args.pkgs = import self.inputs.nixpkgs {
      inherit system;
      config.allowUnfree = true;
      overlays = [ self.inputs.nur-packages.overlays.default ] ++ builtins.attrValues self.overlays;
    };

    checks = import ./tests {
      inherit (self.inputs) nixpkgs;
      inherit pkgs;
    };

    packages = {
      fastfetch = pkgs.callPackage ./pkgs/fastfetch { };
      neovim = pkgs.callPackage ./pkgs/neovim-with-config { };
      po4a_0_74 = (
        pkgs.po4a.overrideAttrs (oldAttrs: {
          version = "0.74";
          src = pkgs.fetchurl {
            url = "https://github.com/mquinson/po4a/releases/download/v0.74/po4a-0.74.tar.gz";
            hash = "sha256-JfwyPyuje71Iw68Ov0mVJkSw5GgmH5hjPpEhmoOP58I=";
          };
          patches = [ ];
          nativeBuildInputs = oldAttrs.nativeBuildInputs ++ [ pkgs.libxml2 ];
          doCheck = false;
        })
      );
      html =
        with pkgs;
        let
          org-html-themes = fetchurl {
            url = "https://raw.githubusercontent.com/fniessen/org-html-themes/b3898f4c5b09b3365fd93fd1566f46ecd0a8911f/org/theme-readtheorg.setup";
            hash = "sha256-+5gy+S6NcuvlV61fudbCNoCKmSrCdA9P5CHeGKlDrSM=";
          };
          org-to-html = ./scripts/org-to-html.el;
        in
        stdenvNoCC.mkDerivation {
          name = "dotfiles";
          src = lib.cleanSource ./.;
          postPatch = ''
            substituteInPlace configuration.org \
              --replace-fail "/nix/store/3a3059l625swdwn9x129qqa6q6fvwjrb-theme-readtheorg.setup" "${org-html-themes}"
          '';
          nativeBuildInputs = [
            (emacs.pkgs.withPackages (epkgs: [
              epkgs.htmlize
              epkgs.nix-ts-mode
              (epkgs.treesit-grammars.with-grammars (g: [ g.tree-sitter-nix ]))
            ]))
            gettext
            self'.packages.po4a_0_74
          ];
          buildPhase = ''
            runHook preBuild
            po4a po4a.cfg
            emacs --batch -l ${org-to-html}
            runHook postBuild
          '';

          installPhase = ''
            runHook preInstall
            install -Dm644 configuration.html $out/index.html
            install -Dm644 configuration.ja.html $out/ja/index.html
            runHook postInstall
          '';
        };
    };

    pre-commit = {
      check.enable = true;
      settings = {
        package = pkgs.prek;
        src = ./.;
        hooks =
          let
            check-git-changes = pkgs.writeShellApplication {
              name = "check-git-changes";
              runtimeInputs = [ pkgs.git ];
              text = builtins.readFile ./scripts/check-git-changes.sh;
            };
            emacs-with-org = (pkgs.emacsPackagesFor pkgs.emacs).emacsWithPackages (epkgs: [ epkgs.org ]);
          in
          {
            actionlint = {
              enable = true;
              priority = 10;
            };
            biome = {
              enable = true;
              priority = 10;
            };
            lua-ls = {
              enable = false;
              priority = 10;
            };
            nil = {
              enable = true;
              priority = 10;
            };
            shellcheck = {
              enable = true;
              priority = 10;
            };
            treefmt = {
              enable = true;
              priority = 10;
            };
            typos = {
              enable = true;
              priority = 10;
              excludes = [
                ".sops.yaml"
                "homes/shared/gpg/keys.txt"
                "secrets.yaml"
                "secrets/default.yaml"
                "systems/nixos/tarangire/facter.json"
                "systems/shared/hercules-ci/binary-caches.json"
              ];
              settings.configPath = "typos.toml";
            };
            yamllint = {
              enable = true;
              priority = 10;
              excludes = [
                "secrets/default.yaml"
                "secrets.yaml"
              ];
              settings.configData = "{rules: {document-start: {present: false}}}";
            };
            po4a = {
              enable = true;
              name = "po4a";
              description = "Update translations with po4a";
              priority = 10;
              entry = pkgs.lib.getExe (
                pkgs.writeShellApplication {
                  name = "check-po4a";
                  runtimeInputs = [
                    self'.packages.po4a_0_74
                    pkgs.gettext
                    check-git-changes
                  ];
                  text = builtins.readFile ./scripts/check-po4a.sh;
                }
              );
              files = "(\\.org|po/.*\\.po)$";
              pass_filenames = false;
            };
            "check-org-tangle" = {
              enable = true;
              name = "check-org-tangle";
              description = "Verify org files are tangled and synchronized";
              # Ensure this hook runs before all other hooks
              priority = 0;
              entry = pkgs.lib.getExe (
                pkgs.writeShellApplication {
                  name = "check-org-tangle";
                  runtimeInputs = [
                    emacs-with-org
                    pkgs.gnumake
                    check-git-changes
                  ];
                  text = builtins.readFile ./scripts/check-org-tangle.sh;
                }
              );
              files = "\\.org$";
              pass_filenames = false;
            };
          };
      };
    };

    treefmt = {
      projectRootFile = "flake.nix";
      programs = {
        biome.enable = true;
        nixfmt.enable = true;
        shfmt.enable = true;
        stylua.enable = true;
        taplo.enable = true;
        terraform.enable = true;
        yamlfmt.enable = true;
      };
    };

    <<mcp-servers-config>>

    <<dev-shells>>
  };

3.2. Overlays

3.2.1. Overview

This file defines nixpkgs overlays for patching broken packages, pinning specific versions, and adding local workarounds. Each overlay modifies the package set to address issues that would otherwise block the system build or cause runtime problems.

For details on overlay mechanics (final: prev: pattern, composition order, etc.), see nixpkgs overlay documentation.

Overlays are organized into four categories based on their purpose and expected lifetime:

  • stable: Packages fetched from nixpkgs-stable when broken in unstable and the fix would trigger excessive rebuilds or is too complex to patch locally.
  • temporary-fix: Local overrides (e.g., disabling tests) that don't require a different nixpkgs version. Remove once fixed upstream.
  • pre-release: Alpha, beta, or pre-release packages for testing before they land in nixpkgs.
  • patches: Workarounds not suitable for upstream contribution (e.g., locale-specific fixes, local tooling shims). Expected to remain indefinitely.
{ inputs }:
{
  <<stable>>

  <<temporary-fix>>

  <<pre-release>>

  <<patches>>
}
  1. stable

    Fetch packages from nixpkgs-stable when broken in unstable. This includes cases where the upstream fix would trigger excessive rebuilds, or where the issue is too complex to patch locally.

    stable = final: prev: {
    };
    
  2. temporary-fix

    Local overrides for packages that build but have failing tests or minor issues. Unlike the stable overlay, these don't require fetching packages from a different nixpkgs branch. We simply override specific attributes (like doCheck) on the existing packages. Remove these overrides once the issues are fixed upstream.

    temporary-fix = final: prev: {
      <<python313-package-set>>
    };
    
    1. Python313 package set

      Override problematic Python packages using packageOverrides.

      Python packages in nixpkgs form an interconnected dependency graph. The packageOverrides mechanism ensures that when a package is overridden, all dependent packages automatically see the modified version. This is essential for consistency—a direct overlay override (e.g., python313Packages.foo = ...) would only affect top-level access, leaving internal dependencies using the original broken version.

      See nixpkgs Python documentation for details.

      python313 = prev.python313.override {
        packageOverrides = pyfinal: pyprev: {
          <<rapidocr-onnxruntime>>
          <<lxml-html-clean>>
        };
      };
      
      1. rapidocr-onnxruntime

        The test suite causes a segmentation fault during execution. The root cause is still under investigation.

        This package is pulled in as a transitive dependency. Runtime functionality has been verified to work correctly in actual use, so disabling the test suite is a safe workaround.

        rapidocr-onnxruntime = pyprev.rapidocr-onnxruntime.overridePythonAttrs (_: {
          doCheck = false;
        });
        
      2. lxml-html-clean

        Tests fail due to breaking changes in libxml2 2.14, which modified how certain DOM operations handle whitespace and entity encoding. These changes cause test assertions to fail even though the actual HTML cleaning functionality works correctly.

        Tracked upstream in fedora-python/lxml_html_clean#24. Remove this override once the test suite is updated for libxml2 2.14 compatibility.

        lxml-html-clean = pyprev.lxml-html-clean.overridePythonAttrs (_: {
          doCheck = false;
        });
        
  3. pre-release

    Overlay for testing alpha, beta, or pre-release versions of packages before they land in nixpkgs. Useful for evaluating release candidates, nightly builds, or packages pending upstream review. Once a package is available in nixpkgs, remove it from this overlay.

    pre-release = final: prev: { };
    
  4. patches

    Workarounds that are not suitable for upstream contribution.

    These patches address issues that upstream would likely not accept—either because they are specific to this configuration (e.g., locale settings), bypass intended behavior, or solve problems in unconventional ways. Unlike temporary-fix, these are expected to remain indefinitely.

    patches = final: prev: {
      <<gh-dash>>
      <<command-line-tools-shim>>
    };
    
    1. gh-dash

      The preview pane renders incorrectly when LANG=ja_JP.UTF-8 is set. The issue stems from gh-dash's terminal width calculation, which miscounts the display width of certain UTF-8 characters (particularly CJK characters and some emoji). This causes text wrapping and alignment to break.

      Setting LANG=C.UTF-8 forces ASCII-compatible width calculations while preserving UTF-8 encoding support, which fixes the rendering issue. We use writeShellApplication to create a wrapper that sets this environment variable before invoking the real binary.

      Reported upstream in dlvhdr/gh-dash#316.

      gh-dash =
        (final.writeShellApplication {
          name = "gh-dash";
          text = ''
            LANG=C.UTF-8 ${final.lib.getExe prev.gh-dash} "$@"
          '';
        }).overrideAttrs
          { pname = "gh-dash"; };
      
    2. mkShim

      Shim utility for providing stub implementations of macOS Command Line Tools.

      On darwin systems without Xcode Command Line Tools installed, invoking commands like cc or python3 triggers an annoying system popup prompting installation. These shims intercept such calls and either delegate to Nix-provided tools or return appropriate exit codes, suppressing the popup and preventing spurious build failures.

      See pkgs/mkShim for the implementation details and list of shimmed commands.

      inherit (final.callPackage ../pkgs/mkShim { }) mkShim commandLineToolsShim;
      

3.3. Modules

3.3.1. Overview

This file defines custom modules that extend the standard NixOS, nix-darwin, and home-manager module systems. Each module addresses specific use cases or provides opinionated defaults not available upstream.

Modules are organized by functional domain (e.g., Shell, Networking, Version Control) rather than by module system target. When a domain contains both system-level and user-level configuration, subheaders separate them explicitly.

3.3.2. Nix

Nix package manager and nixpkgs configuration. These modules are shared across NixOS and nix-darwin, providing consistent Nix behavior on all machines.

  1. Core Settings

    Core Nix daemon configuration including flakes, garbage collection, binary caches, and sandbox settings.

    {
      config,
      lib,
      pkgs,
      ...
    }:
    let
      cfg = config.my.nix;
      inherit (lib)
        mkEnableOption
        mkIf
        mkMerge
        mkOption
        optional
        types
        ;
    in
    {
      <<nix-options>>
    
      <<nix-config>>
    }
    
    1. Options
      options.my.nix = {
        enable = mkEnableOption "Nix configuration";
      
        enableFlakes = mkOption {
          default = true;
          example = false;
          description = "Whether to enable flakes.";
          type = types.bool;
        };
      };
      
    2. Configuration
      config = mkIf cfg.enable (mkMerge [
        <<nix-flakes>>
        {
          <<nix-store-optimisation>>
      
          <<nix-warn-dirty>>
      
          <<nix-substituters>>
      
          <<nix-sandbox>>
      
          <<nix-trusted-users>>
      
          <<nix-gc>>
      
          <<nix-extra-options>>
        }
      ]);
      
      1. Flakes

        Flakes are the de facto standard for Nix project management and are used throughout this repository.

        Channels are disabled because they introduce mutable state (/nix/var/nix/profiles/per-user/*/channels) that is difficult to reproduce across machines. However, this does not break legacy commands like nix-shell — NixOS and nix-darwin automatically set NIX_PATH from the flake's inputs by default (see nixpkgs.flake.setNixPath), so <nixpkgs> lookups continue to work on all machines managed by this dotfiles repository.

        (mkIf cfg.enableFlakes {
          nix = {
            settings.experimental-features = [
              "flakes"
              "nix-command"
            ];
            channel.enable = false;
          };
        })
        
      2. Store Optimisation

        Two complementary deduplication mechanisms are enabled:

        • nix.optimise.automatic periodically runs nix-store --optimise to hard-link identical files already in the store.
        • nix.settings.auto-optimise-store deduplicates at build time as new paths are added.

        auto-optimise-store is Linux-only because enabling it on macOS corrupts the store — the build fails with error: cannot link '/nix/store/.tmp-link' to '/nix/store/.links/...': File exists. See NixOS/nix#7273.

        nix.optimise.automatic = true;
        nix.settings.auto-optimise-store = pkgs.stdenv.hostPlatform.isLinux;
        
      3. Garbage Collection

        Periodically runs nix-collect-garbage to reclaim disk space. Generations older than 7 days are deleted automatically — keeping old builds beyond that provides little value since they can always be rebuilt from the flake lock file.

        nix.gc = {
          automatic = true;
          options = "--delete-older-than 7d";
        };
        
      4. Dirty Warning

        Suppress the "Git tree is dirty" warning during flake evaluation. This warning fires on every build when there are uncommitted changes, which is the normal state during development.

        nix.settings.warn-dirty = false;
        
      5. Binary Caches

        Two binary caches are configured: natsukium holds pre-built artifacts for this dotfiles repository (CI-built system closures and custom packages), and nix-community provides caches for community projects including NVIDIA/CUDA packages and other nix-community-maintained derivations.

        nix.settings = {
          substituters = [
            "https://natsukium.cachix.org"
            "https://nix-community.cachix.org"
          ];
        
          trusted-public-keys = [
            "natsukium.cachix.org-1:STD7ru7/5+KJX21m2yuDlgV6PnZP/v5VZWAJ8DZdMlI="
            "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
          ];
        };
        
      6. Sandbox

        The sandbox is set to "relaxed" on Darwin to avoid unexpected build failures. With sandbox = true, some packages fail to build on macOS due to sandbox restrictions — likely related to localhost networking required by test suites that start local servers, though the exact mechanism is not fully understood. Nixpkgs provides __darwinAllowLocalNetworking to allow localhost access within the sandbox, which may address the same class of issues. "relaxed" keeps normal derivations sandboxed while allowing derivations with __noChroot = true to bypass the sandbox, preventing these failures.

        nix.settings.sandbox = if pkgs.stdenv.hostPlatform.isDarwin then "relaxed" else true;
        
      7. Trusted Users

        On macOS, administrative users belong to the admin group, not wheel (which only contains root). Without @admin, the primary user would not be trusted by the Nix daemon on Darwin.

        nix.settings.trusted-users = [
          "root"
          "@wheel"
        ]
        ++ optional pkgs.stdenv.hostPlatform.isDarwin "@admin";
        
      8. Extra Options

        Terminates builds that produce no output for 3600 seconds (1 hour). Some heavy builds (e.g., Chromium, kernel compilation, CUDA-based deep learning libraries) can be silent for extended periods, so the timeout is set generously to avoid false positives while still catching genuinely hung builds.

        nix.extraOptions = ''
          max-silent-time = 3600
        '';
        
  2. Nixpkgs

    Nixpkgs configuration. allowUnfree is enabled by default. While free software is preferred where possible, strictly enforcing it is impractical — NVIDIA hardware requires unfree drivers and CUDA libraries, and specifying each unfree package individually via allowUnfreePredicate would be prohibitively tedious for the number of transitive dependencies involved.

    { config, lib, ... }:
    let
      cfg = config.my.nixpkgs;
      inherit (lib)
        mkEnableOption
        mkIf
        mkOption
        types
        ;
    in
    {
      options.my.nixpkgs = {
        enable = mkEnableOption "Nixpkgs configuration";
    
        allowUnfree = mkOption {
          default = true;
          example = false;
          description = "Whether to allow unfree packages.";
          type = types.bool;
        };
      };
    
      config = mkIf cfg.enable {
        nixpkgs = {
          config.allowUnfree = cfg.allowUnfree;
        };
      };
    }
    
  3. Distributed Builds

    Distributed build configuration using Nix's built-in buildMachines support. This allows any machine in the fleet to offload builds to other machines, significantly reducing build times for resource-intensive derivations.

    nix.distributedBuilds is set to mkDefault true here so that server profiles can override it to false. nix.buildMachines is harmless when distributed builds are disabled, so no conditional wrapping is needed. The server profiles (profiles/nixos/server.nix and profiles/darwin/server.nix) disable it because build servers have the resources to build locally, and offloading from one build server to another creates unnecessary network overhead and potential circular dependencies.

    {
      inputs,
      config,
      lib,
      ...
    }:
    let
      <<build-machines-let>>
    in
    {
      <<distributed-builds-config>>
    
      <<build-machines-known-hosts>>
    }
    
    1. Protocol Selection

      The ssh-ng protocol is preferred over plain ssh because it supports content-addressed derivations and more efficient store path transfers. However, Hydra does not support ssh-ng (see NixOS/hydra#688), so the protocol is dynamically selected based on whether Hydra is enabled on the current machine.

      protocol = if (config.services ? hydra && config.services.hydra.enable) then "ssh" else "ssh-ng";
      inherit (inputs.self.outputs.nixosConfigurations) kilimanjaro serengeti tarangire;
      inherit (inputs.self.outputs.darwinConfigurations) mikumi;
      
    2. Configuration
      nix = {
        distributedBuilds = lib.mkDefault true;
      
        extraOptions = ''
          builders-use-substitutes = true
        '';
      
        <<build-machines-list>>
      };
      
    3. Build Machines

      Each machine excludes itself from the build machine list (via config.networking.hostName checks) to avoid circular build delegation. The mikumi machine is also excluded from the work host because the work machine should not offload builds to personal infrastructure.

      Machine capabilities (maxJobs, system-features) are referenced from each machine's configuration rather than hardcoded. See each machine's definition under systems/ for the actual values (e.g., systems/nixos/tarangire/default.nix for max-jobs = 12).

      buildMachines =
        [ ]
        ++ lib.optional (config.networking.hostName != "tarangire") {
          inherit (tarangire.config.networking) hostName;
          systems = [
            "x86_64-linux"
            "i686-linux"
          ];
          sshUser = "natsukium";
          inherit protocol;
          maxJobs = tarangire.config.nix.settings.max-jobs;
          speedFactor = 1;
          supportedFeatures = tarangire.config.nix.settings.system-features;
          mandatoryFeatures = [ ];
        }
        ++ lib.optional (config.networking.hostName != "kilimanjaro") {
          inherit (kilimanjaro.config.networking) hostName;
          systems = [
            "x86_64-linux"
            "i686-linux"
          ];
          sshUser = "natsukium";
          inherit protocol;
          maxJobs = kilimanjaro.config.nix.settings.max-jobs;
          speedFactor = 1;
          supportedFeatures = kilimanjaro.config.nix.settings.system-features;
          mandatoryFeatures = [ ];
        }
        ++ lib.optional (config.networking.hostName != "serengeti") {
          inherit (serengeti.config.networking) hostName;
          system = "aarch64-linux";
          sshUser = "natsukium";
          inherit protocol;
          maxJobs = serengeti.config.nix.settings.max-jobs;
          speedFactor = 1;
          supportedFeatures = serengeti.config.nix.settings.system-features;
          mandatoryFeatures = [ ];
        }
        ++ lib.optional (config.networking.hostName != "mikumi" && config.networking.hostName != "work") {
          inherit (mikumi.config.networking) hostName;
          systems = [
            "aarch64-darwin"
            "x86_64-darwin"
          ];
          sshUser = "natsukium";
          inherit protocol;
          maxJobs = mikumi.config.nix.settings.max-jobs;
          speedFactor = 1;
          supportedFeatures = [
            "apple-virt"
            "benchmark"
            "big-parallel"
            "nixos-test"
          ];
          mandatoryFeatures = [ ];
        };
      
    4. SSH Known Hosts

      Distributed builds connect to remote machines over SSH, so each build machine's host key must be registered to avoid interactive verification prompts during unattended builds.

      programs.ssh.knownHosts = {
        tarangire.publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEJJYgE/dmYLXYBrVnPicd0qsaUeqcBtXB8H9LHkJ2j4";
        kilimanjaro.publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILhpfAalh6A5xDSE+HOdNE29ZgIjlP7tdlhHs82boSwp";
        serengeti.publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDWwhfhDSZ+M2XDwP2MlC/zFfVpk3WjUxV/JWFgGzgNW";
        mikumi.publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPfWOWKBFuDV08g6xP9MMY78CERI02CNG+5dy8CXQmXs";
      };
      
  4. User-Level Settings

    User-level Nix settings, managed through home-manager because they belong to the user's environment rather than the system configuration.

    XDG base directories are enabled to keep ~/.nix-defexpr and ~/.nix-profile under ~/.config/nix/ and ~/.local/state/nix/, respectively, reducing dotfile clutter in $HOME.

    The result symlink is added to git ignores because nix build creates result symlinks in the project root by default, and these should never be committed. This applies to the user's global gitignore, not to any specific repository.

    { config, ... }:
    {
      nix.settings.use-xdg-base-directories = config.xdg.enable;
    
      programs.git.ignores = [ "result" ];
    }
    

3.3.3. Networking

  1. Tailscale

    Opinionated Tailscale VPN configuration for NixOS systems. This module provides sensible defaults for running Tailscale with MagicDNS, SSH access, and proper firewall configuration.

    { config, lib, ... }:
    let
      cfg = config.my.services.tailscale;
    in
    {
      <<tailscale-options>>
    
      <<tailscale-config>>
    }
    
    1. Options

      Module options for controlling Tailscale behavior.

      options.my.services.tailscale = {
        enable = lib.mkEnableOption "Tailscale VPN";
      
        <<configureResolver-option>>
      };
      
      1. configureResolver

        Desktop systems may experience DNS resolution failures after suspend/resume. When the system resumes, external domain resolution (e.g., github.com) fails while Tailnet hostnames continue to work. This is a known issue with Tailscale's DNS state management during network transitions.

        Enabling configureResolver activates systemd-resolved, which helps mitigate DNS resolution issues after suspend/resume. While this doesn't completely eliminate the issue (the upstream bug remains open), it provides the most reliable MagicDNS experience.

        For headless servers, this option is typically unnecessary because servers don't suspend, and thus never encounter this resume-related DNS bug.

        See tailscale/tailscale#4254 for the upstream discussion.

        configureResolver = lib.mkOption {
          type = lib.types.bool;
          default = false;
          description = ''
            Enable systemd-resolved for Tailscale DNS.
            Recommended for desktop systems to mitigate DNS failures after suspend/resume.
            https://github.com/tailscale/tailscale/issues/4254
          '';
        };
        
    2. Configuration
      config = lib.mkIf cfg.enable (
        lib.mkMerge [
          {
            <<tailscale-service>>
      
            <<tailscale-networking>>
      
            <<tailscale-secrets>>
          }
          <<tailscale-resolver>>
        ]
      );
      
      1. Service Settings

        Core Tailscale service configuration.

        useRoutingFeatures = "server" enables this machine to act as a subnet router or exit node. All machines in this configuration can potentially serve as exit nodes for other devices, providing flexibility when traveling or on restricted networks.

        authKeyFile points to a SOPS-managed secret containing a Tailscale auth key. Using auth keys enables unattended authentication, which is essential for automated deployments and GitOps workflows with tools like comin.

        --ssh enables Tailscale SSH, allowing SSH access over the Tailscale network without managing SSH keys or exposing port 22 to the public internet.

        services.tailscale = {
          enable = true;
          useRoutingFeatures = "server";
          authKeyFile = config.sops.secrets.tailscale-authkey.path;
          extraUpFlags = [ "--ssh" ];
        };
        
      2. Networking

        Firewall and DNS configuration for Tailscale integration.

        The tailscale0 interface is trusted because all traffic on this interface is authenticated by Tailscale. This allows services to be exposed only to the tailnet without additional firewall rules.

        100.100.100.100 is Tailscale's MagicDNS resolver, enabling resolution of tailnet hostnames (e.g., hostname.tail4108.ts.net). 8.8.8.8 provides fallback for non-tailnet queries, though in practice MagicDNS handles forwarding to upstream resolvers.

        The search domain is the tailnet domain, allowing short hostnames (e.g., ssh manyara instead of ssh manyara.tail4108.ts.net).

        networking = {
          firewall = {
            trustedInterfaces = [ "tailscale0" ];
            allowedUDPPorts = [ config.services.tailscale.port ];
          };
          nameservers = [
            "100.100.100.100"
            "8.8.8.8"
          ];
          search = [ "tail4108.ts.net" ];
        };
        
      3. Secrets

        SOPS secret declaration for the Tailscale auth key. The actual key is stored encrypted in the repository's secrets file and decrypted at activation time.

        sops.secrets.tailscale-authkey = { };
        
      4. Resolver

        Conditional systemd-resolved configuration. Only enabled when configureResolver is true, typically on desktop systems that suspend/resume.

        (lib.mkIf cfg.configureResolver {
          services.resolved.enable = cfg.configureResolver;
        })
        

3.3.4. Shell

Interactive shells and scripting shells serve different purposes. An interactive shell does not need to be POSIX-compatible — what matters is usability, broad environment support, and extensibility.

  1. Fish

    Fish is the primary interactive shell, chosen for its out-of-the-box experience: syntax highlighting, autosuggestions, and tab completions work without configuration or plugins. Among shells with broad environment and software support, fish provides the most convenient and extensible interactive experience with the least setup effort.

    { pkgs, ... }:
    {
      programs.fish = {
        enable = true;
    
        <<fish-interactive-shell-init>>
    
        <<fish-abbreviations>>
    
        <<fish-functions>>
    
        <<fish-plugins>>
      };
    }
    
    1. Interactive Shell Init

      Settings applied when fish starts an interactive session: keybindings, plugin configuration, environment variables, and shell integrations.

      interactiveShellInit = ''
        <<fish-keybindings>>
      
        <<fish-done-config>>
      
        <<fish-pinentry>>
      
        <<fish-extra-abbrs>>
      
        <<fish-any-nix-shell>>
      '';
      
      1. Keybindings

        Ctrl+S is bound to zi (zoxide interactive mode) for fuzzy directory jumping.

        bind \cs zi
        
      2. Pinentry

        When connected via SSH, GPG's pinentry is switched to the curses (terminal) variant. The default graphical pinentry cannot display on a remote session without X11/Wayland forwarding, so the TUI fallback is necessary for signing commits and decrypting secrets over SSH. See Gentoo Wiki: GnuPG - Changing pinentry for SSH logins.

        # set environment variable for pinentry
        if test "$SSH_CONNECTION" != ""
          set -x PINENTRY_USER_DATA "USE_CURSES"
        end
        
      3. Extra Abbreviations

        Position-aware abbreviations that use fish's advanced features (--position anywhere, --regex, --function) which are not available through home-manager's shellAbbrs option.

        # extra abbrs
        abbr -a L --position anywhere --set-cursor "% | less"
        abbr -a !! --position anywhere --function _abbr_last_history_item
        abbr -a extract_tar_gz --position command --regex ".+\.tar\.gz" --function _abbr_extract_tar_gz
        abbr -a dotdot --regex '^\.\.+$' --function _abbr_multicd
        
      4. any-nix-shell

        any-nix-shell keeps fish as the interactive shell inside nix shell and nix develop environments. Without it, entering a Nix shell drops into bash (the default builder shell), losing fish's interactive features. This is sourced at the end of interactiveShellInit so it can wrap the shell entry point after all other configuration is applied.

        ${pkgs.any-nix-shell}/bin/any-nix-shell fish | source
        
    2. Abbreviations
      shellAbbrs = {
        <<fish-abbr-general>>
      
        <<fish-abbr-nix>>
      };
      
      1. General
        # spellchecker:off
        l = "ls";
        
      2. Nix Remote Build

        Abbreviations for specifying Nix build target systems, primarily used when working on nixpkgs. Each abbreviation expands to --system <triple> and conditionally appends -j0 when the target system differs from the current host. -j0 sets the local job limit to zero, forcing all builds to be delegated to remote builders (see Distributed Builds). This prevents Nix from accidentally attempting to build on the host, which would fail with an architecture mismatch.

        "--sxl" = {
          position = "anywhere";
          expansion =
            "--system x86_64-linux"
            + pkgs.lib.optionalString (
              pkgs.stdenv.hostPlatform.isDarwin || pkgs.stdenv.hostPlatform.isAarch64
            ) " -j0";
        };
        "--sal" = {
          position = "anywhere";
          expansion =
            "--system aarch64-linux"
            + pkgs.lib.optionalString (
              pkgs.stdenv.hostPlatform.isDarwin || pkgs.stdenv.hostPlatform.isx86_64
            ) " -j0";
        };
        "--sxd" = {
          position = "anywhere";
          expansion =
            "--system x86_64-darwin" + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux " -j0";
        };
        "--sad" = {
          position = "anywhere";
          expansion =
            "--system aarch64-darwin" + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux " -j0";
        };
        # spellchecker:on
        
    3. Functions

      Helper functions used by the abbreviation system. These are separated from the abbreviation definitions because fish requires functions to be defined before they can be referenced by --function abbreviations.

      functions = {
        _abbr_last_history_item = "echo $history[1]";
        _abbr_extract_tar_gz = "echo tar avfx $argv";
        _abbr_multicd = "echo cd (string repeat -n (math (string length -- $argv[1]) - 1) ../)";
      };
      
    4. Plugins
      plugins = [
        {
          name = "done";
          src = pkgs.fishPlugins.done.src;
        }
        {
          name = "fzf-fish";
          src = pkgs.fishPlugins.fzf-fish.src;
        }
      ];
      

      The done plugin provides desktop notifications for long-running commands. The threshold is set to 15 seconds — commands shorter than this are typically interactive and do not benefit from notifications, while longer commands (builds, test suites, large file operations) are often run in the background where a notification is valuable.

      # set done's variable
      set -U __done_min_cmd_duration 15000
      

      fzf-fish integrates fzf with fish's tab completion, history search, and file/directory navigation. It replaces fish's built-in history search (Ctrl+R) with fzf's fuzzy finder, which handles large histories more effectively.

  2. Bash

    Bash is configured as a minimal fallback shell rather than a full interactive environment. The primary use case is ensuring a usable shell exists when fish is unavailable, and providing a POSIX-compatible shell for scripts and tools that assume bash. It also serves as a testing environment when fish exhibits unexpected behaviour — having a clean bash available helps isolate whether an issue is fish-specific.

    {
      pkgs,
      lib,
      config,
      ...
    }:
    {
      home.packages = with pkgs; [ bashInteractive ];
      programs.bash = {
        enable = true;
    
        <<bash-history>>
    
        <<bash-completion>>
    
        <<bash-aliases>>
    
        <<bash-init>>
      };
    }
    
    1. History

      History is stored under XDG_CONFIG_HOME to keep $HOME clean, consistent with the repository's preference for XDG base directories (see User-Level Settings).

      historyFile = "$XDG_CONFIG_HOME/bash/history";
      
    2. Completion

      Bash completion is disabled because bash is not used as the primary interactive shell. Loading completion scripts adds startup time (~100ms+) with no benefit when the shell is only used as a fallback or for running scripts.

      enableCompletion = false;
      
    3. Aliases

      Basic aliases that provide a minimal quality-of-life improvement if bash is used interactively. These are intentionally simple — fish handles the rich interactive experience.

      shellAliases = {
        l = "ls -CF";
        grep = "grep --color=auto";
        fgrep = "fgrep --color=auto";
        egrep = "egrep --color=auto";
      };
      
    4. Init

      This configuration is a remnant from before fish was set as the default login shell. Previously, bash was the login shell and .bashrc launched fish, so these settings were needed in every interactive session. They are retained because bash is still used as a fallback.

      initExtra = ''
        <<bash-terminal-settings>>
      ''
      + lib.optionalString (!config.programs.kitty.enable) ''
        <<bash-tmux-autostart>>
      '';
      
      1. Terminal Settings
        • stty stop undef: Disables Ctrl+S from sending XOFF (terminal pause), freeing it for use as a keybinding in fish and other TUI applications. Without this, pressing Ctrl+S freezes the terminal until Ctrl+Q is pressed — a legacy flow control mechanism that is rarely useful on modern terminals.
        • stty werase undef + bind "\C-w":unix-filename-rubout: Rebinds Ctrl+W to delete the previous path component (stopping at /) rather than the entire word. This is more useful when editing file paths, which is the common case in a shell.
        stty stop undef  # Ctrl-s
        stty werase undef
        bind "\C-w":unix-filename-rubout  # Ctrl-w
        
      2. TMUX Auto-attach

        Automatically starts or attaches to a tmux session when bash is the interactive shell, but only when kitty is not the terminal emulator. Kitty has its own tab/window management (splits, layouts, tabs) that conflicts with tmux's multiplexing — running tmux inside kitty creates redundant nesting. When using a simpler terminal (e.g., xterm, alacritty, or a Linux console), tmux provides the session persistence and window management that would otherwise be missing.

        # TMUX (from ArchWiki)
        if type tmux > /dev/null 2>&1; then
          # if no session is started, start a new session
          test -z $TMUX && tmux
        
          # when quitting tmux, try to attach
          while test -z $TMUX; do
            tmux attach || break
          done
        fi
        
  3. Nushell

    Nushell is enabled for its ability to handle structured data natively, similar to PowerShell. Where traditional shells pipe text between commands, Nushell operates on serialised data formats (JSON, YAML, CSV, etc.) as first-class tables and records, making simple data manipulation possible without reaching for external tools like jq. Like fish, it is non-POSIX, but POSIX compatibility is not a requirement for an interactive shell.

    No custom configuration is added yet — the defaults are sufficient for exploratory use, and committing to Nushell-specific workflows would be premature.

    { config, ... }:
    {
      programs.nushell = {
        enable = true;
      };
    }
    
  4. Starship

    Starship is a cross-shell prompt written in Rust, used across all shells configured here to provide a consistent prompt experience. It has been used since its predecessor spacefish, and its easy TOML-based configuration is the main reason it has been kept.

    Starship does not natively support asynchronous prompt rendering, so this section also includes a custom async prompt module for fish.

    { pkgs, ... }:
    {
      programs.starship = {
        enable = true;
        settings = builtins.fromTOML (builtins.readFile ./starship.toml);
      };
      my.programs.starship.enableFishAsyncPrompt = true;
    }
    
    1. Prompt Format

      The prompt format prepends a shell indicator before the default modules. This makes it immediately visible which shell is active, useful when switching between fish and bash for testing or debugging.

      "$schema" = "https://starship.rs/config-schema.json"
      
      format = "$shell$all$line_break$character"
      
    2. Shell Indicator

      Each shell has a distinct icon: 󰈺 (fish icon) for fish. Bash and nushell use the default indicator.

      [shell]
      fish_indicator = "󰈺"
      powershell_indicator = "󰨊"
      disabled = false
      
    3. Remote Container Detection

      A custom module detects when running inside a VS Code Remote Container (Dev Container) by checking the REMOTE_CONTAINERS environment variable, displaying a whale emoji (🐋) as a visual reminder. This has not been used for several years and may no longer be relevant.

      [custom.remote-container]
      when = """ test "$REMOTE_CONTAINERS" """
      symbol = "🐋"
      format = " in $symbol "
      
    4. Disabled Modules

      gcloud is disabled because it reads the active GCP configuration from the home directory (~/.config/gcloud/), causing it to appear in every repository regardless of whether the project uses GCP.

      [gcloud]
      disabled = true
      
    5. Async Prompt Module

      A custom home-manager module that replaces starship's default fish integration with an asynchronous variant. The async prompt script is cherry-picked from duament's gist, based on fish-async-prompt. See also fish-shell#8223 for upstream discussion on async prompt support.

      {
        config,
        lib,
        ...
      }:
      let
        inherit (lib)
          mkForce
          mkIf
          mkOption
          types
          ;
        cfg = config.my.programs.starship;
      in
      {
        options = {
          my.programs.starship = {
            enableFishAsyncPrompt = mkOption {
              type = types.bool;
              default = false;
            };
          };
        };
        config = mkIf cfg.enableFishAsyncPrompt {
          # use my own script to ensure the execution order
          programs.starship.enableFishIntegration = mkForce false;
      
          programs.fish.interactiveShellInit = ''
            if test "$TERM" != dumb
                ${lib.getExe config.programs.starship.package} init fish | source
                source ${./async_prompt.fish}
            end
          '';
        };
      }
      
      1. Async Prompt Script
        # cherry picked from https://gist.github.com/duament/bac0181935953b97ca71640727c9c029
        status is-interactive
        or exit 0
        
        if test -n "$XDG_RUNTIME_DIR"
            set -g __starship_async_tmpdir "$XDG_RUNTIME_DIR"/fish-async-prompt
        else
            set -g __starship_async_tmpdir /tmp/fish-async-prompt
        end
        mkdir -p "$__starship_async_tmpdir"
        set -g __starship_async_signal SIGUSR1
        
        # Starship
        set -g VIRTUAL_ENV_DISABLE_PROMPT 1
        builtin functions -e fish_mode_prompt
        set -gx STARSHIP_SHELL fish
        set -gx STARSHIP_SESSION_KEY (random 10000000000000 9999999999999999)
        
        # Prompt
        function fish_prompt
            printf '\e[0J' # Clear from cursor to end of screen
            if test -e "$__starship_async_tmpdir"/"$fish_pid"_fish_prompt
                cat "$__starship_async_tmpdir"/"$fish_pid"_fish_prompt
            else
                __starship_async_simple_prompt
            end
        end
        
        # Async task
        function __starship_async_fire --on-event fish_prompt
            switch "$fish_key_bindings"
                case fish_hybrid_key_bindings fish_vi_key_bindings
                    set STARSHIP_KEYMAP "$fish_bind_mode"
                case '*'
                    set STARSHIP_KEYMAP insert
            end
            set STARSHIP_CMD_PIPESTATUS $pipestatus
            set STARSHIP_CMD_STATUS $status
            set STARSHIP_DURATION "$CMD_DURATION"
            set STARSHIP_JOBS (count (jobs -p))
        
            set -l tmpfile "$__starship_async_tmpdir"/"$fish_pid"_fish_prompt
            fish -c '
        starship prompt --terminal-width="'$COLUMNS'" --status='$STARSHIP_CMD_STATUS' --pipestatus="'$STARSHIP_CMD_PIPESTATUS'" --keymap='$STARSHIP_KEYMAP' --cmd-duration='$STARSHIP_DURATION' --jobs='$STARSHIP_JOBS' > '$tmpfile'
        kill -s "'$__starship_async_signal'" '$fish_pid &
            disown
        end
        
        function __starship_async_simple_prompt
            set_color brgreen
            echo -n '❯'
            set_color normal
            echo ' '
        end
        
        function __starship_async_repaint_prompt --on-signal "$__starship_async_signal"
            commandline -f repaint
        end
        
        function __starship_async_cleanup --on-event fish_exit
            rm -f "$__starship_async_tmpdir"/"$fish_pid"_fish_prompt
        end
        
        # https://github.com/acomagu/fish-async-prompt
        # https://github.com/fish-shell/fish-shell/issues/8223
        

3.3.5. Version Control

  1. Git

    Git version control configuration. Git is configured directly through home-manager's programs.git module, which generates ~/.config/git/config declaratively.

    { pkgs, config, ... }:
    {
      programs.git = {
        enable = true;
    
        <<git-settings>>
    
        <<git-signing>>
    
        <<git-ignores>>
    
        <<git-scalar>>
      };
    
      <<git-gh>>
    
      <<git-delta>>
    
      <<git-difftastic>>
    
      <<git-lazygit>>
    
      <<git-fish-abbreviations>>
    }
    
    1. Core Settings

      Basic git behavior settings.

      settings = {
        <<git-user-identity>>
        <<git-editor>>
        <<git-color>>
        <<git-default-branch>>
        <<git-push-safety>>
        <<git-url-rewrite>>
      };
      
      1. User Identity
        user = {
          name = "natsukium";
          email = "[email protected]";
        };
        
      2. Editor

        core.editor = "vim" is set as the fallback editor for git operations. Although the EDITOR environment variable is set to nvim in the home configuration, some contexts (like git commit in a minimal environment) may not inherit the session's environment variables. Setting it explicitly in gitconfig ensures consistent behavior.

        core.editor = "vim";
        
      3. Color

        Auto-detect terminal color support for all git subcommands.

        color = {
          status = "auto";
          diff = "auto";
          branch = "auto";
          interactive = "auto";
          grep = "auto";
        };
        
      4. Default Branch
        init.defaultBranch = "main";
        
      5. Force Push Safety

        push.useForceIfIncludes enables a safety check for force pushes: git verifies that the local branch includes the remote's current tip before allowing a force push. This prevents accidentally overwriting commits pushed by others since the last fetch, which --force-with-lease alone cannot catch if the remote ref was updated in the background (e.g., by lazygit's periodic fetch or a background git fetch).

        push.useForceIfIncludes = true;
        
      6. URL Rewriting

        url."[email protected]:".pushInsteadOf rewrites push URLs from HTTPS to SSH. This allows git clone https://github.com/... to work without SSH (useful in CI or ephemeral environments) while ensuring pushes always use SSH authentication, avoiding the need for personal access tokens.

        In practice, gh provides its own credential helper that handles HTTPS authentication transparently, so this rewrite rule may not be exercised for GitHub repositories. It is retained as an intentional fallback for environments where gh is not available.

        url."[email protected]:".pushInsteadOf = "https://github.com/";
        
    2. Commit Signing

      SSH-based commit signing. SSH keys were chosen over GPG because SSH keys are already required for push authentication, eliminating the need to manage a separate GPG keychain. Git's SSH signing support (added in Git 2.34) provides the same integrity guarantees as GPG signing with simpler key management — one key pair for both authentication and signing.

      GitHub has supported SSH commit verification since August 2022, and since November 2024 verification results are persisted server-side. This persistent verification eliminates the primary concern with SSH signing — that key rotation could invalidate historical signatures — which was GPG's main advantage for long-term identity assurance.

      In the future, migrating to a keyless solution like gitsign (Sigstore-based signing) would further simplify the workflow by removing key management entirely. This is pending GitHub's native support for Sigstore verification.

      signByDefault = true ensures all commits are signed without requiring git commit -S, preventing unsigned commits from slipping through.

      signing = {
        format = "ssh";
        key = "~/.ssh/id_ed25519.pub";
        signByDefault = true;
      };
      
    3. Global Ignores

      Patterns that should never be committed across all repositories. These are global ignores rather than per-repo .gitignore entries because they reflect personal tooling choices that should not be imposed on collaborators.

      ignores = [
        <<git-ignores-os>>
        <<git-ignores-dev>>
        <<git-ignores-workflow>>
        <<git-ignores-private>>
      ];
      
      1. OS Artifacts

        macOS Finder metadata files. Present in the global ignore because this is a cross-platform dotfiles repository used on both Linux and macOS.

        ".DS_Store"
        
      2. Development Environment

        Artifacts generated by development tools that should remain local:

        • .aider* matches aider's conversation history and configuration files. These contain session-specific context and should not leak into repositories.
        • .direnv, .envrc: direnv state and configuration. Each project may define its own .envrc, but since this repository uses flake-based devshells, the .envrc files are auto-generated and should not be committed.
        • .ipynb_checkpoints: Jupyter Notebook autosave files.
        • .pre-commit-config.yaml: Managed per-project by devshell environments rather than committed, to avoid version conflicts across contributors with different tool versions.
        • .vscode/: Editor-specific settings that vary per developer.
        • __pycache__/: Python bytecode cache, regenerated on each run.
        ".aider*"
        ".direnv"
        ".envrc"
        ".ipynb_checkpoints"
        ".pre-commit-config.yaml"
        ".vscode/"
        "__pycache__/"
        
      3. Workflow
        • .worktree: Marker file used by git worktree workflows.
        ".worktree"
        
      4. Private Notes

        .private/ is a directory for personal notes, LLM context files, and other local-only documentation that should never be shared.

        ".private/"
        
    4. Scalar

      Scalar optimizes git performance for large repositories. Enabled specifically for the nixpkgs repository, which contains over 600,000 commits and benefits significantly from Scalar's background maintenance (prefetch, commit-graph, loose-objects) and filesystem monitor integration.

      The Scalar module (programs.git.scalar) is a custom home-manager module defined in home-manager/git-scalar.nix that configures git's built-in performance optimizations (multipack index, preload index, untracked cache, fsmonitor).

      scalar = {
        enable = true;
        repo = [ "${config.programs.git.settings.ghq.root}/github.com/natsukium/nixpkgs" ];
      };
      
    5. GitHub CLI

      gh (GitHub CLI) is enabled alongside git because it provides a credential helper (programs.gh.gitCredentialHelper.enable, on by default) that transparently handles HTTPS authentication for GitHub repositories. This is why the pushInsteadOf setting may not be exercised in practice — gh's auth context covers both fetch and push over HTTPS.

      programs.gh.enable = true;
      
    6. Delta

      delta provides syntax-highlighted diffs in the terminal with line numbers and side-by-side view support. Chosen for its clean visual presentation.

      programs.delta = {
        enable = true;
        enableGitIntegration = true;
      };
      
    7. Difftastic

      Difftastic is a structural diff tool that operates at the AST level rather than comparing lines. It understands that moving a function or renaming a variable is a single semantic change, not a block of deletions and insertions. This is particularly valuable for JSX refactoring, indentation shifts, and nested tag restructuring, where line-based diffs produce noisy, hard-to-read output.

      Both difftastic and delta are kept because they serve complementary roles: delta enhances git's built-in line diffs with syntax highlighting for everyday operations (git diff, git log), while difftastic is reached for via lazygit when structural understanding matters.

      programs.difftastic = {
        enable = true;
      };
      
    8. Lazygit

      Lazygit is a terminal UI for git. A TUI was chosen over raw git commands for interactive operations like staging individual hunks, interactive rebase, and conflict resolution, where the visual feedback loop significantly reduces errors. Previously used gitui, but switched to lazygit for its broader feature set — gitui's performance advantage did not materialize in practice, and it lacked several workflow-critical operations.

      Performance degrades noticeably on large repositories like nixpkgs, but the ergonomics and active development make it a trusted part of the workflow despite this tradeoff.

      programs.lazygit = {
        enable = true;
        settings = {
          <<lazygit-gui>>
      
          <<lazygit-git>>
        };
      };
      
      1. GUI

        Enable Nerd Font icons in the lazygit interface for visual clarity.

        gui = {
          showIcons = true;
        };
        
      2. Git Integration

        overrideGpg = true tells lazygit to run git commands inline instead of spawning a subprocess for GPG/SSH signing. By default, lazygit defers to a subprocess so the user can type a passphrase interactively. However, this subprocess mode prevents lazygit from orchestrating interactive rebase internally — rewording, reordering, or editing any commit other than HEAD is disabled entirely because the rebase would trigger multiple signing prompts that lazygit cannot handle mid-rebase. With overrideGpg = true, lazygit assumes the signing agent (ssh-agent, macOS Keychain) caches the passphrase, so no interactive prompt is needed and all rebase operations work normally.

        The pager configuration integrates both delta and difftastic into lazygit's diff views. Delta provides syntax-highlighted line diffs (--dark --paging=never since lazygit handles its own paging), and difftastic is available as the external diff command for structural comparison.

        git = {
          overrideGpg = true;
          pagers = [
            {
              colorArg = "always";
              pager = "delta --dark --paging=never";
            }
            {
              externalDiffCommand = "difft --color=always";
            }
          ];
        };
        
    9. Fish Abbreviations

      Shell abbreviations for frequent git operations. Abbreviations (not aliases) are used because fish expands them inline before execution, making the actual command visible in history and allowing modification before running.

      programs.fish.shellAbbrs = {
        <<git-abbr-push>>
        <<git-abbr-pull>>
        <<git-abbr-commit>>
        <<git-abbr-status>>
        <<git-abbr-stash>>
        <<git-abbr-switch>>
      };
      
      1. Push

        Force push with lease — safer than --force as it prevents overwriting remote changes not yet fetched.

        gpf = "git push --force-with-lease";
        
      2. Pull

        gpm pulls from the remote's default branch, dynamically detected via git remote show rather than hardcoding main or master. This handles repositories that still use master or other branch naming conventions.

        gpu pulls from upstream — used in fork workflows to sync with the original repository.

        gpm = "git pull (git remote show origin | sed -n '/HEAD branch/s/.*: //p')";
        gpu = "git pull upstream";
        
      3. Commit

        gci creates a commit. The trailing space allows directly appending -m "message".

        gca amends the last commit, frequently used during interactive rebase workflows.

        gci = "git commit ";
        gca = "git commit --amend";
        
      4. Status
        gs = "git status";
        
      5. Stash
        gst = "git stash";
        gstp = "git stash pop";
        
      6. Switch
        gsw = "git switch";
        gswc = "git switch -c";
        

3.3.6. Browser

  1. Zen Browser

    Zen Browser is a Firefox-based browser with a focus on privacy and customization. Firefox derivatives were preferred over Chromium-based browsers because Firefox's extension ecosystem and about:config provide deeper control over browser behavior, and the Nix ecosystem has mature tooling for managing Firefox profiles declaratively via home-manager.

    Zen Browser was chosen over vanilla Firefox for its improved UI/UX while retaining full Firefox compatibility — extensions, search engines, and profile settings work identically.

    {
      inputs,
      config,
      lib,
      pkgs,
      ...
    }:
    let
      cfg = config.my.programs.zen-browser;
    in
    {
      imports = [ inputs.zen-browser.homeModules.beta ];
    
      options.my.programs.zen-browser = {
        enable = lib.mkEnableOption "Zen Browser";
      };
    
      config = lib.mkIf cfg.enable {
        programs.zen-browser = {
          enable = true;
          profiles.natsukium = {
            <<zen-browser-settings>>
    
            <<zen-browser-search>>
    
            <<zen-browser-extensions>>
          };
        };
      };
    }
    
    1. Settings

      Auto-install extensions without user prompts. By default, Firefox shows a confirmation dialog for each extension installed via the profile. Setting extensions.autoDisableScopes to 0 disables this behavior, which is necessary for fully declarative extension management — otherwise the first launch after a profile rebuild would require manual approval for every extension.

      settings = {
        "extensions.autoDisableScopes" = 0;
      };
      
    2. Search Engines

      Custom search engines for quick access to development-related package registries and documentation. Each engine is assigned a short alias (e.g., @np) for use in the address bar, avoiding the need to navigate to each site manually.

      search = {
        force = true;
        engines = {
          <<zen-browser-search-engine-nix-packages>>
      
          <<zen-browser-search-engine-nixos-wiki>>
      
          <<zen-browser-search-engine-noogle>>
      
          <<zen-browser-search-engine-crates-io>>
      
          <<zen-browser-search-engine-npm>>
      
          <<zen-browser-search-engine-pypi>>
        };
      };
      
      1. Nix Packages

        Search the official NixOS/nixpkgs repository. Uses nixos-icons from nixpkgs for the icon to avoid depending on an external URL that could change or become unavailable.

        nix-packages = {
          name = "Nix Packages";
          urls = [
            {
              template = "https://search.nixos.org/packages";
              params = [
                {
                  name = "type";
                  value = "packages";
                }
                {
                  name = "query";
                  value = "{searchTerms}";
                }
              ];
            }
          ];
        
          icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
          definedAliases = [ "@np" ];
        };
        
      2. NixOS Wiki

        Search the NixOS community wiki for configuration examples and troubleshooting guides.

        nixos-wiki = {
          name = "NixOS Wiki";
          urls = [ { template = "https://wiki.nixos.org/w/index.php?search={searchTerms}"; } ];
          icon = "https://wiki.nixos.org/favicon.ico";
          definedAliases = [ "@nw" ];
        };
        
      3. noogle

        noogle is a Nix function search engine — the equivalent of Hoogle for Haskell. Useful for discovering library functions by type signature or name when writing Nix expressions.

        noogle = {
          name = "noogle";
          urls = [ { template = "https://noogle.dev/q?term={searchTerms}"; } ];
          icon = "https://noogle.dev/favicon.png";
          definedAliases = [ "@noogle" ];
        };
        
      4. crates.io

        Search the Rust package registry for crate discovery and version information.

        crates-io = {
          name = "crates.io";
          urls = [ { template = "https://crates.io/search?q={searchTerms}"; } ];
          icon = "https://crates.io/favicon.ico";
          definedAliases = [ "@crates" ];
        };
        
      5. npm

        Search the npm registry for JavaScript/TypeScript package discovery.

        npm = {
          name = "npm";
          urls = [ { template = "https://www.npmjs.com/search?q={searchTerms}"; } ];
          icon = "https://www.google.com/s2/favicons?domain=npmjs.com&sz=64";
          definedAliases = [ "@npm" ];
        };
        
      6. PyPI

        Search the Python Package Index for Python package discovery.

        pypi = {
          name = "PyPI";
          urls = [ { template = "https://pypi.org/search/?q={searchTerms}"; } ];
          icon = "https://pypi.org/favicon.ico";
          definedAliases = [ "@pypi" ];
        };
        
    3. Extensions

      Browser extensions managed declaratively. Extensions are sourced from two overlay-provided sets: firefox-addons (from the NUR firefox-addons repository) and my-firefox-addons (custom additions not available upstream).

      extensions = {
        packages =
          (with pkgs.firefox-addons; [
            bitwarden
            instapaper-official
            keepa
            onepassword-password-manager
            refined-github
            tampermonkey
            vimium
            wayback-machine
            zotero-connector
          ])
          ++ (with pkgs.my-firefox-addons; [
            adguard-adblocker
            calilay
            kiseppe-price-chart-kindle
          ]);
      };
      

3.4. Emacs

3.4.1. early-init.org

  1. disable some bars

    The tool bar duplicates functionality already accessible via keybindings and the menu bar, while the vertical scroll bar wastes horizontal space without providing useful navigation—line numbers and modeline indicators are more precise. Setting these in default-frame-alist rather than calling tool-bar-mode / scroll-bar-mode avoids a brief flash of the bars during startup before the modes disable them.

    (push '(tool-bar-lines . 0) default-frame-alist)
    (push '(vertical-scroll-bars) default-frame-alist)
    
  2. turn off the annoying bell

    The default audible bell is distracting and provides no actionable information. Replacing it with ignore silences it entirely; a visual bell (visible-bell) was not chosen because the screen flash is equally disruptive.

    (setq ring-bell-function 'ignore)
    
  3. disable backup files

    Emacs already skips backup files for version-controlled files by default (vc-make-backup-files is nil), but files outside of a repository still produce backups. Since virtually all editing happens in Git-managed trees, the remaining cases are rare enough that the clutter outweighs the safety net. Auto-save files are likewise redundant when unsaved work can be recovered through Git stashes or reflog.

    (setq make-backup-files nil)
    (setq auto-save-default nil)
    
  4. disable lock files

    Lock files (.#filename) prevent concurrent editing across multiple Emacs instances, but in a single-instance workflow they only clutter the working directory and interfere with file watchers and build tools.

    (setq create-lockfiles nil)
    

3.4.2. init.org

  1. basic
    (add-to-list 'default-frame-alist
    '(undecorated-round . t))
    (use-package doom-themes
      :ensure t
      :config
      (load-theme 'doom-nord :no-confirm))
    

    Enable visual-line-mode globally instead of using auto-fill-mode here to preserve logical line structure while still wrapping long lines visually for better readability without modifying the actual file.

    (visual-line-mode 1)
    

    Highlight the cursor line.

    (global-hl-line-mode 1)
    

    Accept single-character y / n responses instead of requiring the full word yes / no.

    (setq use-short-answers t)
    
    1. Font

      Moralerspace is a monaspace-based composite font family that pairs a Latin programming typeface with Japanese glyphs. Each variant—Argon, Krypton, Radon, Xenon—has its own typographic character, so assigning a different variant to each face produces visually distinct bold, italic, and bold-italic rendering without relying on Emacs's synthetic transformations. :weight 'normal and :slant 'normal prevent Emacs from further synthesizing bold or italic on top of the variant's own design.

      font-lock-comment-face uses Radon (the italic variant) to give code comments a softer, visually distinct appearance.

      (set-face-attribute 'default nil :family "Moralerspace Argon HW" :height 140)
      (set-face-attribute 'bold nil :family "Moralerspace Krypton HW" :weight 'normal)
      (set-face-attribute 'italic nil :family "Moralerspace Radon HW" :slant 'normal)
      (set-face-attribute 'bold-italic nil :family "Moralerspace Xenon HW" :weight 'normal :slant 'normal)
      (set-face-attribute 'font-lock-comment-face nil :family "Moralerspace Radon HW" :slant 'normal)
      

      Without an explicit set-language-environment, Emacs may select a Chinese font for CJK characters based on locale order, causing Japanese text to render with Chinese glyphs.

      (set-language-environment "Japanese")
      
      1. TODO Enable texture healing
    2. Nix wrapper paths

      The Nix Emacs wrapper (extraEmacsPackages) adds binaries to exec-path that are absent from the shell's PATH. Both exec-path-from-shell and envrc replace exec-path during operation, losing these entries. Capturing them once at init allows both packages to merge them back.

      (setq my/nix-exec-path exec-path)
      
    3. Darwin

      On macOS, Emacs cannot inherit environment variables (such as $PATH) from the shell, so exec-path-from-shell is needed as a workaround. emacs-plus injects PATH into Emacs's Info.plist, so applying that patch might eliminate the need for this package.

      exec-path-from-shell-initialize replaces exec-path with the login shell's PATH. Merging my/nix-exec-path back after initialization preserves wrapper-provided paths.

      When launched from an application launcher (Dock, Spotlight), Emacs inherits only the minimal launchd environment, which lacks Nix-related variables. Without these, direnv export may produce incomplete results. (envrc#92)

      Separately, fish sources conf.d/*.fish even for non-interactive invocations (fish -c "..."). nix-darwin places environment setup scripts there that re-initialize PATH from system profiles when guard variables (__fish_nixos_env_preinit_sourced, __NIX_DARWIN_SET_ENVIRONMENT_DONE, __HM_SESS_VARS_SOURCED) are absent. Without propagating these guards, subprocesses spawned by compile or shell-command lose the buffer-local PATH that envrc constructed—even though envrc itself applied the correct environment.

      (use-package exec-path-from-shell
        :ensure t
        :config
        (when (memq window-system '(mac ns x))
          (dolist (var '("SSH_AUTH_SOCK" "SSH_AGENT_PID" "GPG_AGENT_INFO"
                         "LANG" "LC_CTYPE" "NIX_SSL_CERT_FILE" "NIX_PATH"
                         "__fish_nixos_env_preinit_sourced"
                         "__NIX_DARWIN_SET_ENVIRONMENT_DONE"
                         "__HM_SESS_VARS_SOURCED"))
            (add-to-list 'exec-path-from-shell-variables var))
          (exec-path-from-shell-initialize)
          (setq exec-path (delete-dups (append exec-path my/nix-exec-path)))))
      
    4. envrc

      envrc.el provides buffer-local environment variables based on direnv. This enables Emacs to recognize project-specific development environments managed by direnv.

      Unlike direnv.el which sets environment variables globally, envrc.el keeps them buffer-local. This is essential when working on multiple projects simultaneously, each with its own .envrc.

      The mode must be enabled early (via after-init hook) so that other packages can inherit the correct environment.

      When envrc updates a buffer's environment, it replaces exec-path with values from direnv export, losing the Nix wrapper paths. (envrc#9) Merging my/nix-exec-path back after each update preserves access to wrapper-provided executables (e.g. yaml-language-server) inside direnv-managed buffers.

      (use-package envrc
        :ensure t
        :hook (after-init . envrc-global-mode)
        :config
        (advice-add 'envrc--update :after
          (defun my/envrc-preserve-nix-path (&rest _)
            (when (local-variable-p 'exec-path)
              (setq-local exec-path
                          (delete-dups (append exec-path my/nix-exec-path)))))))
      
  2. UI
    1. mode-line
      (use-package moody
        :ensure t
        :config
        (moody-replace-mode-line-front-space)
        (moody-replace-mode-line-buffer-identification)
        (moody-replace-vc-mode))
      
    2. headerline
      (use-package breadcrumb
        :ensure t
        :config
        (breadcrumb-mode))
      
    3. indent-bars

      indent-bars displays configurable vertical guide bars at each indentation level. Tree-sitter integration is available for scope-aware highlighting, where bars outside the current scope are de-emphasized.

      (use-package indent-bars
        :ensure t
        :hook (prog-mode . indent-bars-mode))
      
  3. minibuffer

    Referring to https://protesilaos.com/codelog/2024-11-28-basic-emacs-configuration/

    (use-package vertico
      :ensure t
      :hook (after-init . vertico-mode))
    
    (use-package marginalia
      :ensure t
      :hook (after-init . marginalia-mode))
    
    (use-package orderless
      :ensure t
      :config
      (setq completion-styles '(orderless basic))
      (setq completion-category-defaults nil)
      (setq completion-category-overrides nil))
    

    The built-in savehist package keeps a record of user inputs and stores them across sessions. Thus, the user will always see their latest choices closer to the top (such as with M-x).

    (use-package savehist
      :ensure nil ; it is built-in
      :hook (after-init . savehist-mode))
    
    (use-package corfu
      :ensure t
      :hook (after-init . global-corfu-mode)
      :bind (:map corfu-map ("<tab>" . corfu-complete))
      :config
      (setq tab-always-indent 'complete)
      (setq corfu-auto t)
      (setq corfu-auto-prefix 1)
      (setq corfu-preview-current nil)
      (setq corfu-min-width 20)
    
      (setq corfu-popupinfo-delay '(1.25 . 0.5))
      (corfu-popupinfo-mode 1) ; shows documentation after `corfu-popupinfo-delay'
    
      ;; Sort by input history (no need to modify `corfu-sort-function').
      (with-eval-after-load 'savehist
        (corfu-history-mode 1)
        (add-to-list 'savehist-additional-variables 'corfu-history)))
    
    1. embark
      (use-package embark
        :ensure t
        :bind
        (("C-." . embark-act)         ;; pick some comfortable binding
         ("C-;" . embark-dwim)        ;; good alternative: M-.
         ("C-h B" . embark-bindings)) ;; alternative for `describe-bindings'
      
        :init
      
        ;; Optionally replace the key help with a completing-read interface
        (setq prefix-help-command #'embark-prefix-help-command)
      
        ;; Show the Embark target at point via Eldoc. You may adjust the
        ;; Eldoc strategy, if you want to see the documentation from
        ;; multiple providers. Beware that using this can be a little
        ;; jarring since the message shown in the minibuffer can be more
        ;; than one line, causing the modeline to move up and down:
      
        ;; (add-hook 'eldoc-documentation-functions #'embark-eldoc-first-target)
        ;; (setq eldoc-documentation-strategy #'eldoc-documentation-compose-eagerly)
      
        ;; Add Embark to the mouse context menu. Also enable `context-menu-mode'.
        ;; (context-menu-mode 1)
        ;; (add-hook 'context-menu-functions #'embark-context-menu 100)
      
        :config
      
        ;; Hide the mode line of the Embark live/completions buffers
        (add-to-list 'display-buffer-alist
                     '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
                       nil
                       (window-parameters (mode-line-format . none)))))
      
  4. version control system
    1. git
      (use-package magit
        :ensure t
        :bind
        (("C-x g" . magit-status)))
      
      (use-package diff-hl
        :ensure t
        :init
        (global-diff-hl-mode)
        (diff-hl-flydiff-mode)
        (add-hook 'dired-mode-hook 'diff-hl-dired-mode)
        (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh))
      
      1. forge

        Forge extends Magit to work with GitHub, GitLab, and other forges directly from Emacs—browsing issues, reviewing pull requests, and managing notifications without leaving the editor.

        Forge authenticates via ghub, which reads credentials from auth-sources. The authinfo.age file should contain a GitHub API token entry:

        machine api.github.com login <username>^forge password <token>
        

        The token is a fine-grained personal access token with a 6-month expiration that requires periodic regeneration. The required permissions are:

        • Issues: Read and Write
        • Pull requests: Read and Write
        (use-package forge
          :ensure t
          :after magit)
        
  5. LSP

    lsp-mode is a Language Server Protocol client for Emacs. It provides IDE-like features (completion, diagnostics, navigation) by communicating with external language servers.

    Eglot is built into Emacs 29+ as a lighter alternative, but it assumes one server per major mode, making it cumbersome to run multiple servers simultaneously on the same buffer (e.g. a language server alongside a linter or formatter). lsp-mode handles multiple concurrent servers natively and provides more granular control over LSP behavior.

    Using lsp-deferred instead of lsp to delay server startup until the buffer is visible, avoiding unnecessary processes for buffers opened in the background.

    Disabling lsp-headerline-breadcrumb-mode to avoid conflict with the existing breadcrumb package in the header line.

    (use-package lsp-mode
      :ensure t
      :commands (lsp lsp-deferred)
      :init
      (setq lsp-keymap-prefix "C-c l")
      :config
      (setq lsp-headerline-breadcrumb-enable nil))
    
  6. language support
    1. tree-sitter

      Use maximum font-lock level for tree-sitter modes.

      (setq treesit-font-lock-level 4)
      
    2. Markdown

      https://github.com/jrblevin/markdown-mode

      markdown-mode is a major mode for editing Markdown-formatted text.

      (use-package markdown-mode
        :ensure t
        :mode ("README\\.md\\'" . gfm-mode)
        :init (setq markdown-command "multimarkdown")
        :bind (:map markdown-mode-map
                    ("C-c C-e" . markdown-do)))
      
    3. Nix

      https://github.com/nix-community/nix-ts-mode

      Tree-sitter based major mode for Nix expressions.

      nixd (configured via lsp-mode) provides IDE features for Nix expressions by interoperating with the C++ Nix evaluator. Unlike nil which relies on static analysis, nixd performs actual Nix evaluation, enabling accurate completion for attribute paths (e.g. pkgs.), NixOS options, and flake inputs.

      (use-package nix-ts-mode
        :ensure t
        :mode "\\.nix\\'"
        :hook (nix-ts-mode . lsp-deferred)
        :init
        ;; org-src-lang-modes is defined in org-src, which may not be loaded yet at init time
        (with-eval-after-load 'org-src
          (add-to-list 'org-src-lang-modes '("nix" . nix-ts)))
        :config
        (setq lsp-nix-nixd-formatting-command ["nixfmt"]))
      
    4. Terraform

      terraform-mode provides syntax highlighting and indentation for Terraform (HCL) files.

      No tree-sitter variant is available in MELPA, so using the traditional major mode.

      terraform-ls (configured via lsp-mode) provides IDE features such as completion, diagnostics via terraform validate, and go-to-definition for Terraform configurations.

      (use-package terraform-mode
        :ensure t
        :hook (terraform-mode . lsp-deferred))
      
    5. PO

      po-mode is Emacs's major mode for editing GNU gettext PO (Portable Object) files. PO files store translations for software internationalization.

      Editing PO files as plain text is error-prone because the format has strict requirements for escaping and structure. po-mode provides structured navigation between entries, automatic validation, and prevents common formatting errors.

      1. Common Operations

        PO mode is not derived from text mode. The buffer is read-only and has its own keymap, so standard text editing commands do not work directly. Translations must be edited through the subedit buffer (RET).

        1. Main Commands

          Commands for file operations, validation, and general PO mode management.

          Key Function Description
          _ po-undo Undo last modification
          q po-confirm-and-quit Quit with confirmation
          ? h po-help Show help about PO mode

          Use or q to quit instead of C-x k (kill-buffer), as they properly handle unsaved changes and warn about untranslated entries.

          See Main PO mode Commands for more details.

        2. Entry Positioning

          Commands for navigating between entries in the PO file.

          Key Function Description
          n po-next-entry Move to next entry
          p po-previous-entry Move to previous entry
          < po-first-entry Move to first entry
          > po-last-entry Move to last entry

          See Entry Positioning for more details.

        3. Modifying Translations

          Commands for editing translation strings. Press RET to open a subedit buffer where standard Emacs editing works normally.

          Key Function Description
          RET po-edit-msgstr Open subedit buffer for editing
          C-c C-c po-subedit-exit Finish editing and apply changes
          C-c C-k po-subedit-abort Abort editing and discard changes
          DEL po-fade-out-entry Delete the translation

          See Modifying Translations for more details.

      2. Configuration
        (use-package po-mode
          :ensure t)
        
    6. Protocol Buffers

      protobuf-ts-mode is a tree-sitter-based major mode for editing proto3 files.

      The mode auto-registers .proto files when the proto grammar is available.

      (use-package protobuf-ts-mode
        :ensure t)
      
    7. Justfile

      just-ts-mode is a tree-sitter-based major mode for editing just command runner files. C-c ' opens a dedicated editing buffer for the recipe body at point, with automatic shebang-based language detection.

      (use-package just-ts-mode
        :ensure t)
      
    8. YAML

      yaml-ts-mode is a built-in tree-sitter-based major mode for YAML files. The tree-sitter grammar is already available via treesit-grammars.with-all-grammars, so the mode activates automatically when the grammar is present.

      yaml-language-server (configured via lsp-mode) provides schema validation using SchemaStore. For example, files under .github/workflows/ are automatically matched to the GitHub Actions workflow schema, enabling completion and diagnostics for workflow definitions.

      (use-package yaml-ts-mode
        :ensure nil
        :mode ("\\.ya?ml\\'")
        :hook (yaml-ts-mode . lsp-deferred))
      
  7. org
    1. Semantic Line Breaks

      Semantic Line Breaks (SemBr) is a writing convention where line breaks are placed at logical boundaries in sentences, such as after punctuation marks or between phrases. This makes diffs more meaningful in version control and improves readability without affecting the rendered output.

      The recommended line length is around 80 characters. I set this as an upper limit in the editor to prevent lines from becoming unnecessarily long.

      (add-hook 'text-mode-hook
                (lambda ()
                  (auto-fill-mode 1)
                  (setq fill-column 80)))
      
    2. org-capture
      (global-set-key (kbd "C-c c") 'org-capture)
      (global-set-key (kbd "C-c l") 'org-store-link)
      
      (setq org-root "~/dropbox/org/")
      
      (setq org-capture-templates
            `(("t" "Todo" entry
               (file+headline ,(concat org-root "todo.org") "Tasks")
               "* TODO %?\n  %i\n  %a")
              ("j" "Journal" entry
               (file+olp+datetree ,(concat org-root "journal.org"))
               "* %U\n%?\n  %i\n  %a")
              ("f" "Fleeting" entry
               (file ,(concat org-root "fleeting.org"))
               "* %?\n  %U\n  %i\n  %a")))
      
    3. org-agenda
      (global-set-key (kbd "C-c a") 'org-agenda)
      
      (setq org-agenda-files '("~/dropbox/org"))
      
    4. org-roam
      (use-package org-roam
        :ensure t
        :custom
        (org-roam-directory "~/dropbox/org-roam")
        (org-roam-db-location "~/.local/share/org-roam.db")
        :bind
        (("C-c n l" . org-roam-buffer-toggle)
         ("C-c n f" . org-roam-node-find)
         ("C-c n g" . org-roam-graph)
         ("C-c n i" . org-roam-node-insert)
         ("C-c n c" . org-roam-capture)
         ("C-c n j" . org-roam-dailies-capture-today))
        :config
        (setq org-roam-capture-templates
              '(("p" "permanent" plain "%?"
                 :target (file+head "permanent/${slug}.org" "#+title: ${title}\n")
                 :unnarrowed t)
                ("l" "literature" plain "%?"
                 :target (file+head "literature/${title}.org" "#+title: ${title}\n")
                 :unnarrowed t)))
        (setq org-roam-node-display-template
              (concat "${title:*} "
                      (propertize "${tags:10}" 'face 'org-tag)))
        (org-roam-db-autosync-mode)
        (require 'org-roam-protocol)
        )
      
      
    5. htmlize

      Used when converting Org files to HTML with syntax highlighting for code blocks.

      C-c C-e h h exports the current Org buffer to HTML.

      (use-package htmlize
        :ensure t)
      
  8. document
    1. pdf-tools

      Emacs's built-in DocView mode renders PDFs as rasterized images page-by-page, resulting in blurry text at most zoom levels and lacking interactive features like text selection, incremental search, and annotation.

      pdf-tools replaces DocView with a viewer powered by poppler, providing sharp rendering, isearch integration, annotation support, and SyncTeX for LaTeX workflows.

      Using pdf-loader-install instead of pdf-tools-install to defer initialization until a PDF is actually opened. With Nix's pre-built epdfinfo binary, both functions behave identically, but the loader variant avoids unnecessary work at startup.

      (use-package pdf-tools
        :ensure t
        :mode ("\\.pdf\\'" . pdf-view-mode)
        :config
        (pdf-loader-install))
      
  9. RSS

    elfeed is a web feed reader for Emacs. Using it with elfeed-protocol to read feeds from Miniflux via the Fever API. This keeps feed management centralized in Miniflux and avoids duplicating the subscription list across devices.

    The Fever API must be enabled in Miniflux (Settings -> Integrations -> Fever API) before this configuration will work.

    Credentials are stored in ~/.authinfo.age.

    machine rss.home.natsukium.com login natsukium password <fever-password>"
    
    (use-package elfeed
      :ensure t
      :bind ("C-x w" . elfeed))
    
    (use-package elfeed-protocol
      :ensure t
      :after elfeed
      :config
      (setq elfeed-use-curl t)
      (setq elfeed-protocol-feeds
            '(("fever+http://[email protected]"
               :api-url "http://rss.home.natsukium.com/fever/"
               :use-authinfo t)))
      (setq elfeed-protocol-enabled-protocols '(fever))
      (elfeed-protocol-enable))
    
  10. mail

    notmuch is a tag-based email indexer and searcher. notmuch.el provides an Emacs interface for reading and organizing email. It reads mail from a local maildir synced by mbsync.

    notmuch was chosen over mu4e because the notmuch indexer is already configured for all email accounts. Adding notmuch.el only requires the Emacs frontend, avoiding a second indexer for the same maildir.

    notmuch has no built-in delete operation; it manages metadata (tags) rather than maildir folder placement. Pressing d tags the message with +deleted and removes inbox. The actual deletion happens on the next notmuch new: a post-new hook moves tagged files to Gmail's Trash maildir folder after indexing, so the move is synced to the server on the next mbsync run. D reverses the operation, restoring inbox and removing deleted. This only works before the next notmuch new moves the file; once mbsync has synced the move, the message is in Gmail's Trash.

    (use-package notmuch
      :ensure nil
      :bind ("C-c M" . notmuch)
      :custom
      (notmuch-fcc-dirs nil)
      (notmuch-search-oldest-first nil)
      (notmuch-saved-searches
       '((:name "inbox"    :query "tag:inbox"                                      :key "i")
         (:name "unread"   :query "tag:unread"                                     :key "u")
         (:name "action"   :query "tag:github::action-required"                    :key "a")
         (:name "attmcojp" :query "tag:github::attmcojp"                           :key "w")
         (:name "github"   :query "tag:github and not tag:github::action-required" :key "g")
         (:name "all"      :query "*"                                              :key "A")))
      :config
      (keymap-set notmuch-search-mode-map "d"
        (lambda ()
          (interactive)
          (notmuch-search-tag '("+deleted" "-inbox"))
          (notmuch-search-next-thread)))
      (keymap-set notmuch-show-mode-map "d"
        (lambda ()
          (interactive)
          (notmuch-show-tag '("+deleted" "-inbox"))))
      (keymap-set notmuch-search-mode-map "D"
        (lambda ()
          (interactive)
          (notmuch-search-tag '("-deleted" "+inbox"))
          (notmuch-search-next-thread)))
      (keymap-set notmuch-show-mode-map "D"
        (lambda ()
          (interactive)
          (notmuch-show-tag '("-deleted" "+inbox")))))
    

    ol-notmuch integrates notmuch with Org mode's link system. Calling org-store-link (C-c l) in a notmuch buffer stores a link to the current message or thread, which can then be inserted into org-capture templates via %a.

    (use-package ol-notmuch
      :ensure t
      :after (notmuch org))
    
  11. terminal
    1. vterm

      emacs-libvterm is a fully-fledged terminal emulator based on libvterm. It provides better performance and compatibility than pure Emacs Lisp alternatives, making it suitable for running interactive CLI tools like Claude Code.

      (use-package vterm
        :ensure t)
      
    2. claude-code-ide

      claude-code-ide.el integrates Claude Code CLI with Emacs, enabling AI-powered code assistance directly in the editor.

      The package requires a terminal backend (vterm or eat) and the Claude Code CLI. Using vterm as the backend for better terminal compatibility.

      (use-package claude-code-ide
        :bind ("C-c C-'" . claude-code-ide-menu)
        :custom
        (claude-code-ide-term-backend 'vterm)
        :config
        (claude-code-ide-emacs-tools-setup))
      
  12. encryption

    age.el provides transparent encryption and decryption of .age files in Emacs using the age encryption tool.

    Using SSH keys as identity/recipient so that no separate age keypair is needed.

    (use-package age
      :ensure t
      :custom
      (age-default-identity "~/.ssh/id_ed25519")
      (age-default-recipient "~/.ssh/id_ed25519.pub")
      :config
      (age-file-enable)
      (setq auth-sources '("~/.authinfo.age")))
    
  13. misc
    1. vundo
      (use-package vundo
        :ensure t
        :bind (("C-x u" . vundo))
        :config
        (setq vundo-glyph-alist vundo-unicode-symbols))
      
    2. consult
      ;; Example configuration for Consult
      (use-package consult
        :ensure t
        ;; Replace bindings. Lazily loaded by `use-package'.
        :bind (;; C-c bindings in `mode-specific-map'
               ("C-c M-x" . consult-mode-command)
               ("C-c h" . consult-history)
               ("C-c k" . consult-kmacro)
               ("C-c m" . consult-man)
               ("C-c i" . consult-info)
               ([remap Info-search] . consult-info)
               ;; C-x bindings in `ctl-x-map'
               ("C-x M-:" . consult-complex-command)     ;; orig. repeat-complex-command
               ("C-x b" . consult-buffer)                ;; orig. switch-to-buffer
               ("C-x 4 b" . consult-buffer-other-window) ;; orig. switch-to-buffer-other-window
               ("C-x 5 b" . consult-buffer-other-frame)  ;; orig. switch-to-buffer-other-frame
               ("C-x t b" . consult-buffer-other-tab)    ;; orig. switch-to-buffer-other-tab
               ("C-x r b" . consult-bookmark)            ;; orig. bookmark-jump
               ("C-x p b" . consult-project-buffer)      ;; orig. project-switch-to-buffer
               ;; Custom M-# bindings for fast register access
               ("M-#" . consult-register-load)
               ("M-'" . consult-register-store)          ;; orig. abbrev-prefix-mark (unrelated)
               ("C-M-#" . consult-register)
               ;; Other custom bindings
               ("M-y" . consult-yank-pop)                ;; orig. yank-pop
               ;; M-g bindings in `goto-map'
               ("M-g e" . consult-compile-error)
               ("M-g f" . consult-flymake)               ;; Alternative: consult-flycheck
               ("M-g g" . consult-goto-line)             ;; orig. goto-line
               ("M-g M-g" . consult-goto-line)           ;; orig. goto-line
               ("M-g o" . consult-outline)               ;; Alternative: consult-org-heading
               ("M-g m" . consult-mark)
               ("M-g k" . consult-global-mark)
               ("M-g i" . consult-imenu)
               ("M-g I" . consult-imenu-multi)
               ;; M-s bindings in `search-map'
               ("M-s d" . consult-find)                  ;; Alternative: consult-fd
               ("M-s c" . consult-locate)
               ("M-s g" . consult-grep)
               ("M-s G" . consult-git-grep)
               ("M-s r" . consult-ripgrep)
               ("M-s l" . consult-line)
               ("M-s L" . consult-line-multi)
               ("M-s k" . consult-keep-lines)
               ("M-s u" . consult-focus-lines)
               ;; Isearch integration
               ("M-s e" . consult-isearch-history)
               :map isearch-mode-map
               ("M-e" . consult-isearch-history)         ;; orig. isearch-edit-string
               ("M-s e" . consult-isearch-history)       ;; orig. isearch-edit-string
               ("M-s l" . consult-line)                  ;; needed by consult-line to detect isearch
               ("M-s L" . consult-line-multi)            ;; needed by consult-line to detect isearch
               ;; Minibuffer history
               :map minibuffer-local-map
               ("M-s" . consult-history)                 ;; orig. next-matching-history-element
               ("M-r" . consult-history))                ;; orig. previous-matching-history-element
      
        ;; Enable automatic preview at point in the *Completions* buffer. This is
        ;; relevant when you use the default completion UI.
        :hook (completion-list-mode . consult-preview-at-point-mode)
      
        ;; The :init configuration is always executed (Not lazy)
        :init
      
        ;; Tweak the register preview for `consult-register-load',
        ;; `consult-register-store' and the built-in commands.  This improves the
        ;; register formatting, adds thin separator lines, register sorting and hides
        ;; the window mode line.
        (advice-add #'register-preview :override #'consult-register-window)
        (setq register-preview-delay 0.5)
      
        ;; Use Consult to select xref locations with preview
        (setq xref-show-xrefs-function #'consult-xref
              xref-show-definitions-function #'consult-xref)
      
        ;; Configure other variables and modes in the :config section,
        ;; after lazily loading the package.
        :config
      
        ;; Optionally configure preview. The default value
        ;; is 'any, such that any key triggers the preview.
        ;; (setq consult-preview-key 'any)
        ;; (setq consult-preview-key "M-.")
        ;; (setq consult-preview-key '("S-<down>" "S-<up>"))
        ;; For some commands and buffer sources it is useful to configure the
        ;; :preview-key on a per-command basis using the `consult-customize' macro.
        (consult-customize
         consult-theme :preview-key '(:debounce 0.2 any)
         consult-ripgrep consult-git-grep consult-grep consult-man
         consult-bookmark consult-recent-file consult-xref
         consult-source-bookmark consult-source-file-register
         consult-source-recent-file consult-source-project-recent-file
         ;; :preview-key "M-."
         :preview-key '(:debounce 0.4 any))
      
        ;; Optionally configure the narrowing key.
        ;; Both < and C-+ work reasonably well.
        (setq consult-narrow-key "<") ;; "C-+"
      
        ;; Optionally make narrowing help available in the minibuffer.
        ;; You may want to use `embark-prefix-help-command' or which-key instead.
        ;; (keymap-set consult-narrow-map (concat consult-narrow-key " ?") #'consult-narrow-help)
      )
      
      ;; Consult users will also want the embark-consult package.
      (use-package embark-consult
        :ensure t ; only need to install it, embark loads it after consult if found
        :hook
        (embark-collect-mode . consult-preview-at-point-mode))
      
    3. compilation

      The compilation buffer does not inherit from comint-mode, so ANSI escape sequences are displayed as raw text by default. Adding ansi-color-compilation-filter to the compilation filter hook interprets these sequences and renders them as colors.

      (add-hook 'compilation-filter-hook 'ansi-color-compilation-filter)
      
    4. copy-region-reference

      Copy the absolute path and line range of the selected region in file:start-end format (e.g. /path/to/file.el:10-20). Pasting the result into a coding agent's prompt gives it an unambiguous file reference to work with.

      (defun my/copy-region-reference (start end)
        "Copy the file path and line range of the current region to the clipboard.
      The format is \"/path/to/file:START-END\"."
        (interactive "r")
        (let* ((file (buffer-file-name))
               (line-start (line-number-at-pos start))
               (line-end (line-number-at-pos (1- end)))
               (ref (format "%s:%d-%d" file line-start line-end)))
          (kill-new ref)
          (message "%s" ref)))
      
      (global-set-key (kbd "C-c l") #'my/copy-region-reference)
      
    5. Others
      (which-key-mode)
      
      (setq-default indent-tabs-mode nil)
      
      (require 'org-tempo)
      
      (org-babel-do-load-languages
       'org-babel-load-languages
       '((shell . t)))
      
      (setq org-src-preserve-indentation t)
      
      1. project.el

        Register all repositories cloned by ghq (https://github.com/x-motemen/ghq) as projects.

        Specifically, directories under ~/src/$ACCOUNT/$VCS_HOST/$OWNER/$REPO.

        (defun my/sync-project-list ()
          "Find all projects under ~/src and synchronize the project-list-file."
          (interactive)
          (let* (;; 1. Retrieve directory list as a string using find command
                 (command (format "find %s -mindepth 4 -maxdepth 4 -type d"
                                  (expand-file-name "~/src")))
                 (dir-list-string (shell-command-to-string command))
                 ;; 2. Split string by newlines and exclude empty lines to create a list
                 (dirs (split-string dir-list-string "\n" t)))
        
            ;; 3. Build file contents in a temporary buffer
            (with-temp-buffer
              (insert ";;; -*- lisp-data -*-\n")
              (insert "(\n")
              (dolist (dir dirs)
                (insert (format " (\"%s/\")\n" dir)))
              (insert ")\n")
        
              ;; 4. Write the built contents to file
              (write-file project-list-file))))
        
        (my/sync-project-list)
        

        Sort the project list by modification time, placing recently updated projects first.

        Using advice instead of sorting in my/sync-project-list ensures that projects are sorted by their current mtime at selection time, not at file generation time. This keeps the order fresh even during long Emacs sessions.

        (defun my/sort-projects-by-mtime (projects)
          "Sort PROJECTS by modification time, most recent first."
          (sort projects
                (lambda (a b)
                  (let ((time-a (file-attribute-modification-time
                                 (file-attributes a)))
                        (time-b (file-attribute-modification-time
                                 (file-attributes b))))
                    (time-less-p time-b time-a)))))
        
        (advice-add 'project-known-project-roots :filter-return
                    #'my/sort-projects-by-mtime)
        

3.5. Scripts

Scripts used in flake derivations, pre-commit hooks, and Makefile recipes. Extracted to separate files for proper syntax highlighting and independent invocability.

(require 'org)
(require 'htmlize)
(require 'nix-ts-mode)

(add-to-list 'org-src-lang-modes '("nix" . nix-ts))
(setq treesit-font-lock-level 4)

;; In batch mode, faces lack color attributes. Explicitly set
;; foreground colors so htmlize emits colored inline CSS.
(set-face-attribute 'font-lock-keyword-face nil :foreground "#5317ac")
(set-face-attribute 'font-lock-string-face nil :foreground "#2544bb")
(set-face-attribute 'font-lock-comment-face nil :foreground "#505050")
(set-face-attribute 'font-lock-function-name-face nil :foreground "#721045")
(set-face-attribute 'font-lock-function-call-face nil :foreground "#721045")
(set-face-attribute 'font-lock-variable-name-face nil :foreground "#00538b")
(set-face-attribute 'font-lock-variable-use-face nil :foreground "#005077")
(set-face-attribute 'font-lock-type-face nil :foreground "#005a5f")
(set-face-attribute 'font-lock-constant-face nil :foreground "#0000c0")
(set-face-attribute 'font-lock-builtin-face nil :foreground "#8f0075")
(set-face-attribute 'font-lock-property-name-face nil :foreground "#00538b")
(set-face-attribute 'font-lock-property-use-face nil :foreground "#005077")
(set-face-attribute 'font-lock-number-face nil :foreground "#0000c0")
(set-face-attribute 'font-lock-operator-face nil :foreground "#813e00")
(set-face-attribute 'font-lock-bracket-face nil :foreground "#5f5f5f")
(set-face-attribute 'font-lock-delimiter-face nil :foreground "#5f5f5f")
(set-face-attribute 'font-lock-punctuation-face nil :foreground "#5f5f5f")
(set-face-attribute 'font-lock-escape-face nil :foreground "#a0132f")

(find-file "configuration.org")
(org-html-export-to-html)

(find-file "configuration.ja.org")
(org-html-export-to-html)

The two pre-commit hook scripts share a common pattern: run a command, then check git diff for changes and fail with instructions if any are found. This shared logic lives in check-git-changes.sh.

# Usage: check-git-changes <message> [git-diff-args...]
# Exits 1 if git diff finds changes, with instructions to stage them.
set -euo pipefail

message="$1"
shift

changed=$(git diff --name-only "$@")

if [ -n "$changed" ]; then
  echo "$message"
  echo "Changed files:"
  echo "$changed"
  echo ""
  echo "Please stage the changes and commit again:"
  echo "  git add $changed"
  exit 1
fi
set -euo pipefail

po4a po4a.cfg
check-git-changes "po4a updated translation files." -- po/ '*.ja.org'
set -euo pipefail

make -B tangle -j
check-git-changes "Org files were out of sync and have been auto-tangled."
(require 'ox-md)
(re-search-forward "^\\* Philosophy")
(org-md-export-to-markdown nil t)

The org-export-with-author setting is explicitly disabled because configuration.org has no #+AUTHOR: keyword, so Org falls back to the Emacs variable user-full-name. On macOS, this variable is populated from the system directory service (dscl / getpwuid), producing an unwanted #+author: line. On Linux, especially in CI environments, the value is typically empty, so the line is omitted. Disabling the setting ensures consistent output across platforms.

(require 'ox-org)
(let ((org-export-select-tags (list "readme"))
      (org-export-with-author nil)
      (org-export-with-tags nil)
      (org-export-time-stamp-file nil))
  (org-export-to-file 'org export-readme-dest))

4. Development

This repository provides a Nix development shell with all the tools needed for working on the configurations. Enter the shell by running:

nix develop

The shell includes infrastructure tools (Terraform, sops, ssh-to-age), translation tools (po4a, gettext), and build utilities (nix-fast-build). On entry, it automatically sets up pre-commit hooks, configures MCP servers, and syncs CLAUDE.md from the literate source.

devShells = {
  default = pkgs.mkShell {
    packages = with pkgs; [
      aws-vault
      nix-fast-build
      sops
      ssh-to-age
      (terraform.withPlugins (p: [
        p.carlpett_sops
        p.cloudflare_cloudflare
        p.determinatesystems_hydra
        p.hashicorp_aws
        p.hashicorp_external
        p.hashicorp_null
        p.integrations_github
        p.oracle_oci
      ]))
      <<translation-packages>>
    ];
    shellHook =
      config.pre-commit.installationScript
      + config.mcp-servers.shellHook
      + ''
        echo "Syncing CLAUDE.md..."
        make CLAUDE.md >/dev/null 2>&1 || echo "Warning: Failed to generate CLAUDE.md"
      '';
  };
};

4.1. Translation

This project uses po4a to manage translations.

4.1.1. Requirements

The required packages are included in the development shell.

gettext
self'.packages.po4a_0_74
  • gettext: provides msgfmt and other internationalization utilities
  • po4a_0_74: po4a >= 0.74 is required for Org mode support. Using a pinned derivation since nixpkgs ships an older version (see packages).

4.1.2. Translation workflow

  1. Create po4a configuration

    Configure the target language, location for generated po files, and documents to translate as follows. The -k 0 option forces output of translated files even if the translation is incomplete (default threshold is 80%).

    [po4a_langs] ja
    [po4a_paths] po/dotfiles.pot $lang:po/$lang.po
    [type: org] configuration.org $lang:configuration.$lang.org opt:"-k 0"
    [type: org] .github/README.org $lang:.github/README.$lang.org opt:"-k 0"
    [type: org] applications/emacs/init.org $lang:applications/emacs/init.$lang.org opt:"-k 0"
    [type: org] applications/emacs/early-init.org $lang:applications/emacs/early-init.$lang.org opt:"-k 0"
    [type: org] overlays/configuration.org $lang:overlays/configuration.$lang.org opt:"-k 0"
    [type: org] modules/configuration.org $lang:modules/configuration.$lang.org opt:"-k 0"
    

    For detailed information about po4a.cfg configuration, see man po4a.

  2. Create/Update po

    When documents are updated and you need to create/update po files, run the following command. This generates template (pot) and po files for each language at the paths configured in po4a.cfg.

    po4a --no-translations po4a.cfg
    
  3. Translate

    Edit the target language po using a po editor. Popular options include Emacs po-mode, poedit, GNOME's Gtranslator, and KDE's Lokalize.

  4. Create/Update translation file

    After completing translations, generate files with the following command. Since po files are also updated at this time, in practice you only need to run this command.

    po4a po4a.cfg
    

Author: Nix build user

Created: 2026-03-16 Mon 14:34

Validate