aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/dot_config/zsh/dot_zshrc
blob: 27daa2c8e33413545bf48a620f5b34b6505eef54 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# Interactive zsh configuration.

# ── Terminal ──────────────────────────────────────────────────────────────────
stty -ixon   # disable XON/XOFF flow control (frees Ctrl-S/Ctrl-Q)
ttyctl -f    # freeze terminal state; programs can't leave it broken

# ── Options ───────────────────────────────────────────────────────────────────
# Note: appendhistory, nomatch, notify are zsh defaults — not set here.
setopt autocd              # cd by typing directory name
setopt extendedglob        # extended glob patterns (#, ~, ^)
setopt interactivecomments # allow # comments in interactive shell
setopt rmstarsilent        # don't confirm rm *
setopt prompt_subst        # expand variables/functions in prompt
setopt auto_pushd          # cd pushes old dir onto stack (cd -<TAB> to browse)
setopt pushd_ignore_dups   # don't push duplicate dirs onto stack
unsetopt beep              # no terminal bell

# ── History ───────────────────────────────────────────────────────────────────
HISTFILE="$XDG_STATE_HOME/zsh/history"
HISTSIZE=50000
SAVEHIST=50000
setopt extended_history       # save timestamp and duration per entry
setopt share_history          # share history across concurrent sessions
setopt hist_ignore_all_dups   # remove older duplicate when adding new entry
setopt hist_find_no_dups      # skip duplicates when searching history
setopt hist_reduce_blanks     # trim superfluous whitespace from entries
setopt hist_ignore_space      # commands starting with space are not saved

# ── Emacs keybindings ─────────────────────────────────────────────────────────
bindkey -e

# ── Prompt ────────────────────────────────────────────────────────────────────
autoload -Uz colors && colors
# git-prompt.sh: distro-managed on Arch, nix-store-managed on the VM
for _f in \
	/usr/share/git/completion/git-prompt.sh \
	$HOME/.nix-profile/share/git/contrib/completion/git-prompt.sh; do
	[[ -r $_f ]] && { source "$_f"; break; }
done
unset _f
PROMPT='%B%{$fg[green]%}%n%{$reset_color%}@%{$fg[cyan]%}%m%{$reset_color%}:%b%{$fg[yellow]%}%~%{$reset_color%}$(__git_ps1 " (%s)")%(?..[%{$fg[red]%}%?%{$reset_color%}]) %(!.#.>) '

# ── Completion ────────────────────────────────────────────────────────────────
fpath=($XDG_DATA_HOME/zsh/completion $fpath)
# Pick up completions shipped by nix-installed packages on the remote-dev VM
# (Ubuntu's system zsh doesn't add the nix-profile share dirs to fpath).
for _d in "$HOME/.nix-profile/share/zsh/site-functions" \
          "$HOME/.nix-profile/share/zsh/vendor-completions"; do
	[[ -d $_d ]] && fpath=($_d $fpath)
done
unset _d
autoload -Uz compinit && compinit -d "$XDG_CACHE_HOME/zsh/zcompdump"

zstyle ':completion:*' menu select                       # arrow-key driven menu for ambiguous completions
zstyle ':completion:*' completer _expand_alias _complete _ignored _match _approximate
#                                │              │         │        │      └ fuzzy match (typo tolerance)
#                                │              │         │        └ try pattern matching
#                                │              │         └ include normally hidden completions
#                                │              └ standard completion
#                                └ expand aliases before completing
zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}  # colorize file completions like ls
zstyle ':completion:*' use-cache on                            # cache completions (speeds up pip, dpkg, etc.)
zstyle ':completion:*' cache-path "$XDG_CACHE_HOME/zsh"
zstyle ':completion:*' rehash true                             # rebuild PATH hash on every completion (catches paru, cargo, pip, manual installs)
zstyle ':completion:*:match:*' original only                   # only show original when pattern-matching
zstyle ':completion:*:functions' ignored-patterns '_*'         # hide internal completion functions
zstyle ':completion:*:*:kill:*' menu yes select                # interactive menu for kill completion
zstyle ':completion:*:kill:*' force-list always                # always show process list for kill
zstyle ':completion:*:cd:*' ignore-parents parent pwd          # cd never completes . or ..
zstyle ':completion::complete:*' gain-privileges 1             # use sudo for privileged completions
zstyle -e ':completion:*:approximate:*' \
	max-errors 'reply=($((($#PREFIX+$#SUFFIX)/3))numeric)'    # allow 1 typo per 3 chars typed

