{ 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; let cssSnippetsOptions = { name, config, ... }: { options = { enable = mkOption { type = bool; default = true; description = "Whether to enable the snippet."; }; name = mkOption { type = str; defaultText = literalExpression "name"; description = "Name of the snippet."; }; source = mkOption { type = addCheck path (path: if path != null then lib.filesystem.pathIsRegularFile path else true); description = "Path of the source file."; }; text = mkOption { type = str; description = "Text of the file."; }; }; config.name = mkDefault name; }; pluginsExtraFilesOptions = { name, config, ... }: { options = { source = mkOption { type = path; description = "Path of the source file or directory."; }; text = mkOption { type = str; description = "Text of the file."; }; target = mkOption { type = str; defaultText = literalExpression "name"; description = "Path to target relative to the plugin directory."; }; }; config.target = mkIf (config ? text) (mkDefault name); }; pluginsOptions = { config, ... }: { options = { enable = mkOption { type = bool; default = true; description = "Whether to enable the plugin."; }; pkg = mkOption { type = package; description = "The plugin package."; }; extraFiles = mkOption { type = attrsOf (submodule pluginsExtraFilesOptions); description = "Additional files to include in the plugin directory."; }; }; }; themesOptions = { config, ... }: { options = { enable = mkOption { type = bool; default = false; description = '' Whether to set the theme as active. Only one theme can be active at a time. ''; }; pkg = mkOption { type = package; description = "The theme package."; }; }; }; in { 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" ]; }; cssSnippets = mkOption { description = "CSS snippets to install."; type = raw; default = { }; }; plugins = mkOption { description = "Community plugins to install and activate."; type = raw; default = [ ]; }; themes = mkOption { description = "Themes to install."; type = raw; default = [ ]; }; }; 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; }; cssSnippets = mkOption { description = "CSS snippets to install."; type = attrsOf (submodule cssSnippetsOptions); default = cfg.sharedSettings.cssSnippets; }; plugins = mkOption { description = "Community plugins to install and activate."; type = listOf (submodule pluginsOptions); default = cfg.sharedSettings.plugins; }; themes = mkOption { description = "Themes to install."; type = listOf (submodule themesOptions); default = cfg.sharedSettings.themes; }; }; }; config.target = mkDefault name; } ) ); default = { }; }; }; config = let vaults = builtins.filter (vault: vault.enable == true) (builtins.attrValues cfg.vaults); readDir = dir: builtins.attrNames (builtins.readDir dir); getManifest = pkg: let manifest = builtins.fromJSON (builtins.readFile "${pkg}/manifest.json"); in manifest.id or manifest.name; 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 = let enabledSnippets = builtins.filter (snippet: snippet.enable) ( builtins.attrValues vault.settings.cssSnippets ); activeTheme = lib.lists.findSingle ( theme: theme.enable ) null (throw "Only one theme can be enabled at a time.") vault.settings.themes; in { source = (pkgs.formats.json { }).generate "appearance.json" ( vault.settings.appearance // { enabledCssSnippets = builtins.map (snippet: snippet.name) enabledSnippets; } // lib.attrsets.optionalAttrs (activeTheme != null) { cssTheme = getManifest activeTheme.pkg; } ); }; }; 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: let enabledPlugins = builtins.filter (plugin: plugin.enable) vault.settings.plugins; in [ { name = "${vault.target}/.obsidian/community-plugins.json"; value = { source = (pkgs.formats.json { }).generate "community-plugins.json" ( builtins.map (plugin: getManifest plugin.pkg) enabledPlugins ); }; } ] /* 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/${getManifest plugin.pkg}/${file}"; value = { source = "${plugin.pkg}/${file}"; }; }) (readDir plugin.pkg) ) vault.settings.plugins ++ builtins.map ( plugin: builtins.map (file: { name = "${vault.target}/.obsidian/plugins/${getManifest plugin.pkg}/${file.target}"; value = if file ? source then { source = file.source; } else { text = file.text; }; }) (builtins.attrValues plugin.extraFiles) ) vault.settings.plugins; mkCssSnippets = vault: builtins.map (snippet: { name = "${vault.target}/.obsidian/snippets/${snippet.name}.css"; value = if snippet ? source then { source = snippet.source; } else { text = snippet.text; }; }) (builtins.attrValues vault.settings.cssSnippets); mkThemes = vault: builtins.map (theme: { name = "${vault.target}/.obsidian/themes/${getManifest theme.pkg}"; value = { source = theme.pkg; }; }) vault.settings.themes; in builtins.listToAttrs ( lib.lists.flatten ( builtins.map (vault: [ (mkApp vault) (mkAppearance vault) (mkCorePlugins vault) (mkCommunityPlugins vault) (mkCssSnippets vault) (mkThemes 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 ); }; }; }