From 65c21b0f7937dc2d36443dac5a7c9507018742a0 Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Sun, 23 Feb 2025 22:03:04 +0000 Subject: [PATCH] obsidian: add module Signed-off-by: Nikolaos Karaolidis --- modules/lib/maintainers.nix | 10 + modules/modules.nix | 1 + modules/programs/obsidian.nix | 494 ++++++++++++++++++++++++++++++++++ 3 files changed, 505 insertions(+) create mode 100644 modules/programs/obsidian.nix diff --git a/modules/lib/maintainers.nix b/modules/lib/maintainers.nix index e70fcce1d..2db888b44 100644 --- a/modules/lib/maintainers.nix +++ b/modules/lib/maintainers.nix @@ -691,4 +691,14 @@ github = "ipsavitsky"; githubId = 33558632; }; + karaolidis = { + name = "Nikolaos Karaolidis"; + email = "nick@karaolidis.com"; + github = "karaolidis"; + githubId = 46189100; + keys = [{ + longKeyId = "rsa4096/0xF8549CDF32F178F7"; + fingerprint = "E3E3 0A72 EBB3 4375 414B 23FD F854 9CDF 32F1 78F7"; + }]; + }; } diff --git a/modules/modules.nix b/modules/modules.nix index 9226ed63b..7a61e2d91 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -192,6 +192,7 @@ let ./programs/notmuch.nix ./programs/nushell.nix ./programs/obs-studio.nix + ./programs/obsidian.nix ./programs/octant.nix ./programs/offlineimap.nix ./programs/oh-my-posh.nix diff --git a/modules/programs/obsidian.nix b/modules/programs/obsidian.nix new file mode 100644 index 000000000..72e8f7af8 --- /dev/null +++ b/modules/programs/obsidian.nix @@ -0,0 +1,494 @@ +{ config, pkgs, lib, ... }: +let + inherit (lib) + mkOption mkEnableOption mkPackageOption mkDefault literalExpression types; + + 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 = { + enable = mkEnableOption "obsidian"; + package = mkPackageOption pkgs "obsidian" { }; + + defaultSettings = { + app = mkOption { + description = "Settings to write to app.json."; + type = types.raw; + default = { }; + }; + + appearance = mkOption { + description = "Settings to write to appearance.json."; + type = types.raw; + default = { }; + }; + + corePlugins = mkOption { + description = "Core plugins to activate."; + type = types.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 = types.raw; + default = [ ]; + }; + + cssSnippets = mkOption { + description = "CSS snippets to install."; + type = types.raw; + default = [ ]; + }; + + themes = mkOption { + description = "Themes to install."; + type = types.raw; + default = [ ]; + }; + + hotkeys = mkOption { + description = "Hotkeys to configure."; + type = types.raw; + default = { }; + }; + + extraFiles = mkOption { + description = "Extra files to link to the vault directory."; + type = types.raw; + default = { }; + }; + }; + + vaults = mkOption { + description = "List of vaults to create."; + type = types.attrsOf (types.submodule ({ name, config, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether this vault should be generated."; + }; + + target = mkOption { + type = types.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 = with types; attrsOf anything; + default = cfg.defaultSettings.app; + }; + + appearance = mkOption { + description = "Settings to write to appearance.json."; + type = with types; attrsOf anything; + default = cfg.defaultSettings.appearance; + }; + + corePlugins = let + corePluginsOptions = { config, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether to enable the plugin."; + }; + + name = mkOption { + type = types.enum corePlugins; + description = "The plugin."; + }; + + options = mkOption { + type = with types; attrsOf anything; + description = "Plugin options to include."; + default = { }; + }; + }; + }; + in mkOption { + description = "Core plugins to activate."; + type = with types; + listOf (coercedTo (enum corePlugins) (p: { name = p; }) + (submodule corePluginsOptions)); + default = cfg.defaultSettings.corePlugins; + }; + + communityPlugins = let + communityPluginsOptions = { config, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether to enable the plugin."; + }; + + pkg = mkOption { + type = types.package; + description = "The plugin package."; + }; + + options = mkOption { + type = with types; attrsOf anything; + description = + "Options to include in the plugin's `data.json`."; + default = { }; + }; + }; + }; + in mkOption { + description = "Community plugins to install and activate."; + type = with types; + listOf (coercedTo package (p: { pkg = p; }) + (submodule communityPluginsOptions)); + default = cfg.defaultSettings.communityPlugins; + }; + + cssSnippets = let + checkCssPath = path: + lib.filesystem.pathIsRegularFile path + && lib.strings.hasSuffix ".css" path; + toCssName = path: + lib.strings.removeSuffix ".css" (builtins.baseNameOf path); + cssSnippetsOptions = { config, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether to enable the snippet."; + }; + + name = mkOption { + type = types.str; + defaultText = literalExpression '' + lib.strings.removeSuffix ".css" (builtins.baseNameOf source)''; + description = "Name of the snippet."; + }; + + source = mkOption { + type = with types; nullOr (addCheck path checkCssPath); + description = "Path of the source file."; + default = null; + }; + + text = mkOption { + type = with types; nullOr str; + description = "Text of the file."; + default = null; + }; + }; + + config.name = mkDefault (toCssName config.source); + }; + in mkOption { + description = "CSS snippets to install."; + type = with types; + listOf + (coercedTo (addCheck path checkCssPath) (p: { source = p; }) + (submodule cssSnippetsOptions)); + default = cfg.defaultSettings.cssSnippets; + }; + + themes = let + themesOptions = { config, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether to set the theme as active."; + }; + + pkg = mkOption { + type = types.package; + description = "The theme package."; + }; + }; + }; + in mkOption { + description = "Themes to install."; + type = with types; + listOf + (coercedTo package (p: { pkg = p; }) (submodule themesOptions)); + default = cfg.defaultSettings.themes; + }; + + hotkeys = let + hotkeysOptions = { config, ... }: { + options = { + modifiers = mkOption { + type = with types; listOf str; + description = "The hotkey modifiers."; + default = [ ]; + }; + + key = mkOption { + type = types.str; + description = "The hotkey."; + }; + }; + }; + in mkOption { + description = "Hotkeys to configure."; + type = with types; attrsOf (listOf (submodule hotkeysOptions)); + default = cfg.defaultSettings.hotkeys; + }; + + extraFiles = let + extraFilesOptions = { name, config, ... }: { + options = { + source = mkOption { + type = with types; nullOr path; + description = "Path of the source file or directory."; + default = null; + }; + + text = mkOption { + type = with types; nullOr str; + description = "Text of the file."; + default = null; + }; + + target = mkOption { + type = types.str; + defaultText = literalExpression "name"; + description = + "Path to target relative to the vault's directory."; + }; + }; + + config.target = mkDefault name; + }; + in mkOption { + description = "Extra files to link to the vault directory."; + type = with types; attrsOf (submodule extraFilesOptions); + default = cfg.defaultSettings.extraFiles; + }; + }; + }; + + config.target = mkDefault name; + })); + default = { }; + }; + }; + + config = let + vaults = builtins.filter (vault: vault.enable == true) + (builtins.attrValues cfg.vaults); + getManifest = item: + let + manifest = + builtins.fromJSON (builtins.readFile "${item.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 + enabledCssSnippets = builtins.filter (snippet: snippet.enable) + 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) 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 (plugin: plugin.name) 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 == plugin.name && plugin.enable) + 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 (plugin: plugin.options != { }) + 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 (plugin: plugin.enable) + vault.settings.communityPlugins)); + }] ++ builtins.map (plugin: { + name = "${vault.target}/.obsidian/plugins/${getManifest plugin}"; + value = { + source = plugin.pkg; + 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 (plugin: plugin.options != { }) + vault.settings.communityPlugins); + + mkCssSnippets = vault: + builtins.map (snippet: { + name = "${vault.target}/.obsidian/snippets/${snippet.name}.css"; + value = if snippet.source != null then { + inherit (snippet) source; + } else { + inherit (snippet) text; + }; + }) vault.settings.cssSnippets; + + mkThemes = vault: + builtins.map (theme: { + name = "${vault.target}/.obsidian/themes/${getManifest theme}"; + value.source = theme.pkg; + }) 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 == null || snippet.text == null) + && (snippet.source != null || snippet.text != null)) + vault.settings.cssSnippets) (builtins.attrValues cfg.vaults); + message = "Each CSS snippet must have one of 'source' or 'text' set"; + } + { + assertion = builtins.all (vault: + builtins.all (file: + (file.source == null || file.text == null) + && (file.source != null || file.text != null)) + (builtins.attrValues vault.settings.extraFiles)) + (builtins.attrValues cfg.vaults); + message = "Each extra file must have one of 'source' or 'text' set"; + } + ]; + }; + + meta.maintainers = [ lib.hm.maintainers.karaolidis ]; +}