🍲 Hotpot

May 8, 2026 · View on GitHub

Hotpot Logo

🍲 Hotpot Github Tag Badge LuaRocks Release Badge

You take this home, throw it in a pot, add some broth, some neovim... baby, you got a stew going!

~ Fennel Programmers (probably)

Hotpot is a Fennel compiler plugin for Neovim that allows you to write your Neovim config and plugins in Fennel.

Version 2

Important

Hotpot version 2's configuration is incompatible with version 1 (née version 0). For most users, the migration should be simple, see migrating from version 1.

The most dramatic change to all users is the requirement that all macro files must use the extension .fnlm.

If you are unable or do not want to update your configuration, pin your plugin manager version to the v1.0.0 tag.

Version 2 has a simpler configuration, better support for directories such as lsp as well as improved support working in multiple project directories with isolated configuration.

See Changes from Version 1

Warning

Again, The most dramatic change to all users is the requirement that all macro files must use the extension .fnlm.

Requirements

  • Neovim 0.11.6+ to run Hotpot
    • The compiled output will run on any version of Neovim that your code is written to support.
  • Fanatical devotion to parentheses.

You may also want an LSP $/progress notification renderer, eg fidget.nivm for event notifications, or otherwise enable verbose mode.

Installation

Hotpot provides semantic-version releases via git tags. It's recommended you use your package managers version tracking system if available, instead of tracking the main branch which provides no stability guarantees.

Installing with vim.pack

-- init.lua
vim.pack.add({
  {src = "https://github.com/rktjmp/hotpot.nvim",
   version = vim.version.range("^2.0.0")}
})
require("hotpot")
-- Most users will then require their "config" module stored
-- somewhere like `fnl/config/init.fnl`...
require("config")

Avoid lazy-loading Hotpot unless you are only using it for plugin development. Hotpot already performs the minimum amount of work on demand. Users wanting to configure vim.pack.add's behaviour via its options table should read the advanced vim.pack configuration notes. Most users should use the above instructions.

Installing with Lazy.nvim

Lazy.nvim

You likely will want to bootstrap Hotpot in the same way you bootstrap Lazy.nvim itself. See Lazy.nvim's own install instructions or the example ensure_installed function below.

It's recommended you check the version in the script below matches the latest tagged version release, or run Lazy.nvim's update function immediately after your initial install.

-- init.lua
local function ensure_installed(plugin, branch)
  local user, repo = string.match(plugin, "(.+)/(.+)")
  local repo_path = vim.fn.stdpath("data") .. "/lazy/" .. repo
  if not (vim.uv or vim.loop).fs_stat(repo_path) then
    vim.notify("Installing " .. plugin .. " " .. branch)
    local repo_url = "https://github.com/" .. plugin .. ".git"
    local out = vim.fn.system({
      "git",
      "clone",
      "--filter=blob:none",
      "--branch=" .. branch,
      repo_url,
      repo_path
    })
    if vim.v.shell_error ~= 0 then
      vim.api.nvim_echo({
        { "Failed to clone " .. plugin .. ":\n", "ErrorMsg" },
        { out, "WarningMsg" },
        { "\nPress any key to exit..." },
      }, true, {})
      vim.fn.getchar()
      os.exit(1)
    end
  end
  return repo_path
end

-- Install hotpot in the same manner as lazy.nvim, into Lazy's own plugin directory.
local lazy_path = ensure_installed("folke/lazy.nvim", "stable")
local hotpot_path = ensure_installed("rktjmp/hotpot.nvim", "v2.1.1")
-- As per Lazy's install instructions, but also include hotpot as
-- we have installed it to lazy.nvim's managed directory outside of Neovim's
-- runtimepath.
vim.opt.runtimepath:prepend({hotpot_path, lazy_path})

-- Important! When using Lazy.nvim you *must* require the hotpot module
-- before Lazy.nvim to ensure the module is loaded into memory prior to
-- lazy.nvim altering neovims behaviour.
require("hotpot")

