{ config, pkgs, lib, ... }: let cfg = config.programs.obsidian; corePlugins = [ "audio-recorder" "backlink" "bookmarks" "canvas" "command-palette" "daily-notes" "editor-status" "file-explorer" "file-recovery" "global-search" "graph" "markdown-importer" "note-composer" "outgoing-link" "outline" "page-preview" "properties" "publish" "random-note" "slash-command" "slides" "switcher" "sync" "tag-pane" "templates" "word-count" "workspaces" "zk-prefixer" ]; in { options.programs.obsidian = with lib; with types; { enable = mkEnableOption "obsidian"; package = mkPackageOption pkgs "obsidian" { }; sharedSettings = { app = mkOption { description = "Settings to write to app.json."; type = raw; default = { }; }; appearance = mkOption { description = "Settings to write to appearance.json."; type = raw; default = { }; }; corePlugins = mkOption { description = "Core plugins to activate."; type = raw; default = [ "backlink" "bookmarks" "canvas" "command-palette" "daily-notes" "editor-status" "file-explorer" "file-recovery" "global-search" "graph" "note-composer" "outgoing-link" "outline" "page-preview" "switcher" "tag-pane" "templates" "word-count" ]; }; plugins = mkOption { description = "Community plugins to activate."; type = raw; default = [ ]; }; cssSnippets = mkOption { description = "CSS snippets to install."; type = raw; default = [ ]; }; theme = mkOption { description = "Obsidian theme package."; type = raw; default = null; }; }; vaults = mkOption { description = "List of vaults to create."; type = attrsOf ( submodule ( { name, config, ... }: { options = { enable = mkOption { type = bool; default = true; description = "Whether this vault should be generated."; }; target = mkOption { type = str; defaultText = literalExpression "name"; description = "Path to target vault relative to the user's {env}`HOME`."; }; settings = { app = mkOption { description = "Settings to write to app.json."; type = attrsOf anything; default = cfg.sharedSettings.app; }; appearance = mkOption { description = "Settings to write to appearance.json."; type = attrsOf anything; default = cfg.sharedSettings.appearance; }; corePlugins = mkOption { description = "Core plugins to activate."; type = listOf (enum corePlugins); default = cfg.sharedSettings.corePlugins; }; plugins = mkOption { description = "Community plugins to activate."; type = listOf package; default = cfg.sharedSettings.plugins; }; cssSnippets = mkOption { description = "CSS snippets to install."; type = listOf path; default = cfg.sharedSettings.cssSnippets; }; theme = mkOption { description = "Obsidian theme package."; type = nullOr package; default = cfg.sharedSettings.theme; }; }; }; config.target = mkDefault name; } ) ); default = { }; }; }; config = let vaults = builtins.filter (vault: vault.enable == true) (builtins.attrValues cfg.vaults); getManifestId = pkg: let manifest = builtins.fromJSON (builtins.readFile "${pkg}/manifest.json"); in manifest.id or manifest.name; readDir = dir: builtins.attrNames (builtins.readDir dir); in lib.mkIf cfg.enable { home = { packages = [ cfg.package ]; file = let mkApp = vault: { name = "${vault.target}/.obsidian/app.json"; value = { source = (pkgs.formats.json { }).generate "app.json" vault.settings.app; }; }; mkAppearance = vault: { name = "${vault.target}/.obsidian/appearance.json"; value = { source = (pkgs.formats.json { }).generate "appearance.json" ( vault.settings.appearance // { enabledCssSnippets = builtins.map ( snippet: lib.strings.removeSuffix ".css" (builtins.baseNameOf snippet) ) vault.settings.cssSnippets; } // lib.attrsets.optionalAttrs (vault.settings.theme != null) { cssTheme = getManifestId vault.settings.theme; } ); }; }; mkCorePlugins = vault: [ { name = "${vault.target}/.obsidian/core-plugins.json"; value = { source = (pkgs.formats.json { }).generate "core-plugins.json" vault.settings.corePlugins; }; } { name = "${vault.target}/.obsidian/core-plugins-migration.json"; value = { source = (pkgs.formats.json { }).generate "core-plugins-migration.json" ( builtins.listToAttrs ( builtins.map (plugin: { name = plugin; value = builtins.elem plugin vault.settings.corePlugins; }) corePlugins ) ); }; } ]; mkCommunityPlugins = vault: [ { name = "${vault.target}/.obsidian/community-plugins.json"; value = { source = (pkgs.formats.json { }).generate "community-plugins.json" ( builtins.map getManifestId vault.settings.plugins ); }; } ] /* We can't do the following since plugins often write files in their directories, and symlinking the entire folder does not give us write permissions. builtins.map (plugin: { name = "${vault.target}/.obsidian/plugins/${getManifestId plugin}"; value = { source = plugin; }; }) vault.settings.plugins; This is why we do a double loop over plugins and their files. */ ++ builtins.map ( plugin: builtins.map (file: { name = "${vault.target}/.obsidian/plugins/${getManifestId plugin}/${file}"; value = { source = "${plugin}/${file}"; }; }) (readDir plugin) ) vault.settings.plugins; mkCssSnippets = vault: builtins.map (snippet: { name = "${vault.target}/.obsidian/snippets/${builtins.baseNameOf snippet}"; value = { source = snippet; }; }) vault.settings.cssSnippets; mkTheme = vault: lib.attrsets.optionalAttrs (vault.settings.theme != null) { name = "${vault.target}/.obsidian/themes/${getManifestId vault.settings.theme}"; value = { source = vault.settings.theme; }; }; in builtins.listToAttrs ( lib.lists.flatten ( builtins.map (vault: [ (mkApp vault) (mkAppearance vault) (mkCorePlugins vault) (mkCommunityPlugins vault) (mkCssSnippets vault) (mkTheme vault) ]) vaults ) ); }; xdg.configFile."obsidian/obsidian.json".source = (pkgs.formats.json { }).generate "obsidian.json" { vaults = builtins.listToAttrs ( builtins.map (vault: { name = builtins.hashString "md5" vault.target; value = { path = "${config.home.homeDirectory}/${vault.target}"; } // (lib.attrsets.optionalAttrs ((builtins.length vaults) == 1) { open = true; }); }) vaults ); }; }; }