OrgとNixによる文芸的設定集

Table of Contents

1. 概要

このリポジトリはNixOrg modeを使って複数マシンのシステム設定を管理しています。すべての設定は文芸的プログラムとして書かれており、Org文書の中で各決定の理由を説明する文章がNixコードブロックを囲み、そこからタングルによって実際の設定ファイルが生成されます。

1.1. ドキュメント

設定ドキュメントの全文は以下で公開しています:

1.2. Nix

Nixは純粋関数型のパッケージマネージャー兼ビルドシステムです。このリポジトリでは以下のNixエコシステムツールを使用しています:

  • Flakesによる再現可能な依存関係管理
  • NixOSによる宣言的なLinuxシステム設定
  • nix-darwinによる宣言的なmacOSシステム設定
  • home-managerによるユーザー環境管理
  • nix-on-droidによるAndroid (Termux) 環境

1.3. マシン

名前 プラットフォーム デバイス 用途
kilimanjaro NixOS (x86_64) i5-12400F / RTX 3080 メインデスクトップ
tarangire NixOS (x86_64) Ryzen 9 9950X ビルドサーバー
manyara NixOS (x86_64) Beelink Mini S12 ホームサーバー
arusha NixOS (x86_64) WSL2 WSL環境
serengeti NixOS (aarch64) OCI A1 Flex ビルドサーバー
katavi macOS (aarch64) M1 MacBook Air メインラップトップ
work macOS (aarch64) M4 MacBook Pro 仕事用ラップトップ
mikumi macOS (aarch64) M1 Mac mini ビルドサーバー
android nix-on-droid Galaxy S24 FE スマートフォン

2. 設計思想

2.1. 文芸的設定

Nixは宣言的なシステムです。 Nixで書かれたコードを読めばシステムがどうあるべきかわかりますし、 Nix自身がその状態にするために自動的に処理してくれます。 しかしそのコードからもビルドシステムからも、なぜその設定になっているのか、 なぜ他の選択肢を取らなかったのか知ることはできません。

なぜzshやbashではなくfishを選んだのか? どうしてこのサービスはデスクトップ環境でのみ使われているのか? この古いバージョンのパッケージを使い続けてるのはなぜなのか? コードは決定事項は語りますが、その背景となる事由には口をつぐみます。 もしこの決定の理由がわからなければ将来の変更に 意図したトレードオフが失われたり既に検討し却下したアプローチを 再度採用したりしてしまう危険性が伴うことになるでしょう。

このリポジトリではこの設定の意図を残すために文芸的プログラミングの手法を採用して います。 設定はOrg modeのドキュメントとして、コードとともに記録されます。 高レベルのアーキテクチャ設計から個々のパッケージのオーバーライドにいたるまで、 あらゆる決定についてその選択理由や採用されなかった代替案が記述されています。

設定はその背景となる事柄、その他の選択肢、最終決定の理由とともに文書化されます。 他にも一時的な回避策の場合、その措置をいつ削除できるのかといった条件も記録されます。

3. 設定

3.1. Flake

このflakeはmacOS、Linux、Androidの複数プラットフォームにまたがるマシンのNixOS、nix-darwin、home-manager設定を管理しています。flake-partsによるモジュール構成を採用し、開発、コードフォーマット、pre-commitフックのためのツールも含んでいます。

