Introduction
Welcome to NixCats Book
, a comprehensive guide crafted to help Neovim users transition to the Nix way of configuration. This book is designed to provide clarity and simplicity, allowing users to embrace Nix without losing control over their configurations by burying them with unmaintainable code.
NixCats (aka. nixCats-nvim) bridges the gap for Neovim users by defining practical rules and patterns for configuring Neovim using Lua, ensuring your setup remains both declarative and maintainable. The goal is to make use of the declarative and reproducible nature of Nix with neovim configuration, empowering you to make full use of the text editor without hassles. NixCats follow the idea that "You configure once and forget about it".
Whether you're new to Nix or a seasoned user looking to refine your approach, this guide will be your companion in building a reproducible, clean, and efficient Neovim configuration.
Prerequisites
Before you get started with this book, it is assumed that you have a moderate knowledge of:
If you feel like you lack knowledge in certain areas within the above mentioned, it is recommended to have a look at respective resources. A few resources are mentioned in the References Chapter for you to get started.
Additionally, the sub-chapters introduce about few concepts for beginners in lua and nix. Feel free to skip to Overview if you are well versed with these.
Terminology
-
Flakes: An experimental feature within Nix that allows users to grab resources from the web as
inputs
and can provideoutput
of the packaged application. You can read more about flakes in here -
Flake-Parts: A distributed framework for writing Nix flakes which is a direct lightweight replacement of the Nix flake schema
-
Modules (aka. Flake Modules): Flakes are configuration. The module system lets you refactor configuration into modules that can be shared.
Lua
Lua
is a lightweight, high-level programming language designed for simplicity and efficiency. Created with a focus on embeddability, Lua is widely used in game development, embedded systems, and scripting environments. Its key features include a small footprint, fast execution, and a clean, minimal syntax that makes it accessible even for beginners. Lua's power lies in its flexibility, allowing developers to extend its capabilities through meta-programming and integration with other languages. It balances ease of use with robust features, such as first-class functions, coroutines, and dynamic typing.
Lua and Neovim
Neovim has embraced Lua as a first-class citizen, making it a key component for both scripting and plugin development. Traditionally, Vim relied on VimScript, which, while functional, has limitations in performance, extensibility and was very limiting for beginners to jump into due to poor availability of resources. Lua addresses these issues, offering a modern and efficient alternative.
Here’s how Neovim integrates Lua:
-
Configuration: Lua can replace the traditional
init.vim
file with aninit.lua
file, allowing for a cleaner and more powerful configuration setup.vim.keymap.set('n', '<leader>s', ':w<CR>')
This binds
<leader>s
(commonly\s
unless redefined) to save the current file in normal mode. -
API Access: Neovim’s Lua API provides access to core editor features and functions, making it easy to interact with the editor programmatically.
print(vim.fn.expand('%'))
This prints the name of the currently open file in the Neovim command line.
-
Plugin Development: Lua provides a robust foundation for developing plugins. Unlike VimScript, Lua-based plugins benefit from improved performance and access to a rich ecosystem of external libraries.
vim.api.nvim_create_user_command('SayHello', function() print('Hello, Neovim user!') end, {})
Type
:SayHello
in Neovim to see the message. -
Event Handling: Lua enables efficient handling of asynchronous events, which is crucial for modern Neovim workflows, such as managing background processes or integrating with external tools.
vim.api.nvim_create_autocmd('CmdlineLeave', { pattern = '*', callback = function() vim.cmd('set hlsearch') end, })
This highlights search results every time the search command is exited.
-
Extensibility: Many popular Neovim plugins, such as telescope.nvim, nvim-treesitter, and lualine.nvim, are written in Lua, showcasing its power and flexibility.
Why Lua?
The adoption of Lua in Neovim is not just a technical upgrade—it represents a shift toward making Neovim more user-friendly and developer-friendly. Lua’s speed and integration capabilities allow users to create custom workflows and plugins without the overhead of learning an entirely new scripting language.
By integrating Lua, Neovim has positioned itself as a modern text editor capable of meeting the needs of both casual users and advanced developers. If you would like to learn more about lua, check out these resources
Nix
Nix
is a functional programming language and Nix-Cli(aka nix)
is a declarative package manager designed to create reproducible and declarative software environments. It works by treating configurations as code, ensuring that every aspect of your environment is consistent, version-controlled, and isolated. One of Nix’s standout features is its ability to manage software dependencies seamlessly while maintaining exact version control.
Key Concepts of Nix
-
Declarative Configurations: Environments and configurations are defined in .nix files, making them reproducible.
-
Immutable Packages: Nix stores packages in a unique, hashed location
/nix/store/$hash-$packagename
, ensuring no conflicts between versions where$hash
represents the hash id of the package and$packagename
refers to the name of the package itself. For example, if I do the follwing in my terminal:$ nix-shell -p gcc
Nix would go and evaluate the package derivation from nixpkgs and store it in
/nix/store/xzfmarrq8x8s4ivpya24rrndqsq2ndiz-gcc-13.3.0
NOTE:
/nix/store
is an immutable file-system which means the user can only access it read-only. -
Version Locking: Using tools like
flakes
ornixpkgs
, you can pin specific versions of packages for consistent builds.
What is nixpkgs?
nixpkgs
is a central repository that contains thousands of software packages available in Nix. It acts as the foundation for most Nix-based configurations and is continuously updated to include new packages and fixes. You can view the repo in GitHub.
$ nix-shell -p neovim git
This command starts a bash
shell environment with neovim and git installed, without affecting your system configuration.
What is flakes?
Flakes are an experimental feature that enhances reproducibility by locking dependencies and configurations in a standardized format. It is intended to replace default.nix
which you would normally use in a Nix system. This brings a new schema where you can define your packages from the web in inputs
and build package or a shell for the user. You could say that flakes work similar to the Rust package manager cargo
or Javascript package manager npm
which locks your dependencies in a separate file (Cargo.lock
and packages-lock.json
respectively), flake.lock
in this case.
# flake.nix
{
description = "An example NixOS configuration";
inputs = {
nixpkgs = { url = "github:nixos/nixpkgs/nixos-unstable"; };
nur = { url = "github:nix-community/NUR"; };
};
outputs = inputs: {
nixosConfigurations = {
mysystem = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./configuration.nix ];
specialArgs = { inherit inputs; };
};
};
};
}
This allow user to use flakes
to build a system configuration using nixosConfigurations
defined in the flake.nix
.
You can learn more about nix and flakes by referring to resources
Overview
This chapter will explore the different architectural designs implemented within nixCats-nvim
and will provide a concise understanding of the concepts involved. Later, we will explore a few ways to get started with nixCats-nvim
.
Investigation
There are many ways you could approach the way to handle neovim using nix. Lets go through them briefly, which will give you a clear idea of the reason why nixcats is superior to its alternatives. In order to have neovim in our NixOS system. We can add the nixpkgs
option or the package itself. It would look something like this (use only one of these, setting both would most probably give you an infinite recursion):
programs.neovim.enable = true;
# OR
environment.systemPackages = with pkgs; [
neovim
];
The nixpkgs derivations do already a great job in wrapping neovim itself and build from source. The next step obviously would be to add a few plugins to get our workflow going smoothly as we want to.
Home Manager
When using home-manager
, you are able to link files into place.
home.".config/nvim/".source = ./mynvimconfig;
Whats the problem with this! Its great! I can download stuff with a neovim package manager and mason... right?
Well, you can try. But if you want treesitter grammers, you're also going to want this.
home.packages = with pkgs; [
stdenv.cc.cc
];
And mason just isn't going to work on nixos, so youre going to want to swap that for just lspconfig and add those too.
home.packages = with pkgs; [
stdenv.cc.cc
nixd
lua-language-server
rust-analyzer
];
Uh oh!!!
How do we pass in info from nix into our configuration? I'm using agenix!
Well, now we have to probably write some stuff in nix. Lets change our approach and use the home manager module.
programs.neovim = {
enable = true;
plugins = with pkgs.vimPlugins [
lze;
{
plugin = telescope-nvim;
config = ''
require("lze").load {
"telescope.nvim",
cmd = "Telescope",
}
'';
type = "lua";
optional = true;
}
{
plugin = sweetie-nvim;
config = ''
require("lze").load {
"sweetie.nvim",
colorscheme = "sweetie",
}
'';
type = "lua";
optional = true;
}
];
};
Oh no... This is all wrong... Where is our auto complete! Where did our directory go?
You think I'm going to write all my lua in nix strings just so that I can "${interpolate}"
if I need to?
Also, this is tied to home manager! What if you don't want home manager on a machine?
What if you want to run it on another person's machine without putting your whole home-manager configuration on their computer?
A naieve standalone approach
Lets try pkgs.wrapNeovim
in a flake
.
{
inputs = {
nixpkgs = {
url = "github:nixos/nixpkgs/nixpkgs-unstable";
};
};
outputs = { nixpkgs, neovim-nightly, ...}@inputs: let
forAllSys = nixpkgs.lib.genAttrs nixpkgs.lib.platforms.all;
in {
packages = forAllSys (system: let
pkgs = import nixpkgs { inherit system; };
myNeovim = let
luaRC = final.writeText "init.lua" ''
local configdir = "${./mynvimconfig}";
vim.opt.packpath:prepend(configdir)
vim.opt.runtimepath:prepend(configdir)
vim.opt.runtimepath:append(configdir .. "/after")
if vim.fn.filereadable(configdir .. "/init.lua") == 1 then
dofile(configdir .. "/init.lua")
end
'';
in
pkgs.wrapNeovim pkgs.neovim-unwrapped {
configure = {
customRC = ''lua dofile("${luaRC}")'';
packages.all.start = with pkgs.vimPlugins; [
nvim-treesitter.withAllGrammars
lze
telescope-nvim
];
packages.all.opt = with pkgs.vimPlugins; [
];
};
extraMakeWrapperArgs = builtins.concatStringsSep " " [
''--prefix PATH : "${pkgs.lib.makeBinPath (with pkgs; [
stdenv.cc.cc
nixd
lua-language-server
rust-analyzer
])}"''
];
extraLuaPackages = (_: []);
extraPythonPackages = (_: []);
withPython3 = true;
extraPython3Packages = (_: []);
withNodeJs = false;
withRuby = true;
vimAlias = false;
viAlias = false;
extraName = "";
};
in
{
default = myNeovim;
});
};
}
Well, this is kinda cool. We can run this from the command line, it pulls in our directory... It's pretty close to what we want.
But still... It could be better.
How do we pass info from nix into our directory?
The answer? Usually people just add a bunch of global variables.
Obviously that's not super ideal.
Also, every time we want to change it, we have to reload!
What do we want?
The ideal neovim configuration would necessarily need a few feature to be maintainable:
- A way to write
lua
separately in alua
file andnix
in its own file. - A way to seamlessly pass extra arbitrary info from
nix
to our configuration, without creating a long list ofvim.g.global_variables
. - Run our configuration outside of our system that have access to
nix
(CLI). - Have utilities for easily installing plugins that aren't on
nixpkgs
. - Have multiple configuration profiles within a same configuration base.
- Have a mode that allows editing
lua
and seeing the results without rebuilding via nix, assuming the required things are already installed. - Our configuration should be exported* as
nixosModule
,homeManagerModule
,overlay
andpackage
.
This would enable users to integrate with any necessary system with ease.
Now lets enter the nixCats
world. The next pages will talk about the options and specific architectural pattern within nixCats
.
*
: The exports could differ from template to template. Thus, make sure you choose a template that fits your need.
Architecture
Copy of https://nixcats.org/nixCats_installation.html#nixCats.templates
For the following 100 lines, it is most effective to cross reference with a template!
First choose a path for luaPath
as your new neovim directory to be loaded into
the store.
Then in categoryDefinitions
:
You have a SET to add LISTS of plugins to the packpath (one for both
pack/*/start
and pack/*/opt
), a SET to add LISTS of things to add to the path,
a set to add lists of shared libraries,
a set of lists to add... pretty much anything.
Full list of these sets is at :h nixCats.flake.outputs.categories
Those lists are in sets, and thus have names.
You do this in categoryDefintions
, which is a function provided a pkgs set.
It also receives the values from packageDefintions
of the package it is being called with.
It returns those sets of things mentioned above.
packageDefintions
is a set, containing functions that also are provided a
pkgs set. They return a set of categories you wish to include.
If, from your categoryDefintions
, you returned:
startupPlugins = {
general = [
pkgs.vimPlugins.lz-n
pkgs.vimPlugins.nvim-treesitter.withAllGrammars
pkgs.vimPlugins.telescope
# etc ...
];
};
In your packageDefintions
, if you wanted to include it in a package named
myCoolNeovimPackage
, launched with either myCoolNeovimPackage
or vi
,
you could have:
# see :help nixCats.flake.outputs.packageDefinitions
packageDefinitions = {
myCoolNeovimPackage = { pkgs, ... }@misc: {
settings = {
aliases = [ "vi" ];
};
categories = {
# setting the value to true will include it!
general = true;
# yes you can nest them
};
};
# You can return as many packages as you want
};
defaultPackageName = "myCoolNeovimPackage";
They also return a set of settings, for the full list see :h nixCats.flake.outputs.settings
Then, a package is exported and built based on that using the nixCats builder function, and various flake exports such as modules based on your config are made using utility functions provided. The templates take care of that part for you, just add stuff to lists.
But the cool part. That set of settings and categories is translated verbatim from a nix set to a lua table, and put into a plugin that returns it. It also passes the full set of plugins included via nix and their store paths in the same manner. This gives full transparency to your neovim of everything in nix. Passing extra info is rarely necessary outside of including categories and setting settings, but it can be useful, and anything other than nix functions may be passed. You then have access to the contents of these tables anywhere in your neovim, because they are literally a set hard-coded into a lua file on your runtimpath.
You may use the :NixCats
user command to view these
tables for your debugging. There is a global function defined that
makes checking subcategories easier. Simply call nixCats('the.category')!
It will return the nearest parent category value, but nil if it was a table,
because that would mean a different sub category was enabled, but this one was
not. It is simply a getter function for the table require('nixCats').cats
see :h nixCats
for more info.
That is what enables full transparency of your nix configuration to your neovim! Everything you could have needed to know from nix is now easily passed, or already available, through the nixCats plugin!
It has a shorthand for importing plugins that aren't on nixpkgs, covered in
:h nixCats.flake.inputs
and the templates set up the outputs for you.
Info about those outputs is detailed in nixCats.flake.outputs.exports
You can also add overlays accessible to the pkgs object above, and set config
values for it, how to do that is at the top of the templates, and covered in
help at :h nixCats.flake.outputs.overlays
and :h nixCats.flake.outputs.overlays
It also has a template containing some lua functions that can allow you
to adapt your configuration to work without nix. For more info see :h nixCats.luaUtils
It contains useful functions,
such as "did nix load neovim" and "if not nix do this, else do that"
It also contains a simple wrapper for lazy.nvim
that does the rtp
reset
properly, and then can be used to tell lazy not
to download stuff in an easy to use fashion.
The goal of the starter templates is so that the usage at the start can be as simple
as adding plugins to lists and calling require('theplugin').setup()
Most further complexity is optional, and very complex things can be achieved
with only minor changes in nix, and some nixCats('something')
calls.
You can then import the finished package, and reconfigure it again
without duplication using the override function! see :h nixCats.overriding
.
Getting Started
You can see all the available templates here. Here’s a simple walkthrough to set up nixCats:
-
Clone the Template:
$ mkdir mynixcat && cd mynixcat $ nix flake init -t github:BirdeeHub/nixCats-nvim
-
Edit
flake.nix
: Add your plugins and categories.categoryDefinitions = { pkgs, ... }: { startupPlugins = { general = with pkgs.vimPlugins; [ plenary-nvim nvim-treesitter.withAllGrammers # mkNvimPlugin build a plugin from flake input (mkNvimPlugin inputs.plugins-telescope "telescope") ]; }; } packageDefinitions = { mynixcat = {pkgs, ...}: { settings = { wrapRc = true; aliases = ["vi" "vim" "nvim"]; # Enable to use flake inputs to build nightly version of neovim # neovim-unwrapped = # inputs.neovim-nightly-overlay.packages.${pkgs.system}.default; }; categories = { general = true; }; extra = {}; }; }; defaultPackageName = "mynixcat";
See the comments in each templates for further reference.
-
Open Neovim:
$ nix run .
Templates
nixCats provides several templates to suit different user needs:
- Standalone Flake: A self-contained flake for standalone Neovim configurations.
- Nix Expression Flake Outputs: Combines Neovim into your system flake.
- Lua Utilities: Simplifies adapting non-Nix setups to work within nixCats.
Copy of https://nixcats.org/nixCats_installation.html#nixCats.templates
The templates may also be imported from the utils set via inputs.nixCats.utils.templates The following is the set where they are defined.
You may initialize them into the current directory via
$ nix flake init -t github:BirdeeHub/nixCats-nvim#$TEMPLATE_NAME
{
default = {
path = ./fresh;
description = "starting point template for making your neovim flake";
};
fresh = {
path = ./fresh;
description = "starting point template for making your neovim flake";
};
example = {
path = ./example;
description = "an idiomatic nixCats example configuration using lze for lazy loading and paq.nvim for backup when not using nix";
};
module = {
path = ./module;
description = ''
starting point for creating a nixCats module for your system and home-manager
'';
};
nixExpressionFlakeOutputs = {
path = ./nixExpressionFlakeOutputs;
description = ''
how to import as just the outputs section of the flake, so that you can export
its outputs with your system outputs
It is best practice to avoid using the system pkgs and its overlays in this method
as then you could not output packages for systems not defined in your system flake.
It creates a new one instead to use, just like the flake template does.
Call it from your system flake and call it with inputs as arguments.
'';
};
overwrite = {
path = ./overwrite;
description = ''
How to CONFIGURE nixCats FROM SCRATCH,
given only an existing nixCats package,
achieved via the OVERRIDE function.
Equivalent to the default flake template
or nixExpressionFlakeOutputs except
for using overrides
every nixCats package is a full nixCats-nvim
'';
};
luaUtils = {
path = ./luaUtils;
description = ''
A template that includes lua utils for using neovim package managers
when your config file is not loaded via nix.
'';
};
kickstart-nvim = {
path = ./kickstart-nvim;
description = ''
The entirety of kickstart.nvim implemented as a nixCats flake.
With additional nix lsps for editing the nix part.
This is to serve as the tutorial for using the nixCats lazy wrapper.
'';
};
overriding = {
path = ./overriding;
description = ''
How to RECONFIGURE nixCats WITHOUT DUPLICATION,
given only an existing nixCats package,
achieved via the OVERRIDE function.
In addition, it is also a demonstration of how to export a nixCats configuration
as an AppImage.
It is a 2 for 1 example of 2 SEPARATE things one could do.
'';
};
overlayHub = {
path = ./overlayHub;
description = ''
A template for overlays/default.nix
:help nixCats.flake.nixperts.overlays
'';
};
overlayFile = {
path = ./overlayfile;
description = ''
A template for an empty overlay file defined as described in
:help nixCats.flake.nixperts.overlays
'';
};
}
Alternative Projects to NixCats
Here are some noteworthy projects that provide alternative approaches to Neovim configuration and Nix integration. While each has its strengths, NixCats aims to address specific gaps in functionality and design. Explore these projects to find the one that best suits your needs:
kickstart.nvim
A minimalist, ready-to-use Neovim configuration starter for beginners.
- How it Differs:
- Does NOT use Nix for plugin management.
- Focuses on simplicity and traditional Lua-based configurations.
kickstart-nix.nvim
A Nix-based configuration built on wrapNeovimUnstable
with no additional abstractions.
- How it Differs:
- Maintains a standard Neovim structure while leveraging Nix for reproducibility.
- Emphasizes raw control with minimal abstraction.
NixVim
A module-based Neovim configuration system, somewhat akin to Home Manager.
- How it Differs:
- Provides a large library of pre-configured plugin modules.
- Falls back to
programs.neovim
for unsupported plugins.
Luca's super simple Neovim flake
A highly minimal example of integrating Nix with Neovim.
- How it Differs:
- Focuses on simplicity, providing a beginner-friendly introduction to Nix and Neovim integration.
- Serves as a great springboard for learning the basics.
nixPatch-nvim
A specialized tool for managing lazy.nvim
configurations using Nix and Zig.
- How it Differs:
- Parses and replaces plugin URLs at build time.
- Focused exclusively on
lazy.nvim
with unique build-time functionality.
References
Learning Lua
- Programming in Lua - The official guide by Lua’s creators.
- Learn Lua - A concise tutorial covering Lua basics.
- Lua Reference Manual - Detailed technical documentation for Lua 5.4.
- Awesome Lua - A curated list of Lua resources, libraries, and tools.
Configuring Neovim with Lua
- Neovim Lua Guide - A beginner-friendly guide to using Lua in Neovim.
- Neovim Documentation - Official documentation with Lua API references.
- Awesome Neovim - A collection of Lua plugins and configurations for Neovim.
Nix
- NixOS and Flakes Book - A practical guide to using flakes effectively.
- Nix Flakes Overview - Introduction to Nix flakes, their features, and usage.
- Official RFC 0049: Flakes - The proposal that introduced flakes to Nix.
flake-parts
- flake-parts - Official website with guides and documentation.
- flake-parts GitHub Repository - The official repo with documentation and examples.
- Using flake-parts - Community discussion and tutorials on leveraging flake-parts for modular configurations.