diff --git a/home/features/neovim/config/lua/my/tempest.lua b/home/features/neovim/config/lua/my/tempest.lua
index 18c6e72..bc930c9 100644
--- a/home/features/neovim/config/lua/my/tempest.lua
+++ b/home/features/neovim/config/lua/my/tempest.lua
@@ -118,6 +118,10 @@ local function recursive_assign(source, destination)
 end
 
 function M.configure(opts, context)
+  if type(opts) == "function" then
+    return M.configure(opts(context), context)
+  end
+
   if type(opts.mk_context) == "function" then
     context = opts.mk_context(context)
   end
@@ -223,5 +227,34 @@ function M.withSavedCursor(callback)
   vim.api.nvim_win_set_cursor(0, cursor)
 end
 -- }}}
+-- {{{ Fixup lazy spec generated by nix
+function M.prepareLazySpec(spec)
+  for _, module in ipairs(spec) do
+    if spec.tempest ~= nil then
+      spec.config = function(lazy, opts)
+        M.configure(spec.tempest, { lazy = lazy, opts = opts })
+      end
+    end
+
+    if spec.dependencies ~= nil then
+      spec.dependencies = spec.dependencies.lua
+    end
+
+    if spec.keys ~= nil then
+      local keys = spec.keys
+      if spec.keys.mapping ~= nil then
+        keys = { keys }
+      end
+
+      for _, key in ipairs(keys) do
+        key[1] = key.mapping
+        if key.mode ~= nil then
+          key.mode = H.string_chars(key.mode)
+        end
+      end
+    end
+  end
+end
+-- }}}
 
 return M
