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:
- English: https://natsukium.github.io/dotfiles/
- Japanese: https://natsukium.github.io/dotfiles/ja/
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.
- Core
- nixpkgs
https://github.com/NixOS/nixpkgs
Nix Packages collection & NixOS
The primary package set. Using
nixos-unstable-smallinstead ofnixos-unstablefor faster channel updates. The-smallvariant 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://withshallow=1instead ofgithub:for slightly faster file extraction on large repositories like nixpkgs.nixpkgs.url = "git+https://github.com/nixos/nixpkgs?shallow=1&ref=nixos-unstable-small";
- nixpkgs-stable
Provides stable packages when unstable has build failures or regressions. Mainly used by the
stableoverlay (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";
- nixpkgs
- Flake Infrastructure
- 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"; };
- flake-parts
- Transitive Dependencies
- flake-utils
https://github.com/numtide/flake-utils
Pure Nix flake utility functions
Common flake utilities. Only used transitively via
followsto unify the flake-utils version across inputs that depend on it.flake-utils.url = "github:numtide/flake-utils";
- flake-utils
- System Configuration
- 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"; };
- 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"; };
- 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"; };
- 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 = ""; };
- 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"; };
- 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";
- 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"; };
- 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";
- darwin
- Infrastructure
- 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"; };
- 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"; };
- 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"; };
- comin
- Development Tools
- 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"; };
- 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"; };
- git-hooks
- Desktop & Theming
- 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"; };
- 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"; };
- nix-colors
- Applications
- 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-apiinput 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"; };
- 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"; };
- 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"; };
- 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"; };
- 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"; };
- 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"; };
- 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 = ""; };
- 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"; };
- 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"; };
- 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"; };
- brew-nix
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.
- 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-substitutersandextra-trusted-public-keysinstead ofsubstitutersandtrusted-public-keysensures 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 theirnix.confor 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=" ];
- 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
- natsukium.cachix.org
Personal binary cache containing outputs from this flake. Packages not available in the official
cache.nixos.orgornix-community.cachix.orgare built on GitHub Actions and pushed here.
- nix-community.cachix.org
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 documentationterraform: Terraform registry lookup for providers, modules, and policiesgrafana: 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>> };
- katavi
Main laptop (M1 MacBook Air).
katavi = { system = "aarch64-darwin"; };
- mikumi
Build server (M1 Mac mini).
mikumi = { system = "aarch64-darwin"; };
- work
Laptop for work (M4 MacBook Pro).
work = { system = "aarch64-darwin"; };
- 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 > Enabledkilimanjaro = { system = "x86_64-linux"; };
- arusha
WSL (dual boot with kilimanjaro).
arusha = { system = "x86_64-linux"; };
- 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 Statemanyara = { system = "x86_64-linux"; };
- serengeti
Build server (OCI A1 Flex).
serengeti = { system = "aarch64-linux"; };
- 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 > Enabledtarangire = { system = "x86_64-linux"; };
- 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>> }
- 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: { };
- temporary-fix
Local overrides for packages that build but have failing tests or minor issues. Unlike the
stableoverlay, these don't require fetching packages from a different nixpkgs branch. We simply override specific attributes (likedoCheck) on the existing packages. Remove these overrides once the issues are fixed upstream.temporary-fix = final: prev: { <<python313-package-set>> };
- Python313 package set
Override problematic Python packages using
packageOverrides.Python packages in nixpkgs form an interconnected dependency graph. The
packageOverridesmechanism 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>> }; };
- 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; });
- 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; });
- rapidocr-onnxruntime
- Python313 package set
- 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: { };
- 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>> };
- gh-dash
The preview pane renders incorrectly when
LANG=ja_JP.UTF-8is 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-8forces ASCII-compatible width calculations while preserving UTF-8 encoding support, which fixes the rendering issue. We usewriteShellApplicationto 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"; };
- mkShim
Shim utility for providing stub implementations of macOS Command Line Tools.
On darwin systems without Xcode Command Line Tools installed, invoking commands like
ccorpython3triggers 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;
- gh-dash
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.
- 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>> }
- Options
options.my.nix = { enable = mkEnableOption "Nix configuration"; enableFlakes = mkOption { default = true; example = false; description = "Whether to enable flakes."; type = types.bool; }; };
- 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>> } ]);
- 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 likenix-shell— NixOS and nix-darwin automatically setNIX_PATHfrom the flake's inputs by default (seenixpkgs.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; }; })
- Store Optimisation
Two complementary deduplication mechanisms are enabled:
nix.optimise.automaticperiodically runsnix-store --optimiseto hard-link identical files already in the store.nix.settings.auto-optimise-storededuplicates at build time as new paths are added.
auto-optimise-storeis Linux-only because enabling it on macOS corrupts the store — the build fails witherror: 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;
- Garbage Collection
Periodically runs
nix-collect-garbageto 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"; };
- 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;
- Binary Caches
Two binary caches are configured:
natsukiumholds pre-built artifacts for this dotfiles repository (CI-built system closures and custom packages), andnix-communityprovides 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=" ]; };
- Sandbox
The sandbox is set to
"relaxed"on Darwin to avoid unexpected build failures. Withsandbox = 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__darwinAllowLocalNetworkingto allow localhost access within the sandbox, which may address the same class of issues."relaxed"keeps normal derivations sandboxed while allowing derivations with__noChroot = trueto bypass the sandbox, preventing these failures.nix.settings.sandbox = if pkgs.stdenv.hostPlatform.isDarwin then "relaxed" else true;
- Trusted Users
On macOS, administrative users belong to the
admingroup, notwheel(which only containsroot). 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";
- 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 '';
- Flakes
- Options
- Nixpkgs
Nixpkgs configuration.
allowUnfreeis 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 viaallowUnfreePredicatewould 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; }; }; }
- Distributed Builds
Distributed build configuration using Nix's built-in
buildMachinessupport. This allows any machine in the fleet to offload builds to other machines, significantly reducing build times for resource-intensive derivations.nix.distributedBuildsis set tomkDefault truehere so that server profiles can override it tofalse.nix.buildMachinesis harmless when distributed builds are disabled, so no conditional wrapping is needed. The server profiles (profiles/nixos/server.nixandprofiles/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>> }
- Protocol Selection
The
ssh-ngprotocol is preferred over plainsshbecause it supports content-addressed derivations and more efficient store path transfers. However, Hydra does not supportssh-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;
- Configuration
nix = { distributedBuilds = lib.mkDefault true; extraOptions = '' builders-use-substitutes = true ''; <<build-machines-list>> };
- Build Machines
Each machine excludes itself from the build machine list (via
config.networking.hostNamechecks) to avoid circular build delegation. Themikumimachine is also excluded from theworkhost 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 undersystems/for the actual values (e.g.,systems/nixos/tarangire/default.nixformax-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 = [ ]; };
- 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"; };
- Protocol Selection
- 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-defexprand~/.nix-profileunder~/.config/nix/and~/.local/state/nix/, respectively, reducing dotfile clutter in$HOME.The
resultsymlink is added to git ignores becausenix buildcreatesresultsymlinks 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
- 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>> }
- Options
Module options for controlling Tailscale behavior.
options.my.services.tailscale = { enable = lib.mkEnableOption "Tailscale VPN"; <<configureResolver-option>> };
- 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
configureResolveractivates 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 ''; };
- configureResolver
- Configuration
config = lib.mkIf cfg.enable ( lib.mkMerge [ { <<tailscale-service>> <<tailscale-networking>> <<tailscale-secrets>> } <<tailscale-resolver>> ] );
- 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.authKeyFilepoints 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.--sshenables 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" ]; };
- Networking
Firewall and DNS configuration for Tailscale integration.
The
tailscale0interface 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.100is Tailscale's MagicDNS resolver, enabling resolution of tailnet hostnames (e.g.,hostname.tail4108.ts.net).8.8.8.8provides 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 manyarainstead ofssh 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" ]; };
- 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 = { };
- Resolver
Conditional systemd-resolved configuration. Only enabled when
configureResolveris true, typically on desktop systems that suspend/resume.(lib.mkIf cfg.configureResolver { services.resolved.enable = cfg.configureResolver; })
- Service Settings
- Options
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.
- 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>> }; }
- 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>> '';
- Keybindings
Ctrl+Sis bound tozi(zoxide interactive mode) for fuzzy directory jumping.bind \cs zi
- 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
- Extra Abbreviations
Position-aware abbreviations that use fish's advanced features (
--position anywhere,--regex,--function) which are not available through home-manager'sshellAbbrsoption.# 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
- any-nix-shell
any-nix-shell keeps fish as the interactive shell inside
nix shellandnix developenvironments. Without it, entering a Nix shell drops into bash (the default builder shell), losing fish's interactive features. This is sourced at the end ofinteractiveShellInitso it can wrap the shell entry point after all other configuration is applied.${pkgs.any-nix-shell}/bin/any-nix-shell fish | source
- Keybindings
- Abbreviations
shellAbbrs = { <<fish-abbr-general>> <<fish-abbr-nix>> };
- General
# spellchecker:off l = "ls";
- 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-j0when the target system differs from the current host.-j0sets 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
- General
- 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
--functionabbreviations.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) ../)"; };
- 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.
- Interactive Shell Init
- 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>> }; }
- History
History is stored under
XDG_CONFIG_HOMEto keep$HOMEclean, consistent with the repository's preference for XDG base directories (see User-Level Settings).historyFile = "$XDG_CONFIG_HOME/bash/history";
- 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;
- 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"; };
- Init
This configuration is a remnant from before fish was set as the default login shell. Previously, bash was the login shell and
.bashrclaunched 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>> '';
- Terminal Settings
stty stop undef: DisablesCtrl+Sfrom sendingXOFF(terminal pause), freeing it for use as a keybinding in fish and other TUI applications. Without this, pressingCtrl+Sfreezes the terminal untilCtrl+Qis pressed — a legacy flow control mechanism that is rarely useful on modern terminals.stty werase undef+bind "\C-w":unix-filename-rubout: RebindsCtrl+Wto 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
- 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
- Terminal Settings
- History
- 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; }; }
- 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; }
- 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"
- 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
- Remote Container Detection
A custom module detects when running inside a VS Code Remote Container (Dev Container) by checking the
REMOTE_CONTAINERSenvironment 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 "
- Disabled Modules
gcloudis 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
- 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 ''; }; }
- 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
- Async Prompt Script
- Prompt Format
3.3.5. Version Control
- Git
Git version control configuration. Git is configured directly through home-manager's
programs.gitmodule, which generates~/.config/git/configdeclaratively.{ 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>> }
- Core Settings
Basic git behavior settings.
settings = { <<git-user-identity>> <<git-editor>> <<git-color>> <<git-default-branch>> <<git-push-safety>> <<git-url-rewrite>> };
- User Identity
user = { name = "natsukium"; email = "[email protected]"; };
- Editor
core.editor = "vim"is set as the fallback editor for git operations. Although theEDITORenvironment variable is set tonvimin the home configuration, some contexts (likegit commitin a minimal environment) may not inherit the session's environment variables. Setting it explicitly in gitconfig ensures consistent behavior.core.editor = "vim";
- Color
Auto-detect terminal color support for all git subcommands.
color = { status = "auto"; diff = "auto"; branch = "auto"; interactive = "auto"; grep = "auto"; };
- Default Branch
init.defaultBranch = "main";
- Force Push Safety
push.useForceIfIncludesenables 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-leasealone cannot catch if the remote ref was updated in the background (e.g., by lazygit's periodic fetch or a backgroundgit fetch).push.useForceIfIncludes = true;
- URL Rewriting
url."[email protected]:".pushInsteadOfrewrites push URLs from HTTPS to SSH. This allowsgit 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,
ghprovides 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 whereghis not available.url."[email protected]:".pushInsteadOf = "https://github.com/";
- User Identity
- 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 = trueensures all commits are signed without requiringgit commit -S, preventing unsigned commits from slipping through.signing = { format = "ssh"; key = "~/.ssh/id_ed25519.pub"; signByDefault = true; };
- Global Ignores
Patterns that should never be committed across all repositories. These are global ignores rather than per-repo
.gitignoreentries 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>> ];
- 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" - 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.envrcfiles 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__/"
- Workflow
.worktree: Marker file used by git worktree workflows.
".worktree" - Private Notes
.private/is a directory for personal notes, LLM context files, and other local-only documentation that should never be shared.".private/"
- OS Artifacts
- 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" ]; };
- 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 thepushInsteadOfsetting may not be exercised in practice —gh's auth context covers both fetch and push over HTTPS.programs.gh.enable = true;
- 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; };
- 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; };
- 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>> }; };
- GUI
Enable Nerd Font icons in the lazygit interface for visual clarity.
gui = { showIcons = true; };
- Git Integration
overrideGpg = truetells 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. WithoverrideGpg = 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=neversince 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"; } ]; };
- GUI
- 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>> };
- Push
Force push with lease — safer than
--forceas it prevents overwriting remote changes not yet fetched.gpf = "git push --force-with-lease";
- Pull
gpmpulls from the remote's default branch, dynamically detected viagit remote showrather than hardcodingmainormaster. This handles repositories that still usemasteror other branch naming conventions.gpupulls 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";
- Commit
gcicreates a commit. The trailing space allows directly appending-m "message".gcaamends the last commit, frequently used during interactive rebase workflows.gci = "git commit "; gca = "git commit --amend";
- Status
gs = "git status";
- Stash
gst = "git stash"; gstp = "git stash pop";
- Switch
gsw = "git switch"; gswc = "git switch -c";
- Push
- Core Settings
3.3.6. Browser
- 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:configprovide 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>> }; }; }; }
- Settings
Auto-install extensions without user prompts. By default, Firefox shows a confirmation dialog for each extension installed via the profile. Setting
extensions.autoDisableScopesto 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; };
- 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>> }; };
- Nix Packages
Search the official NixOS/nixpkgs repository. Uses
nixos-iconsfrom 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" ]; };
- 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" ]; };
- 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" ]; };
- 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" ]; };
- 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" ]; };
- 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" ]; };
- Nix Packages
- Extensions
Browser extensions managed declaratively. Extensions are sourced from two overlay-provided sets:
firefox-addons(from the NUR firefox-addons repository) andmy-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 ]); };
- Settings
3.4. Emacs
3.4.1. early-init.org
- 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-alistrather than callingtool-bar-mode/scroll-bar-modeavoids 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)
- turn off the annoying bell
The default audible bell is distracting and provides no actionable information. Replacing it with
ignoresilences it entirely; a visual bell (visible-bell) was not chosen because the screen flash is equally disruptive.(setq ring-bell-function 'ignore) - disable backup files
Emacs already skips backup files for version-controlled files by default (
vc-make-backup-filesisnil), 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)
- 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
- 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/nresponses instead of requiring the full wordyes/no.(setq use-short-answers t)- 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 'normaland:slant 'normalprevent Emacs from further synthesizing bold or italic on top of the variant's own design.font-lock-comment-faceuses 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") - Nix wrapper paths
The Nix Emacs wrapper (
extraEmacsPackages) adds binaries toexec-paththat are absent from the shell'sPATH. Both exec-path-from-shell and envrc replaceexec-pathduring operation, losing these entries. Capturing them once at init allows both packages to merge them back.(setq my/nix-exec-path exec-path) - 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-initializereplacesexec-pathwith the login shell'sPATH. Mergingmy/nix-exec-pathback 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 exportmay produce incomplete results. (envrc#92)Separately, fish sources
conf.d/*.fisheven 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 bycompileorshell-commandlose 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)))))
- 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-pathwith values fromdirenv export, losing the Nix wrapper paths. (envrc#9) Mergingmy/nix-exec-pathback 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)))))))
- Font
- UI
- mode-line
(use-package moody :ensure t :config (moody-replace-mode-line-front-space) (moody-replace-mode-line-buffer-identification) (moody-replace-vc-mode))
- headerline
(use-package breadcrumb :ensure t :config (breadcrumb-mode))
- 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))
- mode-line
- 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)))
- 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)))))
- embark
- version control system
- 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))
- 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 fromauth-sources. Theauthinfo.agefile 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)
- forge
- git
- 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-deferredinstead oflspto delay server startup until the buffer is visible, avoiding unnecessary processes for buffers opened in the background.Disabling
lsp-headerline-breadcrumb-modeto avoid conflict with the existingbreadcrumbpackage 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))
- language support
- tree-sitter
Use maximum font-lock level for tree-sitter modes.
(setq treesit-font-lock-level 4) - 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)))
- 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"]))
- 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))
- 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.
- 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).- Main Commands
Commands for file operations, validation, and general PO mode management.
Key Function Description _po-undoUndo last modification qpo-confirm-and-quitQuit with confirmation ?hpo-helpShow help about PO mode Use or
qto quit instead ofC-x k(kill-buffer), as they properly handle unsaved changes and warn about untranslated entries.See Main PO mode Commands for more details.
- Entry Positioning
Commands for navigating between entries in the PO file.
Key Function Description npo-next-entryMove to next entry ppo-previous-entryMove to previous entry <po-first-entryMove to first entry >po-last-entryMove to last entry See Entry Positioning for more details.
- Modifying Translations
Commands for editing translation strings. Press
RETto open a subedit buffer where standard Emacs editing works normally.Key Function Description RETpo-edit-msgstrOpen subedit buffer for editing C-c C-cpo-subedit-exitFinish editing and apply changes C-c C-kpo-subedit-abortAbort editing and discard changes DELpo-fade-out-entryDelete the translation See Modifying Translations for more details.
- Main Commands
- Configuration
(use-package po-mode :ensure t)
- Common Operations
- Protocol Buffers
protobuf-ts-mode is a tree-sitter-based major mode for editing proto3 files.
The mode auto-registers
.protofiles when theprotogrammar is available.(use-package protobuf-ts-mode :ensure t)
- 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)
- YAML
yaml-ts-modeis a built-in tree-sitter-based major mode for YAML files. The tree-sitter grammar is already available viatreesit-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))
- tree-sitter
- org
- 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))) - 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")))
- org-agenda
(global-set-key (kbd "C-c a") 'org-agenda) (setq org-agenda-files '("~/dropbox/org"))
- 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) )
- htmlize
Used when converting Org files to HTML with syntax highlighting for code blocks.
C-c C-e h hexports the current Org buffer to HTML.(use-package htmlize :ensure t)
- Semantic Line Breaks
- document
- 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-installinstead ofpdf-tools-installto defer initialization until a PDF is actually opened. With Nix's pre-builtepdfinfobinary, 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))
- pdf-tools
- 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))
- 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
dtags the message with+deletedand removesinbox. The actual deletion happens on the nextnotmuch 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.Dreverses the operation, restoringinboxand removingdeleted. This only works before the nextnotmuch newmoves 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))
- terminal
- 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)
- 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))
- vterm
- encryption
age.el provides transparent encryption and decryption of
.agefiles 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")))
- misc
- vundo
(use-package vundo :ensure t :bind (("C-x u" . vundo)) :config (setq vundo-glyph-alist vundo-unicode-symbols))
- 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))
- 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-filterto the compilation filter hook interprets these sequences and renders them as colors.(add-hook 'compilation-filter-hook 'ansi-color-compilation-filter)
- copy-region-reference
Copy the absolute path and line range of the selected region in
file:start-endformat (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)
- 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)
- 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-listensures 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)
- project.el
- vundo
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 utilitiespo4a_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
- Create po4a configuration
Configure the target language, location for generated po files, and documents to translate as follows. The
-k 0option 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.cfgconfiguration, seeman po4a. - 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
- Translate
Edit the target language po using a po editor. Popular options include Emacs po-mode, poedit, GNOME's Gtranslator, and KDE's Lokalize.
- 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