CI / flake (push) Skipped
CI / flake (pull_request) Successful in 7m8s
tmux-yank autodetects WSL and pipes the selection to clip.exe, which decodes its stdin as the console OEM codepage rather than UTF-8. Copying an em dash out of a pane put "ΓÇö" on the Windows clipboard; the same applies to every non-ASCII character. Override the copy command to route through tmux's own buffer. With set-clipboard on, that emits OSC 52 and the terminal receives UTF-8 directly, with no Windows-side helper in the path. Windows Terminal honours OSC 52; verified against the running client. Guarded on /proc/version so only WSL is affected. iTerm2 does not accept OSC 52 by default, so the Darwin hosts keep pbcopy. Set in the plugin's extraConfig rather than the shared block because yank.tmux bakes the copy command into its key bindings at load time, and home-manager emits plugin extraConfig before the run-shell.
488 lines
20 KiB
Nix
488 lines
20 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
|
|
|
|
# Replacements for the classic coreutils/BSD tools. Only the read-only ones
|
|
# are aliased over the original name (see shellAliases below); the rest keep
|
|
# their own name so nothing changes shape under a script's feet. The alias
|
|
# map and the flag-compatibility differences are documented in
|
|
# ../docs/shell.md, "Replacing the classics".
|
|
pkgs.dust # du: tree-shaped, size-sorted disk usage
|
|
pkgs.dysk # df: mounted filesystems (duf is unmaintained upstream)
|
|
pkgs.procs # ps: process list with tree, ports and container columns
|
|
pkgs.trash-cli # rm: XDG trash; `trash` / `trash-list` / `trash-restore`
|
|
pkgs.doggo # dig: DNS lookups
|
|
pkgs.xh # curl, for interactive HTTP poking (curl stays for scripts)
|
|
pkgs.ouch # tar/unzip/7z/zstd: one command for every archive format
|
|
pkgs.jnv # interactive jq filter builder (jq itself stays for scripts)
|
|
pkgs.hexyl # hex viewer
|
|
pkgs.fq # jq for binary formats
|
|
];
|
|
|
|
# tldr pages: worked examples for a command, next to (not instead of) man.
|
|
# enableAutoUpdates defaults on and installs a tldr-update user timer, which
|
|
# keeps the page cache fresh -- without it `tldr` fails until first `--update`.
|
|
programs.tealdeer = {
|
|
enable = true;
|
|
settings.display.compact = true;
|
|
};
|
|
|
|
# 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
|
|
# stdout is a tty -> VS Code (macOS) resolves the shell environment on
|
|
# startup by running an interactive login shell with
|
|
# stdout piped, no controlling terminal. Without this
|
|
# guard `exec tmux` runs there, fails ("open terminal
|
|
# failed: not a terminal"), exits non-zero, and VS
|
|
# Code reports "Unable to resolve your shell
|
|
# environment". A real terminal always has a tty here.
|
|
# not VS Code env -> also skip VS Code's env-resolution probe explicitly,
|
|
# in case a future version allocates a pty for it.
|
|
# $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* ]] \
|
|
&& [[ -t 1 ]] \
|
|
&& [[ -z "$VSCODE_RESOLVING_ENVIRONMENT" ]] \
|
|
&& [[ -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";
|
|
|
|
# Shadow the classics with their modern equivalents. Only read-only
|
|
# commands are shadowed: a wrong flag costs a retype, never data. The
|
|
# flag vocabularies are NOT compatible (`du -sh`, `df -h`, `ps aux` all
|
|
# fail here) -- see ../docs/shell.md, "Replacing the classics".
|
|
#
|
|
# Blast radius is bounded by where these live: shellAliases lands in
|
|
# .zshrc, so only interactive zsh sees them. Scripts, `sudo <cmd>` and
|
|
# anything exec'd by another program still get the real binary. To reach
|
|
# the original in an interactive shell: `command du` or `\du`.
|
|
cat = "bat --paging=never"; # bat is already the PAGER/MANPAGER
|
|
du = "dust";
|
|
df = "dysk";
|
|
ps = "procs";
|
|
|
|
# `rm` is deliberately NOT aliased to trash-put. Retraining `rm` to mean
|
|
# "recoverable" is a habit that follows you onto machines where it does
|
|
# not (every remote host, every root shell, every container), and trash
|
|
# semantics break down anyway on a different filesystem or on
|
|
# root-owned paths. Type `trash` when you want a trash can.
|
|
};
|
|
};
|
|
|
|
# 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
|
|
{
|
|
# On WSL, tmux-yank pipes the selection to clip.exe, which decodes its
|
|
# stdin as the OEM codepage instead of UTF-8 -- an em dash reaches the
|
|
# Windows clipboard as three characters. Route through tmux's own
|
|
# buffer instead: with set-clipboard on, tmux emits OSC 52 and the
|
|
# terminal takes the text as UTF-8. Windows Terminal honours OSC 52;
|
|
# iTerm2 does not by default, hence the runtime guard rather than
|
|
# overriding pbcopy/xsel on every host. yank.tmux bakes the command
|
|
# into its key bindings when it loads, so this must be set first, which
|
|
# is what plugin extraConfig gives us.
|
|
plugin = yank;
|
|
extraConfig = ''
|
|
if-shell 'grep -qi microsoft /proc/version 2>/dev/null' \
|
|
"set -g @override_copy_command 'tmux load-buffer -w -'"
|
|
'';
|
|
}
|
|
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;
|
|
};
|
|
};
|
|
};
|
|
|
|
# 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
|
|
'';
|
|
}
|