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.
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.
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.
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.