Compare commits

...

10 Commits

Author SHA1 Message Date
Emma Thorpe d1548644f5 feat(ssh): pin the Gitea host to its IP, overriding DNS
Set HostName 10.187.1.76 on the code.emmathe.dev block so the Gitea
remote resolves to the fixed IP without relying on DNS (same user, port
30009 and key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:23:04 +01:00
Emma Thorpe 7b41584d5c fix(shell): migrate ssh to the settings API; reset stale zcompdump
The home-manager bump deprecated programs.ssh.addKeysToAgent /
matchBlocks / the implicit default block. Move to programs.ssh.settings
with enableDefaultConfig = false, carrying the old defaults under
settings."*" plus AddKeysToAgent, the Darwin UseKeychain, and the
code.emmathe.dev (Port 30009) host. Silences all three ssh warnings.

Also drop ~/.zcompdump on each activation: a stale dump caches /nix/store
paths to completion functions, and once a rebuild or the weekly nh 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.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:55:46 +01:00
Emma Thorpe faf2242539 feat(ssh): pin the Gitea remote in the managed ssh config
The flake's origin (ssh://git@code.emmathe.dev) must resolve on every host.
Add a matchBlock for code.emmathe.dev: user git, Port 30009 (Gitea's
non-default SSH port -- the critical bit), the dedicated
~/.ssh/code.emmathe.dev key, and identitiesOnly. The work box keeps its own
ssh config (programs.ssh forced off there) which already has the entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:38:26 +01:00
Emma Thorpe 27069e324f feat(git): personal email and commitizen aliases
Set user.email = iam@emmathe.dev on the personal hosts (mkDefault, so the
work module's address still wins on the work box). Add git aliases for
commitizen -- `git cz <sub>` (e.g. `git cz c`) and `git cc` for the commit
prompt; commitizen is already installed on every host (home.packages) and
defaults to the Conventional Commits ruleset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:30:09 +01:00
Emma Thorpe 829f209300 feat(shell): start tmux in every terminal; ssh-agent with auto-add
Move the tmux auto-start out of the graphical-only desktop layer into the
shared shell config so it also covers WSL, iTerm2 and the Linux console
(folded into programs.zsh.initContent via mkMerge alongside the SSH PS1
block). Same guards: interactive, not-already-in-tmux, not-SSH,
not-VS-Code, tmux-present.

ssh: run a user ssh-agent on Linux (macOS uses launchd) and add keys on
first use (addKeysToAgent), so the passphrase is entered once per login
session instead of per commit/push -- which also feeds commit signing.
macOS additionally caches in the login keychain (UseKeychain). The work
box keeps its own ~/.ssh/config (programs.ssh forced off there); its
ssh-agent still runs via the work module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:30:09 +01:00
Emma Thorpe 06bc420948 feat(tmux): auto-start in graphical terminals
Opening a terminal (foot) execs `tmux new-session -A -s main`, so every new
terminal lands in the multiplexer; panes run a plain non-login zsh. Guarded
to interactive, not-already-in-tmux, not-SSH, not-VS-Code, tmux-present --
preventing re-exec loops, hijacked scp/ssh shells, and lockout. Lives in the
graphical desktop layer, so the WSL work box keeps a plain shell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:49 +01:00
Emma Thorpe b806359fd6 feat(git): rebase pulls, better diffs/merges, aliases, ignores, signing
settings: pull.rebase + rebase autostash/autosquash, fetch.prune,
merge.conflictStyle=zdiff3, diff histogram + colorMoved, rerere,
commit.verbose, branch.sort, column.ui, help.autocorrect, and a small alias
set (st/co/sw/br/ci/last/unstage/lg). Global ignore file (result, .direnv,
*.swp, .DS_Store).

SSH commit/tag signing on personal hosts too, reusing the existing key
(the work module already signs on the work host). gpgsign is mkDefault so a
host lacking the key in its ssh-agent can disable it -- otherwise commits
there would fail. No personal user.email is set (unknown); signing does not
require one, but author email still falls back to user@host until set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:49 +01:00
Emma Thorpe 0c4f555dec feat(vim): add vim-tmux-navigator
Vim half of the tmux plugin so Ctrl-h/j/k/l moves seamlessly between vim
splits and tmux panes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:49 +01:00
Emma Thorpe 19dfb32cf6 feat(shell): zsh tooling, tmux plugins, nix-index, nh
zsh: history tuning (100k, dedup, share, timestamps); oh-my-zsh sudo /
colored-man-pages / extract; fzf, zoxide, direnv (+nix-direnv), eza, bat;
ls-family aliases. command-not-found via the prebuilt nix-index DB (+comma).
nh with $NH_FLAKE and a weekly user-GC timer.

tmux: escape-time 10 (was the 500ms default -> laggy vim ESC), focus-events,
base-index 1; plugins sensible / vim-tmux-navigator / yank / catppuccin
(mocha statusline) / resurrect / continuum (restore on); renumber-windows
and set-clipboard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:49 +01:00
Emma Thorpe 79b325676d chore(flake): add nix-index-database input
Prebuilt nix-index database (follows nixpkgs) so command-not-found works
immediately without a manual `nix-index` run. Consumed in shell.nix.
Lock change is purely additive; existing pins are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:49 +01:00
6 changed files with 296 additions and 17 deletions
Generated
+21
View File
@@ -150,6 +150,26 @@
"type": "github"
}
},
"nix-index-database": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1780816331,
"narHash": "sha256-0BYqs8yKWkOz2Q7+SP18N5E5gmDKSo6LSxIVIa0wWes=",
"owner": "nix-community",
"repo": "nix-index-database",
"rev": "1a2ea89c917781e88508d9fd2b507f2d2a0e173c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-index-database",
"type": "github"
}
},
"nixos-apple-silicon": {
"inputs": {
"flake-compat": "flake-compat",
@@ -231,6 +251,7 @@
"home-manager": "home-manager",
"nix-darwin": "nix-darwin",
"nix-homebrew": "nix-homebrew",
"nix-index-database": "nix-index-database",
"nixos-apple-silicon": "nixos-apple-silicon",
"nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs",
+6
View File
@@ -28,6 +28,12 @@
url = "gitlab:rycee/nur-expressions?dir=pkgs/firefox-addons";
inputs.nixpkgs.follows = "nixpkgs";
};
# Prebuilt nix-index database so "command not found -> which package
# provides it" works immediately (no manual `nix-index` run). See shell.nix.
nix-index-database = {
url = "github:nix-community/nix-index-database";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
+1
View File
@@ -10,6 +10,7 @@
vim-fugitive
vim-indent-guides
catppuccin-vim
vim-tmux-navigator # Ctrl-h/j/k/l moves between vim splits and tmux panes
];
settings = {
expandtab = false;
+62 -5
View File
@@ -1,6 +1,11 @@
# Version control: git + delta pager + commitizen. The work host layers
# commit signing and an email override on top (see work/default.nix).
{ pkgs, fullName, ... }:
{
pkgs,
lib,
fullName,
...
}:
{
home.packages = [
pkgs.commitizen
@@ -11,13 +16,65 @@
package = pkgs.gitFull;
settings = {
user.name = fullName;
push = {
autoSetupRemote = true;
# Personal identity. mkDefault so the work module overrides it on the work
# host (and to merge cleanly with that plain definition there).
user.email = lib.mkDefault "iam@emmathe.dev";
push.autoSetupRemote = true;
init.defaultBranch = "main";
# Rebase-centric pulls (matches the "always a branch, linear history"
# workflow); stash/restore and reorder fixups automatically.
pull.rebase = true;
rebase = {
autoStash = true;
autoSquash = true;
};
init = {
defaultBranch = "main";
fetch.prune = true; # drop deleted remote-tracking branches
merge.conflictStyle = "zdiff3"; # show the common ancestor in conflicts
diff = {
algorithm = "histogram";
colorMoved = "default";
};
rerere.enabled = true; # remember + replay conflict resolutions
commit.verbose = true; # full diff in the commit-message editor
branch.sort = "-committerdate"; # most-recent branches first
column.ui = "auto";
help.autocorrect = "prompt";
alias = {
st = "status";
co = "checkout";
sw = "switch";
br = "branch";
ci = "commit";
last = "log -1 HEAD";
unstage = "reset HEAD --";
lg = "log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(auto)%d%C(reset)' --all";
# commitizen (Conventional Commits, its default ruleset): `git cz c` ->
# `cz commit`, `git cz bump`, etc. `git cc` is a shortcut for the prompt.
cz = "!cz";
cc = "!cz commit";
};
# SSH commit signing on personal hosts too (the work module sets the same
# on the work host). mkDefault so a host without the key in its ssh-agent
# can override to false -- otherwise commits there would fail. Reuses the
# existing ssh key; a dedicated personal key can be swapped in later.
gpg.format = "ssh";
user.signingkey = "key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAJMVgeRKnfX1G8coU3nAobI485aeUpGTMqH7+zbKI8o emma.thorpe@cloud.com";
commit.gpgsign = lib.mkDefault true;
tag.gpgsign = lib.mkDefault true;
};
# Global ignore file (~/.config/git/ignore).
ignores = [
"result"
"result-*"
".direnv"
"*.swp"
".DS_Store"
];
};
programs.delta = {
+201 -11
View File
@@ -1,6 +1,17 @@
# Interactive shell: zsh + tmux. Wanted on every host.
{ lib, ... }:
{
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;
@@ -21,25 +32,109 @@
"^[OB"
];
};
history.append = true;
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;
# Prefix the prompt with the hostname over SSH.
initContent = lib.mkOrder 1500 ''
if [ "$SSH_CLIENT" ] || [ "$SSH_TTY" ]; then
export PS1="%M $PS1"
fi
'';
envExtra = ''
alias cls=clear
'';
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
(lib.mkOrder 200 ''
if [[ $- == *i* ]] \
&& [[ -z "$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) + a weekly user-GC timer.
programs.nh = {
enable = true;
flake = "$HOME/code/nixfiles";
clean = {
enable = true;
dates = "weekly";
extraArgs = "--keep 5 --keep-since 3d";
};
};
programs.tmux = {
@@ -50,6 +145,33 @@
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 = ''
@@ -75,6 +197,74 @@
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 is
# unknown to non-Apple openssh, so only emit it on Darwin.
// lib.optionalAttrs pkgs.stdenv.hostPlatform.isDarwin {
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 GC (the
# weekly nh clean) 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
'';
}
+5 -1
View File
@@ -1,6 +1,10 @@
{ pkgs, ... }:
{ pkgs, lib, ... }:
{
# The work box keeps its own (corporate) ~/.ssh/config; don't let the personal
# programs.ssh (shell.nix) take it over. The ssh-agent below still runs.
programs.ssh.enable = lib.mkForce false;
programs.git = {
settings = {
commit.gpgsign = true;