{ 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" ]; toCssName = path: lib.strings.removeSuffix ".css" (builtins.baseNameOf path); in { options.programs.obsidian = with lib; with types; let checkCssPath = path: lib.filesystem.pathIsRegularFile path && lib.strings.hasSuffix ".css" path; cssSnippetsOptions = { config, ... }: { options = { enable = mkOption { type = bool; default = true; description = "Whether to enable the snippet."; }; name = mkOption { type = str; defaultText = literalExpression "lib.strings.removeSuffix \".css\" (builtins.baseNameOf source)"; description = "Name of the snippet."; }; source = mkOption { type = nullOr (addCheck path checkCssPath); description = "Path of the source file."; default = null; }; text = mkOption { type = nullOr str; description = "Text of the file."; default = null; }; }; config.name = mkDefault (toCssName config.source); }; corePluginsOptions = { config, ... }: { options = { enable = mkOption { type = bool; default = true; description = "Whether to enable the plugin."; }; name = mkOption { type = enum corePlugins; description = "The plugin."; }; options = mkOption { type = attrsOf anything; description = "Plugin options to include."; default = { }; }; }; }; communityPluginsOptions = { config, ... }: { options = { enable = mkOption { type = bool; default = true; description = "Whether to enable the plugin."; }; pkg = mkOption { type = package; description = "The plugin package."; }; options = mkOption { type = attrsOf anything; description = "Options to include in the plugin's `data.json`."; default = { }; }; }; }; themesOptions = { config, ... }: { options = { enable = mkOption { type = bool; default = true; description = "Whether to set the theme as active."; }; pkg = mkOption { type = package; description = "The theme package."; }; }; }; hotkeysOptions = { config, ... }: { options = { modifiers = mkOption { type = listOf str; description = "The hotkey modifiers."; default = [ ]; }; key = mkOption { type = str; description = "The hotkey."; }; }; }; extraFilesOptions = { name, config, ... }: { options = { source = mkOption { type = nullOr path; description = "Path of the source file or directory."; default = null; }; text = mkOption { type = nullOr str; description = "Text of the file."; default = null; }; target = mkOption { type = str; defaultText = literalExpression "name"; description = "Path to target relative to the vault's directory."; }; }; config.target = mkDefault name; }; 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" ]; }; communityPlugins = mkOption { description = "Community plugins to install and activate."; type = raw; default = [ ]; }; cssSnippets = mkOption { description = "CSS snippets to install."; type = raw; default = [ ]; }; themes = mkOption { description = "Themes to install."; type = raw; default = [ ]; }; hotkeys = mkOption { description = "Hotkeys to configure."; type = raw; default = { }; }; extraFiles = mkOption { description = "Extra files to link to the vault directory."; 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 (either (enum corePlugins) (submodule corePluginsOptions)); default = cfg.sharedSettings.corePlugins; }; communityPlugins = mkOption { description = "Community plugins to install and activate."; type = listOf (either package (submodule communityPluginsOptions)); default = cfg.sharedSettings.communityPlugins; }; cssSnippets = mkOption { description = "CSS snippets to install."; type = listOf (either (addCheck path checkCssPath) (submodule cssSnippetsOptions)); default = cfg.sharedSettings.cssSnippets; }; themes = mkOption { description = "Themes to install."; type = listOf (either package (submodule themesOptions)); default = cfg.sharedSettings.themes; }; hotkeys = mkOption { description = "Hotkeys to configure."; type = attrsOf (listOf (submodule hotkeysOptions)); default = cfg.sharedSettings.hotkeys; }; extraFiles = mkOption { description = "Extra files to link to the vault directory."; type = attrsOf (submodule extraFilesOptions); default = cfg.sharedSettings.extraFiles; }; }; }; config.target = mkDefault name; } ) ); default = { }; }; }; config = let vaults = builtins.filter (vault: vault.enable == true) (builtins.attrValues cfg.vaults); toName = item: if item ? name then item.name else item; toPkg = item: if item ? pkg then item.pkg else item; isEnabled = item: if item ? enable then item.enable else true; hasOptions = item: item ? options; getCssName = item: if builtins.isAttrs item then item.name else toCssName item; getManifest = item: let manifest = builtins.fromJSON (builtins.readFile "${toPkg item}/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 enabledCssSnippets = builtins.filter isEnabled vault.settings.cssSnippets; activeTheme = lib.lists.findSingle isEnabled 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 getCssName enabledCssSnippets; } // lib.attrsets.optionalAttrs (activeTheme != null) { cssTheme = getManifest activeTheme; } ); }; }; mkCorePlugins = vault: [ { name = "${vault.target}/.obsidian/core-plugins.json"; value.source = (pkgs.formats.json { }).generate "core-plugins.json" ( builtins.map toName 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 (name: { inherit name; value = builtins.any ( plugin: name == (toName plugin) && isEnabled plugin ) vault.settings.corePlugins; }) corePlugins ) ); } ] ++ builtins.map (plugin: { name = "${vault.target}/.obsidian/${plugin.name}.json"; value.source = (pkgs.formats.json { }).generate "${plugin.name}.json" plugin.options; }) (builtins.filter hasOptions vault.settings.corePlugins); mkCommunityPlugins = vault: [ { name = "${vault.target}/.obsidian/community-plugins.json"; value.source = (pkgs.formats.json { }).generate "community-plugins.json" ( builtins.map getManifest (builtins.filter isEnabled vault.settings.communityPlugins) ); } ] ++ builtins.map (plugin: { name = "${vault.target}/.obsidian/plugins/${getManifest plugin}"; value = { source = toPkg plugin; recursive = true; }; }) vault.settings.communityPlugins ++ builtins.map (plugin: { name = "${vault.target}/.obsidian/plugins/${getManifest plugin}/data.json"; value.source = (pkgs.formats.json { }).generate "data.json" plugin.options; }) (builtins.filter hasOptions vault.settings.communityPlugins); mkCssSnippets = vault: builtins.map (snippet: { name = "${vault.target}/.obsidian/snippets/${getCssName snippet}.css"; value = if snippet ? source || snippet ? text then if snippet.source != null then { inherit (snippet) source; } else { inherit (snippet) text; } else { source = snippet; }; }) vault.settings.cssSnippets; mkThemes = vault: builtins.map (theme: { name = "${vault.target}/.obsidian/themes/${getManifest theme}"; value.source = toPkg theme; }) vault.settings.themes; mkHotkeys = vault: { name = "${vault.target}/.obsidian/hotkeys.json"; value.source = (pkgs.formats.json { }).generate "hotkeys.json" vault.settings.hotkeys; }; mkExtraFiles = vault: builtins.map (file: { name = "${vault.target}/.obsidian/${file.target}"; value = if file.source != null then { inherit (file) source; } else { inherit (file) text; }; }) (builtins.attrValues vault.settings.extraFiles); in builtins.listToAttrs ( lib.lists.flatten ( builtins.map (vault: [ (mkApp vault) (mkAppearance vault) (mkCorePlugins vault) (mkCommunityPlugins vault) (mkCssSnippets vault) (mkThemes vault) (mkHotkeys vault) (mkExtraFiles 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 ); updateDisabled = true; }; assertions = [ { assertion = builtins.all ( vault: builtins.all ( snippet: (!snippet ? source && !snippet ? text) || (snippet.source == null || snippet.text == null) ) vault.settings.cssSnippets ) (builtins.attrValues cfg.vaults); message = "Only one of `source` and `text` must be set"; } { assertion = builtins.all ( vault: builtins.all (file: file.source == null || file.text == null) ( builtins.attrValues vault.settings.extraFiles ) ) (builtins.attrValues cfg.vaults); message = "Only one of `source` and `text` must be set"; } ]; }; }