diff --git a/modules/common/korora-lua.nix b/modules/common/korora-lua.nix
new file mode 100644
index 0000000..205a0a9
--- /dev/null
+++ b/modules/common/korora-lua.nix
@@ -0,0 +1,292 @@
+{ lib, korora, ... }:
+let
+  k = korora;
+
+  # {{{ Lua encoders
+  # {{{ Helpers 
+  helpers = rec {
+    xor = a: b: (a || b) && (!a || !b);
+    implies = a: b: !a || b;
+    hasProp = obj: p: (obj.${p} or null) != null;
+    propXor = a: b: obj: xor (hasProp obj a) (hasProp obj b);
+    propOnlyOne = props: obj:
+      1 == lib.count (prop: obj ? prop);
+    propImplies = a: b: obj: implies (hasProp obj a) (hasProp obj b);
+    mkVerify = checks: obj:
+      let
+        results = lib.lists.map checks obj;
+        errors = lib.lists.filter (v: v != null) results;
+      in
+      if errors == [ ]
+      then null
+      else
+        let prettyErrors =
+          lib.lists.map (s: "\n- ${s}") errors;
+        in
+        "Multiple errors occured: ${prettyErrors}";
+
+    intersection = l: r:
+      k.typedef'
+        "${l.name} ∧ ${r.name}"
+        (helpers.mkVerify [ l r ]);
+
+    dependentAttrsOf =
+      name: mkType:
+      let
+        typeError = name: v: "Expected type '${name}' but value '${toPretty v}' is of type '${typeOf v}'";
+        addErrorContext = context: error: if error == null then null else "${context}: ${error}";
+
+        withErrorContext = addErrorContext "in ${name} value";
+      in
+      k.typedef' name
+        (v:
+        if ! lib.isAttrs v then
+          typeError name v
+        else
+          withErrorContext
+            (mkVerify
+              (lib.mapAttrsToList (k: _: mkType k) v)
+              v));
+
+    mkRawLuaObject = chunks:
+      ''
+        {
+          ${lib.concatStringsSep "," (lib.filter (s: s != "") chunks)}
+        }
+      '';
+
+    mkAttrName = s:
+      let
+        # These list *are* incomplete
+        forbiddenChars = lib.stringToCharacters "<>[]{}()'\".,;";
+        keywords = [ "if" "then" "else" "do" "for" "local" "" ];
+      in
+      if lib.any (c: lib.hasInfix c s) forbiddenChars || lib.elem s keywords then
+        "[${m.string s}]"
+      else s;
+
+  };
+  # }}}
+
+  # We provide a custom set of helpers for generating lua code for nix.enable
+  #
+  # An encoder is a function from some nix value to a string containing lua code. 
+  # This object provides combinators for writing such encoders.
+  m = {
+    # {{{ General helpers 
+    typed = type: toLua: type // {
+      unsafeToLua = toLua;
+      toLua = v: toLua (type.check v);
+      override = a: m.typed (type.override a) toLua;
+    };
+    withDefault = type: default: type // {
+      default.value = default;
+    };
+    typedWithDefault = type: toLua: default:
+      m.withDefault (m.typed type toLua) default;
+    unsafe = type: type // { toLua = type.unsafeToLua; };
+    untyped = m.typed k.any;
+    untype = type: m.untyped type.toLua;
+    withName = name: type: type // { inherit name; }; # TODO: should we use override here?
+    # `const` is mostly useful together with `bind`. See the lua encoder for 
+    # lazy modules for example usage.
+    const = code: m.untyped (_: code);
+    # Conceptually, this is the monadic bind operation for encoders.
+    # This implementation is isomoprhic to that of the reader monad in haskell.
+    bind = name: higherOrder:
+      m.typed
+        (k.typedef' name (v: (higherOrder v).verify v))
+        (given: (higherOrder given).toLua given);
+    # This is probably the most useful combinnator defined in this entire object.
+    # Most of the combinators in the other categories are based on this.
+    conditional = predicate: caseTrue: caseFalse:
+      let base = m.bind
+        m.bind
+        "${caseTrue.name} ∨ ${caseFalse.name}"
+        (given: if predicate given then caseTrue else caseFalse);
+      in
+      if caseTrue ? default then
+        m.withDefault base caseTrue.default
+      else if caseFalse ? default then
+        m.withDefault base caseFalse.default
+      else
+        base;
+    try = caseTrue: caseFalse:
+      let base = m.bind
+        "${caseTrue.name} ∨ ${caseFalse.name}"
+        (given: if caseTrue.verify given == null then m.unsafe caseTrue else caseFalse);
+      in
+      if caseTrue ? default then
+        m.withDefault base caseTrue.default
+      else if caseFalse ? default then
+        m.withDefault base caseFalse.default
+      else
+        base;
+    oneOf = lib.foldr m.try
+      (m.bottom (v: "No variants matched for value ${builtins.toJSON v}"));
+    # This is simply left-composition of functions
+    cmap = f: t:
+      m.typed
+        (lib.typedef' t.name (v: t.verify (f v)))
+        (given: t.toLua (f given));
+    # This is simply right-composition of functions
+    map = f: t: m.typed t (given: f (t.toLua given));
+    filter = predicate: type: given:
+      m.conditional predicate type (m.untype m.nil);
+    # This is mostly useful for debugging
+    trace = message: m.cmap (f: lib.traceSeq message (lib.traceVal f));
+    bottom = mkMessage: m.typed (m.typedef "⊥" mkMessage) lib.id;
+    # }}}
+    # {{{ Base types
+    string = m.typed k.string (given: ''"${lib.escape ["\"" "\\"] (toString given)}"'');
+    bool = m.typed k.bool (bool: if bool then "true" else "false");
+    integer = m.typed k.int toString;
+    float = m.typed k.float toString;
+    number = m.typed k.number toString;
+    ignored = type: m.typed type (_: "nil");
+    nil = m.typedWithDefault
+      (k.typedef "null" (v: v == null))
+      (_: "nil")
+      null;
+    stringOr = m.try m.string;
+    boolOr = m.try m.bool;
+    numberOr = m.try m.number;
+    nullOr = m.try m.nil;
+    anything = m.withName "⊤" (m.oneOf [
+      m.markedLuaCode # Lua code expressions have priority over attrsets
+      m.string
+      m.number
+      m.bool
+      m.null
+      (m.listOf m.anything)
+      (m.attrsetOf m.anything)
+    ]);
+    # }}}
+    # {{{ Lua code
+    identity = m.typed k.string lib.id;
+    markedLuaCode =
+      m.typed
+        (k.struct "marked lua code" {
+          value = k.string;
+          __luaEncoderTag = k.enum "lua encoder tag" [ "lua" ];
+        })
+        (obj: obj.value);
+    # This is the most rudimentary (and currently only) way of handling paths.
+    luaImport = tag:
+      m.typed (k.typedef "path" lib.isPath)
+        (path: "dofile(${m.string "${path}"}).${tag}");
+    # Accepts both tagged and untagged strings of lua code.
+    luaString = m.try m.markedLuaCode m.identity;
+    # This simply combines the above combinators into one.
+    luaCode = tag: m.try (m.luaImport tag) m.luaString;
+    # }}}
+    # {{{ Operators
+    conjunction = left: right: given:
+      m.typed (helpers.intersection left right) (
+        let
+          l = left.toLua given;
+          r = right.toLua given;
+        in
+        if l == "nil" then r
+        else if r == "nil" then l
+        else "${l} and ${r}"
+      );
+    all = lib.foldr m.conjunction m.nil;
+    # Similar to `all` but takes in a list and
+    # treats every element as a condition.
+    allIndices = name: type: 
+      m.bind name 
+        (g: lib.pipe g [
+          (lib.lists.imap0
+            (i: _:
+              m.cmap 
+                (builtins.elemAt i) 
+                type))
+          m.all
+        ]);
+    # }}}
+    # {{{ Lists
+    listOf = type: list:
+      m.typedWithDefault
+        (k.listOf type)
+        (helpers.mkRawLuaObject (lib.lists.map type.toLua list))
+        [ ];
+    listOfOr = type: m.try (m.listOf type);
+    # Returns nil when given empty lists
+    tryNonemptyList = type:
+      m.typedWithDefault
+        (k.listOf type)
+        (m.filter
+          (l: l != [ ])
+          (m.listOf type))
+        [ ];
+    oneOrMany = type: m.listOfOr type type;
+    # Can encode:
+    # - zero values as nil
+    # - one value as itself
+    # - multiple values as a list
+    zeroOrMany = type: m.nullOr (m.oneOrMany type);
+    # Coerces non list values to lists of one element.
+    oneOrManyAsList = type: m.listOfOr type (m.map (e: [ e ]) type);
+    # Coerces lists of one element to said element.
+    listAsOneOrMany = type:
+      m.cmap
+        (l: if lib.length l == 1 then lib.head l else l)
+        (m.oneOrMany type);
+    # }}}
+    # {{{ Attrsets
+    attrsetOf = type:
+      m.typed (k.attrsOf type)
+        (object:
+          helpers.mkRawLuaObject (lib.mapAttrsToList
+            (name: value:
+              let result = type.toLua value;
+              in
+              lib.optionalString (result != "nil")
+                "${helpers.mkAttrName name} = ${result}"
+            )
+            object
+          )
+        );
+    # This is the most general combinator provided in this section.
+    #
+    # We accept:
+    # - order of props that should be interpreted as list elements
+    # - spec of props that should be interpreted as list elements
+    # - record of props that should be interpreted as attribute props
+    attrset = name: listOrder: spec: attrset:
+      m.cmap
+        (given: lib.mapAttrs
+          (key: type:
+            if given ? ${key} then
+              given.${key}
+            else
+              type.default or null)
+          spec)
+        (m.typed (k.struct name spec) (
+          let
+            listChunks = lib.lists.map
+              (attr:
+                let result = spec.${attr}.toLua (attrset.${attr} or null);
+                in
+                lib.optionalString (result != "nil") result
+              )
+              listOrder;
+            objectChunks = lib.mapAttrsToList
+              (attr: type:
+                let result = type.toLua (attrset.${attr} or null);
+                in
+                lib.optionalString (!(lib.elem attr listOrder) && result != "nil")
+                  "${helpers.mkAttrName attr} = ${result}"
+              )
+              spec;
+          in
+          helpers.mkRawLuaObject (listChunks ++ objectChunks)
+        ));
+    withAttrsCheck = type: verify:
+      type.override { inherit verify; };
+    # }}}
+  };
+  # }}}
+in
+m // { inherit helpers; }
diff --git a/modules/common/korora-neovim.nix b/modules/common/korora-neovim.nix
new file mode 100644
index 0000000..6a07d68
--- /dev/null
+++ b/modules/common/korora-neovim.nix
@@ -0,0 +1,36 @@
+attrs@{ lib, ... }:
+let
+  e = import ./korora-lua.nix attrs;
+  h = e.helpers;
+  m = {
+    lazyModule = e.withAttrsCheck
+      (e.attrset "lazy module" [ "package" ] {
+        package = e.nullOr e.string;
+        dir = e.nullOr e.type;
+        version = e.nullOr e.string;
+        tag = e.nullOr e.string;
+        name = e.nullOr e.string;
+        main = e.nullOr e.string;
+        lazy = e.nullOr e.bool;
+        dependencies = e.tryNonemptyList (e.stringOr m.lazyModule);
+        config = e.anything;
+        cond = e.nullOr (e.bind (value: e.all [
+          # TODO: we want a zip here
+          (e.nullOr (e.luaCode "cond"))
+        ]));
+        init = e.nullOr (e.luaCode "init");
+        event = e.zeroOrMany e.string;
+        cmd = e.zeroOrMany e.string;
+        ft = e.zeroOrMany e.string;
+        keys = e.listOf e.anything;
+        passthrough = e.anything;
+        opts = e.anything;
+      })
+      (h.mkVerify [
+        (h.propOnlyOne [ "dir" "package" ])
+        (h.propImplies "tag" "package")
+        (h.propImplies "version" "package")
+      ]);
+  };
+in
+m