NixOS Dev Log 002

In previous devlogs, I’ve discussed installing NixOS and then organizing our system’s configuration using Home Manager and Flakes. With this setup, we have everything we need to install and configure software on our system. A couple of other pieces of software that I configure through Home Manager include direnv to manage automatically activating environment flakes in directories, git (to manage different signing keys on different hosts), and Ghostty (my terminal of choice).

When a configuration either gets too complex to live in just my home.nix file or I want different configuration based on which system I’m building, I import those configurations as modules. In my case, direnv always has the same configuration no matter what, and it’s a small enough configuration block at the moment that it’s fine to leave in home.nix. For Ghostty, I define a bunch of additional key maps, which would bloat my home.nix – it’s much cleaner if I define it in its own module, and then just import it to my home.nix. This is my ghostty.nix:


{
  config,
  pkgs,
  ...
}: {
  programs.ghostty = {
    enable = true;

    settings = {
      theme = "Gruvbox Dark Hard";

      # Keybindings
      # Note: keyd remaps super (Cmd) to ctrl, so Ghostty receives ctrl events
      keybind = [
        "ctrl+c=copy_to_clipboard"
        "ctrl+v=paste_from_clipboard"
        "ctrl+shift+c=copy_to_clipboard"
        "ctrl+shift+v=paste_from_clipboard"
        "ctrl+equal=increase_font_size:1"
        "ctrl+minus=decrease_font_size:1"
        "ctrl+0=reset_font_size"
        "ctrl+q=quit"
        "ctrl+shift+comma=reload_config"
        "ctrl+k=clear_screen"
        "ctrl+n=new_window"
        "ctrl+shift+w=close_surface"
        "ctrl+t=new_tab"
        "ctrl+shift+left_bracket=previous_tab"
        "ctrl+shift+right_bracket=next_tab"
        "ctrl+d=new_split:right"
        "ctrl+shift+d=new_split:down"
        "ctrl+right_bracket=goto_split:next"
      ];
    };
  };
}

And then I import it into home.nix:

{
  config,
  pkgs,
  inputs,
  flakeName,
  lib,
  ...
}: {
  imports =
    [
	    ...,
      ../../modules/home-manager/ghostty.nix
    ]
    ++ (...)
}

In the case of git, I have a specific configuration per host, so I import the right one during build time based on the flake being built:

{
  config,
  pkgs,
  inputs,
  flakeName,
  lib,
  ...
}: {
  imports =
    [
      ../../modules/home-manager/ghostty.nix
    ]
    ++ (
      if flakeName == "framework-desktop"
      then [../../modules/home-manager/git-framework-desktop.nix]
      else if flakeName == "vm-aarch64"
      then [../../modules/home-manager/git-vm-aarch64.nix]
      else []
    );
}

Now, I’m able to move more detailed configurations to their own modules, and also have specific configurations per host.

Configuring Neovim

Neovim is my editor of choice. On MacOS hosts, I’ve walked the tip of Occam’s razor and opted to use LazyVim to manage my configuration. I wanted something that would work out of box, free from the performance crushing bloat of electron apps. LazyVim has great defaults, made it easy to add common plugins as lazy extras, and also made it easy to extend your configuration with more complex choices, should you choose.

NixOS and Nix more generally come at the philosophical opposite end of the spectrum. In this devlog, we’re literally cataloguing my experience in manually configuring my system from scratch to be reproducible. So the exercise with NixOS is one of finding comfort in delving into the details – a skill that I think every engineer can’t spend enough time honing. To this end, when it came to configuring Neovim, I wanted to start from scratch and manually define each key map and plugin that I wanted. To do this, I looked at nvf as a Neovim configuration tool in NixOS.

NVF

The best description of NVF comes from its authors. It is, in short:

[A] modular, extensible and distro-agnostic Neovim configuration framework for Nix/NixOS

After some research, it seemed like NVF was a pretty standard choice for configuring Neovim with NixOS. Some folks like to still use dotfiles, which you can totally do too, but I wanted to lean into tooling native to NixOS.

