Compare commits

...

8 Commits

Author SHA1 Message Date
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 267 additions and 17 deletions
Generated
+21
View File
@@ -150,6 +150,26 @@
"type": "github" "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": { "nixos-apple-silicon": {
"inputs": { "inputs": {
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
@@ -231,6 +251,7 @@
"home-manager": "home-manager", "home-manager": "home-manager",
"nix-darwin": "nix-darwin", "nix-darwin": "nix-darwin",
"nix-homebrew": "nix-homebrew", "nix-homebrew": "nix-homebrew",
"nix-index-database": "nix-index-database",
"nixos-apple-silicon": "nixos-apple-silicon", "nixos-apple-silicon": "nixos-apple-silicon",
"nixos-wsl": "nixos-wsl", "nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
+6
View File
@@ -28,6 +28,12 @@
url = "gitlab:rycee/nur-expressions?dir=pkgs/firefox-addons"; url = "gitlab:rycee/nur-expressions?dir=pkgs/firefox-addons";
inputs.nixpkgs.follows = "nixpkgs"; 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 = outputs =
+1
View File
@@ -10,6 +10,7 @@
vim-fugitive vim-fugitive
vim-indent-guides vim-indent-guides
catppuccin-vim catppuccin-vim
vim-tmux-navigator # Ctrl-h/j/k/l moves between vim splits and tmux panes
]; ];
settings = { settings = {
expandtab = false; expandtab = false;
+62 -5
View File
@@ -1,6 +1,11 @@
# Version control: git + delta pager + commitizen. The work host layers # Version control: git + delta pager + commitizen. The work host layers
# commit signing and an email override on top (see work/default.nix). # commit signing and an email override on top (see work/default.nix).
{ pkgs, fullName, ... }: {
pkgs,
lib,
fullName,
...
}:
{ {
home.packages = [ home.packages = [
pkgs.commitizen pkgs.commitizen
@@ -11,13 +16,65 @@
package = pkgs.gitFull; package = pkgs.gitFull;
settings = { settings = {
user.name = fullName; user.name = fullName;
push = { # Personal identity. mkDefault so the work module overrides it on the work
autoSetupRemote = true; # 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 = { programs.delta = {
+172 -11
View File
@@ -1,6 +1,17 @@
# Interactive shell: zsh + tmux. Wanted on every host. # 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 = { programs.zsh = {
enable = true; enable = true;
enableCompletion = true; enableCompletion = true;
@@ -21,25 +32,109 @@
"^[OB" "^[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 = { oh-my-zsh = {
enable = true; enable = true;
plugins = [ plugins = [
"git" "git"
"man" "man"
"sudo" # double-Esc prefixes the last command with sudo
"colored-man-pages"
"extract" # `extract <archive>` for any format
]; ];
theme = "robbyrussell"; theme = "robbyrussell";
}; };
syntaxHighlighting.enable = true; syntaxHighlighting.enable = true;
# Prefix the prompt with the hostname over SSH. initContent = lib.mkMerge [
initContent = lib.mkOrder 1500 '' # Auto-start tmux in every interactive terminal -- foot, iTerm2, the WSL
if [ "$SSH_CLIENT" ] || [ "$SSH_TTY" ]; then # shell, the Linux console -- so a new terminal lands straight in the
export PS1="%M $PS1" # multiplexer (session "main": attach if present, else create). Panes run
fi # 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
envExtra = '' # that setup is wasted. Guards, each preventing a real breakage:
alias cls=clear # 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 = { programs.tmux = {
@@ -50,6 +145,33 @@
keyMode = "vi"; keyMode = "vi";
historyLimit = 500000; historyLimit = 500000;
mouse = true; 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 # `reverseSplit = true` already binds s -> vertical and v -> horizontal
# split (the dotfiles' vim-style splits). # split (the dotfiles' vim-style splits).
extraConfig = '' extraConfig = ''
@@ -75,6 +197,45 @@
set -as terminal-features ",foot*:title" set -as terminal-features ",foot*:title"
set -as terminal-features ",foot*:ccolour" set -as terminal-features ",foot*:ccolour"
set -as terminal-features ",foot*:cstyle" 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;
addKeysToAgent = "yes";
# macOS: also cache in the login keychain (no prompt after first unlock).
# UseKeychain is unknown to non-Apple openssh, so only emit it on Darwin.
extraConfig = lib.optionalString pkgs.stdenv.hostPlatform.isDarwin ''
UseKeychain yes
'';
# Gitea remote (the flake's origin) -- required on every host. Pins the
# dedicated key so the right identity is offered. identitiesOnly avoids
# "too many authentication failures" when the agent holds several keys.
matchBlocks."code.emmathe.dev" = {
user = "git";
port = 30009; # Gitea listens on a non-default SSH port
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;
} }
+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 = { programs.git = {
settings = { settings = {
commit.gpgsign = true; commit.gpgsign = true;