俺流! home-managerのhome.file

始めに

Ghosttyの設定をする記事を書いたタイミングで、config-fileのオプショナルな設定の読み込みを活用したいと思って、 home-managerの設定を色々触っていたらリファクタリングが止まらなくなってしまいました。

これを機にhome-managerでのファイル配置について「俺流」の考え方や、今回のリファクタの内容を書き残しておこうと思います。

ファイル配置の考え方

私のdotfilesのhomeディレクトリは、ユーザーのホームディレクトリ配下に配置するファイル群と同じ階層にしています。

home
├── .config
├── .docker
├── .icons
├── .Xresources.d
├── .zsh
├── bin
└── Library

上記のディレクトリ一覧を確認する限り、Library/に関してはmacOSのみで配置が必要です。 この他にも、前回のGhosttyのようなmacOSかLinuxによって設定ファイルの配置を切り替えるパターンが、紹介は省きますがいくつかあります。

基本のディレクトリ構成はLinuxをベースとして、macOSのみで必要な対応は、都度対応するという方針にしています。

home-managerのhome.file

凄く雑に紹介すると、home-managerのhome.fileはユーザーディレクトリ配下にファイルを配置してくれます。

{
  home.file = {
    # ディレクトリならこんな感じ
    ".zsh" = {
      source = ./.zsh;
      recursive = true;
    };
    # ファイル単体ならこれで良い
    ".vimrc".source = ./.vimrc;
  };
}

ちょうど冒頭のコミット前は上記のような構造体を羅列していますね。
環境毎の切り替えにif式を書くとゴチャゴチャしそうだったので、次のようなAttrSetに包んでから使用する環境毎に展開してマージするという運用をしています。

{
  homeDirectory = {
    # LinuxとmacOS共通で$HOME直下に配置されるファイルやディレクトリの配置設定を記述
  };
  dotConfig = {
    # LinuxとmacOS共通で$XDG_CONFIG_HOME直下に配置されるファイルやディレクトリの配置設定を記述
  };
  MacOS = {
    homeDirectory = {
      # macOSのみで$HOME直下に配置されるファイルやディレクトリの配置設定を記述
    };
    library = {
      # macOSのLibraryというディレクトリ構成をいまだに理解できていない
      # つまりmacOSの気持ちになれない
    };
    dotConfig = {
      # macOSのみで$XDG_CONFIG_HOME直下に配置されるファイルやディレクトリの配置設定を記述
    };
  };
  Linux = {
    homeDirectory = {
      # Linuxのみで$HOME直下に配置されるファイルやディレクトリの配置設定を記述
    };
    dotConfig = {
      # Linuxのみで$XDG_CONFIG_HOME直下に配置されるファイルやディレクトリの配置設定を記述
    };
  };
}

環境毎に展開して、マージするというのは次のリンク先をご覧ください。

もうちょっとシンプルにした結果

ファイルをhome-managerで配置してもらうという部分だけ見ればこのままでもよいですが、同じ記述が多くメンテナンス性は良くないですね。

ここで改めて、私のdotfilesでは一部設定ファイルを除いて、配置元と配置先のファイル名は統一されています。
であれば、ファイル名だけを列挙しhome.fileの構造を作って統合できればよいはずです。
javascriptでいうところの「reduce関数みたいなのがあれば解決できそう」、
ということでnix言語のマニュアルを見ていたらfoldl'という関数を見つけました。

https://nix.dev/manual/nix/2.24/language/builtins#builtins-foldl'

そして、このfoldl'関数を使って作ったのが次のコードです。

今回の主役

# fileMap.nix
let
  fileMap =
    {
      dist,
      src,
      is_recursive,
    }:
    let
      f =
        files:
        builtins.foldl' (
          acc: file:
          let
            distFile = if dist == "" then "${file}" else "${dist}/${file}";
          in
          acc
          // {
            ${distFile} = {
              source = symlink /${src}/${file};
              recursive = is_recursive;
            };
          }
        ) { } files;
    in
    f;

  homeFileMap = fileMap {
    dist = "";
    src = homeDir;
    is_recursive = false;
  };
  homeDirMap = fileMap {
    dist = "";
    src = homeDir;
    is_recursive = true;
  };
  xdgConfigFileMap = fileMap {
    dist = ".config";
    src = xdgConfigHome;
    is_recursive = false;
  };
  xdgConfigDirMap = fileMap {
    dist = ".config";
    src = xdgConfigHome;
    is_recursive = true;
  };
in

使い方は二段階にしていて、配置先と配置元のパス・home.file.<name>.recursiveの値を部分適用した関数を作っておきます。
これによってディレクトリを配置する関数や、ファイルを配置する関数、$HOME用と$XDG_CONFIG_HOME用の関数を作れるようになります。

あとは作っておいた関数を使いたい場所で使用して、配列にファイル名やディレクトリ名を列挙するだけでよくなります。

使っているところ

# fileMap.nix
dotConfig =
  xdgConfigDirMap [
    "alacritty/keybinds"
    "tmux"
    "fastfetch"
    "fd"
    "git"
    "glow"
    "jj"
    "kitty"
    "luacheck"
    "neofetch"
    "nvim"
    "vim"
    "sheldon"
    "yamllint"
    "zeno"
    "wezterm"
    "direnv"
  ]
  // xdgConfigFileMap [
    "alacritty/alacritty.toml"
    "alacritty/nord.toml"
    "ghostty/config"
    "ghostty/core.conf"
    "ghostty/clipboard.conf"
    "ghostty/command.conf"
    "ghostty/font.conf"
    "ghostty/keybinds.conf"
    "ghostty/mouse.conf"
    "ghostty/quick.conf"
    "ghostty/resize.conf"
    "ghostty/theme.conf"
    "ghostty/window.conf"
  ];

このGhosttyの設定ファイル配置の宣言に関しては、元の構造をそのまま列挙する形にしていたら大変な行数になっていたことでしょう。

まとめ

冒頭に書いていたGhosttyのオプショナルなファイル読み込みを試すという目的のために、かなり遠回りした解決法に行き着いたyak shavingにはなりましたが、
ファイル配置のマッピング部分がスッキリした記述になりました。

Nix言語でその場に便利関数を書くということができたので、今後も活用できる場面を見つけてコードをきれいにしていきたいですね。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。