After consulting the man page and docs, I was able to get to a basic configuration that worked for me. Here’s my setup:

{
  config,
  pkgs,
  inputs,
  ...
}: {
  imports = [
    inputs.nvf.homeManagerModules.default
  ];
  # Replace the basic neovim config with nvf
  programs.nvf = {
    enable = true;
    enableManpages = true;

    settings.vim = {
      viAlias = true;
      vimAlias = true;

      # Prevent junk files
      preventJunkFiles = true;

      # Basic settings
      lineNumberMode = "relNumber";

      # Set tab width using luaConfigRC
      luaConfigRC.tabSettings = ''
        vim.opt.tabstop = 2        -- Number of spaces tabs count for
        vim.opt.shiftwidth = 2     -- Size of an indent
        vim.opt.expandtab = true   -- Use spaces instead of tabs
        vim.opt.softtabstop = 2    -- Number of spaces per Tab
        vim.opt.smartindent = true -- Smart autoindenting on new lines
        vim.opt.clipboard = "unnamedplus" -- Use system clipboard for yank/paste
      '';

      # Add commenting support with gc
      comments = {
        comment-nvim = {
          enable = true;
        };
      };

      # Theme - choose one you like
      theme = {
        enable = true;
        name = "gruvbox"; # Options: "catppuccin", "onedark", "tokyonight", "gruvbox", "nord"
        style = "dark"; # For catppuccin: "mocha", "macchiato", "frappe", "latte"
      };

      # LSP support
      lsp = {
        enable = true;
        formatOnSave = true;
        lspkind.enable = true;
        mappings = {
          goToDefinition = "gd";
          goToDeclaration = "gD";
          goToType = "gy";
          listImplementations = "gI";
          listReferences = "gr";
          hover = "K";
          renameSymbol = "<leader>rn";
          codeAction = "<leader>ca";
          nextDiagnostic = "]d";
          previousDiagnostic = "[d";
          signatureHelp = "<C-k>";
        };
      };

      # Debugger
      debugger = {
        nvim-dap = {
          enable = true;
          ui.enable = true;
        };
      };

      # Autocomplete
      autocomplete = {
        nvim-cmp = {
          enable = true;
        };
      };

      # Treesitter for syntax highlighting
      treesitter = {
        enable = true;
        fold = true;
        context.enable = true;
      };

      # fzf-lua for fuzzy finding
      fzf-lua = {
        enable = true;
        profile = "telescope"; # Use telescope-like profile for familiar UX
        setupOpts = {
          winopts = {
            border = "rounded";
          };
        };
      };

      # File explorer
      filetree = {
        nvimTree = {
          enable = true;
          openOnSetup = false;
          mappings = {
            toggle = "<leader>e";
            findFile = "<leader>f";
          };
        };
      };

      # Git integration
      git = {
        enable = true;
        gitsigns = {
          enable = true;
          codeActions.enable = true;
        };
      };

      # Status line
      statusline = {
        lualine = {
          enable = true;
          theme = "auto";
        };
      };

      # Buffer/tab line
      tabline = {
        nvimBufferline = {
          enable = true;
        };
      };

      # Terminal
      terminal = {
        toggleterm = {
          enable = true;
          mappings.open = "<C-t>";
        };
      };

      # Dashboard
      dashboard = {
        alpha = {
          enable = true;
          theme = "dashboard"; # Options: "dashboard", "startify", "theta"
        };
      };

      # Customize alpha dashboard buttons to match keybindings
      luaConfigRC.alphaButtons = ''
        local alpha = require('alpha')
        local dashboard = require('alpha.themes.dashboard')

        -- Override buttons with your custom keybindings
        dashboard.section.buttons.val = {
          dashboard.button("SPC SPC", "  Find file", ":lua require('fzf-lua').files()<CR>"),
          dashboard.button("SPC /", "  Find text", ":lua require('fzf-lua').live_grep()<CR>"),
          dashboard.button("SPC f r", "  Recent files", ":lua require('fzf-lua').oldfiles()<CR>"),
          dashboard.button("SPC e", "  File explorer", ":NvimTreeToggle<CR>"),
          dashboard.button("q", "  Quit", ":qa<CR>"),
        }

        alpha.setup(dashboard.opts)
      '';
      #
      # # Utility plugins
      # utility = {
      #   diffview-nvim.enable = true;
      #
      #   # Markdown preview
      #   # markdown-preview.enable = true;
      #
      #   # Comment toggling
      #   comment-nvim.enable = true;
      # };

      # UI enhancements
      ui = {
        noice.enable = true; # Better UI for messages, cmdline and popupmenu
        borders = {
          enable = true;
          globalStyle = "rounded";
        };
      };

      # Language support
      languages = {
        nix = {
          enable = true;
          format.enable = true;
          lsp = {
            enable = true;
            server = "nixd"; # or "nil"
          };
          treesitter.enable = true;
        };

        bash = {
          enable = true;
          lsp.enable = true;
          treesitter.enable = true;
        };

        python = {
          enable = true;
          lsp.enable = true;
          treesitter.enable = true;
          format.enable = true;
        };

        markdown = {
          enable = true;
          lsp.enable = true;
          treesitter.enable = true;
        };

        scala = {
          enable = true;
          lsp.enable = true;
          treesitter.enable = true;
        };

        java = {
          enable = true;
          lsp.enable = true;
          treesitter.enable = true;
        };

        # Add more languages as needed:
        # rust = {
        #   enable = true;
        #   lsp.enable = true;
        #   treesitter.enable = true;
        # };
        #
        # ts = {  # TypeScript/JavaScript
        #   enable = true;
        #   lsp.enable = true;
        #   treesitter.enable = true;
        # };
        #
        # go = {
        #   enable = true;
        #   lsp.enable = true;
        #   treesitter.enable = true;
        # };
      };

      # Custom keybindings
      keymaps = [
        # Insert mode: Restore Ctrl+W for word deletion
        # (keyd remaps it system-wide, so we need to handle it)
        {
          key = "<C-w>";
          mode = "i";
          action = "<C-o>db";
          silent = true;
          desc = "Delete word backwards";
        }
        # fzf-lua keybindings
        {
          key = "<leader><space>";
          mode = "n";
          action = "<cmd>lua require('fzf-lua').files()<CR>";
          silent = true;
          desc = "Find files";
        }
        {
          key = "<leader>/";
          mode = "n";
          action = "<cmd>lua require('fzf-lua').live_grep()<CR>";
          silent = true;
          desc = "Live grep";
        }
        {
          key = "<leader>fb";
          mode = "n";
          action = "<cmd>lua require('fzf-lua').buffers()<CR>";
          silent = true;
          desc = "Find buffers";
        }
        {
          key = "<leader>fh";
          mode = "n";
          action = "<cmd>lua require('fzf-lua').help_tags()<CR>";
          silent = true;
          desc = "Help tags";
        }
        {
          key = "<leader>fr";
          mode = "n";
          action = "<cmd>lua require('fzf-lua').oldfiles()<CR>";
          silent = true;
          desc = "Recent files";
        }
        {
          key = "<leader>fc";
          mode = "n";
          action = "<cmd>lua require('fzf-lua').commands()<CR>";
          silent = true;
          desc = "Find commands";
        }
        {
          key = "<leader>gs";
          mode = "n";
          action = "<cmd>lua require('fzf-lua').git_status()<CR>";
          silent = true;
          desc = "Git status";
        }
        # Buffer management
        {
          key = "bd";
          mode = "n";
          action = "<cmd>bdelete<CR>";
          silent = true;
          desc = "Close active buffer";
        }
      ];
    };
  };
}

I’ve defined some language support, a couple plugins, and some keymaps. I can easily add more things in the future, but this works quite well for me right now.

My Neovim dashboard.

Conclusion

I’m slowly but surely building my ideal development environment, configuration block after configuration block. I’m researching and placing each stone, so it’s a great feeling as an engineer to be so close to the details of my OS. In future dev logs, I want to expand on my configurations, and how I used AI to help me surmount the NixOS learning curve.