Files
nixfiles/lyrathorpe/home/shell.nix
T
Emma Thorpe ffedf769a0 fix(shell.nix): let gh own its config.yml so auth login works
home-manager renders ~/.config/gh/config.yml as a read-only /nix/store
symlink whenever programs.gh is enabled (unconditionally, not gated on
settings). gh rewrites that file on 'gh auth login' and 'gh config set',
which then fail with a permission error.

Suppress the managed config.yml via xdg.configFile and drop the
settings.git_protocol declaration that created it; gh now owns the file.
The token lives in hosts.yml, which home-manager never manages. Set the
SSH protocol at runtime with 'gh config set git_protocol ssh'.
2026-06-16 11:21:51 +01:00

418 lines
16 KiB
Nix

# 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 <archive>` 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 <term>` 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 <fragment>`.
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
'';
}