{
  config,
  lib,
  pkgs,
  ...
}: let
  # this is a recursive attribute with all the functions below
  inherit (lib) rp;
in {
  # string -> type -> string -> a
  # this is an internal function and shouldn't be
  # used unless you know what you're doing. it takes
  # a string (the name of the property, i.e., flavor
  # or accent), the type of the property, the name of
  # the module, followed by local config attrset
  mkBasicOpt = attr: type: name:
    lib.mkOption {
      inherit type;
      default = config.rose-pine.${attr};
      description = "Rose Pine ${attr} for ${name}";
    };

  # string -> a
  # this creates a flavor option for modules
  # the first string should be the name of the module,
  # followed by the local config attrset
  mkFlavorOpt = rp.mkBasicOpt "flavor" rp.types.flavorOption;

  types = {
    flavorOption = lib.types.enum [
      "dawn"
      "moon"
      "main"
    ];
    accentOption = lib.types.enum [
      "love"
      "gold"
      "rose"
      "pine"
      "foam"
      "iris"
    ];
  };

  # string -> string
  # this capitalizes the first letter in a string,
  # which is sometimes needed in order to format
  # the names of themes correctly
  mkUpper = str:
    (lib.toUpper (builtins.substring 0 1 str)) + (builtins.substring 1 (builtins.stringLength str) str);

  # a -> path -> a
  # fromJSON but for yaml (and without readFile)
  # a should be the local pkgs attrset
  fromYaml = file: let
    # convert to json
    json = pkgs.runCommand "converted.json" {} ''
      ${lib.getExe pkgs.yj} < ${file} > $out
    '';
  in
    builtins.fromJSON (builtins.readFile json);

  # a -> path -> a
  # fromJSON but for ini (and without readFile)
  # a should be the local pkgs attrset
  fromINI = file: let
    # convert to json
    json = pkgs.runCommand "converted.json" {} ''
      ${lib.getExe pkgs.jc} --ini < ${file} > $out
    '';
  in
    builtins.fromJSON (builtins.readFile json);

  # a -> path -> a
  # fromJSON but for raw ini (and without readFile)
  # a should be the local pkgs attrset
  fromINIRaw = file: let
    inherit (builtins) fromJSON readFile;

    # convert to json
    json = with pkgs;
      runCommand "converted.json" {} ''
        ${jc}/bin/jc --ini -r < ${file} > $out
      '';
  in
    fromJSON (readFile json);

  # string -> a
  # this creates a basic attrset only containing an
  # enable and flavor option. `name` should be the name
  # of the module, while `enableDefault` is a boolean
  # representing the default of the created `enable`
  # option
  mkRosePineOpt = {
    name,
    enableDefault ? config.rose-pine.enable,
  }: {
    enable =
      lib.mkEnableOption "Rose Pine theme for ${name}"
      // {
        default = enableDefault;
      };

    flavor = rp.mkFlavorOpt name;
  };

  # string -> a
  # this creates an accent option for modules
  # the first string should be the name of the module,
  # followed by the local config attrset
  mkAccentOpt = rp.mkBasicOpt "accent" rp.types.accentOption;

  # a -> a -> a
  # see https://nlewo.github.io/nixos-manual-sphinx/development/option-types.xml.html
  # by default enums cannot be merged, but they keep their passed value in `functor.payload`.
  # `functor.binOp` can merge those values
  mergeEnums = a: b: lib.types.enum (a.functor.binOp a.functor.payload b.functor.payload);

  # string
  # returns the current release version of nixos or home-manager. throws an evaluation error if neither are
  # found
  getModuleRelease =
    config.home.version.release
    or config.system.nixos.release
    or (throw "Couldn't determine release version!");

  # string -> a -> a
  # if the current module release is less than `minVersion`, all options are made no-ops with
  # `lib.mkSinkUndeclaredOptions`
  mkVersionedOpts = minVersion: option:
    if lib.versionAtLeast rp.getModuleRelease minVersion
    then option
    else lib.mkSinkUndeclaredOptions {};

  # string -> a
  # this is to ensure users are running a supported version of nixos/home-manager
  assertMinimumVersion = version: {
    assertion = lib.versionAtLeast rp.getModuleRelease version;
    message = "`rose-pine/nix` requires at least version ${version} of NixOS/home-manager";
  };
}