Skip to content

Conversation

@karaolidis
Copy link
Contributor

@karaolidis karaolidis commented Feb 18, 2025

Motivation

Currently, Obsidian lacks first-class Nix integration (#6321). Users have to manually install the app and manage settings, plugins, and themes outside of Nix. This module aims to make Obsidian declarative by handling vaults, themes, and plugin management via Home-Manager.

Overview

This module provides:

  • Vault Management - Create and configure vaults declaratively.
  • Plugin Management - Install both core and community plugins with settings.
  • Theme & Snippets Support - Declarative theme selection and CSS snippets.
  • Hotkey Customization - Keybindings in Nix.
  • extraFiles - Symlinking arbitrary files in a vault.

Example Configuration

home-manager.users.${user} = {
  programs.obsidian = {
    enable = true;

    vaults."Documents/Obsidian/master".enable = true;

    defaultSettings = {
      app = {
        defaultViewMode = "preview";
        livePreview = false;
        readableLineLength = true;
        showLineNumber = true;
        tabSize = 2;
        ...
      };

      corePlugins = [
        "bookmarks"
        {
          name = "canvas";
          options = {
            newFileLocation = "folder";
            newFileFolderPath = "Inbox";
            defaultWheelBehavior = "zoom";
          };
        }
        "canvas"
        ...
      ];

      communityPlugins = [
        {
          pkg = pkgs.callPackage ./config/plugins/custom-sort { };
          options = {
            suspended = false;
            statusBarEntryEnabled = false;
            notificationsEnabled = false;
            customSortContextSubmenu = false;
            bookmarksGroupToConsumeAsOrderingReference = "Sort";
            bookmarksContextMenus = false;
          };
        }
        {
          pkg = pkgs.callPackage ./config/plugins/dataview { };
          options = {
            enableDataviewJs = true;
            enableInlineDataviewJs = true;
            warnOnEmptyResult = false;
            defaultDateFormat = "dd/MM/yyyy";
            defaultDateTimeFormat = "HH:mm - dd/MM/yyyy";
          };
        }
        (pkgs.callPackage ./config/plugins/style-settings { })
        ...
      ];

      cssSnippets = [ ./config/snippets/file-explorer-separators.css ];

      themes = [ (pkgs.callPackage ./config/themes/minimal { }) ];

      hotkeys = {
        "command-palette:open" = [ { key = "F1"; } ];
        "app:open-help" = [ ];
        "editor:swap-line-down" = [
          {
            modifiers = [ "Alt" ];
            key = "ArrowDown";
          }
        ];
        ...
      };
    };
  };
};

Open Questions

  • Plugins and themes follow a fetch-from-source model and are version-dependent. Should they be included in Nixpkgs, NUR, or somewhere else?
  • Should default settings be deeply merged or overridden? Should we even include defaultSettings?
  • Should we keep the option names options and settings, or is something else more appropriate, e.g. config?

Checklist

  • Change is backwards compatible.

  • Code formatted with ./format.

  • Code tested through nix-shell --pure tests -A run.all
    or nix build --reference-lock-file flake.lock ./tests#test-all using Flakes.

  • Test cases updated/added. See example.

  • Commit messages are formatted

  • Added myself as module maintainer. See example.

Maintainer CC

@rycee

@karaolidis karaolidis changed the title obsidian: RFC: declarative Obsidian module obsidian: (RFC) add module Feb 18, 2025
@karaolidis
Copy link
Contributor Author

For reference, here is an example plugin package:

{ pkgs, ... }:
pkgs.stdenv.mkDerivation rec {
  pname = "obsidian.plugins.tasks";
  version = "7.15.0";

  src = pkgs.fetchFromGitHub {
    owner = "obsidian-tasks-group";
    repo = "obsidian-tasks";
    rev = version;
    hash = "sha256-BF9ye4ocE6vZh+ChkmuLkQpNWtH425EX0EHQs+wbTZc=";
  };

  offlineCache = pkgs.fetchYarnDeps {
    yarnLock = src + "/yarn.lock";
    hash = "sha256-Tf1K048Ox+hImIfrdBWQHsiDe+3FGUQLFBcf/Bbbo1U=";
  };

  nativeBuildInputs = with pkgs; [
    nodejs
    yarnConfigHook
    yarnBuildHook
    npmHooks.npmInstallHook
  ];

  installPhase = ''
    mkdir -p $out
    cp ./manifest.json $out/manifest.json
    cp ./main.js $out/main.js
    cp ./styles.css $out/styles.css
  '';
}

@karaolidis karaolidis force-pushed the obsidian branch 3 times, most recently from a82a14c to 6db31ab Compare February 23, 2025 22:03
@karaolidis karaolidis force-pushed the obsidian branch 4 times, most recently from dfdb0ac to dd5d4e7 Compare March 8, 2025 22:58
@karaolidis karaolidis force-pushed the obsidian branch 2 times, most recently from 6db31ab to 65c21b0 Compare March 17, 2025 10:56
@sofiedotcafe
Copy link

+1

Copy link
Collaborator

@khaneliman khaneliman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, just had a couple questions that I may have figured out while trying to review it..

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
@khaneliman khaneliman merged commit c3d48a1 into nix-community:master May 24, 2025
3 checks passed
@karaolidis karaolidis deleted the obsidian branch May 25, 2025 00:26
@karaolidis
Copy link
Contributor Author

@karaolidis do u know how to create stylix based theme, which will be in programs.obsidian.defaultSettings.themes?

I don't use stylix myself, I use https://github.com/InioX/matugen with a custom Nix theming module, but you can probably adapt what I have for stylix?

I installed Minimal and Style Settings, and then I do all my theme configuration in style settings' data.json:

.obsidian/plugins/obsidian-style-settings/data.json
{
  "minimal-style@@base@@light": "{{colors.surface.light.hex}}",
  "minimal-style@@base@@dark": "{{colors.surface.dark.hex}}",
  "minimal-style@@bg1@@light": "{{colors.surface.light.hex}}",
  "minimal-style@@bg1@@dark": "{{colors.surface.dark.hex}}",
  "minimal-style@@bg2@@light": "{{colors.surface_container.light.hex}}",
  "minimal-style@@bg2@@dark": "{{colors.surface_container.dark.hex}}",
  "minimal-style@@bg3@@light": "{{colors.surface_container_highest.light.hex}}",
  "minimal-style@@bg3@@dark": "{{colors.surface_container_highest.dark.hex}}",
  "minimal-style@@ui1@@light": "{{colors.outline_variant.light.hex}}",
  "minimal-style@@ui1@@dark": "{{colors.outline_variant.dark.hex}}",
  "minimal-style@@ui2@@light": "{{colors.outline.light.hex}}",
  "minimal-style@@ui2@@dark": "{{colors.outline.dark.hex}}",
  "minimal-style@@ui3@@light": "{{colors.primary.light.hex}}",
  "minimal-style@@ui3@@dark": "{{colors.primary.dark.hex}}",
  "minimal-style@@ax1@@light": "{{colors.primary.light.hex}}",
  "minimal-style@@ax1@@dark": "{{colors.primary.dark.hex}}",
  "minimal-style@@ax2@@light": "{{colors.on_primary_container.light.hex}}",
  "minimal-style@@ax2@@dark": "{{colors.on_primary_container.dark.hex}}",
  "minimal-style@@ax3@@light": "{{colors.primary.light.hex}}",
  "minimal-style@@ax3@@dark": "{{colors.primary.dark.hex}}",
  "minimal-style@@sp1@@light": "{{colors.on_primary_container.light.hex}}",
  "minimal-style@@sp1@@dark": "{{colors.on_primary_container.dark.hex}}",
  "minimal-style@@color-red@@light": "{{colors.red.light.hex}}",
  "minimal-style@@color-red@@dark": "{{colors.red.dark.hex}}",
  "minimal-style@@color-orange@@light": "{{colors.orange.light.hex}}",
  "minimal-style@@color-orange@@dark": "{{colors.orange.dark.hex}}",
  "minimal-style@@color-yellow@@light": "{{colors.yellow.light.hex}}",
  "minimal-style@@color-yellow@@dark": "{{colors.yellow.dark.hex}}",
  "minimal-style@@color-green@@light": "{{colors.green.light.hex}}",
  "minimal-style@@color-green@@dark": "{{colors.green.dark.hex}}",
  "minimal-style@@color-cyan@@light": "{{colors.cyan.light.hex}}",
  "minimal-style@@color-cyan@@dark": "{{colors.cyan.dark.hex}}",
  "minimal-style@@color-blue@@light": "{{colors.blue.light.hex}}",
  "minimal-style@@color-blue@@dark": "{{colors.blue.dark.hex}}",
  "minimal-style@@color-purple@@light": "{{colors.magenta.light.hex}}",
  "minimal-style@@color-purple@@dark": "{{colors.magenta.dark.hex}}",
  "minimal-style@@color-pink@@light": "{{colors.pink.light.hex}}",
  "minimal-style@@color-pink@@dark": "{{colors.pink.dark.hex}}",
  "minimal-style@@blockquote-color@@light": "{{colors.on_surface_variant.light.hex}}",
  "minimal-style@@blockquote-color@@dark": "{{colors.on_surface_variant.dark.hex}}",
  "minimal-style@@canvas-dot-pattern@@light": "{{colors.outline_variant.light.hex}}",
  "minimal-style@@canvas-dot-pattern@@dark": "{{colors.outline_variant.dark.hex}}",
  "minimal-style@@tag-color@@light": "{{colors.on_primary_container.light.hex}}",
  "minimal-style@@tag-color@@dark": "{{colors.on_primary_container.dark.hex}}",
  "minimal-style@@tag-background@@light": "{{colors.primary_container.light.hex}}",
  "minimal-style@@tag-background@@dark": "{{colors.primary_container.dark.hex}}",
  "minimal-style@@tag-background-hover@@light": "{{colors.on_primary.light.hex}}",
  "minimal-style@@tag-background-hover@@dark": "{{colors.on_primary.dark.hex}}",
  "minimal-style@@tx1@@light": "{{colors.on_surface.light.hex}}",
  "minimal-style@@tx1@@dark": "{{colors.on_surface.dark.hex}}",
  "minimal-style@@tx2@@light": "{{colors.on_surface_variant.light.hex}}",
  "minimal-style@@tx2@@dark": "{{colors.on_surface_variant.dark.hex}}",
  "minimal-style@@tx3@@light": "{{colors.outline.light.hex}}",
  "minimal-style@@tx3@@dark": "{{colors.outline.dark.hex}}",
  "minimal-style@@hl1@@light": "{{colors.primary_container.light.hex}}",
  "minimal-style@@hl1@@dark": "{{colors.primary_container.dark.hex}}",
  "minimal-style@@text-formattin@@light": "{{colors.outline_variant.light.hex}}",
  "minimal-style@@text-formattin@@dark": "{{colors.outline_variant.dark.hex}}",
  "minimal-style@@code-comment@@light": "{{colors.outline.light.hex}}",
  "minimal-style@@code-comment@@dark": "{{colors.outline.dark.hex}}",
  "minimal-style@@code-function@@light": "{{colors.functions.light.hex}}",
  "minimal-style@@code-function@@dark": "{{colors.functions.dark.hex}}",
  "minimal-style@@code-keyword@@light": "{{colors.keywords.light.hex}}",
  "minimal-style@@code-keyword@@dark": "{{colors.keywords.dark.hex}}",
  "minimal-style@@code-important@@light": "{{colors.info.light.hex}}",
  "minimal-style@@code-important@@dark": "{{colors.info.dark.hex}}",
  "minimal-style@@code-property@@light": "{{colors.properties.light.hex}}",
  "minimal-style@@code-property@@dark": "{{colors.properties.dark.hex}}",
  "minimal-style@@code-string@@light": "{{colors.strings.light.hex}}",
  "minimal-style@@code-string@@dark": "{{colors.strings.dark.hex}}",
  "minimal-style@@code-tag@@light": "{{colors.properties.light.hex}}",
  "minimal-style@@code-tag@@dark": "{{colors.properties.dark.hex}}",
  "minimal-style@@code-value@@light": "{{colors.numbers.light.hex}}",
  "minimal-style@@code-value@@dark": "{{colors.numbers.dark.hex}}",
  "minimal-style@@image-radius": "{{custom.radius}}",
  "minimal-style@@blockquote-border-thickness": 4,
  "minimal-style@@minimal-code-scroll": true,
  "minimal-style@@h1-l": true,
  "minimal-style@@h2-l": true,
  "minimal-style@@image-muted": 1,
  "minimal-style@@active-line-on": true,
  "minimal-style@@minimal-strike-lists": true,
  "minimal-style@@metadata-heading-off": true,
  "minimal-style@@metadata-icons-off": true,
  "minimal-style@@hide-help": true,
  "minimal-style@@row-hover": true
}

@karaolidis
Copy link
Contributor Author

ok, understood, but can you send your repo? too see implementation

I've set it to private since it contains personal details (encrypted with sops-nix but better safe than sorry 😉), but I'm attaching my theme module and some snippets of the obsidian one here:

theme/options.nix
{
  user ? throw "user argument is required",
  home ? throw "home argument is required",
}:
{
  config,
  inputs,
  lib,
  pkgs,
  ...
}:
let
  cfg = config.theme;

  init = pkgs.writeShellApplication {
    name = "theme-init";
    runtimeInputs = with pkgs; [
      matugen
      imagemagick
    ];
    text = ''
      mkdir -p "${cfg.configDir}"

      [[ ! -f "${cfg.configDir}"/wallpaper ]] && magick -size 1x1 xc:"#000000" png:"${cfg.configDir}"/wallpaper
      [[ ! -f "${cfg.configDir}"/mode ]] && echo "dark" > "${cfg.configDir}"/mode
      [[ ! -f "${cfg.configDir}"/flavor ]] && echo "tonal-spot" > "${cfg.configDir}"/flavor
      [[ ! -f "${cfg.configDir}"/contrast ]] && echo "0" > "${cfg.configDir}"/contrast

      matugen image "${cfg.configDir}"/wallpaper \
        --mode "$(<"${cfg.configDir}/mode")" \
        --type "scheme-$(<"${cfg.configDir}/flavor")" \
        --contrast "$(<"${cfg.configDir}/contrast")"

      ${cfg.initExtraConfig}
      wait
    '';
  };

  reload = pkgs.writeShellApplication {
    name = "theme-reload";
    text = ''
      ${cfg.reloadExtraConfig}
      wait
    '';
  };

  theme = pkgs.writeShellApplication {
    name = "theme";
    runtimeInputs = with pkgs; [
      coreutils
      imagemagick
    ];
    runtimeEnv = {
      CONFIG = cfg.configDir;
      INIT = lib.meta.getExe init;
      RELOAD = lib.meta.getExe reload;
    };
    text = builtins.readFile ./theme.sh;
  };
in
{
  # https://github.com/Theaninova/TheaninovOS/blob/master/modules/home-manager/theme/md3-evo.nix
  options.theme =
    with lib;
    with types;
    {
      enable = mkEnableOption "theme";

      configDir = mkOption {
        type = str;
        default = "${home}/.config/theme";
        description = "The path to the theme config directory.";
      };

      pkg = mkOption {
        type = package;
        default = theme;
        readOnly = true;
        description = "The package containing the `theme` script";
      };

      initExtraConfig = mkOption {
        type = lines;
        default = "";
        description = "Extra configuration lines to add to the theme initialization script.";
      };

      reloadExtraConfig = mkOption {
        type = lines;
        default = "";
        description = "Extra configuration lines to add to the theme reload script.";
      };

      template = mkOption {
        type = attrsOf (
          submodule (
            { name, config, ... }:
            {
              options = {
                source = mkOption {
                  type = path;
                  description = "Path of the source file or directory.";
                  default = null;
                };

                target = mkOption {
                  type = str;
                  defaultText = literalExpression "name";
                  description = "Path to target relative to the user's {env}`HOME`.";
                };
              };

              config.target = mkDefault name;
            }
          )
        );
        default = { };
        description = "Templates to fill with theme colors.";
      };

      opacity = mkOption {
        type = numbers.between 0 1;
        default = 1;
        description = "The opacity of apps.";
      };

      radius = mkOption {
        type = ints.unsigned;
        default = 0;
        description = "The radius of corners.";
      };

      padding = mkOption {
        type = ints.unsigned;
        default = 0;
        description = "The padding of windows.";
      };

      blur = mkOption {
        type = ints.unsigned;
        default = 0;
        description = "The blur amount of windows.";
      };

      color = {
        semantic = {
          blend = mkOption {
            type = bool;
            default = true;
            description = "Blend the colors.";
          };

          danger = mkOption {
            type = str;
            default = "#ff0000";
            description = "The color of danger.";
          };

          warning = mkOption {
            type = str;
            default = "#ffff00";
            description = "The color of warning.";
          };

          success = mkOption {
            type = str;
            default = "#00ff00";
            description = "The color of success.";
          };

          info = mkOption {
            type = str;
            default = "#0000ff";
            description = "The color of info.";
          };
        };

        syntax = {
          blend = mkOption {
            type = bool;
            default = true;
            description = "Blend the colors.";
          };

          keywords = mkOption {
            type = str;
            default = "#ff8000";
            description = "The color of keywords.";
          };

          functions = mkOption {
            type = str;
            default = "#0000ff";
            description = "The color of functions.";
          };

          properties = mkOption {
            type = str;
            default = "#ff00ff";
            description = "The color of properties.";
          };

          constants = mkOption {
            type = str;
            default = "#ff00ff";
            description = "The color of constants.";
          };

          strings = mkOption {
            type = str;
            default = "#00ff00";
            description = "The color of variables.";
          };

          numbers = mkOption {
            type = str;
            default = "#00ffff";
            description = "The color of numbers.";
          };

          structures = mkOption {
            type = str;
            default = "#ffff00";
            description = "The color of structures.";
          };

          types = mkOption {
            type = str;
            default = "#00ffff";
            description = "The color of types.";
          };
        };

        basic = {
          blend = mkOption {
            type = bool;
            default = true;
            description = "Blend the colors.";
          };

          red = mkOption {
            type = str;
            default = "#ff0000";
            description = "The color of red.";
          };

          orange = mkOption {
            type = str;
            default = "#ff8000";
            description = "The color of orange.";
          };

          yellow = mkOption {
            type = str;
            default = "#ffff00";
            description = "The color of yellow.";
          };

          green = mkOption {
            type = str;
            default = "#00ff00";
            description = "The color of green.";
          };

          cyan = mkOption {
            type = str;
            default = "#00ffff";
            description = "The color of cyan.";
          };

          blue = mkOption {
            type = str;
            default = "#0000ff";
            description = "The color of blue.";
          };

          magenta = mkOption {
            type = str;
            default = "#ff00ff";
            description = "The color of magenta.";
          };

          pink = mkOption {
            type = str;
            default = "#ffc0cb";
            description = "The color of pink.";
          };
        };
      };

      font = {
        sansSerif = {
          names = mkOption {
            type = listOf str;
            default = [ "Roboto" ];
            description = "The sans serif font families.";
          };

          packages = mkOption {
            type = listOf package;
            default = with pkgs; [ roboto ];
            description = "The sans serif font packages.";
          };
        };

        serif = {
          names = mkOption {
            type = listOf str;
            default = [ "Roboto Serif" ];
            description = "The serif font families.";
          };

          packages = mkOption {
            type = listOf package;
            default = with pkgs; [ roboto-serif ];
            description = "The serif font packages.";
          };
        };

        monospace = {
          names = mkOption {
            type = listOf str;
            default = [ "JetBrainsMono Nerd Font" ];
            description = "The monospace font families.";
          };

          packages = mkOption {
            type = listOf package;
            default = with pkgs; [ nerd-fonts.jetbrains-mono ];
            description = "The monospace font packages.";
          };
        };

        emoji = {
          names = mkOption {
            type = listOf str;
            default = [
              "Noto Emoji"
              "Font Awesome"
            ];
            description = "The emoji font families.";
          };

          packages = mkOption {
            type = listOf package;
            default = with pkgs; [
              noto-fonts-color-emoji
              font-awesome
            ];
            description = "The emoji font packages.";
          };
        };

        size = mkOption {
          type = ints.positive;
          default = 12;
          description = "The font size.";
        };
      };

      icon = {
        names = mkOption {
          type = listOf str;
          default = [ "Adwaita" ];
          description = "The icon theme names.";
        };

        packages = mkOption {
          type = listOf package;
          default = with pkgs; [
            adwaita-icon-theme
            nixos-icons
          ];
          description = "The icon theme packages.";
        };
      };

      cursor = {
        names = mkOption {
          type = listOf str;
          default = [ "Adwaita" ];
          description = "The cursor names.";
        };

        packages = mkOption {
          type = listOf package;
          default = with pkgs; [ adwaita-icon-theme ];
          description = "The cursor theme packages.";
        };

        size = mkOption {
          type = ints.positive;
          default = 24;
          description = "The cursor size.";
        };
      };
    };

  config = lib.mkIf cfg.enable {
    home = {
      activation.themeInit = inputs.home-manager.lib.hm.dag.entryAfter [
        "writeBoundary"
        "dconfSettings"
      ] "run ${lib.meta.getExe init}";

      packages =
        with pkgs;
        [
          matugen
          theme
        ]
        ++ cfg.font.sansSerif.packages
        ++ cfg.font.serif.packages
        ++ cfg.font.monospace.packages
        ++ cfg.font.emoji.packages
        ++ cfg.icon.packages
        ++ cfg.cursor.packages;

      pointerCursor = {
        name = builtins.head cfg.cursor.names;
        package = builtins.head cfg.cursor.packages;
        inherit (cfg.cursor) size;
      };
    };

    fonts.fontconfig = {
      enable = true;
      defaultFonts = {
        sansSerif = cfg.font.sansSerif.names;
        serif = cfg.font.serif.names;
        monospace = cfg.font.monospace.names;
        emoji = cfg.font.emoji.names;
      };
    };

    xdg.configFile."matugen/config.toml".source = (
      (pkgs.formats.toml { }).generate "config.toml" {
        config = {
          custom_colors =
            let
              mkColor = category: color: {
                color = cfg.color.${category}.${color};
                blend = cfg.color.${category}.blend;
              };
            in
            {
              danger = mkColor "semantic" "danger";
              warning = mkColor "semantic" "warning";
              success = mkColor "semantic" "success";
              info = mkColor "semantic" "info";

              red = mkColor "basic" "red";
              orange = mkColor "basic" "orange";
              yellow = mkColor "basic" "yellow";
              green = mkColor "basic" "green";
              cyan = mkColor "basic" "cyan";
              blue = mkColor "basic" "blue";
              magenta = mkColor "basic" "magenta";
              pink = mkColor "basic" "pink";

              keywords = mkColor "syntax" "keywords";
              functions = mkColor "syntax" "functions";
              properties = mkColor "syntax" "properties";
              constants = mkColor "syntax" "constants";
              strings = mkColor "syntax" "strings";
              numbers = mkColor "syntax" "numbers";
              structures = mkColor "syntax" "structures";
              types = mkColor "syntax" "types";
            };

          custom_keywords =
            let
              zeroPad = hex: if builtins.stringLength hex == 1 then "0${hex}" else hex;
              percentageToHex = percentage: zeroPad (lib.trivial.toHexString (builtins.floor (percentage * 255)));
            in
            {
              radius = builtins.toString cfg.radius;
              padding = builtins.toString cfg.padding;
              padding_double = builtins.toString (cfg.padding * 2);
              blur = builtins.toString cfg.blur;
              opacity = builtins.toString cfg.opacity;
              opacity_hex = builtins.toString (percentageToHex cfg.opacity);
              opacity_shadow = builtins.toString (cfg.opacity * 0.75);
              opacity_shadow_hex = builtins.toString (percentageToHex (cfg.opacity * 0.75));
              font_size = builtins.toString cfg.font.size;
              font_sans_serif = builtins.head cfg.font.sansSerif.names;
              font_sans_serif_all = builtins.concatStringsSep ", " cfg.font.sansSerif.names;
              font_serif = builtins.head cfg.font.serif.names;
              font_serif_all = builtins.concatStringsSep ", " cfg.font.serif.names;
              font_monospace = builtins.head cfg.font.monospace.names;
              font_monospace_all = builtins.concatStringsSep ", " cfg.font.monospace.names;
              font_emoji = builtins.head cfg.font.emoji.names;
              font_emoji_all = builtins.concatStringsSep ", " cfg.font.emoji.names;
            };
        };

        templates = builtins.mapAttrs (name: template: {
          input_path = template.source;
          output_path = "${home}/${template.target}";
        }) cfg.template;
      }
    );

    programs.zsh.initContent = builtins.readFile ./theme.completion.zsh;
  };
}
theme/theme.sh
# shellcheck shell=bash

wallpaper=""
color=""
mode=""
flavor=""
contrast=""

set_wallpaper() {
  if [[ -f "$1" ]]; then
    wallpaper="$(realpath "$1")"
  else
    echo "Invalid wallpaper path: $1"
    exit 1
  fi
}

set_color() {
  local re='^#([A-Fa-f0-9]{6})$'
  if [[ "$1" =~ $re ]]; then
    color="$1"
  else
    echo "Invalid color format: $1"
    exit 1
  fi
}

set_flavor() {
  case "$1" in
    content|expressive|fidelity|fruit-salad|monochrome|neutral|rainbow|tonal-spot)
      flavor="$1"
      ;;
    *)
      echo "Invalid flavor: $1"
      exit 1
      ;;
  esac
}