_comp_options+=(globdots)  # include hidden files in completion

# ── Terminal key setup ─────────────────────────────────────────────────────────
# Application mode ensures terminfo values are valid during line editing.
# Without this, some terminals send wrong sequences for special keys.
if (( ${+terminfo[smkx]} && ${+terminfo[rmkx]} )); then
	autoload -Uz add-zle-hook-widget
	# shellcheck disable=all  # zsh brace-group-as-function-body isn't bash syntax
	function zle_application_mode_start { echoti smkx }
	function zle_application_mode_stop  { echoti rmkx }
	add-zle-hook-widget -Uz zle-line-init zle_application_mode_start
	add-zle-hook-widget -Uz zle-line-finish zle_application_mode_stop
fi

# Up/Down stored for history-substring-search bindings (set after plugin source)
typeset -g -A key
key[Up]="${terminfo[kcuu1]}"
key[Down]="${terminfo[kcud1]}"

# ── Custom keybindings ────────────────────────────────────────────────────────
bindkey \^U backward-kill-line

# Word navigation (Ctrl-Right also accepts autosuggestion word-by-word — fish-like)
bindkey '^[[1;5C' forward-word        # Ctrl-Right
bindkey '^[[1;5D' backward-word       # Ctrl-Left
bindkey '^[[1;3C' forward-word        # Alt-Right
bindkey '^[[1;3D' backward-word       # Alt-Left
bindkey '^H'      backward-kill-word  # Ctrl-Backspace
bindkey '^[[3;5~' kill-word           # Ctrl-Delete