{
  description = "dotfiles";

    # Core
    # Flake Infrastructure
    # Transitive Dependencies
    # System Configuration
    # Infrastructure
    # Development Tools
    # Desktop & Theming
    # Applications
  inputs = {
    <<nixpkgs>>
    <<nixpkgs-stable>>
    <<flake-parts>>
    <<flake-utils>>
    <<darwin>>
    <<home-manager>>
    <<nixos-wsl>>
    <<nix-on-droid>>
    <<disko>>
    <<impermanence>>
    <<lanzaboote>>
    <<nixos-facter-modules>>
    <<comin>>
    <<sops-nix>>
    <<tsnsrv>>
    <<git-hooks>>
    <<treefmt-nix>>
    <<nix-colors>>
    <<nix-wallpaper>>
    <<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. 入力

外部flake依存関係です。

依存関係グラフの肥大化による評価時間の増加を抑えるため、ほぼすべてのflakeで同一のnixpkgsや共通のinputをfollowするように設定しています。

  1. コア
    1. nixpkgs

      https://github.com/NixOS/nixpkgs

      Nixパッケージコレクション & NixOS

      メインのパッケージセットです。チャンネル更新を高速にするため、=nixos-unstable= ではなく=nixos-unstable-small= を使用しています。=-small= バリアントは重要度の低い一部のCIテストを省略するため、コアパッケージの安定性を維持しつつ新しいパッケージバージョンをより早く取得できます。

      チャンネルの選び方については https://nix.dev/concepts/faq.html#which-channel-branch-should-i-use を参照してください。

      チャンネルの状態は https://status.nixos.org/ で確認できます。

      nixpkgsのような大規模リポジトリでのファイル展開をわずかに高速化するため、=github:= の代わりに=git+https://= と shallow=1 を使用しています。

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

      unstableでビルド失敗やリグレッションが発生した際に安定版パッケージを提供します。主にunstableで壊れているパッケージに対して=stable= overlay(overlays/configuration.org参照)で使用されています。

      nixpkgs-stable.url = "git+https://github.com/nixos/nixpkgs?shallow=1&ref=nixos-25.05";
      
  2. Flakeインフラストラクチャー
    1. flake-parts

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

      モジュールシステムによるNix Flakeの簡素化

      flake出力を整理するためのフレームワークです。flakeにモジュールシステムを提供し、関心の分離によって複雑な設定の保守性を高めます。

      flake-parts = {
        url = "github:hercules-ci/flake-parts";
        inputs.nixpkgs-lib.follows = "nixpkgs";
      };
      
  3. 推移的依存関係
    1. flake-utils

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

      純粋なNix flakeユーティリティ関数

      一般的なflakeユーティリティです。flake-utilsに依存するinput間でバージョンを統一するため、=follows= 経由の推移的な利用のみです。

      flake-utils.url = "github:numtide/flake-utils";
      
  4. システム設定
    1. darwin

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

      Nixを使ったmacOSの管理

      nix-darwinはmacOSにNixOSスタイルのシステム設定を提供します。macOSのシステム設定、launchdサービス、Homebrewを宣言的に管理するために欠かせません。

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

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

      Nixを使ったユーザー環境の管理

      ユーザー環境管理ツールです。dotfiles、ユーザーサービス、ユーザーごとのパッケージを宣言的に管理します。このリポジトリにおけるユーザーレベル設定の中核です。

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

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

      WSL上のNixOS

      Windows Subsystem for Linux上のNixOSです。WSL2内でNixOSの体験を提供し、Linux開発環境が必要なWindowsマシンで活用できます。

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

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

      AndroidデバイスのためのNix対応環境

      Termuxを介したAndroid向けNix環境です。モバイルデバイスでも同じ宣言的な設定アプローチを利用できます。

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

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

      Nixを使った宣言的なディスクパーティショニングとフォーマット

      再現可能なNixOSインストールのために使用します。パーティションレイアウト、ファイルシステムの作成、暗号化のセットアップを自動化します。

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

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

      一時的なルートストレージを持つシステムで永続的な状態を管理するためのモジュール

      一時的なルートファイルシステムを持つシステムでステートフルなパスを管理します。btrfsスナップショットと組み合わせて、明示的に宣言された状態のみが再起動後も保持されるようにします。

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

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

      NixOS向けSecure Boot

      カスタムキーでブートコンポーネントに署名し、NixOSマシンでSecure Bootを有効にします。

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

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

      nixos-facterと組み合わせて使用するNixOSモジュール群

      NixOS向けのハードウェア検出ツールです。検出されたハードウェアに基づいてハードウェア設定を自動生成し、初期システムセットアップを簡素化します。

      nixos-facter-modules.url = "github:numtide/nixos-facter-modules";
      
  5. インフラストラクチャー
    1. comin

      https://github.com/nlewo/comin

      NixOSマシンのためのGitOps

      リポジトリへのプッシュ時に設定変更を自動デプロイし、手動で nixos-rebuild switch を実行せずにサーバーの継続的デプロイメントを実現します。

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

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

      sopsベースのNixOS向けアトミックなシークレットプロビジョニング

      Mozilla SOPSを使ったシークレット管理です。リポジトリ内でシークレットを暗号化し、アクティベーション時にageキーで復号します。

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

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

      tailnet上のサービスを公開するリバースプロキシ(独立したTailscale参加者として)

      Tailscaleサービスプロキシです。ローカルサービスを自動HTTPS証明書付きでTailscaleネットワークに公開します。

      tsnsrv = {
        url = "github:boinkor-net/tsnsrv";
        inputs.flake-parts.follows = "flake-parts";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
  6. 開発ツール
    1. git-hooks

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

      pre-commit.comのGitフックとNixのシームレスな統合

      pre-commitフックをNix derivationとして提供します。グローバルなツールのインストールを必要とせず、すべての開発環境で一貫したコード品質チェックを実行できます。

      このflakeに影響しないinputはロックファイルの肥大化を防ぐために削除しています。

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

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

      treefmt-nixの設定

      統一的なコードフォーマッター設定です。複数のフォーマッター(nixfmt、shfmtなど)を単一のインターフェースで実行し、リポジトリ全体で一貫したフォーマットを保証します。

      treefmt-nix = {
        url = "github:numtide/treefmt-nix";
        inputs.nixpkgs.follows = "nixpkgs";
      };
      
  7. デスクトップ & テーマ
    1. nix-colors

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

      Nixでのテーマ設定を素晴らしくするモジュールとスキーム

      Nix向けBase16カラースキームフレームワークです。単一のカラースキーム定義により、アプリケーション間で一貫したテーマを提供します。

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

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

      Nixシステム向けのカスタマイズ可能な壁紙

      Nixロゴの壁紙を生成します。追加のロゴバリエーションをサポートするカスタムブランチ(=custom-logo=)を使用しています。

      nix-wallpaper = {
        url = "github:natsukium/nix-wallpaper/custom-logo";
        inputs.flake-utils.follows = "flake-utils";
        inputs.nixpkgs.follows = "nixpkgs";
        inputs.pre-commit-hooks.follows = "git-hooks";
      };
      
  8. アプリケーション
    1. brew-nix

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

      HomebrewのすべてのmacOS caskを自動的にパッケージ化する実験的なNix式

      Homebrew caskをdarwin向けNixパッケージとして提供します。nixpkgsにないプロプライエタリなmacOSアプリケーションに便利です。

      brew-api inputはデータソース(HomebrewのAPIからのJSON APIダンプ)にすぎないため、non-flakeとしてマークされています。

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

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

      Linux向けClaude DesktopのNix Flake

      Linux向けのClaude Desktopを提供します。AnthropicがLinuxを公式にはまだサポートしていないため、コミュニティflakeを使用しています。

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

      https://github.com/natsukium/edgepkgs

      最新パッケージのための個人リポジトリです。nixpkgsにまだないパッケージ、修正が必要なパッケージ、アップストリームに受け入れられないパッケージ(ニッチなソフトウェアや実験的なソフトウェアなど)を含みます。

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

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

      最新版Emacs overlay

      ネイティブコンパイルやpure GTKバリアントを含む最新のEmacsビルドを提供します。nixpkgsよりも頻繁に更新されるMELPAパッケージも含みます。主にorgファイルを解析して依存パッケージを自動設定するユーティリティのために使用しています。

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

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

      Nix User Repositoryへの組み込みに適したNix式

      Nix向けにパッケージ化されたFirefox/ブラウザ拡張機能です。home-managerを通じた宣言的なブラウザ拡張機能管理が可能になります。

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

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

      すぐに使えるパッケージ付きのModel Control Protocol (MCP) サーバー向けNixベース設定フレームワーク

      Nix向けの設定フレームワークとパッケージ化されたMCPサーバーの両方を提供します。Claude Codeやその他のMCP互換AIアシスタントで使用します。設定の詳細はMCP Serversを参照してください。

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

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

      NixによるNiriの設定

      niriはスクロール可能なタイリングWaylandコンポジターです。このflakeはコンポジターとNixOS/home-manager統合のための関連モジュールを提供します。

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

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

      個人のNUR (Nix User Repository) です。nixpkgsにはニッチすぎるものやカスタマイズが必要な個人的にメンテナンスしているパッケージを含みます。

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

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

      Wake-on-LAN (WoL) デバイスを管理するWebアプリケーション

      個人的なWake-on-LAN管理ツール。WoLパケットの送信とデバイス構成の管理のためのWebインターフェースを提供します。

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

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

      Zen Browser向けコミュニティ主導のNix Flake

      プライバシーに重点を置いたFirefoxベースのブラウザです。home-manager統合を含むNixパッケージングを提供するコミュニティflakeです。

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

3.1.2. Nixの設定

このflakeで使用するNixの設定です。これらの設定はすべての管理対象マシンで既に設定済みですが、ここに記述することで初期セットアップの助けになり、他の人がこのflakeを利用する際にも役立ちます。

各設定の詳細なドキュメントは https://nix.dev/manual/nix/latest/command-ref/conf-file.html を参照してください。

注意: flake.nix はコードの再利用を妨げるNix言語の制限されたサブセットを使用しています(https://github.com/NixOS/nix/issues/4945 参照)。以下の値は現在このセクションにハードコードされています。マシン設定がOrg modeに移行されれば、nowebリファレンスによりflakeとマシン固有の設定の両方でこれらの値を共有できるようになります。

  1. バイナリキャッシュ

    このflakeをビルドするためのバイナリキャッシュ(substituter)設定です。必須ではありませんが、これらのキャッシュを設定するとソースからコンパイルする代わりにビルド済みバイナリをダウンロードするため、ビルド時間を大幅に短縮できます。

    substituterstrusted-public-keys の代わりに=extra-substituters= と extra-trusted-public-keys を使うことで、このflakeのキャッシュ設定がユーザーの既存設定を置き換えるのではなく追加的になるようにしています。これによりユーザーが nix.conf やシステム設定で既に設定しているキャッシュが尊重されます。

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

      コミュニティバイナリキャッシュで、主にCUDA関連パッケージに使用されます。2024年11月以降、CUDAバイナリはnix-community名前空間で配布されています。ビルド状況は https://hydra.nix-community.org/project/nixpkgs で確認できます。

      詳細は https://discourse.nixos.org/t/cuda-cache-for-nix-community/56038 を参照してください。

      代替として、CUDAパッケージにFloxのバイナリキャッシュも利用できます。2025年9月時点で、FloxはNVIDIAと提携して再配布権を取得しています。https://discourse.nixos.org/t/nix-flox-nvidia-opening-up-cuda-redistribution-on-nix/69189 を参照してください。

    2. natsukium.cachix.org

      このflakeの出力を含む個人バイナリキャッシュです。公式の cache.nixos.orgnix-community.cachix.org にないパッケージはGitHub Actionsでビルドされ、ここにプッシュされます。

3.1.3. MCPサーバー

開発シェルで有効化されるmcp-servers-nixの設定です。=flavors.claude-code= プリセットにより、Claude Codeの期待するフォーマットと互換性のある .mcp.json ファイルが生成されます。

  • nixos: NixOSパッケージ/オプション検索とHome Managerのドキュメント
  • terraform: Terraformレジストリのプロバイダー、モジュール、ポリシー検索
  • grafana: ホームサーバーのGrafanaインスタンスからダッシュボード、データソース、メトリクスを照会

有効なサーバー:

passwordCommand オプションは rbw (Bitwarden CLI) を使って実行時にシークレットを取得し、リポジトリに平文の認証情報を含めることを避けています。

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 = {
  <<host-katavi>>
  <<host-mikumi>>
  <<host-work>>
  <<host-kilimanjaro>>
  <<host-arusha>>
  <<host-manyara>>
  <<host-serengeti>>
  <<host-tarangire>>
  <<host-android>>
};
  1. katavi

    メインラップトップ(M1 MacBook Air)。

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

    ビルドサーバー(M1 Mac mini)。

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

    仕事用ラップトップ(M4 MacBook Pro)。

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

    メインデスクトップ(Intel Core i5-12400F)。

    Wake-on-LAN (WoL) は以下のBIOS設定で有効化されています:=Advanced > APM Configuration > Power On By PCI-E > Enabled=

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

    WSL(kilimanjaroとデュアルブート)。

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

    Intel N100搭載のミニPCで、軽量なホームサーバーとして使用しています。https://www.bee-link.com/products/beelink-mini-s12-pro-n100

    停電後に自動的に電源が入るよう、以下のBIOS設定がされています:=Chipset > PCH-IO Configuration > State After G3 > S0 State=

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

    ビルドサーバー(OCI A1 Flex)。

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

    ビルドサーバー(Ryzen 9 9950X)。

    Wake-on-LAN (WoL) は以下のBIOS設定で有効化されています:=Advanced > APM Configuration > Power On By PCI-E > Enabled=

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

    スマートフォン(Pixel 7a)。

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

3.1.5. 出力

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
          '';
        };
    };

              # Ensure this hook runs before all other hooks
    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";
              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. Overlay

3.2.1. 概要

このファイルでは、壊れたパッケージの修正、特定バージョンのピン留め、ローカルな回避策の追加のためのnixpkgs overlayを定義しています。各overlayはパッケージセットを変更し、システムのビルドを妨げたり実行時の問題を引き起こす課題に対処します。

overlayの仕組み(=final: prev:= パターン、合成順序など)の詳細は nixpkgs overlayドキュメントを参照してください。

overlayは目的と想定される存続期間に基づいて4つのカテゴリに分類されています:

  • stable: unstableで壊れており、修正すると大量のリビルドが発生するか、 ローカルでパッチするには複雑すぎる場合にnixpkgs-stableから取得するパッケージ。
  • temporary-fix: 別のnixpkgsバージョンを必要としないローカルオーバーライド (例: テストの無効化)。アップストリームで修正されたら削除します。
  • pre-release: nixpkgsに入る前にテストするためのアルファ版、ベータ版、プレリリースパッケージ。
  • patches: アップストリームへの貢献に適さない回避策(例: ロケール固有の修正、 ローカルツールのshim)。恒久的に残る想定です。
{ inputs }:
{
  <<stable>>

  <<temporary-fix>>

  <<pre-release>>

  <<patches>>
}
  1. stable

    unstableで壊れている場合にnixpkgs-stableからパッケージを取得します。アップストリームの修正が大量のリビルドを引き起こす場合や、ローカルでパッチするには複雑すぎる場合が該当します。

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

    ビルドはできるがテストが失敗したり軽微な問題があるパッケージのローカルオーバーライドです。=stable= overlayとは異なり、別のnixpkgsブランチからパッケージを取得する必要はありません。既存パッケージの特定の属性(=doCheck= など)をオーバーライドするだけです。アップストリームで問題が修正されたらこれらのオーバーライドを削除します。

    temporary-fix = final: prev: {
      <<python313-package-set>>
    };
    
    1. Python313パッケージセット

      packageOverrides を使用して問題のあるPythonパッケージをオーバーライドします。

      nixpkgsのPythonパッケージは相互に接続された依存関係グラフを形成しています。=packageOverrides= の仕組みにより、パッケージがオーバーライドされると、依存するすべてのパッケージが自動的に変更後のバージョンを参照します。これは一貫性のために不可欠です。直接的なoverlayオーバーライド(例: =python313Packages.foo = …=)ではトップレベルのアクセスにしか影響せず、内部の依存関係は元の壊れたバージョンを使い続けてしまいます。

      詳細はnixpkgs Pythonドキュメントを参照してください。

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

        テストスイートの実行中にセグメンテーションフォールトが発生します。根本原因はまだ調査中です。

        このパッケージは推移的な依存関係として取り込まれています。実際の使用でランタイム機能が正しく動作することは確認済みのため、テストスイートの無効化は安全な回避策です。

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

        libxml2 2.14の破壊的変更により、特定のDOM操作における空白文字やエンティティエンコーディングの処理方法が変更されたためテストが失敗します。実際のHTMLクリーニング機能は正しく動作しているにもかかわらず、テストのアサーションが失敗します。

        アップストリームでは fedora-python/lxml_html_clean#24で追跡されています。テストスイートがlibxml2 2.14互換に更新されたらこのオーバーライドを削除します。

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

    nixpkgsに入る前のアルファ版、ベータ版、プレリリース版パッケージをテストするためのoverlayです。リリース候補、ナイトリービルド、アップストリームのレビュー待ちパッケージの評価に便利です。nixpkgsで利用可能になったら、このoverlayから削除します。

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

    アップストリームへの貢献に適さない回避策です。

    これらのパッチは、アップストリームが受け入れないであろう問題に対処します。この設定固有のもの(例: ロケール設定)、意図された動作を迂回するもの、または非標準的な方法で問題を解決するものが該当します。=temporary-fix= とは異なり、恒久的に残る想定です。

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

      LANG=ja_JP.UTF-8 が設定されているとプレビューペインが正しく描画されません。gh-dashの端末幅計算が特定のUTF-8文字(特にCJK文字や一部の絵文字)の表示幅を誤ってカウントすることが原因です。これによりテキストの折り返しと配置が崩れます。

      LANG=C.UTF-8 を設定するとUTF-8エンコーディングのサポートを維持しつつASCI互換の幅計算が強制され、描画の問題が修正されます。=writeShellApplication= を使って、実際のバイナリを呼び出す前にこの環境変数を設定するラッパーを作成しています。

      アップストリームでは dlvhdr/gh-dash#316で報告済みです。

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

      macOS Command Line Toolsのスタブ実装を提供するshimユーティリティです。

      Xcode Command Line Toolsがインストールされていないdarwinシステムでは、=cc= や python3 などのコマンドを実行するとインストールを促す煩わしいシステムポップアップが表示されます。これらのshimはそのような呼び出しをインターセプトし、Nixが提供するツールに委譲するか適切な終了コードを返すことで、ポップアップを抑制し不要なビルド失敗を防ぎます。

      実装の詳細とshim化されたコマンドの一覧は pkgs/mkShimを参照してください。

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

3.3. モジュール

3.3.1. 概要

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パッケージマネージャーとnixpkgsの設定です。これらのモジュールはNixOSとnix-darwinで共有され、すべてのマシンで一貫したNixの動作を提供します。

  1. コア設定

    flake、ガベージコレクション、バイナリキャッシュ、サンドボックス設定を含むNixデーモンのコア設定です。

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

        FlakeはNixプロジェクト管理のデファクトスタンダードであり、このリポジトリ全体で使用されています。

        チャンネルは無効化されています。チャンネルはマシン間で再現が困難な可変状態(=/nix/var/nix/profiles/per-user/*/channels=)を導入するためです。ただし、=nix-shell= などのレガシーコマンドは壊れません。NixOSとnix-darwinはデフォルトでflakeのinputから=NIX_PATH= を自動設定するため(nixpkgs.flake.setNixPath参照)、このdotfilesリポジトリで管理されるすべてのマシンで <nixpkgs> の参照が引き続き機能します。

        (mkIf cfg.enableFlakes {
          nix = {
            settings.experimental-features = [
              "flakes"
              "nix-command"
            ];
            channel.enable = false;
          };
        })
        
      2. ストアの最適化

        2つの補完的な重複排除メカニズムが有効化されています:

        • nix.optimise.automatic は定期的に nix-store --optimise を実行し、 ストア内の同一ファイルをハードリンクします。
        • nix.settings.auto-optimise-store は新しいパスが追加される際にビルド時に重複排除します。

        auto-optimise-store はLinux専用です。macOSで有効にするとストアが破損し、=error: cannot link 'nix/store.tmp-link' to 'nix/store.links/…': File exists= でビルドが失敗します。NixOS/nix#7273を参照してください。

        nix.optimise.automatic = true;
        nix.settings.auto-optimise-store = pkgs.stdenv.hostPlatform.isLinux;
        
      3. ガベージコレクション

        定期的に nix-collect-garbage を実行してディスク容量を回収します。7日以上前の世代は自動的に削除されます。それ以上古いビルドを保持しても価値は薄く、flakeのロックファイルからいつでも再ビルドできるためです。

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

        flake評価時の「Git tree is dirty」警告を抑制します。この警告はコミットされていない変更があるとビルドのたびに表示されますが、開発中はそれが通常の状態です。

        nix.settings.warn-dirty = false;
        
      5. バイナリキャッシュ

        2つのバイナリキャッシュが設定されています。=natsukium= はこのdotfilesリポジトリのビルド済みアーティファクト(CIでビルドされたシステムクロージャーやカスタムパッケージ)を保持し、=nix-community= はNVIDIA/CUDAパッケージやその他のnix-communityが管理するderivationを含むコミュニティプロジェクトのキャッシュを提供します。

        nix.settings = {
          substituters = [
            "https://natsukium.cachix.org"
            "https://nix-community.cachix.org"
          ];
        
          trusted-public-keys = [
            "natsukium.cachix.org-1:STD7ru7/5+KJX21m2yuDlgV6PnZP/v5VZWAJ8DZdMlI="
            "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
          ];
        };
        
      6. サンドボックス

        Darwinではサンドボックスを "relaxed" に設定し、予期しないビルド失敗を回避しています。=sandbox = true= では、サンドボックスの制約により一部のパッケージがmacOSでビルドに失敗します。テストスイートがローカルサーバーを起動する際のlocalhost通信に関連していると考えられますが、正確なメカニズムは完全には解明されていません。nixpkgsはサンドボックス内でlocalhostアクセスを許可する __darwinAllowLocalNetworking を提供しており、同じ種類の問題に対処できる可能性があります。="relaxed"= は通常のderivationをサンドボックス化したまま、=__noChroot = true= を持つderivationがサンドボックスを迂回できるようにし、これらの失敗を防ぎます。

        nix.settings.sandbox = if pkgs.stdenv.hostPlatform.isDarwin then "relaxed" else true;
        
      7. 信頼されたユーザー

        macOSでは、管理者ユーザーは wheel ではなく=admin= グループに属します(=wheel= には root のみが含まれます)。=@admin= がないと、DarwinでプライマリユーザーがNixデーモンから信頼されません。

        nix.settings.trusted-users = [
          "root"
          "@wheel"
        ]
        ++ optional pkgs.stdenv.hostPlatform.isDarwin "@admin";
        
      8. 追加オプション

        3600秒(1時間)出力がないビルドを終了します。一部の重いビルド(例: Chromium、カーネルのコンパイル、CUDAベースのディープラーニングライブラリ)は長時間無出力になることがあるため、本当にハングしたビルドを検出しつつ誤検知を避けるためにタイムアウトは余裕を持って設定されています。

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

    Nixpkgsの設定です。=allowUnfree= はデフォルトで有効化されています。可能な限りフリーソフトウェアが望ましいですが、厳密に強制するのは非現実的です。NVIDIAハードウェアにはunfreeドライバーとCUDAライブラリが必要であり、=allowUnfreePredicate= で個別にunfreeパッケージを指定するのは、関連する推移的な依存関係の数を考えると過度に煩雑です。

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

    Nixの組み込み buildMachines サポートを使用した分散ビルド設定です。フリート内の任意のマシンからビルドを他のマシンにオフロードでき、リソースを多く消費するderivationのビルド時間を大幅に短縮します。

    nix.distributedBuilds はここで mkDefault true に設定されており、サーバープロファイルが false でオーバーライドできるようにしています。=nix.buildMachines= は分散ビルドが無効でも無害なので、条件付きのラッピングは不要です。サーバープロファイル(=profiles/nixos/server.nix= と =profiles/darwin/server.nix=)では無効化されています。ビルドサーバーにはローカルでビルドするリソースがあり、ビルドサーバー間でオフロードすると不必要なネットワークオーバーヘッドや循環依存が生じるためです。

    {
      inputs,
      config,
      lib,
      ...
    }:
    let
      <<build-machines-let>>
    in
    {
      <<distributed-builds-config>>
    
      <<build-machines-known-hosts>>
    }
    
    1. プロトコルの選択

      ssh-ng プロトコルはプレーンな ssh よりも推奨されます。コンテンツアドレスドderivationとより効率的なストアパス転送をサポートするためです。ただし、Hydraは ssh-ng をサポートしていません(NixOS/hydra#688参照)。そのため、現在のマシンでHydraが有効かどうかに基づいてプロトコルが動的に選択されます。

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

      各マシンは循環的なビルド委任を避けるため、自身をビルドマシンリストから除外しています(=config.networking.hostName= のチェックによる)。=mikumi= マシンも work ホストから除外されています。ワークマシンから個人のインフラにビルドをオフロードすべきではないためです。

      マシンの性能(=maxJobs=、=system-features=)はハードコードではなく各マシンの設定から参照されます。実際の値は systems/ 以下の各マシンの定義を参照してください(例: systems/nixos/tarangire/default.nix の =max-jobs = 12=)。

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

      分散ビルドはSSH経由でリモートマシンに接続するため、無人ビルド中の対話的な検証プロンプトを避けるために各ビルドマシンのホスト鍵を登録する必要があります。

      programs.ssh.knownHosts = {
        tarangire.publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEJJYgE/dmYLXYBrVnPicd0qsaUeqcBtXB8H9LHkJ2j4";
        kilimanjaro.publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILhpfAalh6A5xDSE+HOdNE29ZgIjlP7tdlhHs82boSwp";
        serengeti.publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDWwhfhDSZ+M2XDwP2MlC/zFfVpk3WjUxV/JWFgGzgNW";
        mikumi.publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPfWOWKBFuDV08g6xP9MMY78CERI02CNG+5dy8CXQmXs";
      };
      
  4. ユーザーレベルの設定

    ユーザーレベルのNix設定です。システム設定ではなくユーザーの環境に属するため、home-managerで管理されています。

    XDGベースディレクトリを有効にして、=~/.nix-defexpr= と ~/.nix-profile をそれぞれ ~/.config/nix/~/.local/state/nix/ の下に配置し、=$HOME= のドットファイルの散らかりを減らします。

    result シンボリックリンクはgitの無視対象に追加されています。=nix build= はデフォルトでプロジェクトルートに result シンボリックリンクを作成し、コミットすべきではないためです。これは特定のリポジトリではなく、ユーザーのグローバルgitignoreに適用されます。

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

3.3.3. ネットワーク

  1. Tailscale

    NixOSシステム向けの独自のTailscale VPN設定です。このモジュールはMagicDNS、SSHアクセス、適切なファイアウォール設定とともにTailscaleを実行するための合理的なデフォルトを提供します。

    { config, lib, ... }:
    let
      cfg = config.my.services.tailscale;
    in
    {
      <<tailscale-options>>
    
      <<tailscale-config>>
    }
    
    1. オプション

      Tailscaleの動作を制御するモジュールオプションです。

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

        デスクトップシステムではサスペンド/レジューム後にDNS解決の失敗が発生することがあります。システムがレジュームすると、外部ドメインの解決(例: github.com)が失敗する一方、Tailnetのホスト名は引き続き動作します。これはネットワーク遷移時のTailscaleのDNS状態管理に関する既知の問題です。

        configureResolver を有効にするとsystemd-resolvedがアクティベートされ、サスペンド/レジューム後のDNS解決の問題が軽減されます。これで問題が完全に解消されるわけではありませんが(アップストリームのバグは未解決)、最も信頼性の高いMagicDNS体験を提供します。

        ヘッドレスサーバーでは、このオプションは通常不要です。サーバーはサスペンドしないため、レジュームに関連するこのDNSバグに遭遇することがないためです。

        アップストリームの議論は tailscale/tailscale#4254を参照してください。

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

        Tailscaleサービスのコア設定

        useRoutingFeatures = "server" はこのマシンをサブネットルーターまたはイグジットノードとして機能させます。この設定のすべてのマシンは他のデバイスのイグジットノードとして機能でき、旅行中や制限されたネットワーク上での柔軟性を提供します。

        authKeyFile はTailscale認証キーを含むSOPS管理のシークレットを指します。認証キーを使用することで無人認証が可能になり、cominなどのツールを使った自動デプロイやGitOpsワークフローに不可欠です。

        --ssh はTailscale SSHを有効にし、SSHキーの管理やポート22のパブリックインターネットへの公開なしにTailscaleネットワーク経由でSSHアクセスを可能にします。

        services.tailscale = {
          enable = true;
          useRoutingFeatures = "server";
          authKeyFile = config.sops.secrets.tailscale-authkey.path;
          extraUpFlags = [ "--ssh" ];
        };
        
      2. ネットワーク

        Tailscale統合のためのファイアウォールとDNS設定です。

        tailscale0 インターフェースは信頼されています。このインターフェース上のすべてのトラフィックはTailscaleによって認証されるためです。これにより、追加のファイアウォールルールなしにサービスをtailnetのみに公開できます。

        100.100.100.100 はTailscaleのMagicDNSリゾルバーで、tailnetホスト名(例: hostname.tail4108.ts.net=)の解決を可能にします。=8.8.8.8 はtailnet以外のクエリのフォールバックを提供しますが、実際にはMagicDNSがアップストリームリゾルバーへの転送を処理します。

        検索ドメインはtailnetドメインに設定されており、短いホスト名(例: ssh manyara.tail4108.ts.net の代わりに =ssh manyara=)が使えます。

        networking = {
          firewall = {
            trustedInterfaces = [ "tailscale0" ];
            allowedUDPPorts = [ config.services.tailscale.port ];
          };
          nameservers = [
            "100.100.100.100"
            "8.8.8.8"
          ];
          search = [ "tail4108.ts.net" ];
        };
        
      3. シークレット

        Tailscale認証キーのSOPSシークレット宣言です。実際のキーはリポジトリのシークレットファイルに暗号化されて保存され、アクティベーション時に復号されます。

        sops.secrets.tailscale-authkey = { };
        
      4. リゾルバー

        条件付きのsystemd-resolved設定です。=configureResolver= がtrueの場合にのみ有効になり、通常はサスペンド/レジュームするデスクトップシステムで使用されます。

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

3.3.4. シェル

対話的シェルとスクリプト用シェルは異なる目的を持ちます。対話的シェルはPOSIX互換である必要はなく、重要なのは使いやすさ、幅広い環境サポート、拡張性です。

  1. Fish

    fishはプライマリの対話的シェルです。すぐに使えるエクスペリエンス(シンタックスハイライト、オートサジェスト、タブ補完が設定やプラグインなしで動作する)が選定理由です。幅広い環境とソフトウェアのサポートを持つシェルの中で、fishは最小限のセットアップで最も便利で拡張可能な対話的エクスペリエンスを提供します。

    { pkgs, ... }:
    {
      programs.fish = {
        enable = true;
    
        <<fish-interactive-shell-init>>
    
        <<fish-abbreviations>>
    
        <<fish-functions>>
    
        <<fish-plugins>>
      };
    }
    
    1. 対話シェルの初期化

      fishが対話セッションを開始する際に適用される設定です。キーバインド、プラグイン設定、環境変数、シェル統合が含まれます。

      interactiveShellInit = ''
        <<fish-keybindings>>
      
        <<fish-done-config>>
      
        <<fish-pinentry>>
      
        <<fish-extra-abbrs>>
      
        <<fish-any-nix-shell>>
      '';
      
      1. キーバインド

        Ctrl+S に =zi=(zoxideのインタラクティブモード)をバインドし、ファジーなディレクトリジャンプに使用します。

        bind \cs zi
        
      2. Pinentry

        SSH経由で接続している場合、GPGのpinentryをcurses(ターミナル)バリアントに切り替えます。デフォルトのグラフィカルなpinentryはX11/Waylandフォワーディングなしではリモートセッションで表示できないため、SSH経由でのコミット署名やシークレット復号にTUIのフォールバックが必要です。Gentoo Wiki: GnuPG - Changing pinentry for SSH loginsを参照してください。

        # set environment variable for pinentry
        if test "$SSH_CONNECTION" != ""
          set -x PINENTRY_USER_DATA "USE_CURSES"
        end
        
      3. 追加の略語

        位置認識型の略語で、fishの高度な機能(=–position anywhere=、=–regex=、=–function=)を使用しています。これらはhome-managerの shellAbbrs オプションでは利用できません。

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

        any-nix-shellは=nix shell= や nix develop 環境内でfishを対話的シェルとして維持します。これがないと、Nixシェルに入る際にbash(デフォルトのビルダーシェル)に落ち、fishの対話機能が失われます。他のすべての設定が適用された後にシェルのエントリーポイントをラップできるよう、=interactiveShellInit= の末尾でソースされます。

        ${pkgs.any-nix-shell}/bin/any-nix-shell fish | source
        
    2. 略語
      shellAbbrs = {
        <<fish-abbr-general>>
      
        <<fish-abbr-nix>>
      };
      
      1. 一般
        # spellchecker:off
        l = "ls";
        
      2. Nixリモートビルド

        Nixのビルドターゲットシステムを指定する略語で、主にnixpkgsで作業する際に使用されます。各略語は --system <triple> に展開され、ターゲットシステムが現在のホストと異なる場合は条件付きで -j0 が追加されます。=-j0= はローカルジョブ数の上限をゼロに設定し、すべてのビルドをリモートビルダーに委任させます(分散ビルド参照)。これにより、Nixがホスト上でビルドを試みてアーキテクチャの不一致で失敗することを防ぎます。

        # spellchecker:on
        "--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";
        };
        
    3. 関数

      略語システムで使用されるヘルパー関数です。fishでは --function 略語が参照する前に関数を定義する必要があるため、略語の定義とは分離されています。

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

      doneプラグインは長時間実行されるコマンドのデスクトップ通知を提供します。閾値は15秒に設定されています。これより短いコマンドは通常対話的で通知の恩恵がなく、長いコマンド(ビルド、テストスイート、大きなファイル操作)はバックグラウンドで実行されることが多く、通知が有用です。

      # set done's variable
      set -U __done_min_cmd_duration 15000
      

      fzf-fishはfzfをfishのタブ補完、履歴検索、ファイル/ディレクトリナビゲーションと統合します。fishの組み込み履歴検索(=Ctrl+R=)をfzfのファジーファインダーに置き換え、大きな履歴をより効果的に処理します。

  2. Bash

    Bashは完全な対話環境ではなく、最小限のフォールバックシェルとして設定されています。主な用途はfishが利用できない場合に使えるシェルを確保すること、およびbashを前提とするスクリプトやツールにPOSIX互換シェルを提供することです。また、fishが予期しない動作を示す際のテスト環境としても機能します。クリーンなbashがあれば、問題がfish固有かどうかの切り分けに役立ちます。

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

      履歴は XDG_CONFIG_HOME 配下に保存し、=$HOME= をクリーンに保ちます。リポジトリのXDGベースディレクトリの方針と一貫しています(ユーザーレベルの設定参照)。

      historyFile = "$XDG_CONFIG_HOME/bash/history";
      
    2. 補完

      Bashの補完はプライマリの対話的シェルとして使用されていないため無効化されています。補完スクリプトの読み込みは起動時間を追加(約100ms以上)しますが、フォールバックやスクリプト実行にのみ使用するシェルではメリットがありません。

      enableCompletion = false;
      
    3. エイリアス

      bashを対話的に使用する場合の最小限の利便性向上のための基本的なエイリアスです。意図的にシンプルにしています。リッチな対話エクスペリエンスはfishが担当します。

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

      この設定はfishがデフォルトのログインシェルに設定される前の名残です。以前はbashがログインシェルで .bashrc からfishを起動していたため、これらの設定はすべての対話セッションに必要でした。bashがフォールバックとして使用されるため保持されています。

      + lib.optionalString (!config.programs.kitty.enable) ''
        <<bash-tmux-autostart>>
      initExtra = ''
        <<bash-terminal-settings>>
      ''
      '';
      
      1. ターミナル設定
        • stty stop undef: Ctrl+SXOFF=(ターミナル一時停止)を送信するのを無効にし、 fishや他のTUIアプリケーションでキーバインドとして利用できるようにします。これがないと、 =Ctrl+S を押すと Ctrl+Q を押すまでターミナルがフリーズします。 これは現代のターミナルではほとんど役に立たないレガシーなフロー制御機構です。
        • stty werase undef + bind "\C-w":unix-filename-rubout: Ctrl+W を単語全体ではなく 前のパスコンポーネント(=/= で停止)を削除するように再バインドします。 シェルでの一般的なケースであるファイルパスの編集時に、より便利です。
        stty stop undef  # Ctrl-s
        stty werase undef
        bind "\C-w":unix-filename-rubout  # Ctrl-w
        
      2. TMUXの自動アタッチ

        bashが対話的シェルの場合にtmuxセッションを自動的に開始またはアタッチしますが、kittyがターミナルエミュレーターの場合のみ除外されます。kittyには独自のタブ/ウィンドウ管理(分割、レイアウト、タブ)があり、tmuxの多重化と競合します。kitty内でtmuxを実行すると冗長なネストが生じます。よりシンプルなターミナル(例: =xterm=、=alacritty=、またはLinuxコンソール)を使用する場合、tmuxはそれ以外では欠けるセッション永続性とウィンドウ管理を提供します。

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

    Nushellは構造化データをネイティブに扱える能力のために有効化されており、PowerShellに似ています。従来のシェルがコマンド間でテキストをパイプするのに対し、Nushellはシリアライズされたデータ形式(JSON、YAML、CSVなど)をファーストクラスのテーブルやレコードとして操作し、=jq= のような外部ツールなしで簡単なデータ操作を可能にします。fishと同様に非POSIXですが、対話的シェルにはPOSIX互換性は要件ではありません。

    カスタム設定はまだ追加されていません。デフォルトで探索的な使用には十分であり、Nushell固有のワークフローにコミットするのは時期尚早です。

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

    StarshipはRustで書かれたクロスシェルプロンプトで、ここで設定されているすべてのシェルで一貫したプロンプト体験を提供するために使用されています。前身のspacefishの頃から使用されており、簡単なTOMLベースの設定が使い続けている主な理由です。

    Starshipはネイティブで非同期プロンプトレンダリングをサポートしていないため、このセクションにはfish用のカスタム非同期プロンプトモジュールも含まれています。

    { pkgs, ... }:
    {
      programs.starship = {
        enable = true;
        settings = builtins.fromTOML (builtins.readFile ./starship.toml);
      };
      my.programs.starship.enableFishAsyncPrompt = true;
    }
    
    1. プロンプトフォーマット

      プロンプトフォーマットはデフォルトのモジュールの前にシェルインジケーターを追加します。これにより、どのシェルがアクティブかが即座に分かり、テストやデバッグでfishとbashを切り替える際に便利です。

      "$schema" = "https://starship.rs/config-schema.json"
      
      format = "$shell$all$line_break$character"
      
    2. シェルインジケーター

      各シェルには固有のアイコンがあります。fishには =󰈺=(fishアイコン)。Bashとnushellはデフォルトのインジケーターを使用します。

      [shell]
      fish_indicator = "󰈺"
      powershell_indicator = "󰨊"
      disabled = false
      
    3. リモートコンテナの検出

      VS Code Remote Container(Dev Container)内で実行されているかを REMOTE_CONTAINERS 環境変数のチェックにより検出するカスタムモジュールで、視覚的なリマインダーとしてクジラの絵文字(🐋)を表示します。数年間使用されておらず、もう関連性がないかもしれません。

      [custom.remote-container]
      when = """ test "$REMOTE_CONTAINERS" """
      symbol = "🐋"
      format = " in $symbol "
      
    4. 無効化されたモジュール

      gcloud はホームディレクトリ(=~/.config/gcloud/=)からアクティブなGCP設定を読み込むため無効化されています。プロジェクトがGCPを使用しているかどうかに関係なく、すべてのリポジトリで表示されてしまいます。

      [gcloud]
      disabled = true
      
    5. 非同期プロンプトモジュール

      Starshipのデフォルトのfish統合を非同期バリアントに置き換えるカスタムhome-managerモジュールです。非同期プロンプトスクリプトはduamentのgistから取得したもので、fish-async-promptに基づいています。非同期プロンプトサポートに関するアップストリームの議論は fish-shell#8223も参照してください。

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

3.3.5. Version Control

  1. Git

    Gitバージョン管理の設定です。Gitはhome-managerの=programs.git= モジュールを通じて直接設定され、=~/.config/git/config= を宣言的に生成します。

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

      基本的なgitの動作設定です。

      settings = {
        <<git-user-identity>>
        <<git-editor>>
        <<git-color>>
        <<git-default-branch>>
        <<git-push-safety>>
        <<git-url-rewrite>>
      };
      
      1. ユーザーID
        user = {
          name = "natsukium";
          email = "[email protected]";
        };
        
      2. エディター

        core.editor = "vim" はgit操作のフォールバックエディターとして設定されています。=EDITOR= 環境変数はホーム設定で nvim に設定されていますが、一部のコンテキスト(最小限の環境での=git commit= など)ではセッションの環境変数を引き継がない場合があります。gitconfigで明示的に設定することで一貫した動作を保証します。

        core.editor = "vim";
        
      3. カラー

        すべてのgitサブコマンドでターミナルのカラーサポートを自動検出します。

        color = {
          status = "auto";
          diff = "auto";
          branch = "auto";
          interactive = "auto";
          grep = "auto";
        };
        
      4. デフォルトブランチ
        init.defaultBranch = "main";
        
      5. 強制プッシュの安全対策

        push.useForceIfIncludes は強制プッシュの安全チェックを有効にします。gitはローカルブランチがリモートの現在のtipを含んでいるか確認してから強制プッシュを許可します。これにより、最後のfetch以降に他の人がプッシュしたコミットを誤って上書きすることを防ぎます。=–force-with-lease= だけでは、リモートrefがバックグラウンドで更新された場合(例: lazygitの定期的なfetchやバックグラウンドの=git fetch=)に検出できません。

        push.useForceIfIncludes = true;
        
      6. URLの書き換え

        url."[email protected]:".pushInsteadOf はプッシュURLをHTTPSからSSHに書き換えます。これにより=git clone https://github.com/…= がSSHなしで動作し(CIや一時的な環境で有用)、プッシュは常にSSH認証を使用するため、パーソナルアクセストークンが不要になります。

        実際には、ghが独自の認証ヘルパーを提供し、HTTPS認証を透過的に処理するため、この書き換えルールはGitHubリポジトリでは実行されないかもしれません。=gh= が利用できない環境のための意図的なフォールバックとして保持されています。

        url."[email protected]:".pushInsteadOf = "https://github.com/";
        
    2. コミット署名

      SSHベースのコミット署名です。SSHキーはプッシュ認証にすでに必要なため、別途GPGキーチェーンを管理する必要がなくなることからGPGよりSSHキーが選ばれました。GitのSSH署名サポート(Git 2.34で追加)は、よりシンプルな鍵管理でGPG署名と同じ整合性保証を提供します。認証と署名の両方に1つの鍵ペアで対応できます。

      GitHubは2022年8月からSSHコミット検証をサポートしており、2024年11月から検証結果がサーバー側に永続化されるようになりました。この永続的検証により、SSH署名の主な懸念点であった鍵のローテーションによる過去の署名の無効化が解消され、長期的なIDの保証におけるGPGの主な利点がなくなりました。

      将来的には、gitsign(Sigstoreベースの署名)のようなキーレスソリューションに移行することで、鍵管理を完全に不要にしワークフローをさらに簡素化できます。GitHubがSigstore検証をネイティブサポートするのを待っている状態です。

      signByDefault = true はすべてのコミットを=git commit -S= なしで署名し、未署名のコミットが紛れ込むのを防ぎます。

      signing = {
        format = "ssh";
        key = "~/.ssh/id_ed25519.pub";
        signByDefault = true;
      };
      
    3. グローバルIgnore

      すべてのリポジトリでコミットすべきでないパターンです。リポジトリごとの .gitignore エントリではなくグローバルignoreとしているのは、個人的なツール選択を反映しており、共同作業者に押し付けるべきではないためです。

      ignores = [
        <<git-ignores-os>>
        <<git-ignores-dev>>
        <<git-ignores-workflow>>
        <<git-ignores-private>>
      ];
      
      1. OSアーティファクト

        macOS Finderのメタデータファイルです。LinuxとmacOSの両方で使用されるクロスプラットフォームなdotfilesリポジトリのため、グローバルignoreに含めています。

        ".DS_Store"
        
      2. 開発環境

        ローカルに留めるべき開発ツールが生成するアーティファクトです:

        • .aider* はaiderの会話履歴と設定ファイルにマッチします。 セッション固有のコンテキストを含むため、リポジトリに漏れるべきではありません。
        • .direnv=、.envrc=: direnvの状態と設定。各プロジェクトが独自の .envrc を定義できますが、このリポジトリではflakeベースのdevshellを使用するため、 .envrc ファイルは自動生成されコミットすべきではありません。
        • .ipynb_checkpoints: Jupyter Notebookの自動保存ファイル。
        • .pre-commit-config.yaml: コミットではなくdevshell環境でプロジェクトごとに管理し、 ツールバージョンの異なるコントリビューター間のバージョン競合を避けます。
        • .vscode/: 開発者ごとに異なるエディター固有の設定。
        • __pycache__/: Pythonバイトコードキャッシュ。実行ごとに再生成されます。
        ".aider*"
        ".direnv"
        ".envrc"
        ".ipynb_checkpoints"
        ".pre-commit-config.yaml"
        ".vscode/"
        "__pycache__/"
        
      3. ワークフロー
        • .worktree: git worktreeワークフローで使用されるマーカーファイル。
        ".worktree"
        
      4. プライベートノート

        .private/ は個人的なメモ、LLMコンテキストファイル、その他の共有すべきでないローカル専用のドキュメントのためのディレクトリです。

        ".private/"
        
    4. Scalar

      Scalarは大規模リポジトリのgitパフォーマンスを最適化します。600,000件以上のコミットを持つnixpkgsリポジトリ向けに特に有効化されており、Scalarのバックグラウンドメンテナンス(prefetch、commit-graph、loose-objects)とファイルシステムモニターの統合から大きな恩恵を受けます。

      Scalarモジュール(=programs.git.scalar=)はhome-manager/git-scalar.nixで定義されたカスタムhome-managerモジュールで、gitの組み込みパフォーマンス最適化(multipack index、preload index、untracked cache、fsmonitor)を設定します。

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

      gh(GitHub CLI)はgitとともに有効化されています。認証ヘルパー(programs.gh.gitCredentialHelper.enable、デフォルトで有効)を提供し、GitHubリポジトリのHTTPS認証を透過的に処理するためです。これがpushInsteadOf設定が実際には実行されないかもしれない理由です。=gh= の認証コンテキストはHTTPS経由のfetchとpushの両方をカバーします。

      programs.gh.enable = true;
      
    6. Delta

      deltaはターミナルでシンタックスハイライト付きのdiffを提供し、行番号やサイドバイサイド表示をサポートします。クリーンな視覚的表現が選定理由です。

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

      Difftasticは、行単位の比較ではなくAST(抽象構文木)レベルで動作する構造的差分ツールです。関数の移動や変数のリネームを、削除と挿入のブロックではなく、単一の意味的変更として認識します。これはJSXのリファクタリング、インデントの変更、ネストされたタグの再構成において特に有用で、行ベースの差分ではノイズが多く読みにくい出力になる場面で力を発揮します。

      difftasticとdeltaの両方を併用しているのは、それぞれ補完的な役割を持つためです。deltaは日常的な操作(=git diff=、=git log=)でGitの組み込み行差分にシンタックスハイライトを追加し、difftasticは構造的な理解が重要な場面でlazygit経由で使用します。

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

      LazygitはGitのターミナルUIです。個別のhunkのステージング、インタラクティブリベース、コンフリクト解決といった対話的操作では、視覚的なフィードバックループがエラーを大幅に減らすため、生のGitコマンドではなくTUIを選択しました。以前はgituiを使用していましたが、より広い機能セットを持つlazygitに移行しました。gituiのパフォーマンス上の優位性は実際には現れず、ワークフローに不可欠な操作がいくつか欠けていました。

      nixpkgsのような大規模リポジトリではパフォーマンスが顕著に低下しますが、操作性と活発な開発により、このトレードオフにもかかわらずワークフローの信頼できる一部となっています。

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

        視認性向上のため、lazygitのインターフェースでNerd Fontアイコンを有効にします。

        gui = {
          showIcons = true;
        };
        
      2. Git連携

        overrideGpg = true は、lazygitにGPG/SSH署名用のサブプロセスを生成する代わりにGitコマンドをインラインで実行するよう指示します。デフォルトでは、lazygitはユーザーが対話的にパスフレーズを入力できるようサブプロセスに委譲します。しかし、このサブプロセスモードではlazygitが内部でインタラクティブリベースを制御できなくなります。リベース中に複数の署名プロンプトが発生し、lazygitがそれを処理できないため、HEAD以外のコミットのリワード、並べ替え、編集が完全に無効化されます。=overrideGpg = true= を設定すると、lazygitは署名エージェント(ssh-agent、macOS Keychain)がパスフレーズをキャッシュしていると想定するため、対話的プロンプトが不要になり、すべてのリベース操作が正常に動作します。

        ページャー設定により、deltadifftasticの両方がlazygitの差分ビューに統合されます。Deltaはシンタックスハイライト付きの行差分を提供し(lazygitが独自にページングを行うため --dark --paging=never を指定)、difftasticは構造的比較のための外部diffコマンドとして利用できます。

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

      頻繁に使うgit操作のシェル略語です。エイリアスではなく略語を使用しているのは、fishが実行前にインラインで展開するため、履歴に実際のコマンドが表示され、実行前の修正も可能になるためです。

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

        lease付きの強制プッシュです。まだfetchされていないリモートの変更の上書きを防ぐため、=–force= よりも安全です。

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

        gpm はリモートのデフォルトブランチからpullします。=main= や master をハードコードするのではなく、=git remote show= で動的に検出します。これにより=master= や他のブランチ命名規約を使用するリポジトリにも対応できます。

        gpu はupstreamからpullします。フォークワークフローでオリジナルのリポジトリと同期するために使用されます。

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

        gci はコミットを作成します。末尾のスペースにより -m "message" を直接追加できます。

        gca は直前のコミットを修正します。インタラクティブrebaseのワークフローで頻繁に使用されます。

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

3.3.6. Browser

  1. Zen Browser

    Zen Browserはプライバシーとカスタマイズに焦点を当てたFirefoxベースのブラウザです。Firefoxの拡張機能エコシステムと about:config がブラウザの動作をより深く制御でき、NixエコシステムにはFirefoxのプロファイルをhome-managerで宣言的に管理する成熟したツールがあるため、ChromiumベースのブラウザよりFirefox派生が選ばれました。

    Zen Browserは、Firefoxの完全な互換性を保ちつつUI/UXが改善されているため、素のFirefoxより選ばれました。拡張機能、検索エンジン、プロファイル設定は同じように動作します。

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

      ユーザープロンプトなしで拡張機能を自動インストールします。デフォルトでは、Firefoxはプロファイル経由でインストールされる各拡張機能に確認ダイアログを表示します。=extensions.autoDisableScopes= を0に設定するとこの動作が無効になり、完全に宣言的な拡張機能管理に必要です。そうしないと、プロファイル再ビルド後の初回起動時にすべての拡張機能について手動承認が必要になります。

      settings = {
        "extensions.autoDisableScopes" = 0;
      };
      
    2. 検索エンジン

      開発関連のパッケージレジストリやドキュメントに素早くアクセスするためのカスタム検索エンジンです。各エンジンには短いエイリアス(例: =@np=)が割り当てられており、アドレスバーで使用でき、各サイトに手動で移動する必要がなくなります。

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

        公式のNixOS/nixpkgsリポジトリを検索します。変更や利用不能になる可能性のある外部URLに依存しないよう、アイコンにはnixpkgsの=nixos-icons= を使用しています。

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

        NixOSコミュニティwikiで設定例やトラブルシューティングガイドを検索します。

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

        noogleはNix関数の検索エンジンで、HaskellにおけるHoogleに相当します。Nix式を書く際に型シグネチャや名前でライブラリ関数を見つけるのに便利です。

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

        Rustパッケージレジストリでcrateの検索とバージョン情報を調べます。

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

        npmレジストリでJavaScript/TypeScriptパッケージを検索します。

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

        Python Package IndexでPythonパッケージを検索します。

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

      宣言的に管理されるブラウザ拡張機能です。拡張機能は2つのoverlayで提供されるセットから取得されます。=firefox-addons=(NUR firefox-addonsリポジトリから)と =my-firefox-addons=(アップストリームにないカスタムの追加)です。

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

3.4. Emacs

3.4.1. early-init.org

  1. 不要なバーを消す

    ツールバーはキーバインドやメニューバーから既にアクセスできる機能と重複しており、垂直スクロールバーは有用なナビゲーションを提供せず水平方向のスペースを浪費します。行番号やモードラインの表示の方がより正確です。~tool-bar-mode~ / scroll-bar-mode を呼ぶのではなく default-frame-alist で設定することで、モードが無効化される前にバーが一瞬表示されるちらつきを防ぎます。

    (push '(tool-bar-lines . 0) default-frame-alist)
    (push '(vertical-scroll-bars) default-frame-alist)
    
  2. 煩わしいベルが鳴らないようにする

    デフォルトのビープ音は注意を散漫にし、有用な情報を提供しません。~ignore~ に置き換えることで完全に消音します。ビジュアルベル(~visible-bell~)は画面のフラッシュも同様に邪魔なため採用しませんでした。

    (setq ring-bell-function 'ignore)
    
  3. ファイルのバックアップを作らないようにする

    Emacsはデフォルトでバージョン管理下のファイルのバックアップを作成しません(~vc-make-backup-files~ が ~nil~)が、リポジトリ外のファイルにはバックアップが作られます。実質的にすべての編集はGit管理下のツリーで行うため、残りのケースは稀であり、散らかりの方が安全策としての価値を上回ります。自動保存ファイルも同様に、未保存の作業はGitのstashやreflogで復元できるため不要です。

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

    ロックファイル(~.#filename~)は複数のEmacsインスタンス間での同時編集を防止しますが、シングルインスタンスのワークフローでは作業ディレクトリを散らかし、ファイル監視やビルドツールの妨げになるだけです。

    (setq create-lockfiles nil)
    

3.4.2. init.org

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

    auto-fill-modeではなくvisual-line-modeをグローバルに有効にします。これにより、実際のファイルを変更せずに論理的な行構造を保ちつつ、長い行を視覚的に折り返して読みやすくします。

    (visual-line-mode 1)
    

    カーソル行をハイライトします。

    (global-hl-line-mode 1)
    

    yes / no とタイプする代わりに y / n の 一文字で回答できるようにする。

    (setq use-short-answers t)
    
    1. フォント

      Moralerspacemonaspaceベースの 合成フォントファミリーで、ラテン文字のプログラミング用書体と 日本語グリフを組み合わせたものです。各バリアント (Argon、Krypton、Radon、Xenon)はそれぞれ固有の書体特性を 持つため、フェイスごとに異なるバリアントを割り当てることで、 Emacsの合成変換に頼らずに視覚的に区別可能なボールド、 イタリック、ボールドイタリックの描画が得られます。 :weight 'normal:slant 'normal により、各バリアント固有の デザインの上にEmacsがさらにボールドやイタリックを 合成するのを防ぎます。

      font-lock-comment-face にはRadon(イタリック体のバリアント)を 使用し、コードコメントをやわらかく視覚的に区別しやすい 見た目にしています。

      (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)
      

      明示的に set-language-environment を設定しないと、Emacsは ロケールの優先順位に基づいてCJK文字に中国語フォントを 選択することがあり、日本語テキストが中国語のグリフで 表示されてしまいます。

      (set-language-environment "Japanese")
      
      1. TODO テクスチャヒーリングを有効にする
    2. Nixラッパーのパス

      NixのEmacsラッパー(=extraEmacsPackages=)は、シェルの PATH に 存在しないバイナリを exec-path に追加します。 exec-path-from-shellとenvrcはどちらも動作時に exec-path を 置き換えるため、これらのエントリが失われます。 初期化時に一度保存しておくことで、両パッケージから マージし直せるようにしています。

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

      macOSでは、Emacsはシェルから環境変数(~$PATH~ など)を 継承できないため、回避策として exec-path-from-shellが 必要です。 emacs-plusは PATHをEmacsのInfo.plistに注入するため、そのパッチを 適用すればこのパッケージは不要になるかもしれません。

      exec-path-from-shell-initialize はログインシェルの PATHexec-path を置き換えます。初期化後に my/nix-exec-path を マージし直すことで、ラッパー由来のパスを保持します。

      アプリケーションランチャー(Dock、Spotlight)から起動した場合、 EmacsはNix関連の変数を含まない最小限のlaunchd環境のみを 継承します。これらがないと direnv export が不完全な結果を 返すことがあります。 (envrc#92

      また、fishは非対話的な呼び出し(=fish -c "…")でも =conf.d/*.fish を読み込みます。nix-darwinはそこに環境セットアップ スクリプトを配置しており、ガード変数 (=__fish_nixos_env_preinit_sourced=、 __NIX_DARWIN_SET_ENVIRONMENT_DONE=、 =__HM_SESS_VARS_SOURCED=)がない場合、 システムプロファイルからPATHを再初期化します。 これらのガード変数を伝播しないと、 =compileshell-command が生成するサブプロセスは、 envrc自体は正しい環境を適用していたにもかかわらず、 envrcが構築したバッファローカルのPATHを失います。

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

      envrc.elはdirenvに基づく バッファローカルの環境変数を提供します。これにより、Emacsが direnvで管理されたプロジェクト固有の開発環境を 認識できるようになります。

      環境変数をグローバルに設定するdirenv.elとは異なり、envrc.elは バッファローカルに保持します。それぞれ独自の.envrcを持つ 複数のプロジェクトを同時に扱う場合に不可欠です。

      他のパッケージが正しい環境を継承できるよう、このモードは 早期に(after-initフックで)有効にする必要があります。

      envrcがバッファの環境を更新すると、=exec-path= を direnv export の値で置き換え、Nixラッパーのパスが 失われます。 (envrc#9) 更新のたびに my/nix-exec-path をマージし直すことで、 direnv管理下のバッファ内でラッパー由来の実行ファイル (例: yaml-language-server)へのアクセスを保持します。

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

      indent-barsは 各インデントレベルに設定可能な縦のガイドバーを表示します。 tree-sitter連携によるスコープ認識ハイライトも利用でき、 現在のスコープ外のバーは控えめに表示されます。

      (use-package indent-bars
        :ensure t
        :hook (prog-mode . indent-bars-mode))
      
  3. ミニバッファ
    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))
    

    組み込みのsavehistパッケージはユーザーの入力を記録し、 セッションをまたいで保存します。これにより、ユーザーは 最新の選択肢を常に上位に表示できます (M-xなどで利用時)。

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

        ForgeはMagitを拡張し、 Emacsから直接GitHub、GitLab、その他のforgeと連携します。 エディタを離れずにissueの閲覧、プルリクエストのレビュー、 通知の管理が行えます。

        Forgeは ghub を通じて認証を行い、 auth-sources から認証情報を読み取ります。 authinfo.age ファイルにはGitHub APIトークンの エントリを記載する必要があります:

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

        トークンは有効期限6ヶ月のfine-grainedパーソナルアクセストークンで、 定期的な再生成が必要です。必要な権限は以下の通りです:

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

    lsp-modeはEmacs用の Language Server Protocolクライアントです。外部の言語サーバーと 通信することで、IDEライクな機能(補完、診断、ナビゲーション)を 提供します。

    EglotはEmacs 29+に軽量な代替として組み込まれていますが、 メジャーモードごとにサーバー1つを前提としており、 同じバッファで複数のサーバーを同時に動かす (例: 言語サーバーとリンターやフォーマッター)のが煩雑です。 lsp-modeは複数のサーバーの同時実行をネイティブに扱え、 LSPの挙動をより細かく制御できます。

    lsp の代わりに lsp-deferred を使用し、バッファが表示される までサーバーの起動を遅延させます。バックグラウンドで開かれた バッファに対する不要なプロセスを回避するためです。

    ヘッダーラインの既存の breadcrumb パッケージとの競合を 避けるため、=lsp-headerline-breadcrumb-mode= を無効にしています。

    (use-package lsp-mode
      :ensure t
      :commands (lsp lsp-deferred)
      :init
      (setq lsp-keymap-prefix "C-c l")
      :config
      (setq lsp-headerline-breadcrumb-enable nil))
    
  6. 言語サポート
    1. tree-sitter

      tree-sitterモードでfont-lockレベルを最大にします。

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

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

      markdown-modeはMarkdown形式のテキストを編集するためのメジャーモードです。

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

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

      Nix式のためのtree-sitterベースのメジャーモードです。

      nixd(lsp-mode経由で設定) はC++のNixエバリュエーターと連携し、Nix式のIDE機能を提供します。 静的解析に頼るnilとは異なり、nixdは実際のNix評価を行うため、 属性パス(例: =pkgs.=)やNixOSオプション、 flakeの入力に対する正確な補完が可能です。

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

      terraform-modeは Terraform(HCL)ファイルのシンタックスハイライトと インデントを提供します。

      tree-sitter版はMELPAで提供されていないため、従来の メジャーモードを使用しています。

      terraform-ls (lsp-mode経由で設定)は、補完、=terraform validate= による診断、 Terraform設定に対する定義ジャンプなどのIDE機能を提供します。

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

      po-modeは GNU gettext PO(Portable Object)ファイルを編集するための Emacsのメジャーモードです。POファイルはソフトウェアの 国際化のための翻訳を格納します。

      POファイルをプレーンテキストとして編集するのはエラーが 起きやすく、エスケープや構造に厳格な要件があるためです。 po-modeはエントリ間の構造化されたナビゲーション、 自動バリデーション、よくあるフォーマットエラーの防止を 提供します。

      1. 共通の操作

        PO modeはtext modeから派生していません。バッファは読み取り専用で 独自のキーマップを持つため、通常のテキスト編集コマンドは 直接使えません。翻訳はサブエディットバッファ(=RET=)を 通じて編集する必要があります。

        1. 主要なコマンド

          ファイル操作、バリデーション、PO mode全般の管理に使う コマンドです。

          キー 関数 説明
          _ po-undo 最後の変更を取り消す
          q po-confirm-and-quit 確認して終了する
          ? h po-help PO modeのヘルプを表示する

          C-x k=(=kill-buffer=)ではなく =q で終了してください。 未保存の変更や未翻訳のエントリについて適切に警告してくれます。

          詳しくはMain PO mode Commandsを 参照してください。

        2. エントリの移動

          POファイル内のエントリ間を移動するコマンドです。

          キー 関数 説明
          n po-next-entry 次のエントリに移動する
          p po-previous-entry 前のエントリに移動する
          < po-first-entry 最初のエントリに移動する
          > po-last-entry 最後のエントリに移動する

          詳しくはEntry Positioningを 参照してください。

        3. 翻訳を修正する

          翻訳文字列を編集するコマンドです。=RET= を押すと 通常のEmacs編集が使えるサブエディットバッファが開きます。

          キー 関数 説明
          RET po-edit-msgstr 編集用のサブエディットバッファを開く
          C-c C-c po-subedit-exit 編集を完了して変更を適用する
          C-c C-k po-subedit-abort 編集を中止して変更を破棄する
          DEL po-fade-out-entry 翻訳を削除する

          詳しくはModifying Translationsを 参照してください。

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

      protobuf-ts-modeは proto3ファイルを編集するためのtree-sitterベースの メジャーモードです。

      proto グラマーが利用可能な場合、このモードは .proto ファイルを自動登録します。

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

      just-ts-modejustコマンドランナーファイルを 編集するためのtree-sitterベースのメジャーモードです。 C-c ' でポイント位置のレシピ本文用の専用編集バッファが開き、 shebangに基づく自動言語検出が行われます。

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

      yaml-ts-mode はYAMLファイル用の組み込みtree-sitterベースの メジャーモードです。tree-sitterグラマーは treesit-grammars.with-all-grammars で既に利用可能なため、 グラマーが存在すればモードは自動で有効になります。

      yaml-language-server(lsp-mode経由で設定)は SchemaStoreを使った スキーマバリデーションを提供します。例えば、 .github/workflows/ 配下のファイルはGitHub Actions ワークフロースキーマに自動でマッチし、 ワークフロー定義の補完と診断が有効になります。

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

      Semantic Line Breaks (SemBr)は、 句読点の後やフレーズの間など、文の論理的な境界で 改行を入れる書き方の規約です。これにより、 バージョン管理でのdiffがより意味のあるものになり、 レンダリング結果に影響を与えずに可読性が向上します。

      推奨される行の長さは約80文字です。行が不必要に 長くなるのを防ぐため、エディタで上限として設定しています。

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

      Orgをコードブロックのシンタックスハイライトを保ったままHTMLに変換する。

      カレントバッファーをHTMLに変換するには C-c C-e h h を押すとよい。

      (use-package htmlize
        :ensure t)
      
  8. ドキュメント
    1. pdf-tools

      Emacsの組み込みDocViewモードはPDFをページごとにラスタライズした 画像として表示するため、ほとんどのズームレベルで文字がぼやけ、 テキスト選択やインクリメンタルサーチ、注釈といった インタラクティブな機能がありません。

      pdf-toolspoppler を 利用したビューアでDocViewを置き換え、鮮明な描画、 isearch連携、注釈サポート、LaTeXワークフロー向けの SyncTeXを提供します。

      pdf-tools-install の代わりに pdf-loader-install を使用し、 PDFが実際に開かれるまで初期化を遅延させています。 Nixのビルド済み epdfinfo バイナリでは両関数の動作は同じですが、 loader版は起動時の不要な処理を回避できます。

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

    elfeedはEmacs用の ウェブフィードリーダーです。 elfeed-protocolと 組み合わせて、Fever API経由でMinifluxからフィードを読んでいます。 これによりフィード管理をMinifluxに集約し、 デバイス間で購読リストが重複するのを避けています。

    この設定が機能するには、Minifluxで事前にFever APIを 有効にする必要があります (Settings -> Integrations -> Fever API)。

    認証情報は ~/.authinfo.age に格納しています。

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

    notmuchはタグベースのメール インデクサー兼検索ツールです。 notmuch.elは メールの閲覧と整理のためのEmacsインターフェースを提供します。 mbsyncで同期されたローカルのmaildirからメールを読み取ります。

    mu4eではなくnotmuchを選んだのは、notmuchインデクサーが 既にすべてのメールアカウントに対して設定済みだからです。 notmuch.elの追加にはEmacsフロントエンドのみが必要で、 同じmaildirに対する2つ目のインデクサーを避けられます。

    notmuchには組み込みの削除操作がなく、maildirフォルダの配置ではなく メタデータ(タグ)を管理します。 d を押すとメッセージに +deleted タグを付け、 inbox を除去します。実際の削除は次の notmuch new で行われます: post-newフックがインデックス後にタグ付きファイルをGmailの Trashメールディレクトリフォルダに移動し、次のmbsync実行時に サーバーへ同期されます。 D はこの操作を元に戻し、=inbox= を復元して deleted を 除去します。これは次の notmuch new がファイルを移動する前にのみ 有効です。mbsyncが移動を同期した後は、メッセージは Gmailの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は notmuchをOrg modeのリンクシステムと統合します。 notmuchバッファで org-store-link=(=C-c l=)を呼び出すと 現在のメッセージまたはスレッドへのリンクが保存され、 org-captureテンプレートに =%a で挿入できます。

    (use-package ol-notmuch
      :ensure t
      :after (notmuch org))
    
  11. ターミナル
    1. vterm

      emacs-libvtermは libvtermベースの本格的なターミナルエミュレーターです。 純粋なEmacs Lispの代替手段よりもパフォーマンスと互換性に優れ、 Claude Codeのような対話型CLIツールの実行に適しています。

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

      claude-code-ide.elは Claude Code CLIをEmacsに統合し、エディタ内で直接 AIによるコード支援を可能にします。

      このパッケージにはターミナルバックエンド(vtermまたはeat)と Claude Code CLIが必要です。ターミナル互換性を重視して vtermをバックエンドとして使用しています。

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

    age.elage暗号化ツールを使い、 Emacsで .age ファイルの透過的な暗号化・復号を提供します。

    SSH鍵をidentity/recipientとして使用しており、 別途ageの鍵ペアを用意する必要がありません。

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

      compilationバッファはcomint-modeを継承しないため、ANSIエスケープシーケンスがデフォルトでは生のテキストとして表示されます。compilationフィルタフックに ansi-color-compilation-filter を追加すると、これらのシーケンスが解釈されて色として描画されます。

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

      選択リージョンの絶対パスと行範囲を file:start-end 形式(例: =/path/to/file.el:10-20=)でコピーします。コーディングエージェントのプロンプトに貼り付けると、曖昧さのないファイル参照として利用できます。

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

        ghq(https://github.com/x-motemen/ghq%EF%BC%89%E3%81%A7%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%B3%E3%81%97%E3%81%9F%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%82%92%E3%81%99%E3%81%B9%E3%81%A6%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%A8%E3%81%97%E3%81%A6%E7%99%BB%E9%8C%B2%E3%81%97%E3%81%BE%E3%81%99

        具体的には、=~/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)
        

        プロジェクト一覧を更新日時順にソートし、最近更新されたプロジェクトを先頭に表示します。

        my/sync-project-list 内でソートするのではなくadviceを使うことで、ファイル生成時ではなく選択時の現在のmtimeでソートされます。これにより、長時間のEmacsセッション中でも常に最新の順序が維持されます。

        (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)))))
        
                    #'my/sort-projects-by-mtime)
        (advice-add 'project-known-project-roots :filter-return
        

3.5. スクリプト

flake derivation、pre-commitフック、Makefileレシピで使用されるスクリプトです。適切なシンタックスハイライトと単独実行を可能にするため、別ファイルに分離しています。

(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)

2つのpre-commitフックスクリプトは共通のパターンを持っています。コマンドを実行し、=git diff= で変更を確認し、変更があればエラーとともに手順を表示します。この共通ロジックは 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)

org-export-with-author 設定は明示的に無効にしています。=configuration.org= には #+AUTHOR: キーワードがないため、Org modeはEmacsの変数 user-full-name にフォールバックします。macOSではこの変数がシステムディレクトリサービス(=dscl= / getpwuid=)から設定され、不要な =#+author: 行が出力されます。Linuxでは、特にCI環境では値が通常空のため、この行は省略されます。この設定を無効にすることで、プラットフォーム間で一貫した出力を保証します。

(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. 開発

このリポジトリは設定作業に必要なすべてのツールを備えたNix開発シェルを提供します。以下のコマンドでシェルに入れます:

nix develop

このシェルにはインフラツール(Terraform、sops、ssh-to-age)、翻訳ツール(po4a、gettext)、ビルドユーティリティ(nix-fast-build)が含まれています。シェルに入ると自動的にpre-commitフックのセットアップ、MCPサーバーの設定、文芸的ソースからの CLAUDE.md 同期が行われます。

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. 翻訳

このプロジェクトでは翻訳にpo4aを使っています。

4.1.1. 必要なソフトウェア

必要なパッケージは開発シェルに含まれています。

gettext
self'.packages.po4a_0_74
  • gettext: msgfmtやその他の国際化ユーティリティを提供
  • po4a_0_74: Org modeサポートにはpo4a >= 0.74が必要です。 nixpkgsには古いバージョンが含まれているため、ピン留めしたderivationを使用しています(packages参照)。

4.1.2. 翻訳作業

  1. po4aを設定する

    対象となる言語、生成するpoファイルを置くディレクトリ、それから翻訳対象のドキュメントを以下のように設定します。=-k 0=というオプションは翻訳が不完全な場合でも翻訳されたファイルを出力するものです。(デフォルトでは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"
    

    =po4a.cfg=について、詳しくは=man po4a=を参照してください。

  2. poを生成/更新する

    ドキュメントを更新したら、次のコマンドでpoファイルを更新する必要があります。このコマンドはテンプレート(pot)と各言語に対応したpoを=po4a.cfg=で設定したパスに生成します。

    po4a --no-translations po4a.cfg
    
  3. 翻訳する

    対象となる言語のpoをpoエディタで編集します。Emacsのpo-modeやpoedit、GNOMEのGtranslator、KDEのLokalizeが有名です。

  4. 翻訳ファイルを生成/更新する

    翻訳が終わったら次のコマンドでファイルを生成します。このときpoも更新されるため、実運用上はこのコマンドを実行するだけで良いでしょう。

    po4a po4a.cfg
    

Author: Nix build user

Created: 2026-03-16 Mon 14:34

Validate