# Interactive shell: zsh + tmux. Wanted on every host. { config, lib, pkgs, inputs, ... }: let # Shared Catppuccin Mocha palette: raw 6-hex strings, no leading "#". ctp = import ../catppuccin-mocha.nix; in { imports = [ # Prebuilt nix-index database -> working command-not-found # ("cmd not found -> which nix package provides it"), no manual indexing. inputs.nix-index-database.homeModules.default ]; # CLI staples wanted on every host (search, parse, monitor). ripgrep/fd also # back fzf and editor integrations; tea is the Gitea CLI for code.emmathe.dev. home.packages = [ pkgs.ripgrep pkgs.fd pkgs.jq pkgs.tea pkgs.hyperfine # command-line benchmarking pkgs.sd # saner find-and-replace than sed ]; # Resource monitor, themed Catppuccin Mocha to match the rest of the desktop. # btop does not bundle the theme, so vendor it from catppuccin/btop (pinned). programs.btop = { enable = true; settings.color_theme = "catppuccin_mocha"; }; xdg.configFile."btop/themes/catppuccin_mocha.theme".source = pkgs.fetchurl { url = "https://raw.githubusercontent.com/catppuccin/btop/f437574b600f1c6d932627050b15ff5153b58fa3/themes/catppuccin_mocha.theme"; hash = "sha256-THRpq5vaKCwf9gaso3ycC4TNDLZtBB5Ofh/tOXkfRkQ="; }; programs.zsh = { enable = true; # Keep zsh dotfiles under XDG (~/.config/zsh) rather than the legacy $HOME # layout, matching xdg.enable. history.path is pinned below so the existing # ~/.zsh_history is reused, not orphaned by the dotDir move. dotDir = "${config.xdg.configHome}/zsh"; enableCompletion = true; enableVteIntegration = true; autosuggestion.enable = true; # Bind Up/Down for history-substring-search in BOTH cursor-key modes: CSI # (^[[A/^[[B -- normal mode, and what the Linux TTY sends) and SS3 # (^[OA/^[OB -- application mode, used by foot, tmux and iTerm2). Binding # only the default CSI form leaves it dead at the prompt in foot/iTerm2. historySubstringSearch = { enable = true; searchUpKey = [ "^[[A" "^[OA" ]; searchDownKey = [ "^[[B" "^[OB" ]; }; history = { # Stay at the legacy ~/.zsh_history (default would follow dotDir into # ~/.config/zsh and orphan the existing file). Keeps history intact. path = "${config.home.homeDirectory}/.zsh_history"; append = true; # append, don't overwrite, on shell exit size = 100000; # in-memory (HISTSIZE) save = 100000; # on-disk (SAVEHIST) ignoreDups = true; # drop consecutive duplicates ignoreSpace = true; # leading-space commands stay out of history expireDuplicatesFirst = true; share = true; # live-share history across sessions extended = true; # record timestamps }; oh-my-zsh = { enable = true; plugins = [ "git" "man" "sudo" # double-Esc prefixes the last command with sudo "colored-man-pages" "extract" # `extract ` for any format ]; theme = "robbyrussell"; }; syntaxHighlighting.enable = true; initContent = lib.mkMerge [ # Auto-start tmux in every interactive terminal -- foot, iTerm2, the WSL # shell, the Linux console -- so a new terminal lands straight in the # multiplexer (session "main": attach if present, else create). Panes run # a plain non-login zsh (tmux's default-command "${SHELL}"). Order 200 # runs before oh-my-zsh/compinit so the exec replaces the shell before # that setup is wasted. Guards, each preventing a real breakage: # interactive only -> don't hijack scp / `ssh host cmd` / scripted shells # $TMUX empty -> a pane's zsh won't re-exec tmux (infinite loop) # not SSH -> don't force inbound SSH logins into a server tmux # not VS Code -> its integrated terminal manages itself # tmux on PATH -> a failed exec would otherwise kill the login shell # $NO_TMUX unset -> escape hatch: `NO_TMUX=1 ` opens a bare shell (lib.mkOrder 200 '' if [[ $- == *i* ]] \ && [[ -z "$TMUX" ]] \ && [[ -z "$NO_TMUX" ]] \ && [[ -z "$SSH_CONNECTION" && -z "$SSH_TTY" ]] \ && [[ "$TERM_PROGRAM" != "vscode" ]] \ && command -v tmux >/dev/null 2>&1; then exec tmux new-session -A -s main fi '') # Prefix the prompt with the hostname over SSH (mkAfter). (lib.mkOrder 1500 '' if [ "$SSH_CLIENT" ] || [ "$SSH_TTY" ]; then export PS1="%M $PS1" fi '') ]; shellAliases = { # eza's zsh integration also defines these; set explicitly so the # icons/git intent is obvious. ls = "eza --icons --git"; ll = "eza --icons --git -l"; la = "eza --icons --git -la"; lt = "eza --icons --git --tree"; cls = "clear"; }; }; # Fuzzy finder: Ctrl-R fuzzy history, Ctrl-T files, Alt-C cd. programs.fzf = { enable = true; enableZshIntegration = true; # Catppuccin Mocha colours (rendered into FZF_DEFAULT_OPTS --color). Each # value needs a leading "#"; the palette stores raw hex. colors = { "bg" = "#${ctp.base}"; "bg+" = "#${ctp.surface1}"; # current line / selected row "fg" = "#${ctp.text}"; "fg+" = "#${ctp.text}"; "hl" = "#${ctp.blue}"; # match highlights "hl+" = "#${ctp.blue}"; "header" = "#${ctp.red}"; "info" = "#${ctp.mauve}"; "marker" = "#${ctp.green}"; "pointer" = "#${ctp.pink}"; "prompt" = "#${ctp.mauve}"; "spinner" = "#${ctp.pink}"; "border" = "#${ctp.surface1}"; }; }; # Frecency directory jumping: `z `. programs.zoxide = { enable = true; enableZshIntegration = true; }; # Per-project environments auto-loaded on cd, with the Nix dev-shell cache. programs.direnv = { enable = true; nix-direnv.enable = true; }; # Modern ls (drives the ls aliases above). programs.eza = { enable = true; git = true; icons = "auto"; # boolean form is deprecated }; # Syntax-highlighting pager, used as `bat` (acts like cat when piped). bat # ships no Catppuccin theme, so vendor the upstream tmTheme from catppuccin/bat # (delta in git.nix reuses it as its syntax-theme). programs.bat = { enable = true; config.theme = "Catppuccin Mocha"; themes."Catppuccin Mocha" = { src = pkgs.fetchFromGitHub { owner = "catppuccin"; repo = "bat"; rev = "6810349b28055dce54076712fc05fc68da4b8ec0"; sha256 = "1y5sfi7jfr97z1g6vm2mzbsw59j1jizwlmbadvmx842m0i5ak5ll"; }; file = "themes/Catppuccin Mocha.tmTheme"; }; }; # command-not-found backed by the prebuilt nix-index DB (module imported # above). `comma` runs an uninstalled program once: `, cowsay hi`. programs.nix-index.enable = true; programs.nix-index-database.comma.enable = true; # Nicer nixos-rebuild/home-manager (diffs) + $NH_FLAKE. No automatic clean: # the scheduled GC's only benefit is reclaiming disk, but it can reap store # paths the current generation still references (notably on nix-darwin, where # it broke completion by removing an in-use oh-my-zsh). GC manually instead: # `nh clean all` / `nix-collect-garbage -d` when nothing important is running. programs.nh = { enable = true; flake = "$HOME/code/nixfiles"; }; # GitHub CLI. `programs.gh.settings` is deliberately unset: home-manager renders # ~/.config/gh/config.yml as a read-only /nix/store symlink whenever the module # is enabled, but gh must rewrite that file on `gh auth login` and `gh config # set`, which then fail with a permission error. Suppress the managed config.yml # (below) and let gh own it. The token lives in hosts.yml, which is never # Nix-managed. Set the SSH protocol once at runtime: `gh config set git_protocol # ssh` (it can't be declarative here without recreating the immutable file). programs.gh.enable = true; xdg.configFile."gh/config.yml".enable = lib.mkForce false; programs.tmux = { enable = true; reverseSplit = true; # tmux-256color (not tmux-direct): the standard inside-tmux terminfo. # tmux-direct's capabilities desync zsh's line redraw on some terminals # (e.g. iTerm2 -> duplicated chars on Tab, stray newlines). Truecolor is # advertised per outer terminal via the RGB terminal-features below. terminal = "tmux-256color"; newSession = true; keyMode = "vi"; historyLimit = 500000; mouse = true; escapeTime = 10; # was the 500ms default -> laggy ESC in vim focusEvents = true; # let vim see focus changes (autoread) baseIndex = 1; # sets both base-index and pane-base-index plugins = with pkgs.tmuxPlugins; [ sensible vim-tmux-navigator # Ctrl-h/j/k/l across vim splits and tmux panes yank extrakto # prefix+Tab: fzf-grab paths/URLs/text from the pane into the prompt { # Catppuccin Mocha statusline (v2 API: flavour + window options must be # set before the plugin loads, which home-manager does for plugin # extraConfig; the status modules below go in the main extraConfig, # which HM appends after all plugins). plugin = catppuccin; extraConfig = '' set -g @catppuccin_flavor 'mocha' set -g @catppuccin_window_status_style 'rounded' ''; } resurrect # save/restore sessions { plugin = continuum; # auto-save + restore on tmux start (after resurrect) extraConfig = '' set -g @continuum-restore 'on' ''; } ]; # `reverseSplit = true` already binds s -> vertical and v -> horizontal # split (the dotfiles' vim-style splits). extraConfig = '' # Run a non-login shell in new panes/windows. set -g default-command "''${SHELL}" # Drop the stock split keys in favour of the s/v binds above. unbind % unbind '"' # Alt+Arrow pane navigation bind -n M-Left select-pane -L bind -n M-Right select-pane -R bind -n M-Up select-pane -U bind -n M-Down select-pane -D # Truecolor for the outer terminals (foot reports xterm-ish too; iTerm2 is # xterm-256color). Without this, with tmux-256color as default-terminal, # 24-bit colour would be quantised to 256. set -as terminal-features ",xterm-256color:RGB" # Tell tmux which capabilities the foot terminal supports, so truecolor, # synchronised output, the system clipboard (OSC 52), window titles and # cursor styling all pass through. set -as terminal-features ",foot*:RGB" set -as terminal-features ",foot*:sync" set -as terminal-features ",foot*:clipboard" set -as terminal-features ",foot*:title" set -as terminal-features ",foot*:ccolour" set -as terminal-features ",foot*:cstyle" # No home-manager options for these. set -g renumber-windows on set -g set-clipboard on # Catppuccin v2 statusline. Must run after the plugin has loaded; # home-manager appends this extraConfig after the whole plugin list. set -g status-left-length 100 set -g status-right-length 100 set -g status-left "" set -g status-right "#{E:@catppuccin_status_application}" set -ag status-right "#{E:@catppuccin_status_session}" ''; }; # Add the key to the agent on first use, so the passphrase is typed once per # login session rather than per commit/push (commit signing uses this agent). # The work box keeps its own ssh config (see work.nix), so this only # manages ~/.ssh/config on the personal hosts. programs.ssh = { enable = true; # The module's built-in default "*" block is being deprecated; opt out and # carry the defaults we want ourselves under settings."*". enableDefaultConfig = false; settings = { # Global defaults (rendered last, as ssh_config wants). AddKeysToAgent # adds the key on first use so the passphrase is typed once per session. "*" = { AddKeysToAgent = "yes"; ForwardAgent = false; Compression = false; ServerAliveInterval = 0; ServerAliveCountMax = 3; HashKnownHosts = false; UserKnownHostsFile = "~/.ssh/known_hosts"; ControlMaster = "no"; ControlPath = "~/.ssh/master-%r@%n:%p"; ControlPersist = "no"; } # macOS: also cache the passphrase in the login keychain. UseKeychain # exists only in Apple's ssh; nixpkgs' openssh (which may be the `ssh` on # PATH) rejects it as "Bad configuration option". IgnoreUnknown (emitted # first by the module) makes any non-Apple ssh skip it instead of erroring, # while Apple's ssh still honours it. Darwin-only. // lib.optionalAttrs pkgs.stdenv.hostPlatform.isDarwin { IgnoreUnknown = "UseKeychain"; UseKeychain = "yes"; }; # Gitea remote (the flake's origin) -- required on every host. HostName # pins the IP so it resolves without DNS. Port 30009 is non-default; pin # the dedicated key (identitiesOnly avoids "too many authentication # failures" when the agent holds several keys). "code.emmathe.dev" = { HostName = "10.187.1.76"; User = "git"; Port = 30009; IdentityFile = "~/.ssh/code.emmathe.dev"; IdentitiesOnly = true; }; }; }; # Run a user ssh-agent on Linux (macOS provides one via launchd). EDaaS also # enables this in the work module; both being true merges cleanly. services.ssh-agent.enable = lib.mkIf pkgs.stdenv.hostPlatform.isLinux true; # Classic process viewer (complements btop). htop has no custom-theme support # -- only a handful of built-in color schemes -- so it can't be hex-themed like # btop/bat/fzf. color_scheme = 0 (Default) draws from the terminal's ANSI # palette, which is Catppuccin Mocha (foot/iTerm2), so it matches by deferring # to the terminal rather than vendoring a theme. programs.htop = { enable = true; settings = { color_scheme = 0; # Default -> uses the terminal's Catppuccin palette delay = 15; # refresh every 1.5s cpu_count_from_one = 1; show_cpu_frequency = 1; show_cpu_usage = 1; # per-core usage shown in the CPU bars highlight_base_name = 1; # highlight the program name within the path highlight_megabytes = 1; highlight_threads = 1; hide_kernel_threads = 1; show_program_path = 0; # show just the command, not the full path tree_view = 1; # start in process-tree mode tree_view_always_by_pid = 0; account_guest_in_cpu_meter = 0; fields = with config.lib.htop.fields; [ PID USER PRIORITY NICE M_SIZE M_RESIDENT M_SHARE STATE PERCENT_CPU PERCENT_MEM TIME COMM ]; } // ( with config.lib.htop; leftMeters [ (bar "AllCPUs2") (bar "Memory") (bar "Swap") ] ) // ( with config.lib.htop; rightMeters [ (text "Tasks") (text "LoadAverage") (text "Uptime") ] ); }; # Drop the zsh completion dump on every activation. A stale .zcompdump caches # /nix/store paths to completion functions; once a rebuild or a manual GC # removes them, compinit fails with "_git: function definition file not found" # for every completion. Deleting it forces a fresh rebuild from the current # fpath on the next shell. compinit dumps to $ZDOTDIR (~/.config/zsh now); the # $HOME and cache paths are also swept to clear any legacy leftovers. home.activation.resetZcompdump = lib.hm.dag.entryAfter [ "writeBoundary" ] '' $DRY_RUN_CMD rm -f \ "${config.xdg.configHome}"/zsh/.zcompdump* \ "$HOME"/.zcompdump* \ "''${XDG_CACHE_HOME:-$HOME/.cache}"/zsh/.zcompdump* 2>/dev/null || true ''; }