Separate user identity (data) from the reusable modules, and let a host declare any number of users instead of exactly one. - users/registry.nix: per-user identity (name, email, groups, authorized and signing keys) as the single source of identity; no user data is hardcoded in the modules. - mkHost takes a `users` set keyed by username; per-user identity is injected into each home config via the `identity` module arg (extraSpecialArgs is per-host, so it cannot carry per-user data). - modules/users.nix builds accounts from the registry; modules/ssh.nix no longer defines authorized keys (the registry owns them); home/git.nix and home/desktop.nix read `identity`; users/emmathorpe/work.nix drops its now-redundant git identity override. - Restructure the tree: users/, home/, modules/, hosts/, lib/ replace the former lyrathorpe/ and system/ layout. - Add standalone homeConfigurations (the portable subset: shell, git, editor, claude) and an exported homeModules output for use on machines not managed by this flake, or as an input to other flakes. Behaviour-preserving for existing hosts: lyrathorpe-mbp and emmathorpe-edaas evaluate to identical derivations; lyrathorpe-t400, lyrathorpe-macpro31 and lyrathorpe-rpi5 differ only by de-duplicating a repeated authorized_keys entry. Fixes the SSH authorized-key leak (one user's key was applied to every account), the hardcoded default git identity, and the hardcoded EDaaS linger setting.
421 lines
16 KiB
Nix
421 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 ../lib/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;
|
|
};
|
|
"dockerpi.inf.cbg.emmaisvery.gay" = {
|
|
User = "emmathorpe";
|
|
};
|
|
};
|
|
};
|
|
|
|
# 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
|
|
'';
|
|
}
|