From 8e57c37ac07b2b5a1cd7de73fc990ee6a335d8d6 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 11:08:49 +0100 Subject: [PATCH 01/16] 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) --- flake.lock | 21 +++++++++++++++++++++ flake.nix | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/flake.lock b/flake.lock index 581ed2b..822ec07 100644 --- a/flake.lock +++ b/flake.lock @@ -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", diff --git a/flake.nix b/flake.nix index 07ee130..d36170b 100644 --- a/flake.nix +++ b/flake.nix @@ -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 = From 52e5a0ba5c03e0209702a05afc82d7efa2bac4bb Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 11:08:49 +0100 Subject: [PATCH 02/16] 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) --- lyrathorpe/home/shell.nix | 122 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/lyrathorpe/home/shell.nix b/lyrathorpe/home/shell.nix index f9ce679..259aacf 100644 --- a/lyrathorpe/home/shell.nix +++ b/lyrathorpe/home/shell.nix @@ -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,12 +32,24 @@ "^[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 ` for any format ]; theme = "robbyrussell"; }; @@ -37,9 +60,59 @@ export PS1="%M $PS1" fi ''; - envExtra = '' - alias cls=clear - ''; + 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 `. + 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 +123,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 +175,18 @@ 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}" ''; }; } From 8001d89c58e2ded62eba96ccf69ca48157639d2a Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 11:08:49 +0100 Subject: [PATCH 03/16] 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) --- lyrathorpe/home/editor.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/lyrathorpe/home/editor.nix b/lyrathorpe/home/editor.nix index de6216b..188375d 100644 --- a/lyrathorpe/home/editor.nix +++ b/lyrathorpe/home/editor.nix @@ -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; From 327c363232e9111f6b024b6ce483670e3e3b89e1 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 11:08:49 +0100 Subject: [PATCH 04/16] 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) --- lyrathorpe/home/git.nix | 60 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/lyrathorpe/home/git.nix b/lyrathorpe/home/git.nix index 8aca636..7e557b9 100644 --- a/lyrathorpe/home/git.nix +++ b/lyrathorpe/home/git.nix @@ -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,58 @@ package = pkgs.gitFull; settings = { user.name = fullName; - push = { - autoSetupRemote = true; + 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"; + }; + + # 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 = { From 27fc7ae6d38075a9ccf1b61284f5b3da14300b37 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 11:08:49 +0100 Subject: [PATCH 05/16] 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) --- lyrathorpe/home/desktop.nix | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lyrathorpe/home/desktop.nix b/lyrathorpe/home/desktop.nix index 77e374d..8bfd10c 100644 --- a/lyrathorpe/home/desktop.nix +++ b/lyrathorpe/home/desktop.nix @@ -6,6 +6,7 @@ pkgs, config, inputs, + lib, username, ... }: @@ -95,4 +96,25 @@ }; }; }; + + # Auto-start tmux in graphical terminals (foot): opening a terminal drops + # straight into a session named "main" (attach if it exists, else create). + # Panes inside run a plain non-login zsh (tmux's default-command "${SHELL}"). + # Order 200 runs this before oh-my-zsh/compinit so the exec replaces the shell + # before that setup is wasted. Guards (each prevents a real breakage): + # interactive only -> don't hijack scp/ssh-command/non-interactive 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 -> exec failure would otherwise kill the login shell + # Lives here (graphical hosts only); the WSL work box never imports this. + programs.zsh.initContent = 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 + ''; } From 860d4ccaa9956c598f625323ec242940c5dfd215 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 11:30:09 +0100 Subject: [PATCH 06/16] 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) --- lyrathorpe/home/desktop.nix | 22 -------------- lyrathorpe/home/shell.nix | 52 +++++++++++++++++++++++++++++---- system/modules/work/default.nix | 6 +++- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/lyrathorpe/home/desktop.nix b/lyrathorpe/home/desktop.nix index 8bfd10c..77e374d 100644 --- a/lyrathorpe/home/desktop.nix +++ b/lyrathorpe/home/desktop.nix @@ -6,7 +6,6 @@ pkgs, config, inputs, - lib, username, ... }: @@ -96,25 +95,4 @@ }; }; }; - - # Auto-start tmux in graphical terminals (foot): opening a terminal drops - # straight into a session named "main" (attach if it exists, else create). - # Panes inside run a plain non-login zsh (tmux's default-command "${SHELL}"). - # Order 200 runs this before oh-my-zsh/compinit so the exec replaces the shell - # before that setup is wasted. Guards (each prevents a real breakage): - # interactive only -> don't hijack scp/ssh-command/non-interactive 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 -> exec failure would otherwise kill the login shell - # Lives here (graphical hosts only); the WSL work box never imports this. - programs.zsh.initContent = 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 - ''; } diff --git a/lyrathorpe/home/shell.nix b/lyrathorpe/home/shell.nix index 259aacf..385ede8 100644 --- a/lyrathorpe/home/shell.nix +++ b/lyrathorpe/home/shell.nix @@ -54,12 +54,34 @@ 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 - ''; + 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. @@ -189,4 +211,22 @@ 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 + ''; + }; + + # 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; } diff --git a/system/modules/work/default.nix b/system/modules/work/default.nix index 936468e..854ee9c 100644 --- a/system/modules/work/default.nix +++ b/system/modules/work/default.nix @@ -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; From 2b3725e0fb88c964666db99968b8e57db2458062 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 11:30:09 +0100 Subject: [PATCH 07/16] 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 ` (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) --- lyrathorpe/home/git.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lyrathorpe/home/git.nix b/lyrathorpe/home/git.nix index 7e557b9..2b1f819 100644 --- a/lyrathorpe/home/git.nix +++ b/lyrathorpe/home/git.nix @@ -16,6 +16,9 @@ package = pkgs.gitFull; settings = { user.name = fullName; + # 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"; @@ -48,6 +51,10 @@ 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 From 14ec441479ed20238ae5d63d8452d3aedc1feacf Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 11:36:14 +0100 Subject: [PATCH 08/16] 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) --- lyrathorpe/home/shell.nix | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lyrathorpe/home/shell.nix b/lyrathorpe/home/shell.nix index 385ede8..9236917 100644 --- a/lyrathorpe/home/shell.nix +++ b/lyrathorpe/home/shell.nix @@ -224,6 +224,15 @@ 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 From 8284a03f57821fccc8e109a8f6b95a9acbce0842 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 11:55:46 +0100 Subject: [PATCH 09/16] 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) --- lyrathorpe/home/shell.nix | 55 +++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/lyrathorpe/home/shell.nix b/lyrathorpe/home/shell.nix index 9236917..a1ede63 100644 --- a/lyrathorpe/home/shell.nix +++ b/lyrathorpe/home/shell.nix @@ -218,24 +218,51 @@ # 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; + # 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. Port 30009 + # is non-default; pin the dedicated key (identitiesOnly avoids "too many + # authentication failures" when the agent holds several keys). + "code.emmathe.dev" = { + 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 + ''; } From 11a08c8b98dbd5498fcdc674ed84a617c4f8fcae Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 13:21:59 +0100 Subject: [PATCH 10/16] 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) --- lyrathorpe/home/shell.nix | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lyrathorpe/home/shell.nix b/lyrathorpe/home/shell.nix index a1ede63..0c64ba2 100644 --- a/lyrathorpe/home/shell.nix +++ b/lyrathorpe/home/shell.nix @@ -241,10 +241,12 @@ // lib.optionalAttrs pkgs.stdenv.hostPlatform.isDarwin { UseKeychain = "yes"; }; - # Gitea remote (the flake's origin) -- required on every host. Port 30009 - # is non-default; pin the dedicated key (identitiesOnly avoids "too many - # authentication failures" when the agent holds several keys). + # 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"; From 2013bffcb1d13b9ed247a666f26440810389232b Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 13:25:25 +0100 Subject: [PATCH 11/16] docs: document the interactive shell environment Add lyrathorpe/home/README.md covering the zsh / CLI tools / tmux / git / ssh features and nice-to-haves configured across shell.nix and git.nix (history, fzf/zoxide/direnv/eza/bat, nix-index, nh, tmux plugins + auto-start, git aliases/settings/signing, ssh agent + Gitea host, the zcompdump/GC maintenance behaviours, and per-host differences). Link it from the top-level README alongside the keybindings reference. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 8 ++- lyrathorpe/home/README.md | 125 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 lyrathorpe/home/README.md diff --git a/README.md b/README.md index ba0e045..5616204 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,12 @@ sudo nixos-rebuild switch --flake .# darwin-rebuild switch --flake .#lyrathorpe-mac ``` -## Keybindings +## Shell environment & keybindings -All Sway / tmux / foot / zsh keyboard shortcuts are documented in -[`lyrathorpe/home/KEYBINDINGS.md`](./lyrathorpe/home/KEYBINDINGS.md). +- Interactive shell features (zsh, tmux, git, ssh, CLI tools, auto-tmux): + [`lyrathorpe/home/README.md`](./lyrathorpe/home/README.md). +- All Sway / tmux / foot / zsh keyboard shortcuts: + [`lyrathorpe/home/KEYBINDINGS.md`](./lyrathorpe/home/KEYBINDINGS.md). ## Login / greeter diff --git a/lyrathorpe/home/README.md b/lyrathorpe/home/README.md new file mode 100644 index 0000000..6fbbe6e --- /dev/null +++ b/lyrathorpe/home/README.md @@ -0,0 +1,125 @@ +# Interactive shell environment + +Everything the shell, terminal multiplexer, git and ssh do beyond their defaults, +and where each is defined. All of it is managed declaratively through +home-manager — edit the listed file and rebuild, never the generated dotfiles. + +Keyboard shortcuts have their own reference: [`KEYBINDINGS.md`](./KEYBINDINGS.md). + +| Area | Defined in | +| --- | --- | +| zsh, CLI tools, tmux, ssh, auto-tmux | [`shell.nix`](./shell.nix) | +| git (+ delta, commitizen) | [`git.nix`](./git.nix) | +| vim | [`editor.nix`](./editor.nix) | +| GUI apps, GTK/Firefox theming, cursor | [`desktop.nix`](./desktop.nix) (graphical hosts only) | + +Shared by every host via [`default.nix`](./default.nix); the work box also layers +[`../../system/modules/work/default.nix`](../../system/modules/work/default.nix) +on top (work email, its own ssh config, extra packages). + +--- + +## zsh + +| Feature | Notes | +| --- | --- | +| oh-my-zsh | plugins `git`, `man`, `sudo` (Esc-Esc to prepend sudo), `colored-man-pages`, `extract`; theme `robbyrussell` | +| Autosuggestion | fish-style history suggestions as you type (→ to accept) | +| Syntax highlighting | commands coloured by validity as you type | +| Completion | menu completion; the dump is rebuilt on every activation (see Maintenance) | +| History | 100k in-memory/on-disk, deduped, space-prefixed commands ignored, timestamped, **shared live across sessions** | +| History substring search | type a fragment, then ↑/↓ cycles matching past commands — works in foot, iTerm2 and the Linux TTY (both CSI and SS3 arrow encodings bound) | +| Prompt | hostname is prefixed when over SSH | + +**Aliases:** `ls`/`ll`/`la`/`lt` → `eza` (icons + git), `cls` → `clear`. git aliases live in git.nix (below). + +## CLI tools + +| Tool | What it gives you | +| --- | --- | +| `fzf` | `Ctrl-R` fuzzy history, `Ctrl-T` file picker, `Alt-C` fuzzy cd | +| `zoxide` | `z ` jumps to frecent directories | +| `direnv` + `nix-direnv` | per-project environments auto-loaded on `cd` (cached Nix dev shells) | +| `eza` | modern `ls` (drives the ls aliases) | +| `bat` | syntax-highlighting pager; behaves like `cat` when piped | +| `nix-index` | `command-not-found`: an unknown command tells you which Nix package provides it (prebuilt DB, no manual indexing) | +| `comma` (`,`) | run an uninstalled program once: `, cowsay hi` | +| `nh` | nicer `nixos-rebuild`/`home-manager` with diffs; `$NH_FLAKE` set to the repo; weekly user-GC timer (`--keep 5 --keep-since 3d`) | + +## tmux + +**Auto-start:** opening any interactive terminal — foot, iTerm2, the WSL shell, the +Linux console — drops you straight into a tmux session named `main` (attach if it +exists, else create). Panes run a plain non-login zsh. It deliberately does **not** +fire for SSH sessions, VS Code's integrated terminal, already-inside-tmux, or +non-interactive shells. + +| Setting | Value | +| --- | --- | +| Mode keys | vi | +| Mouse | on | +| Scrollback | 500000 lines | +| `escape-time` | 10ms (the 500ms default lagged vim's ESC) | +| `focus-events` | on (vim autoread) | +| `base-index` / `pane-base-index` | 1 | +| Splits | `prefix s` vertical, `prefix v` horizontal (stock `%`/`"` unbound) | +| Pane nav | `Alt`+arrows (no prefix) | +| Clipboard | `set-clipboard on`; foot `terminal-features` advertise truecolor/sync/OSC52/title/cursor | + +**Plugins:** `sensible`, `vim-tmux-navigator` (Ctrl-h/j/k/l across vim ↔ tmux), +`yank`, `catppuccin` (Mocha statusline), `resurrect` + `continuum` +(sessions auto-save and restore across reboots). + +## git + +Pager is **delta**. **commitizen** is installed on every host; `cz` defaults to +Conventional Commits. + +| Aliases | | +| --- | --- | +| `st` `co` `sw` `br` `ci` | status / checkout / switch / branch / commit | +| `last` `unstage` | last commit / unstage | +| `lg` | graph log, all branches | +| `cz` `cc` | `git cz ` (e.g. `git cz c`) and `git cc` → commitizen prompt | + +| Behaviour | | +| --- | --- | +| Pulls | rebase, with autostash + autosquash | +| Fetch | prune deleted remote branches | +| Conflicts | `zdiff3` (shows the common ancestor) | +| Diffs | histogram algorithm, colour-moved | +| `rerere` | remembers + replays conflict resolutions | +| Commit editor | full diff shown (`commit.verbose`) | +| Misc | branches sorted by date, `column.ui = auto`, `help.autocorrect = prompt`, `push.autoSetupRemote` | +| Global ignores | `result`, `result-*`, `.direnv`, `*.swp`, `.DS_Store` | +| Signing | SSH commit + tag signing (`mkDefault`, so a host without the key in its agent can disable it). Personal email `iam@emmathe.dev`; the work box overrides email + signing. | + +## ssh + +| Feature | Notes | +| --- | --- | +| ssh-agent | runs on Linux (launchd on macOS); keys added on **first use** so the passphrase is typed once per login session — this also feeds git commit signing | +| macOS | `UseKeychain` caches the passphrase in the login keychain | +| Gitea remote | `code.emmathe.dev` → `HostName 10.187.1.76` (DNS-override), `Port 30009`, user `git`, dedicated key, `identitiesOnly` | +| Defaults | the module's deprecated default block is opted out; equivalents kept under `settings."*"` | + +The **work box keeps its own `~/.ssh/config`** (home-manager's `programs.ssh` is +forced off there) but still runs the agent. + +## Maintenance behaviours + +- **zcompdump reset** — `~/.zcompdump*` is removed on every activation, so a stale + dump (pointing at `/nix/store` paths a rebuild or the weekly GC removed) can't + break completion with `_git: function definition file not found`. +- **Weekly GC** — `nh clean` runs weekly as a user timer, keeping the last 5 + generations and anything newer than 3 days. + +## Per-host differences + +| | Personal Linux (sway) | macOS | Work WSL (EDaaS) | +| --- | --- | --- | --- | +| Auto-tmux | yes (foot/TTY) | yes (iTerm2) | yes (WSL shell) | +| git email | `iam@emmathe.dev` | `iam@emmathe.dev` | `…@citrix.com` (work) | +| ssh config managed | yes | yes | no (keeps corporate config) | +| ssh-agent | yes | launchd | yes (work module) | +| GUI / theming (desktop.nix) | yes | no | no | From 26807cdb55ef0bb8755d47c090acff4bc46343e4 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 13:51:05 +0100 Subject: [PATCH 12/16] fix(nh): drop the automatic GC timer; keep nh for rebuilds The scheduled `nh clean` only reclaimed disk and risked reaping store paths the current generation still references (notably on nix-darwin). Keep `programs.nh` (nicer rebuilds + $NH_FLAKE) but remove clean.enable; GC manually (`nh clean all` / `nix-collect-garbage -d`) when nothing important is running. The resetZcompdump activation stays as a safety net for stale completion dumps across rebuilds/manual GC. Co-Authored-By: Claude Opus 4.8 (1M context) --- lyrathorpe/home/shell.nix | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lyrathorpe/home/shell.nix b/lyrathorpe/home/shell.nix index 0c64ba2..83e4396 100644 --- a/lyrathorpe/home/shell.nix +++ b/lyrathorpe/home/shell.nix @@ -126,15 +126,14 @@ programs.nix-index.enable = true; programs.nix-index-database.comma.enable = true; - # Nicer nixos-rebuild/home-manager (diffs) + a weekly user-GC timer. + # 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"; - clean = { - enable = true; - dates = "weekly"; - extraArgs = "--keep 5 --keep-since 3d"; - }; }; programs.tmux = { @@ -260,10 +259,10 @@ 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. + # 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 ''; From af8ee1609bd5a4ab10d6e00b8c21a944ec860d7d Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 14:04:45 +0100 Subject: [PATCH 13/16] fix(tmux): use tmux-256color (not tmux-direct); add NO_TMUX hatch tmux-direct as default-terminal desyncs zsh's line redraw on some terminals (iTerm2: duplicated characters on Tab, stray newlines). Switch to the standard tmux-256color and advertise truecolor per outer terminal via terminal-features (add xterm-256color:RGB alongside the foot ones). Also add a NO_TMUX escape hatch to the auto-start guard, so `NO_TMUX=1 ` opens a bare shell. Co-Authored-By: Claude Opus 4.8 (1M context) --- lyrathorpe/home/shell.nix | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lyrathorpe/home/shell.nix b/lyrathorpe/home/shell.nix index 83e4396..8d4bd9f 100644 --- a/lyrathorpe/home/shell.nix +++ b/lyrathorpe/home/shell.nix @@ -66,9 +66,11 @@ # 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 ` 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 @@ -139,7 +141,11 @@ programs.tmux = { enable = true; reverseSplit = true; - terminal = "tmux-direct"; + # 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; @@ -187,6 +193,10 @@ 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. From af3cfe4b9a29d8ebafdea268849493113b0d5ea6 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 14:08:06 +0100 Subject: [PATCH 14/16] feat(fonts): JetBrains Mono Nerd Font on every host The tmux statusline draws powerline/Nerd glyphs that default fonts lack, so they render as blank/"?". tmux runs on every host (not just the Sway ones), so install the font in the shared common-nixos module rather than swaywm -- a future console-only or non-Sway host gets it too. The Mac installs it via the Darwin config (/Library/Fonts). foot names it as its main font (home/sway.nix). On macOS, iTerm2's font is still a GUI setting: Settings -> Profiles -> Text -> Font -> "JetBrainsMono Nerd Font". Co-Authored-By: Claude Opus 4.8 (1M context) --- lyrathorpe/home/sway.nix | 3 +++ system/machine/Darwin/configuration.nix | 5 +++++ system/modules/common-nixos.nix | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/lyrathorpe/home/sway.nix b/lyrathorpe/home/sway.nix index 118a65f..b5d2623 100644 --- a/lyrathorpe/home/sway.nix +++ b/lyrathorpe/home/sway.nix @@ -237,6 +237,9 @@ in # text). Only colors-dark is needed; we never set initial-color-theme=light. settings = { main = { + # Nerd Font: monospace plus the powerline/Nerd glyphs the tmux + # statusline uses (otherwise they render as blank/"?"). + font = "JetBrainsMono Nerd Font:size=11"; # Advertise as xterm-256color so remote hosts without foot's terminfo # still behave (tmux re-adds foot's RGB/sync/etc. features -- see # shell.nix). The [main] section is the unheadered top of foot.ini. diff --git a/system/machine/Darwin/configuration.nix b/system/machine/Darwin/configuration.nix index 4f54c4e..e894e08 100644 --- a/system/machine/Darwin/configuration.nix +++ b/system/machine/Darwin/configuration.nix @@ -6,6 +6,11 @@ { programs.zsh.enable = true; + # Install the Nerd Font into /Library/Fonts so iTerm2 can use it (set it in + # iTerm2 -> Settings -> Profiles -> Text -> Font: "JetBrainsMono Nerd Font"). + # Provides the powerline/Nerd glyphs the tmux statusline draws. + fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ]; + # CLI tooling sourced from nixpkgs instead of Homebrew formulae. Pure library # dependencies are omitted; nix pulls them into closures automatically. environment.systemPackages = with pkgs; [ diff --git a/system/modules/common-nixos.nix b/system/modules/common-nixos.nix index 5c4a78d..476aa1e 100644 --- a/system/modules/common-nixos.nix +++ b/system/modules/common-nixos.nix @@ -12,4 +12,10 @@ git fastfetch ]; + + # Terminal font with powerline/Nerd glyphs. Installed on every host because + # the tmux statusline (which uses these glyphs) runs everywhere, not just on + # the Sway/graphical hosts. foot names it explicitly (home/sway.nix); the Mac + # installs it via the Darwin config. + fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ]; } From 4ca136f2b437f5dfc5d1ea090a8583bcb2c23300 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 14:20:58 +0100 Subject: [PATCH 15/16] fix(ssh): guard macOS UseKeychain with IgnoreUnknown 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) --- lyrathorpe/home/shell.nix | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lyrathorpe/home/shell.nix b/lyrathorpe/home/shell.nix index 8d4bd9f..eae0552 100644 --- a/lyrathorpe/home/shell.nix +++ b/lyrathorpe/home/shell.nix @@ -245,9 +245,13 @@ 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. + # 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 From 2bdca1c469f831b068f756ab21b704ce3c4a5938 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 14:25:18 +0100 Subject: [PATCH 16/16] docs: sync shell/keybinding docs with the rest of the branch Update the interactive-shell README and keybindings reference for changes made after the initial docs commit: no scheduled GC (manual only), NO_TMUX escape hatch, default-terminal tmux-256color + truecolor, the JetBrainsMono Nerd Font (new Fonts section + iTerm2 caveat), the UseKeychain IgnoreUnknown guard, and the vim-tmux-navigator (Ctrl-hjkl) + resurrect save/restore tmux bindings. Co-Authored-By: Claude Opus 4.8 (1M context) --- lyrathorpe/home/KEYBINDINGS.md | 5 +++++ lyrathorpe/home/README.md | 24 +++++++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lyrathorpe/home/KEYBINDINGS.md b/lyrathorpe/home/KEYBINDINGS.md index dc9a4cf..2382edc 100644 --- a/lyrathorpe/home/KEYBINDINGS.md +++ b/lyrathorpe/home/KEYBINDINGS.md @@ -133,16 +133,21 @@ Prefix is **`Ctrl`+`b`** (default). Copy mode uses **vi** keys. | --- | --- | | `Ctrl`+`b` then `v` | Split into left/right panes | | `Ctrl`+`b` then `s` | Split into top/bottom panes | +| `Ctrl`+`h`/`j`/`k`/`l` | Move between panes — and into/out of vim splits — seamlessly (vim-tmux-navigator, no prefix) | | `Alt`+`←`/`→`/`↑`/`↓` | Switch pane by direction (no prefix needed) | | `Ctrl`+`b` then `[` | Enter copy mode (then vi motions; `Space`/`Enter` to select/copy) | | `Ctrl`+`b` then `z` | Zoom / unzoom the focused pane | | `Ctrl`+`b` then `c` | New window | | `Ctrl`+`b` then `n` / `p` | Next / previous window | | `Ctrl`+`b` then `d` | Detach | +| `Ctrl`+`b` then `Ctrl`+`s` / `Ctrl`+`r` | Save / restore the session (resurrect; continuum also auto-saves and restores on start) | | Mouse | Enabled — click to focus, drag borders, scroll, select | > The stock split keys `%` and `"` are unbound; use `v` / `s` above. `Ctrl`+`b` > then `s` is therefore a split, not the session tree. +> +> Sessions persist across reboots (resurrect + continuum). Terminals auto-start +> tmux; `NO_TMUX=1 ` opens a bare shell instead. --- diff --git a/lyrathorpe/home/README.md b/lyrathorpe/home/README.md index 6fbbe6e..0307a28 100644 --- a/lyrathorpe/home/README.md +++ b/lyrathorpe/home/README.md @@ -44,7 +44,7 @@ on top (work email, its own ssh config, extra packages). | `bat` | syntax-highlighting pager; behaves like `cat` when piped | | `nix-index` | `command-not-found`: an unknown command tells you which Nix package provides it (prebuilt DB, no manual indexing) | | `comma` (`,`) | run an uninstalled program once: `, cowsay hi` | -| `nh` | nicer `nixos-rebuild`/`home-manager` with diffs; `$NH_FLAKE` set to the repo; weekly user-GC timer (`--keep 5 --keep-since 3d`) | +| `nh` | nicer `nixos-rebuild`/`home-manager` with diffs; `$NH_FLAKE` set to the repo. No scheduled GC (it could reap paths a running generation still references) — collect garbage manually with `nh clean all` / `nix-collect-garbage -d` | ## tmux @@ -52,7 +52,7 @@ on top (work email, its own ssh config, extra packages). Linux console — drops you straight into a tmux session named `main` (attach if it exists, else create). Panes run a plain non-login zsh. It deliberately does **not** fire for SSH sessions, VS Code's integrated terminal, already-inside-tmux, or -non-interactive shells. +non-interactive shells. Escape hatch: `NO_TMUX=1 ` opens a bare shell. | Setting | Value | | --- | --- | @@ -64,11 +64,21 @@ non-interactive shells. | `base-index` / `pane-base-index` | 1 | | Splits | `prefix s` vertical, `prefix v` horizontal (stock `%`/`"` unbound) | | Pane nav | `Alt`+arrows (no prefix) | +| Terminal | `default-terminal tmux-256color`; truecolor advertised per outer terminal (`foot*`, `xterm-256color`/iTerm2) via `terminal-features … RGB` | | Clipboard | `set-clipboard on`; foot `terminal-features` advertise truecolor/sync/OSC52/title/cursor | **Plugins:** `sensible`, `vim-tmux-navigator` (Ctrl-h/j/k/l across vim ↔ tmux), `yank`, `catppuccin` (Mocha statusline), `resurrect` + `continuum` -(sessions auto-save and restore across reboots). +(sessions auto-save and restore across reboots). The statusline draws Nerd-Font +glyphs — see Fonts. + +## Fonts + +**JetBrainsMono Nerd Font** is installed on every host (in `common-nixos.nix`, +because tmux runs everywhere; the Mac installs it to `/Library/Fonts` via the +Darwin config). foot uses it as its main font automatically. iTerm2's font is a +GUI setting — set it to *JetBrainsMono Nerd Font* (Settings → Profiles → Text → +Font) so the tmux statusline glyphs render instead of `?`. ## git @@ -99,7 +109,7 @@ Conventional Commits. | Feature | Notes | | --- | --- | | ssh-agent | runs on Linux (launchd on macOS); keys added on **first use** so the passphrase is typed once per login session — this also feeds git commit signing | -| macOS | `UseKeychain` caches the passphrase in the login keychain | +| macOS | `UseKeychain` caches the passphrase in the login keychain (guarded by `IgnoreUnknown`, so a non-Apple `ssh` skips it instead of erroring) | | Gitea remote | `code.emmathe.dev` → `HostName 10.187.1.76` (DNS-override), `Port 30009`, user `git`, dedicated key, `identitiesOnly` | | Defaults | the module's deprecated default block is opted out; equivalents kept under `settings."*"` | @@ -109,10 +119,10 @@ forced off there) but still runs the agent. ## Maintenance behaviours - **zcompdump reset** — `~/.zcompdump*` is removed on every activation, so a stale - dump (pointing at `/nix/store` paths a rebuild or the weekly GC removed) can't + dump (pointing at `/nix/store` paths a rebuild or a manual GC removed) can't break completion with `_git: function definition file not found`. -- **Weekly GC** — `nh clean` runs weekly as a user timer, keeping the last 5 - generations and anything newer than 3 days. +- **GC** — no scheduled timer; collect garbage deliberately (`nh clean all` / + `nix-collect-garbage -d`) when no important session is running. ## Per-host differences