-- Most users will then require their "config" module stored
-- somewhere like `fnl/config/init.fnl`...
require("config")

You must also include Hotpot in your plugins list for Lazy.nvim to correctly manage updates. It is recommended you do not apply any lazy loading methods to Hotpot.

;; fnl/config/init.fnl

(let [lazy (require :lazy)
      api (require :hotpot.api)
      context (assert (api.context (vim.fn.stdpath :config)))]
  (lazy.setup
    ;; Define your package specs in any way you like, this is just an example.
    ;; Be sure to include hotpot, as calling `lazy.setup` interferes with lua module
    ;; loading, which will cause hotpot to malfunction when loads further parts
    ;; of itself on demand.
    {:spec [{:url "https://github.com/folke/lazy.nvim" :branch :stable}
            {:url "https://github.com/rktjmp/hotpot.nvim" :version "^2.0.0"}]
     ;; You must include hotpots output directory in `performance.rtp.paths`!
     :performance {:rtp {:paths [(context.locate :destination)]}}}))

;; If you wish to use `Hotpot fennel update` to download fennel new versions of
;; fennel from the internet, you will also have to add the `target directory`
;; path, as listed in `:checkhealth hotpot` under the `Hotpot fennel update`
;; section.
;;
;; If you have configured your config directory to use `:target :colocate`
;; (which is *not* the default), you may skip the `performance.rtp.paths` step.

Usage

~/.config/nvim

Just as any Lua code can be put in lua/**/*.lua, you can now put Fennel code in fnl/**/*.fnl and require it. You can add as little or as much Fennel as you would like. Your existing lua/**/*.lua code will continue to work as before and Lua and Fennel modules are interoperable.

You can also put .fnl files in any standard runtime directories such as lsp/, plugin/ or ftplugin/.

Saving any .fnl files will cause Hotpot to sync any changes needed to the associated .lua files.

;; ~/.config/nvim/fnl/my-config/hello.fnl
(print :hello)
;; ~/.config/nvim/lsp/my-lang.fnl
(print :setup-some-lsp)

Hotpot's default configuration for Neovim's config directory stores any compiled .lua files in a separate location to maintain a clean directory tree, so you won't see any .lua files.

There is one special exception to the above: .config/nvim/init.fnl will always compile to .config/nvim/init.lua.

That's all you need to know to write your config in Fennel. You might also want to see commands for evaluating or compiling Fennel snippets or interacting with Hotpot.

To adjust aspects of Hotpot's behaviour such as how notifications are delivered, colocating .lua files in-tree, ignoring files or configuring the Fennel compiler, see configuration.

To interact with Hotpot from your own commands, keybinds or functions, see the API.

Use .fnlm Extension for Macros

Important

Hotpot requires that Fennel macro files (those that you call import-macros for) must use the modern .fnlm extension. Regular Fennel modules (those that you call require for) should use the .fnl extension.

Plugins

To enable Fennel compilation for a plugin, you must place a .hotpot.fnl file in the root of the plugin directory. At a minimum, this file must specify the schema and target keys, as shown below.

;; projects/my-plugin/.hotpot.fnl
{:schema :hotpot/2
 ;; Plugins must ship `.lua` code to users.
 ;; Setting the `target` to `colocate` will keep any `.lua` files "in-tree".
 ;; It is an error to set `target` to `cache` for a plugin.
 :target :colocate}

Tip

Before saving any changes to your .hotpot.fnl file, you might want to run :trust, to save yourself being prompted later.

After creating your .hotpot.fnl file, open any .fnl file and save it to trigger a build, or use the sync command.

See configuration for details on customising Hotpot's behaviour, ignoring files .fnl or .lua files or configuring the Fennel compiler.

Commands

Use :Hotpot to interact with Hotpot, :Fnl and its related commands to evaluate or compile Fennel code from the buffer or command line.

:Hotpot

The :Hotpot command exposes the following subcommands:

  • sync: manually trigger a compile & clean cycle.
  • locate: find or open a files .lua or .fnl counterpart.
  • watch: enable or disable compile-on-save.
  • fennel: update the bundled fennel version with the latest from fennel-lang.org.

