4ca136f2b4
nixpkgs' openssh lacks Apple's keychain patch, so `UseKeychain yes` is rejected as "Bad configuration option" when that ssh is on PATH. Prefix it with `IgnoreUnknown UseKeychain` (the module emits IgnoreUnknown first) so a non-Apple ssh skips it while Apple's ssh still honours it. Still Darwin-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
284 lines
11 KiB
Nix
284 lines
11 KiB
Nix
# Interactive shell: zsh + tmux. Wanted on every host.
|
|
{
|
|
lib,
|
|
pkgs,
|
|
inputs,
|
|
...
|
|
}:
|
|
{
|
|
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
|
|
];
|
|
|
|
programs.zsh = {
|
|
enable = true;
|
|
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 = {
|
|
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;
|
|
};
|
|
|
|
# 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).
|
|
programs.bat.enable = true;
|
|
|
|
# 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";
|
|
};
|
|
|
|
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
|
|
{
|
|
# 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/default.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;
|
|
|
|
# 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.
|
|
home.activation.resetZcompdump = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
|
$DRY_RUN_CMD rm -f "$HOME"/.zcompdump* "''${XDG_CACHE_HOME:-$HOME/.cache}"/zsh/.zcompdump* 2>/dev/null || true
|
|
'';
|
|
}
|