Shell and Editor Configuration
Shell and Editor Configuration
Goal: Declare complete user workstation environments—interactive shells (Bash, Zsh, Fish), modern prompt engines (Starship), terminal multiplexers (Tmux, Zellij), and code editors (Neovim, Helix, VS Code)—entirely within Home Manager, wiring Language Server Protocol (LSP) binaries and treesitter parsers without mutable runtime state.
This chapter builds upon 03-home-manager.qmd and 04-hm-files-xdg.qmd. While previous chapters focused on raw files and user account creation, this chapter covers the deep configuration of daily interactive developer tooling using structured programs.* modules.
Why Shell and Editor Declarations Matter
Traditional workstation setup relies on mutable imperative managers: curl | sh scripts, Oh-My-Zsh plugins, Lazy.nvim downloading git repositories dynamically into ~/.local/share, or Visual Studio Code extensions updating automatically.
This imperative approach breaks in three ways: 1. Machine Synchronization Lag: When setting up a second laptop or pairing station, plugins, themes, and keybindings are inevitably forgotten or drift across machines. 2. Missing System Binaries: An editor configuration that assumes ripgrep, fd, clangd, pyright, or gopls are installed globally in the system $PATH breaks when opened on a minimal host. 3. Non-Hermetic Plugin Caching: A plugin manager that pulls directly from GitHub master branches at startup introduces breaking changes and breaks offline work.
By declaring shells, multiplexers, and editors in Home Manager, all binaries, plugins, themes, and configurations become immutable store paths pinned in your flake lockfile.
Declarative Shell Environments
Home Manager provides dedicated modules for all major interactive shells. Rather than managing complex nested .bashrc or .zshrc files with raw strings, use typed options.
Modern Shell Declaration (Bash and Fish)
# homes/developer/programs/shell.nix
{ pkgs, ... }: {
# Bash configuration
programs.bash = {
enable = true;
enableCompletion = true;
historySize = 100000;
historyFileSize = 200000;
historyControl = [ "ignoredups" "ignorespace" ];
shellAliases = {
ll = "ls -lah --color=auto";
g = "git";
nr = "nixos-rebuild switch --use-remote-sudo --flake .";
nd = "nix develop -c $SHELL";
};
initExtra = ''
# Fast vi mode and custom keybindings
set -o vi
bind '"\e[A": history-search-backward'
bind '"\e[B": history-search-forward'
'';
};
# Fish configuration (alternative modern interactive shell)
programs.fish = {
enable = true;
interactiveShellInit = ''
set -g fish_greeting "" # suppress greeting banner
fish_vi_key_bindings
'';
shellAliases = {
cat = "bat --paging=never";
find = "fd";
};
plugins = [
{
name = "z";
src = pkgs.fetchFromGitHub {
owner = "jethrokuan";
repo = "z";
rev = "e0e1b9deb977e84c8298f1caf7c6d467e5b4d460";
hash = "sha256-j28y4O9h/P4b41eyvdQdo286q13E8k7i0/U74zQzZ80=";
};
}
];
};
}Universal Prompt Engineering with Starship
The starship prompt provides a fast, cross-shell prompt written in Rust. Home Manager integrates it directly into Bash, Zsh, and Fish with zero shell-specific boilerplate:
# homes/developer/programs/starship.nix
{ ... }: {
programs.starship = {
enable = true;
enableBashIntegration = true;
enableFishIntegration = true;
enableZshIntegration = true;
settings = {
add_newline = false;
character = {
success_symbol = "[➜](bold green)";
error_symbol = "[✗](bold red)";
};
nix_shell = {
symbol = "❄️ ";
format = "via [$symbol\\($name\\)]($style) ";
style = "bold blue";
};
git_branch = {
symbol = "🌱 ";
truncation_length = 24;
};
directory = {
truncation_length = 5;
truncate_to_repo = true;
};
aws.disabled = true;
gcloud.disabled = true;
};
};
}Terminal Multiplexers: Tmux and Zellij
Multiplexers preserve developer sessions across SSH connections and provide split-pane workspaces. Home Manager can configure both traditional tmux and modern zellij:
# homes/developer/programs/multiplexers.nix
{ pkgs, ... }: {
# Traditional tmux with declarative plugins
programs.tmux = {
enable = true;
shortcut = "a"; # Ctrl-a prefix
baseIndex = 1;
escapeTime = 0;
terminal = "screen-256color";
mouse = true;
keyMode = "vi";
plugins = with pkgs.tmuxPlugins; [
sensible
pain-control
yank
{
plugin = catppuccin;
extraConfig = ''
set -g @catppuccin_flavor 'mocha'
set -g @catppuccin_window_tabs_enabled on
'';
}
];
extraConfig = ''
# Split panes using | and -
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
'';
};
# Modern Rust-based Zellij
programs.zellij = {
enable = true;
settings = {
theme = "catppuccin-mocha";
default_layout = "compact";
pane_frames = false;
copy_command = "wl-copy";
};
};
}Declarative Editor Mastery: Neovim and Helix
Declarative Neovim with Pinned Plugins and Built-in LSPs
Rather than letting Neovim execute git clones at runtime, Home Manager’s programs.neovim builds a hermetic Neovim derivation. It wraps nvim with a generated init.lua and injects exact language server binaries into its private execution environment:
# homes/developer/programs/neovim.nix
{ pkgs, ... }: {
programs.neovim = {
enable = true;
defaultEditor = true;
viAlias = true;
vimAlias = true;
# System binaries made available specifically to Neovim
extraPackages = with pkgs; [
# Language servers (LSP)
nil # Nix LSP
gopls # Go LSP
rust-analyzer # Rust LSP
pyright # Python LSP
lua-language-server
# Formatters & Linters
alejandra # Opinionated Nix formatter
nixpkgs-fmt
rustfmt
black
stylua
# Core tools for fuzzy finding
ripgrep
fd
git
];
# Immutable plugins fetched through Nix
plugins = with pkgs.vimPlugins; [
# Syntax & Highlighting
nvim-treesitter.withAllGrammars
# UI & Theming
catppuccin-nvim
lualine-nvim
nvim-web-devicons
# Navigation & Fuzzy Search
telescope-nvim
plenary-nvim
# LSP & Completion
nvim-lspconfig
nvim-cmp
cmp-nvim-lsp
cmp-buffer
cmp-path
luasnip
];
# Pure Lua configuration pointing to the immutable paths
extraLuaConfig = ''
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.expandtab = true
vim.opt.shiftwidth = 2
vim.opt.tabstop = 2
vim.opt.termguicolors = true
-- Set color scheme
vim.cmd.colorscheme "catppuccin-mocha"
-- Configure LSP through nvim-lspconfig
local lspconfig = require('lspconfig')
local capabilities = require('cmp_nvim_lsp').default_capabilities()
-- Nix LSP (nil)
lspconfig.nil_ls.setup {
capabilities = capabilities,
settings = {
['nil'] = {
formatting = { command = { "alejandra" } },
},
},
}
-- Go LSP
lspconfig.gopls.setup { capabilities = capabilities }
-- Rust Analyzer
lspconfig.rust_analyzer.setup { capabilities = capabilities }
-- Telescope Keybindings
local builtin = require('telescope.builtin')
vim.keymap.set('n', '<leader>ff', builtin.find_files, {})
vim.keymap.set('n', '<leader>fg', builtin.live_grep, {})
vim.keymap.set('n', '<leader>fb', builtin.buffers, {})
'';
};
}Minimal Modal Editing: Helix
For developers seeking a zero-plugin modal editor with built-in LSP, Treesitter, and fuzzy-finding, helix provides a clean declarative configuration:
# homes/developer/programs/helix.nix
{ pkgs, ... }: {
programs.helix = {
enable = true;
settings = {
theme = "catppuccin_mocha";
editor = {
line-number = "relative";
cursorline = true;
color-modes = true;
cursor-shape = {
insert = "bar";
normal = "block";
select = "underline";
};
lsp = {
display-messages = true;
display-inlay-hints = true;
};
};
};
languages = {
language = [
{
name = "nix";
auto-format = true;
formatter = { command = "${pkgs.alejandra}/bin/alejandra"; };
language-servers = [ "nil" ];
}
{
name = "rust";
auto-format = true;
language-servers = [ "rust-analyzer" ];
}
];
};
};
}Visual Studio Code Declarative Management
When a graphical IDE is required, Home Manager can manage VS Code, its settings, and its extensions without manual marketplace installations:
# homes/developer/programs/vscode.nix
{ pkgs, ... }: {
programs.vscode = {
enable = true;
package = pkgs.vscode;
extensions = with pkgs.vscode-extensions; [
bbenoist.nix
jnoortheen.nix-ide
rust-lang.rust-analyzer
golang.go
catppuccin.catppuccin-vsc
eamodio.gitlens
];
userSettings = {
"workbench.colorTheme" = "Catppuccin Mocha";
"editor.formatOnSave" = true;
"nix.enableLanguageServer" = true;
"nix.serverPath" = "${pkgs.nil}/bin/nil";
"nix.serverSettings" = {
"nil" = {
"formatting" = { "command" = [ "alejandra" ]; };
};
};
"telemetry.telemetryLevel" = "off";
};
};
}Common Traps and State Separation
| Trap | Root Cause | Declarative Correction |
|---|---|---|
lazy.nvim or Vundle trying to clone Git repos |
Running a mutable plugin manager inside an immutable Nix Neovim build | Declare plugins via pkgs.vimPlugins.* in plugins = [ ... ]. |
| LSP binary not found in editor | Relying on host $PATH instead of injecting binaries |
Use extraPackages = with pkgs; [ nil gopls ... ] in Home Manager Neovim. |
| History or cache permission errors | Attempting to link mutable cache files (e.g. .bash_history) to /nix/store |
Keep history paths dynamic in user $HOME or $XDG_STATE_HOME. Only config is read-only. |
| Treesitter compilation failure | Neovim trying to run local gcc to compile C grammars |
Use nvim-treesitter.withAllGrammars from pkgs.vimPlugins. |
Checkpoint
- Shells (Bash, Zsh, or Fish) are managed via
programs.*with declarative aliases and options. - Starship prompt is integrated uniformly across all configured shells.
- Multiplexers (Tmux/Zellij) are declared with plugins pulled from
pkgs. - Editor (Neovim/Helix/VS Code) bundles language servers directly without polluting system-wide packages.
- Rebuilding your flake on a completely clean machine yields an identical interactive terminal environment.