:Hotpot sync

Sync a given context's .fnl and .lua files. This is the same operation that occurs when you save a .fnl or .fnlm file.

Supports the following parameters:

  • context=<path>: sets the context for the command, if not given, the current working directory is used.
  • force: force compilation of all files in the context, even if the .lua is up to date.
  • atomic: allow writing successfully compiled files even if others have compilation errors.
  • verbose: output additional compilation messages.

See also the context.sync function exposed by the API.

:Hotpot locate

Find or open a counterpart file, supports the following invocations:

:Hotpot locate <path> -- <commands ...>

Find counterpart file path for <path> and append it to <commands ...>, eg: :Hotpot locate fnl/my-file.fnl -- vnew would open the counterpart .lua file in a vnew split. You may use %% to subsitute the located path in commands where simply appending the path wont work (eg: echo '%%' where echo requires the path be quoted).

If path is not given, the current buffer path is used instead, eg: :Hotpot locate -- <commands ...> is equivalent to :Hotpot locate % -- <commands ...>.

:Hotpot locate <path>

Prints the counterpart path, again, if no <path> is given, the current buffer path is used.

See also the context.locate function exposed by the API.

:Hotpot watch

Enable or disable the compile-on-save behaviour.

Supports the following (mutually exclusive) parameters:

  • enable: enable syncing on save for all contexts in this session.
  • disable: disable syncing on save for all contexts in this session.

:Hotpot fennel

Update or rollback fennel.lua to the latest version from fennel-lang.org.

Requires curl to be installed.

Important

Running this is not without some risk, as an updated version of Fennel may be incompatible with Hotpot. This is pretty unlikely unless the API to evaluate or compile fennel code is changed. If a release is only adding new "forms" (eg: (accumulate ...)) the update should be safe.

Exposes the following sub commands:

:Hotpot fennel version

Reports the currently loaded and used version of Fennel.

:Hotpot fennel update

Supports the following parameters:

  • url=<url>: use given URL instead of finding the latest from fennel-lang.org.
  • force: do not ask whether to update.

:Hotpot fennel rollback

Remove downloaded Fennel file and use version shipped with Hotpot.

:Fnl

Operate on either the command line provided Fennel, or a range from the current buffer either specified on the command line or by selection.

You may always provide a range (:'<,'>Fnl, :%Fnl, etc) or source string (:Fnl (+ 1 1)). If given both, range always takes precedence.

This command is analogous to Neovim's built in :lua command, and supports the following flags:

:Fnl

Evaluate the input range or string. Outputs nothing unless the source itself does.

:Fnl=

Evaluate the input range or string and vim.print the result of the expressions.

:Fnl-

Compile the input range or string and vim.print the result of the compilation.

Note that allowedGlobals = false when compling source via :Fnl-, as most often it's used to compile small snippets for inspection where its common to reference out-of-selection variables. This means you will not see any warnings when referencing misspelled or unknown variable as you would during normal compilation.

:FnlEval

Alias for :Fnl=

:FnlCompile

Alias for :Fnl-

:Fnlfile {file.fnl}

Evaluates the given file, also supports :Fnlfile= file (output evaluation) and :Fnlfile- file (output compilation).

:source {file.fnl}

Sources given .fnl file. See :h :source

Configuration

The majority of Hotpot's behaviour and the Fennel compiler is configured via a .hotpot.fnl file. Some specific integration between Hotpot and Neovim is configured via the setup() function.

Tip

For most users who just want to write their configuration in Fennel, you can ignore this section and use the default settings.

.hotpot.fnl

Hotpot's behaviour, along with the Fennel compiler, is configured by a .hotpot.fnl file, placed in the root of your config or plugin directory.

These files define a context for operations in that directory tree. These contexts are independent of one another and only alter behaviour in the same tree.

If there is no .hotpot.fnl file in Neovim's config directory, a default configuration is loaded. This is not the case for plugins, which must have a .hotpot.fnl file.

Tip