# Ctrl-Z: toggle foreground/background (no need to type 'fg')
toggle-fg-bg() {
	if (( ${#jobstates} )); then
		zle .push-input
		BUFFER="fg"
		zle .accept-line
	else
		zle .push-input
		zle .clear-screen
	fi
}
zle -N toggle-fg-bg
bindkey '^Z' toggle-fg-bg

# Ctrl-D exits even on non-empty line
exit_zsh() { exit }
zle -N exit_zsh
bindkey '^D' exit_zsh

# Ctrl-X Ctrl-E: edit command in $EDITOR
autoload -Uz edit-command-line
zle -N edit-command-line
bindkey "^X^E" edit-command-line

# Ctrl-Y: copy current command line to clipboard (OSC 52 — terminal-native)
copy-line-to-clipboard() { printf '\033]52;c;%s\a' "$(echo -n "$BUFFER" | base64)" }
zle -N copy-line-to-clipboard
bindkey '^Y' copy-line-to-clipboard

# ── Word style ────────────────────────────────────────────────────────────────
# Ctrl-W/Alt-B/Alt-F use shell quoting rules for word boundaries
autoload -Uz select-word-style
select-word-style shell

# ── Smart dot expansion ───────────────────────────────────────────────────────
# Typing .. automatically expands: ... → ../.. , .... → ../../.. , etc.
rationalise-dot() {
	if [[ $LBUFFER = *.. ]]; then
		LBUFFER+=/..
	else
		LBUFFER+=.
	fi
}
zle -N rationalise-dot
bindkey . rationalise-dot

# ── Window title ──────────────────────────────────────────────────────────────
autoload -Uz add-zsh-hook

xterm_title_precmd()  { print -Pn -- '\e]2;%~\a' }
xterm_title_preexec() { print -Pn -- '\e]2;%~ %# ' && print -n -- "${(q)1}\a" }

if [[ "$TERM" == (xterm-ghostty|st*|screen*|xterm*|rxvt*|tmux*|putty*|konsole*|gnome*) ]]; then
	add-zsh-hook -Uz precmd xterm_title_precmd
	add-zsh-hook -Uz preexec xterm_title_preexec
fi

# ── Zellij tab naming (dir:cmd, no position prefix) ──────────────────────────
# Zellij's default "Tab #N" name is baked in at tab creation (the N is the
# immutable creation index, not the live position) and never updates when
# tabs are closed or moved — so the default is actively misleading. We rename
# to `dir:cmd` from the shell hooks; position is implied by visual order in
# the tab bar. Untouched tabs after a resurrect or after closing a middle tab
# will still show their previous dir:cmd until the next prompt fires there,
# but at least there's no wrong *number* attached.
if [[ -n "$ZELLIJ" ]]; then
	_zellij_dir() { [[ "$PWD" == "$HOME" ]] && echo '~' || echo "${PWD##*/}"; }
	_zellij_tab_precmd()  { zellij action rename-tab "$(_zellij_dir)" 2>/dev/null; }
	_zellij_tab_preexec() { zellij action rename-tab "$(_zellij_dir):${1%% *}" 2>/dev/null; }
	add-zsh-hook precmd _zellij_tab_precmd
	add-zsh-hook preexec _zellij_tab_preexec
fi

# ── Recent directories ────────────────────────────────────────────────────────
autoload -Uz chpwd_recent_dirs cdr
add-zsh-hook chpwd chpwd_recent_dirs
[[ -d ${XDG_STATE_HOME}/zsh ]] || mkdir -p "${XDG_STATE_HOME}/zsh"
zstyle ':chpwd:*' recent-dirs-file "$XDG_STATE_HOME/zsh/chpwd-recent-dirs"
zstyle ':completion:*:*:cdr:*:*' menu selection

# ── OSC 7 — report CWD to terminal (zellij uses this for new pane/tab CWD) ──
_osc7_chpwd() {
  printf '\e]7;file://%s%s\e\\' "${HOST}" "${PWD}"
}
add-zsh-hook chpwd _osc7_chpwd
_osc7_chpwd

# ── Help system ───────────────────────────────────────────────────────────────
autoload -Uz run-help run-help-git run-help-ip
(( $+aliases[run-help] )) && unalias run-help
alias help=run-help

# ── Bracketed paste ───────────────────────────────────────────────────────────
autoload -Uz bracketed-paste-magic
zle -N bracketed-paste bracketed-paste-magic

# ── Aliases ───────────────────────────────────────────────────────────────────
# Files
alias l='lsd -l'
alias la='lsd -lA'
alias lt='lsd --tree'
alias mkdir='mkdir -p'
alias du='du -h'
alias df='df -h'
alias free='free -h'

# Grep / diff with color
alias grep='grep --color=auto'
alias fgrep='grep -F --color=auto'
alias egrep='grep -E --color=auto'
alias diff='diff --color=auto'
alias dmesg='dmesg --color=auto'
alias dm='dmesg --color=always | less -r'

# Networking
alias ip="ip -color=auto"
alias lsip="ip -human -color=auto --brief address show"
alias ipa="ip -stats -details -human -color=auto address show"
alias ipecho='curl ipecho.net/plain'
alias ss='sudo ss -tupnl'

# Privilege escalation
alias gimme='sudo chown $USER:$(id -gn $USER)'
alias pacdiff='sudo pacdiff'

# Pacman
alias pacopt='comm -13 <(pacman -Qqdt | sort) <(pacman -Qqdtt | sort)'

# Git
alias g='git'

# Systemd
alias sys='systemctl'
alias ssys='sudo systemctl'
alias sysu='systemctl --user'

# Navigation
alias c='clear'

# Yazi: cd-on-exit wrapper
y() {
	local tmp="$(mktemp -t "yazi-cwd.XXXXXX")"
	command yazi "$@" --cwd-file="$tmp"
	IFS= read -r -d '' cwd < "$tmp"
	[[ "$cwd" != "$PWD" ]] && [[ -d "$cwd" ]] && builtin cd -- "$cwd"
	rm -f -- "$tmp"
}

# Tools
alias curl='curlie'
alias cpr='rsync --archive -hh --partial --info=stats1,progress2 --modify-window=1'
alias mvr='rsync --archive -hh --partial --info=stats1,progress2 --modify-window=1 --remove-source-files'
alias sub='subliminal download -l en'

# wl-copy that also passes stdin through to stdout (tee-like).
# Use `| wlc` to copy AND see the output.
wlc() { tee >(wl-copy "$@"); }

# Copy the *last* command's output to the clipboard, retroactively.
# Works only inside zellij — uses `zellij action dump-screen --full` and
# locates prompt boundaries by matching the prompt prefix `user@host:`.
# Bound to Alt+Shift+Y as a zle widget.
copy-last-output() {
	emulate -L zsh
	if [[ -z $ZELLIJ ]]; then
		zle -M "copy-last-output: not inside a zellij session"
		return 1
	fi
	local dump
	dump=$(zellij action dump-screen --full 2>/dev/null) || {
		zle -M "copy-last-output: dump-screen failed"
		return 1
	}
	local prompt_re='^[[:alnum:]_-]+@[[:alnum:]_-]+:'
	local -a lines
	lines=("${(@f)dump}")
	local -a prompt_idx
	local i
	for (( i = 1; i <= ${#lines}; i++ )); do
		[[ ${lines[i]} =~ $prompt_re ]] && prompt_idx+=($i)
	done
	if (( ${#prompt_idx} < 2 )); then
		zle -M "copy-last-output: need at least 2 prompts in scrollback"
		return 1
	fi
	local start=$(( ${prompt_idx[-2]} + 1 ))
	local end=$(( ${prompt_idx[-1]} - 1 ))
	if (( start > end )); then
		zle -M "copy-last-output: previous command produced no output"
		return 0
	fi
	print -r -- "${(F)lines[start,end]}" | wl-copy
	zle -M "copied $((end - start + 1)) line(s) of last command output"
}
zle -N copy-last-output
bindkey '^[Y' copy-last-output  # Alt+Shift+Y

# Neovim
alias n='nvim'
alias ndiff='nvim -d'
alias nd='nvim -d'
alias nview='nvim -R'
alias nv='nvim -R'
alias ng='nvim +Neogit'

# Zellij: smart attach — 0 sessions: create, 1: attach, many: welcome picker
za() {
	if [[ -n $ZELLIJ ]]; then
		echo "Already inside zellij" >&2
		return 1
	fi
	local -a sessions=("${(@f)$(zellij list-sessions -ns 2>/dev/null)}")
	sessions=(${sessions:#})
	case ${#sessions} in
		0) zellij ;;
		1) zellij attach "${sessions[1]}" ;;
		*) zellij -l welcome ;;
	esac
}

# Re-import session env from the running sway process. Useful inside a
# stale zellij pane whose server was started in a different session
# (e.g. attached over SSH, then reattached locally, or vice versa).
reload-env() {
	local pid
	pid=$(pgrep -u "$UID" -x sway | head -1) || {
		echo "reload-env: no sway process found for $USER" >&2
		return 1
	}
	local kv
	while IFS= read -r -d '' kv; do
		case $kv in
			WAYLAND_DISPLAY=*|SWAYSOCK=*|DISPLAY=*|\
			DBUS_SESSION_BUS_ADDRESS=*|XDG_RUNTIME_DIR=*|\
			XDG_CURRENT_DESKTOP=*|XDG_SESSION_TYPE=*|\
			SSH_AUTH_SOCK=*)
				export "$kv"
				;;
		esac
	done < "/proc/$pid/environ"
}

# Just
alias j='just'

# LLVM / Clang tooling
alias ncmake='cmake -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_FLAGS="$DEV_CFLAGS" -DCMAKE_CXX_FLAGS="$DEV_CFLAGS" -DCMAKE_INSTALL_PREFIX=build/install -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build'
alias ircc='clang -S -emit-llvm -fno-discard-value-names -O0 -Xclang -disable-O0-optnone -o -'
alias irfc='flang -S -emit-llvm -O0 -o -'
alias astcc='clang -Xclang -ast-dump -fsyntax-only'
alias astfc='flang -fc1 -fdebug-dump-parse-tree'
alias symfc='flang -fc1 -fdebug-dump-symbols'
alias gdbr='gdb -ex start --args'

# GitHub Copilot CLI
alias copilot='gh copilot --autopilot --enable-all-github-mcp-tools --yolo --resume'

# ── Alias completions ─────────────────────────────────────────────────────────
# Guard each compdef on the target's completion function actually being
# loaded; otherwise zsh prints `compdef: unknown command or service: foo`
# on every login (the binary being present isn't enough — the zsh
# completion file `_foo` must be in fpath and registered by compinit).
# Notably triggers on the remote-dev VM where nix-installed packages
# ship completions into /home/.../share/zsh/site-functions but Ubuntu's
# system zsh doesn't add that path to fpath by default.
# Usage: _dot_compdef <target-cmd> <alias>=<target> [<alias>=<target>...]
_dot_compdef() { (( $+_comps[$1] )) && compdef "${@:2}" }
_dot_compdef git       g=git
_dot_compdef just      j=just
_dot_compdef nvim      n=nvim ndiff=nvim nd=nvim nview=nvim nv=nvim
_dot_compdef systemctl sys=systemctl ssys=systemctl sysu=systemctl
_dot_compdef lsd       l=lsd la=lsd lt=lsd
unfunction _dot_compdef

# ── GPG agent ─────────────────────────────────────────────────────────────────
# Set GPG_TTY to this shell's actual TTY (not the login console) and tell
# the agent so pinentry prompts appear in the right terminal
export GPG_TTY=$TTY
gpg-connect-agent updatestartuptty /bye &>/dev/null

# ── direnv (per-project env via .envrc; nix-direnv loaded from direnvrc) ─────
command -v direnv >/dev/null && eval "$(direnv hook zsh)"

# ── Zoxide (smart directory jumping) ──────────────────────────────────────────
# z foo → jump to frecency-ranked dir matching "foo"
# zi    → interactive picker with fzf
command -v zoxide >/dev/null && eval "$(zoxide init zsh)"

# ── FZF ───────────────────────────────────────────────────────────────────────
command -v fzf >/dev/null && source <(fzf --zsh)

# Ctrl-X Ctrl-R: search history with fzf and immediately execute
fzf-history-widget-accept() {
	fzf-history-widget
	zle accept-line
}
zle -N fzf-history-widget-accept
bindkey '^X^R' fzf-history-widget-accept

_fzf_compgen_path() { fd --hidden --follow --exclude ".git" . "$1" }
_fzf_compgen_dir()  { fd --type d --hidden --follow --exclude ".git" . "$1" }

# ── Plugins (must be sourced last) ────────────────────────────────────────────
# Plugin locations: Arch system path first, ~/.nix-profile fallback for VM HM.
_source_first() {
	local f
	for f in "$@"; do
		[[ -r $f ]] && { source "$f"; return 0; }
	done
	return 1
}

# Highlight config must be set BEFORE sourcing the plugin
ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets pattern)
typeset -A ZSH_HIGHLIGHT_STYLES
ZSH_HIGHLIGHT_STYLES[comment]='fg=yellow'
_source_first \
	/usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh \
	$HOME/.nix-profile/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

_source_first \
	/usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh \
	$HOME/.nix-profile/share/zsh-autosuggestions/zsh-autosuggestions.zsh
bindkey '^[[Z' autosuggest-accept  # Shift-Tab to accept suggestion

_source_first \
	/usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh \
	$HOME/.nix-profile/share/zsh-history-substring-search/zsh-history-substring-search.zsh
[[ -n "${key[Up]}"   ]] && bindkey -- "${key[Up]}"   history-substring-search-up
[[ -n "${key[Down]}" ]] && bindkey -- "${key[Down]}" history-substring-search-down

unfunction _source_first