-
-
Save piousdeer/b29c272eaeba398b864da6abf6cb5daa to your computer and use it in GitHub Desktop.
| { | |
| home.file."test-file" = { | |
| text = "Hello world"; | |
| force = true; | |
| mutable = true; | |
| }; | |
| } |
| # This module extends home.file, xdg.configFile and xdg.dataFile with the `mutable` option. | |
| { config, lib, ... }: | |
| let | |
| fileOptionAttrPaths = | |
| [ [ "home" "file" ] [ "xdg" "configFile" ] [ "xdg" "dataFile" ] ]; | |
| in { | |
| options = let | |
| mergeAttrsList = builtins.foldl' (lib.mergeAttrs) { }; | |
| fileAttrsType = lib.types.attrsOf (lib.types.submodule ({ config, ... }: { | |
| options.mutable = lib.mkOption { | |
| type = lib.types.bool; | |
| default = false; | |
| description = '' | |
| Whether to copy the file without the read-only attribute instead of | |
| symlinking. If you set this to `true`, you must also set `force` to | |
| `true`. Mutable files are not removed when you remove them from your | |
| configuration. | |
| This option is useful for programs that don't have a very good | |
| support for read-only configurations. | |
| ''; | |
| }; | |
| })); | |
| in mergeAttrsList (map (attrPath: | |
| lib.setAttrByPath attrPath (lib.mkOption { type = fileAttrsType; })) | |
| fileOptionAttrPaths); | |
| config = { | |
| home.activation.mutableFileGeneration = let | |
| allFiles = (builtins.concatLists (map | |
| (attrPath: builtins.attrValues (lib.getAttrFromPath attrPath config)) | |
| fileOptionAttrPaths)); | |
| filterMutableFiles = builtins.filter (file: | |
| (file.mutable or false) && lib.assertMsg file.force | |
| "if you specify `mutable` to `true` on a file, you must also set `force` to `true`"); | |
| mutableFiles = filterMutableFiles allFiles; | |
| toCommand = (file: | |
| let | |
| source = lib.escapeShellArg file.source; | |
| target = lib.escapeShellArg file.target; | |
| in '' | |
| $VERBOSE_ECHO "${source} -> ${target}" | |
| $DRY_RUN_CMD cp --remove-destination --no-preserve=mode ${source} ${target} | |
| ''); | |
| command = '' | |
| echo "Copying mutable home files for $HOME" | |
| '' + lib.concatLines (map toCommand mutableFiles); | |
| in (lib.hm.dag.entryAfter [ "linkGeneration" ] command); | |
| }; | |
| } |
| { config, pkgs, lib, ... }: | |
| let | |
| # Path logic from: | |
| # https://github.com/nix-community/home-manager/blob/3876cc613ac3983078964ffb5a0c01d00028139e/modules/programs/vscode.nix | |
| cfg = config.programs.vscode; | |
| vscodePname = cfg.package.pname; | |
| configDir = { | |
| "vscode" = "Code"; | |
| "vscode-insiders" = "Code - Insiders"; | |
| "vscodium" = "VSCodium"; | |
| }.${vscodePname}; | |
| userDir = if pkgs.stdenv.hostPlatform.isDarwin then | |
| "Library/Application Support/${configDir}/User" | |
| else | |
| "${config.xdg.configHome}/${configDir}/User"; | |
| configFilePath = "${userDir}/settings.json"; | |
| tasksFilePath = "${userDir}/tasks.json"; | |
| keybindingsFilePath = "${userDir}/keybindings.json"; | |
| snippetDir = "${userDir}/snippets"; | |
| pathsToMakeWritable = lib.flatten [ | |
| (lib.optional (cfg.userTasks != { }) tasksFilePath) | |
| (lib.optional (cfg.userSettings != { }) configFilePath) | |
| (lib.optional (cfg.keybindings != [ ]) keybindingsFilePath) | |
| (lib.optional (cfg.globalSnippets != { }) | |
| "${snippetDir}/global.code-snippets") | |
| (lib.mapAttrsToList (language: _: "${snippetDir}/${language}.json") | |
| cfg.languageSnippets) | |
| ]; | |
| in { | |
| home.file = lib.genAttrs pathsToMakeWritable (_: { | |
| force = true; | |
| mutable = true; | |
| }); | |
| } |
@piousdeer I just did some digging to find out why xdg.configFile isn't working.
As I understand it, the intended purpose of mergeAttrsList is to merge the attribute sets containing the updates for the options home.file, xdg.configFile, and xdg.dataFile. The problem here is that mergeAttrsList—or more specifically, lib.mergeAttrs—merges attribute sets shallowly (equivalent to //). Therefore, when lib.mergeAttrs is applied from left to right via foldl', the value of the attribute xdg set for xdg.configFile gets overridden by the value of the attribute xdg set for xdg.dataFile.
The solution would be to merge these attribute sets recursively, which can e.g. be achieved by using lib.recursiveUpdate instead of lib.mergeAttrs in mergeAttrsList. So a quick fix would be to replace
mergeAttrsList = builtins.foldl' (lib.mergeAttrs) { };with
mergeAttrsList = builtins.foldl' (lib.recursiveUpdate) { };@piousdeer I just did some digging to find out why
xdg.configFileisn't working.As I understand it, the intended purpose of
mergeAttrsListis to merge the attribute sets containing the updates for the optionshome.file,xdg.configFile, andxdg.dataFile. The problem here is thatmergeAttrsList—or more specifically,lib.mergeAttrs—merges attribute sets shallowly (equivalent to//). Therefore, whenlib.mergeAttrsis applied from left to right viafoldl', the value of the attributexdgset forxdg.configFilegets overridden by the value of the attributexdgset forxdg.dataFile.The solution would be to merge these attribute sets recursively, which can e.g. be achieved by using
lib.recursiveUpdateinstead oflib.mergeAttrsinmergeAttrsList. So a quick fix would be to replacemergeAttrsList = builtins.foldl' (lib.mergeAttrs) { };with
mergeAttrsList = builtins.foldl' (lib.recursiveUpdate) { };
MWAH I love you, this worked for me.
I was having some issues with the VSCode module conflicting with home.file, I think because I configure VSCode through home-manager program options rather than home.file. Not very familiar with nix but I / Claude generated this alternative which works (at least on nix-darwin):
# vscode-mutability.nix
# This module makes VS Code config files mutable (editable after generation)
# by replacing the nix-store symlinks with writable copies after home-manager
# creates them.
#
# Unlike using the `mutable` home.file option, this approach doesn't conflict
# with the VS Code module's file definitions since we just run an activation
# script after linkGeneration.
{ config, pkgs, lib, ... }:
let
cfg = config.programs.vscode.profiles.default;
configDir = "Code";
userDir = if pkgs.stdenv.hostPlatform.isDarwin then
"Library/Application Support/${configDir}/User"
else
"${config.xdg.configHome}/${configDir}/User";
# List of files to make mutable
filesToMakeMutable = lib.flatten [
(lib.optional (cfg.userSettings != { }) "${userDir}/settings.json")
(lib.optional (cfg.keybindings != [ ]) "${userDir}/keybindings.json")
(lib.optional (cfg.userTasks != { }) "${userDir}/tasks.json")
];
# Generate the shell commands to replace symlinks with copies
makeFileMutable = file: ''
target="$HOME/${file}"
if [ -L "$target" ]; then
real_source=$(readlink "$target")
echo "Making mutable: $target"
rm "$target"
cp "$real_source" "$target"
chmod u+w "$target"
elif [ -f "$target" ]; then
echo "Already mutable: $target"
fi
'';
in
{
home.activation.vscodeFileMutability = lib.hm.dag.entryAfter [ "linkGeneration" ] ''
echo "Making VS Code config files mutable..."
${lib.concatMapStrings makeFileMutable filesToMakeMutable}
'';
}
Ive been using this module extension for awhile but it recently broke for me for some other reason, the
home-manager-myuseractivation service was failing on system startup because this module wasnt able to find symlinks to make into mutable files even though the symlinks exist afterwards when i look for them. I suspected some sort of race condition so i moved the execution into a regularsystemd.userservice. It works fine for now but it isnt as elegant as having the logic right insidehome.activationafter linkGeneration. Im not sure why links are not available to me even when specifyinghm.dag.entryAfter [ "linkGeneration" ], maybe some interaction with impermanence bindfs. Note that this is a problem that started happening to me about ~1month ago, so perhaps some recent home-manager code change can also be at fault.