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:

  • Lua programming language
  • Nix programming language
  • Git VCS tool
  • Neovim configuration using Lua

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 provide output 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 an init.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 or nixpkgs, 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 a lua file and nix in its own file.
  • A way to seamlessly pass extra arbitrary info from nix to our configuration, without creating a long list of vim.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 and package.

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:

  1. Clone the Template:

    $ mkdir mynixcat && cd mynixcat
    $ nix flake init -t github:BirdeeHub/nixCats-nvim
    
  2. 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.

  3. Open Neovim:

    $ nix run .
    

Templates

nixCats provides several templates to suit different user needs:

  1. Standalone Flake: A self-contained flake for standalone Neovim configurations.
  2. Nix Expression Flake Outputs: Combines Neovim into your system flake.
  3. 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

Configuring Neovim with Lua

Nix

flake-parts