set_contrast() {
  if [[ "$1" =~ ^-?[0-1](\.[0-9]+)?$ ]]; then
    contrast="$1"
  else
    echo "Invalid contrast value: $1"
    exit 1
  fi
}

toggle_mode() {
  if [[ "$(cat "$CONFIG"/mode)" = "light" ]]; then
    mode="dark"
  else
    mode="light"
  fi
}

usage() {
  echo "Usage: $0 [-m {light|dark|toggle}] [-w <file>] [-h <hexcolor>] [-f <flavor>] [-c <contrast>]"
  echo "       Only one of -w (wallpaper) or -h (hex color) can be used."
  echo "       Valid flavors: content, expressive, fidelity, fruit-salad, monochrome, neutral, rainbow, tonal-spot"
  echo "       Contrast must be a number between -1 and 1"
  exit 1
}

finish() {
  [[ -n "$wallpaper" ]] && rm -f "$CONFIG"/wallpaper && ln -sf "$wallpaper" "$CONFIG"/wallpaper
  [[ -n "$color" ]] && rm -f "$CONFIG"/wallpaper && magick -size 1x1 xc:"$color" png:"$CONFIG"/wallpaper
  [[ -n "$mode" ]] && echo "$mode" > "$CONFIG"/mode
  [[ -n "$flavor" ]] && echo "$flavor" > "$CONFIG"/flavor
  [[ -n "$contrast" ]] && echo "$contrast" > "$CONFIG"/contrast

  "$INIT" > /dev/null
  "$RELOAD" > /dev/null
}

# Parse arguments
while getopts "m:w:h:f:c:" opt; do
  case "$opt" in
    m)
      case "$OPTARG" in
        light|dark) mode="$OPTARG" ;;
        toggle) toggle_mode ;;
        *) usage ;;
      esac
      ;;
    w)
      [[ -n "$color" ]] && usage
      set_wallpaper "$OPTARG"
      ;;
    h)
      [[ -n "$wallpaper" ]] && usage
      set_color "$OPTARG"
      ;;
    f)
      set_flavor "$OPTARG"
      ;;
    c)
      set_contrast "$OPTARG"
      ;;
    *)
      usage
      ;;
  esac
done

finish
obsidian/default.nix
...
{
  ...
}:
let
  hmConfig = config.home-manager.users.${user};
in
{
  environment.persistence."/persist/cache"."${home}/.config/obsidian" = { };

  home-manager.users.${user} = {
    programs.obsidian = {
      enable = true;
      ...
    };

    theme.template = lib.attrsets.mapAttrs' (
      _: vault:
      lib.attrsets.nameValuePair "${vault.target}/.obsidian/plugins/obsidian-style-settings/data.json" {
        source = ./theme.json;
      }
    ) hmConfig.programs.obsidian.vaults;

    ...
  };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants