Shell and Editor Configuration

Updated

October 31, 2026

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.

Note

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.