Before saving any changes to your .hotpot.fnl file, you might want to run :trust, to save yourself being prompted later.

;; .hotpot.fnl
{
 ;; Required, string, valid: hotpot/2
 ;; Describes expected schema for table.
 :schema :hotpot/2

 ;; Required, string, valid: cache|colocate
 ;; Describes target location of lua files. `cache` places lua files "out of
 ;; tree" in a directory loadable by neovim, `colocate` places lua files "in
 ;; tree", next to their fennel counterparts.
 ;;
 ;; When no `.hotpot.fnl` file is present in your config directory,
 ;; the target defaults to :cache. You may set it to :colocate by adding a
 ;; .hotpot.fnl file.
 ;; Be aware that its the users responsibility to remove previously
 ;; generated lua files when swapping targets in either direction.
 ;;
 ;; For plugins, the only valid value is `colocate`.
 :target :cache

 ;; All other keys are optional.

 ;; Optional, boolean
 ;; If true (default), any single compilation error will prevent any changes
 ;; from being written.
 :atomic? true

 ;; Optional, boolean
 ;; If true (default: false), output messages after every successful
 ;; compilation instead of just on error.
 :verbose? true

 ;; Optional, function
 ;; If provided, all compiled fennel source is passed to the function, along
 ;; with its path, relative to `.hotpot.fnl`. The function must return the
 ;; modified source.
 ;; Transform is not called automatically when using the compile and eval API.
 :transform (fn [src path] src)

 ;; Optional, list of strings
 ;; Glob patterns to ignore when performing compile and clean operations,
 ;; relative to the .hotpot.fnl file.
 ;;
 ;; Files matching `.lua` patterns are never considered orphans and never removed.
 ;; Files matching `.fnl` patterns are never compiled.
 ;; Files matching `.fnlm` patterns are never considered when performing stale checks.
 :ignore [:some/lib/**/*.lua :junk/*.fnl]

 ;; Optional, table
 ;; Fennel compiler options, passed directly to `fennel.compile-string`.
 ;;
 ;; Hotpot enables strict global checking by default to prevent referencing
 ;; unknown or misspelled variables. To restore Fennel's default
 ;; behaviour, you can set `allowedGlobals` to `false`.
 ;;
 ;; If you wish to reference `vim` in your macros, set `:extra-compiler-env {: vim}`.
 ;;
 ;; Note that `error-pinpoint` is always forced to false and `filename` is
 ;; always set to the correct value.
 ;;
 ;; See Fennel's own API documentation and --help for further details.
 :compiler {:allowedGlobals (icollect [k _ (pairs _G)] k)
            :extra-compiler-env {: vim}
            :error-pinpoint false}
}

setup()

Some advanced configuration options are supported by calling setup({...}) after requiring Hotpot. You do not have to call setup() if you are satisfied with the default behaviour.

Note: you may provide the keys in fennel-style or lua_style.

sync-report-handler

A function to override the default sync event reporter.

If you provide this function it is your responsibility to correctly output compiliation errors.

The function receives 3 arguments, a context object, report table and an invocation-metadata table. Inspect the report table for available fields. invocation-metadata contains reason which may be command, autocommand or api depending on what initiated the sync event.

The default handler generates LSP $/progress messages by default or nvim_echo messages when verbose? = true.

API

Hotpot provides an API to compile and evaluate arbitrary Fennel code, as well as compiling files in a project. All interaction is done via a context object.

context(path|nil)

Creates a context object. Returns context or nil, error.

All other API interactions are performed through the context object.

(let [api (require :hotpot.api)
      ctx (api.context (vim.fn.stdpath :config))]
  (ctx.eval "(+ 1 1)"))

path may be:

  • A path to a file or directory that has a .hotpot.fnl in its file tree,
  • your Neovim config directory, even if that does not have a .hotpot.fnl file,
  • or nil.

If given a valid path, the context is loaded for use. If given nil, a default "api" context is created which does not support some operations that require disk paths, such as sync.

context.compile(string, compiler-options)

Compiles the given string, using the context compiler options. Returns true, compiled string or false, error.

Does not automatically apply any transform specified for by the context configuration. If one was specified, it can be called by context.transform.

context.eval(string, compiler-options)

Evaluates the given string, using the context compiler options. Returns true, ...evaluated values or false, error.

Does not automatically apply any transform specified for by the context configuration. If one was specified, it can be called by context.transform.

context.sync(options|nil)

Syncs the context by compiling files in the context. Returns report table.

Supports the following options:

  • force?: force compilation of all files in the context, even if the .lua is up to date.
  • atomic?: allow writing successfully compiled files even if others have compilation errors.
  • verbose?: output additional compilation messages.
  • compiler: additional Fennel compiler options.

Not available for "api" contexts, eg: those created without any path given.

context.transform(string, filename|nil)

Apply context transform to string, as defined in the context .hotpot.fnl.

Not available if no transform has been defined.

context.locate(string)

Convert given path into its counterpart, eg: given a .fnl file path inside the context source, convert it to the .lua file path in the context destination.

Accepts .fnl or .lua file paths. Will construct paths for files that do not exist if desired.

Not available for "api" contexts, eg: those created without any path given.

context.metadata()

Returns a table of metadata related to the given context.

Migrating from Version 1

Important

You must change all macro files to use the the .fnlm extension. This does not require any code changes.

For many users who have not explicitly configured Hotpot, after renaming any macro files to .fnlm, version 2 should work without drama.

If you are using Lazy.nvim, you should re-check the installation instructions to correctly set the rtp option.

If you have specifically configured compiler options (via the compiler.macros/compiler.modules setup options) or use a .hotpot.lua file, you will need to migrate to a .hotpot.fnl file. See configuration for details on .hotpot.fnl. You no longer need to provide separate macros and modules compiler tables. All compiler options are specified in the compiler key in your .hotpot.fnl file.

The API has been simplified but with simplification, some previously provided functions have been removed, eg: there is now only eval(source, options), no eval-buffer, eval-file, etc.

Previously Hotpot provided in-editor diagnostics. These have been removed in favour of more complete solutions provided by LSP servers.

Changes from Version 1

  • No more Just in Time, now an Ahead of Time compiler
    • Hotpot now compiles all Fennel files that require compiling on save, instead of on-demand (i.e. on require()). This change was made for better compatibility with things such as the lsp/ runtime directory.
    • You can still "hide" .lua files from your config, this is still the default behaviour.
  • Better support for different compiler contexts
    • .hotpot.fnl supports configuring different directories with different compiler options and editing files in one plugin directory while your current working directory is elsewhere works better.
  • Macro files must use the extension .fnlm
    • This is the modern Fennel way, no concessions are currently made to support init-macros.fnl filenames.
  • .hotpot.lua is now .hotpot.fnl
    • See configuration, most of the same features exist, providing direct access to the compiler options, transforming source code and including/ignoring files. You can no longer redirect files on a case-by-case basis, either all are in cache or colocated (special case for <config>/init.fnl which is always colocated).
  • Diagnostics (in-editor compiler warnings) removed
    • LSP (eg: fennel-language-server) provides a richer feature set.
  • Configuring Fennel compiler options via setup()
    • Removed, use .hotpot.fnl file + compiler key if needed.
  • hotpot.api.compile|evaluate_[file|selection|...]
    • Simplified and context aware, see API.
  • Removed :Fnldo commands
    • This seem to have little value personally, if you really do have a use for it please open an issue.

Disable or Uninstall

To disable Hotpot's compile-on-save behaviour, see :Hotpot watch.

To disable Hotpot temporarily, remove the require('hotpot') call from your init.lua|fnl file.

To uninstall Hotpot and remove any files, first run :checkhealth hotpot and note destination directory associated with the Neovim configuration context. This will contain any compiled lua files. You can delete this directory and then remove Hotpot via your plugin manager.

Licenses

Hotpot embeds fennel.lua, see lua/hotpot/vendor/fennel.lua for licensing information.