# -*- mode: sh -*-
#
# History. Adapted from the prezto history module.
#
setopt BANG_HIST # Treat the '!' character specially during expansion.
setopt EXTENDED_HISTORY # Write the history file in the ':start:elapsed;command' format.
setopt SHARE_HISTORY # Share history between all sessions.
setopt HIST_EXPIRE_DUPS_FIRST # Expire a duplicate event first when trimming history.
setopt HIST_IGNORE_DUPS # Do not record an event that was just recorded again.
setopt HIST_IGNORE_ALL_DUPS # Delete an old recorded event if a new event is a duplicate.
setopt HIST_FIND_NO_DUPS # Do not display a previously found event.
setopt HIST_IGNORE_SPACE # Do not record an event starting with a space.
setopt HIST_SAVE_NO_DUPS # Do not write a duplicate event to the history file.
setopt HIST_VERIFY # Do not execute immediately upon history expansion.
setopt HIST_BEEP # Beep when accessing non-existent history.
HISTFILE="$HOME/.zhistory" # The path to the history file.
HISTSIZE=100000 # The maximum number of events to save in the internal history.
SAVEHIST=100000 # The maximum number of events to save in the history file.
#
# Completion. Adapted from the prezto completion module.
#
setopt COMPLETE_IN_WORD # Complete from both ends of a word.
setopt ALWAYS_TO_END # Move cursor to the end of a completed word.
setopt PATH_DIRS # Perform path search even on command names with slashes.
setopt AUTO_MENU # Show completion menu on a successive tab press.
setopt AUTO_LIST # Automatically list choices on ambiguous completion.
setopt AUTO_PARAM_SLASH # If completed parameter is a directory, add a trailing slash.
setopt EXTENDED_GLOB # Needed for file modification glob modifiers with compinit
unsetopt MENU_COMPLETE # Do not autoselect the first completion entry.
unsetopt FLOW_CONTROL # Disable start/stop characters in shell editor.
unsetopt AUTO_REMOVE_SLASH # Never remove trailing slash when completing.
# Load and initialize the completion system ignoring insecure directories with a
# cache time of 20 hours, so it should almost always regenerate the first time a
# shell is opened each day.
autoload -Uz compinit
_comp_path="$HOME/.zcompdump"
# #q expands globs in conditional expressions
if [[ $_comp_path(#qNmh-20) ]]; then
# -C (skip function check) implies -i (skip security check).
compinit -C -d "$_comp_path"
else
mkdir -p "$_comp_path:h"
compinit -i -d "$_comp_path"
# Keep $_comp_path younger than cache time even if it isn't regenerated.
touch "$_comp_path"
fi
unset _comp_path
# Defaults.
zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}
zstyle ':completion:*:default' list-prompt '%S%M matches%s'
# Use caching to make completion for commands such as dpkg and apt usable.
zstyle ':completion::complete:*' use-cache on
zstyle ':completion::complete:*' cache-path "$HOME/.zcompcache"
# Case-sensitive (all), partial-word, and then substring completion.
zstyle ':completion:*' matcher-list 'r:|[._-]=* r:|=*' 'l:|=* r:|=*'
setopt CASE_GLOB
# Group matches and describe.
zstyle ':completion:*:*:*:*:*' menu select
zstyle ':completion:*:matches' group 'yes'
zstyle ':completion:*:options' description 'yes'
zstyle ':completion:*:options' auto-description '%d'
zstyle ':completion:*:corrections' format ' %F{green}-- %d (errors: %e) --%f'
zstyle ':completion:*:descriptions' format ' %F{yellow}-- %d --%f'
zstyle ':completion:*:messages' format ' %F{purple} -- %d --%f'
zstyle ':completion:*:warnings' format ' %F{red}-- no matches found --%f'
zstyle ':completion:*:default' list-prompt '%S%M matches%s'
zstyle ':completion:*' format ' %F{yellow}-- %d --%f'
zstyle ':completion:*' group-name ''
zstyle ':completion:*' verbose yes
# Fuzzy match mistyped completions.
zstyle ':completion:*' completer _complete _match _approximate
zstyle ':completion:*:match:*' original only
zstyle ':completion:*:approximate:*' max-errors 1 numeric
# Increase the number of errors based on the length of the typed word. But make
# sure to cap (at 7) the max-errors to avoid hanging.
zstyle -e ':completion:*:approximate:*' max-errors 'reply=($((($#PREFIX+$#SUFFIX)/3>7?7:($#PREFIX+$#SUFFIX)/3))numeric)'
# Don't complete unavailable commands.
zstyle ':completion:*:functions' ignored-patterns '(_*|pre(cmd|exec))'
# Array completion element sorting.
zstyle ':completion:*:*:-subscript-:*' tag-order indexes parameters
# Directories
zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}
zstyle ':completion:*:*:cd:*' tag-order local-directories directory-stack path-directories
zstyle ':completion:*:*:cd:*:directory-stack' menu yes select
zstyle ':completion:*:-tilde-:*' group-order 'named-directories' 'path-directories' 'users' 'expand'
zstyle ':completion:*' squeeze-slashes true
# History
zstyle ':completion:*:history-words' stop yes
zstyle ':completion:*:history-words' remove-all-dups yes
zstyle ':completion:*:history-words' list false
zstyle ':completion:*:history-words' menu yes
# Environment Variables
zstyle ':completion::*:(-command-|export):*' fake-parameters ${${${_comps[(I)-value-*]#*,}%%,*}:#-*-}
# Populate hostname completion.
zstyle -e ':completion:*:hosts' hosts 'reply=(
${=${=${=${${(f)"$(cat {/etc/ssh/ssh_,~/.ssh/}known_hosts(|2)(N) 2> /dev/null)"}%%[#| ]*}//\]:[0-9]*/ }//,/ }//\[/ }
${=${(f)"$(cat /etc/hosts(|)(N) <<(ypcat hosts 2> /dev/null))"}%%(\#${_etc_host_ignores:+|${(j:|:)~_etc_host_ignores}})*}
${=${${${${(@M)${(f)"$(cat ~/.ssh/config 2> /dev/null)"}:#Host *}#Host }:#*\**}:#*\?*}}
)'
# Ignore multiple entries.
zstyle ':completion:*:(rm|kill|diff):*' ignore-line other
zstyle ':completion:*:rm:*' file-patterns '*:all-files'
# Kill
zstyle ':completion:*:*:*:*:processes' command 'ps -u $LOGNAME -o pid,user,command -w'
zstyle ':completion:*:*:kill:*:processes' list-colors '=(#b) #([0-9]#) ([0-9a-z-]#)*=01;36=0=01'
zstyle ':completion:*:*:kill:*' menu yes select
zstyle ':completion:*:*:kill:*' force-list always
zstyle ':completion:*:*:kill:*' insert-ids single
# Man
zstyle ':completion:*:manuals' separate-sections true
zstyle ':completion:*:manuals.(^1*)' insert-sections true
# SSH/SCP/RSYNC
zstyle ':completion:*:(ssh|scp|rsync):*' tag-order 'hosts:-host:host hosts:-domain:domain hosts:-ipaddr:ip\ address *'
zstyle ':completion:*:(scp|rsync):*' group-order users files all-files hosts-domain hosts-host hosts-ipaddr
zstyle ':completion:*:ssh:*' group-order users hosts-domain hosts-host users hosts-ipaddr
zstyle ':completion:*:(ssh|scp|rsync):*:hosts-host' ignored-patterns '*(.|:)*' loopback ip6-loopback localhost ip6-localhost broadcasthost
zstyle ':completion:*:(ssh|scp|rsync):*:hosts-domain' ignored-patterns '<->.<->.<->.<->' '^[-[:alnum:]]##(.[-[:alnum:]]##)##' '*@*'
zstyle ':completion:*:(ssh|scp|rsync):*:hosts-ipaddr' ignored-patterns '^(<->.<->.<->.<->|(|::)([[:xdigit:].]##:(#c,2))##(|%*))' '127.0.0.<->' '255.255.255.255' '::1' 'fe80::*'
#
# Terminal. Adapted from the prezto terminal module.
#
# Sets the terminal window title.
function set-window-title {
local title_format{,ted}
title_format="%s"
zformat -f title_formatted "$title_format" "s:$argv"
printf '\e]2;%s\a' "${(V%)title_formatted}"
}
# Sets the terminal tab title.
function set-tab-title {
local title_format{,ted}
title_format="%s"
zformat -f title_formatted "$title_format" "s:$argv"
printf '\e]1;%s\a' "${(V%)title_formatted}"
}
# Sets the terminal multiplexer tab title.
function set-multiplexer-title {
local title_format{,ted}
title_format="%s"
zformat -f title_formatted "$title_format" "s:$argv"
printf '\ek%s\e\\' "${(V%)title_formatted}"
}
# Sets the tab and window titles with a given command.
function _terminal-set-titles-with-command {
emulate -L zsh
setopt EXTENDED_GLOB
# Get the command name that is under job control.
if [[ "${2[(w)1]}" == (fg|%*)(\;|) ]]; then
# Get the job name, and, if missing, set it to the default %+.
local job_name="${${2[(wr)%*(\;|)]}:-%+}"
# Make a local copy for use in the subshell.
local -A jobtexts_from_parent_shell
jobtexts_from_parent_shell=(${(kv)jobtexts})
jobs "$job_name" 2> /dev/null > >(
read index discarded
# The index is already surrounded by brackets: [1].
_terminal-set-titles-with-command "${(e):-\$jobtexts_from_parent_shell$index}"
)
else
# Set the command name, or in the case of sudo, ssh or vpn, the next
# command.
local cmd="${${2[(wr)^(*=*|sudo|ssh|vpn|-*)]}:t}"
local truncated_cmd="${cmd/(#m)?(#c15,)/${MATCH[1,12]}...}"
unset MATCH
if [[ "$TERM" == screen* ]]; then
set-multiplexer-title "$truncated_cmd"
fi
set-tab-title "$truncated_cmd"
set-window-title "$cmd"
fi
}
# Sets the tab and window titles with a given path.
function _terminal-set-titles-with-path {
emulate -L zsh
setopt EXTENDED_GLOB
local absolute_path="${${1:a}:-$PWD}"
local abbreviated_path="${absolute_path/#$HOME/~}"
local truncated_path="${abbreviated_path/(#m)?(#c15,)/...${MATCH[-12,-1]}}"
unset MATCH
if [[ "$TERM" == screen* ]]; then
set-multiplexer-title "$truncated_path"
fi
set-tab-title "$truncated_path"
set-window-title "$abbreviated_path"
}
function set-terminal-title {
autoload -Uz add-zsh-hook
if [[ "$TERM_PROGRAM" == 'Apple_Terminal' ]]; then
# Sets the Terminal.app current working directory before the prompt is
# displayed.
function _terminal-set-terminal-app-proxy-icon {
printf '\e]7;%s\a' "file://${HOST}${PWD// /%20}"
}
add-zsh-hook precmd _terminal-set-terminal-app-proxy-icon
# Unsets the Terminal.app current working directory when a terminal
# multiplexer or remote connection is started since it can no longer be
# updated, and it becomes confusing when the directory displayed in the title
# bar is no longer synchronized with real current working directory.
function _terminal-unset-terminal-app-proxy-icon {
if [[ "${2[(w)1]:t}" == (screen|tmux|dvtm|ssh|mosh) ]]; then
print '\e]7;\a'
fi
}
add-zsh-hook preexec _terminal-unset-terminal-app-proxy-icon
# Do not set the tab and window titles in Terminal.app since it sets the tab
# title to the currently running process by default and the current working
# directory is set separately.
else
# Sets titles before the prompt is displayed.
add-zsh-hook precmd _terminal-set-titles-with-path
# Sets titles before command execution.
add-zsh-hook preexec _terminal-set-titles-with-command
fi
}
# Only set titles for regular terminals
case "$TERM" in
dumb|eterm*)
# Ignore unsupported terminals, e.g. TRAMP or an Emacs terminal emulator.
;;
*)
set-terminal-title
;;
esac
#
# Editor
#
# Always use Emacs keybindings (never consider $EDITOR)
bindkey -e
# Bind Shift + Tab to go to the previous menu item.
# kcbt might not be defined in all terminals
[[ -n "$terminfo[kcbt]" ]] && bindkey -M emacs "$terminfo[kcbt]" reverse-menu-complete
# Allow command line editing in an external editor.
autoload -Uz edit-command-line
zle -N edit-command-line
bindkey -M emacs "\C-X\C-E" edit-command-line
#
# Prompt
#
function load-prompt {
# This configures a simple prompt. Its visual appearance is based on the
# Ubuntu bash prompt, from ~2010.
#
# The prompt looks roughly like this:
# user@host:current-directory( git-branch)$
#
# It has the following features:
# - renders quickly
# - the user@host part is only shown when connected through SSH
# - the git branch is shown when the current directory is in a git
# repository
# - the '$' symbol becomes yellow if the previous command exited with
# non-zero status
setopt PROMPT_SUBST
# Call vcs_info before every command
autoload -Uz add-zsh-hook vcs_info colors && colors
add-zsh-hook precmd vcs_info
# Enable git support only
zstyle ':vcs_info:*' enable git
# Display git branch
zstyle ':vcs_info:*' formats ' %F{red}%b%f'
# Add user@host when connected through SSH or using toolbox
local ssh_prefix
if [[ -n "$SSH_CLIENT" || -n "$SSH_TTY" || -n "$SSH_CONNECTION" || -n "$TOOLBOX_PATH" ]]; then
ssh_prefix="%F{green}%n@%m%f:"
fi
# Current directory, with the ~ symbol indicating the home directory
local -r pwd="%F{blue}%~%f"
# Color prompt symbol based on exit status. Inspired by
# https://solovyov.net/blog/2020/useful-shell-prompt/
local -r prompt_symbol="%(?.$.%F{yellow}$%f)"
# Define prompt
PROMPT="${ssh_prefix}${pwd}\${vcs_info_msg_0_}${prompt_symbol} "
}
autoload -Uz promptinit && promptinit
case "$TERM" in
dumb)
# Ignore dumb terminal, e.g. TRAMP.
prompt off
unsetopt ZLE
;;
*)
load-prompt
;;
esac
#
# Misc
#
# Change directory without cd
setopt AUTO_CD
# Interactive comments (like bash)
setopt INTERACTIVE_COMMENTS
# Correct commands
setopt CORRECT
# Make forward-word, backward-word etc. stop at path delimiter
export WORDCHARS=${WORDCHARS/\/}
# Print message if reboot is required
[[ -f "/var/run/reboot-required" ]] && echo "reboot required"
#
# Aliases
#
function cond-alias {
# Split on = and extract 1st word (alias name) and 2nd word (alias value)
local -r name=${1[(ws:=:)1]}
local -r value=${1[(ws:=:)2]}
# Command is the first word of the alias value
local cmd=${value[(w)1]}
# If command match any of the below, the actual command is the second word
case "$cmd" in
sudo|noglob|cd|cat)
# Remove $( and <( prefixes from command
cmd=${value[(w)2]}
cmd=${cmd:s/$\(//}
cmd=${cmd:s/<\(//}
;;
esac
if whence $cmd > /dev/null; then
alias $name=$value
fi
}
cond-alias aptup="sudo apt update && sudo apt upgrade"
cond-alias curl="noglob curl"
cond-alias ec="emacsclient -nq"
cond-alias find="noglob bfs"
cond-alias git-root='cd $(git rev-parse --show-toplevel)'
cond-alias grep="grep --color=auto"
cond-alias hstat="history 0 | awk '{print \$2}' | sort | uniq -c | sort -nr | head"
cond-alias mg="mg -n"
cond-alias ta='tmux new-session -AD -s $LOGNAME'
cond-alias week="date +%V"
cond-alias reload="exec zsh"
cond-alias kp="open /Applications/KeePassXC.app"
if (( $+commands[apt-mark] )); then
# This is the most precise method I've found for answering the question
# "which packages did I install explicitly?"
#
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=727799
# https://stackoverflow.com/q/58309013/22831
alias apt-leaves='sudo grep -oP "Unpacking \K[^: ]+" /var/log/installer/syslog | sort -u | comm -13 /dev/stdin <(apt-mark showmanual | sort)'
fi
# Display ANSI art typically found .nfo files correctly
function nfoless {
iconv -f 437 -t utf-8 "$@" | ${PAGER:-less}
}
# Show restic diff for the most recent snapshot. If offset is given, show the
# diff for the nth most recent snapshot instead
function restic-review {
local -r offset="${1:-0}"
if [[ $# -gt 1 || ! "$offset" =~ ^[0-9]+$ ]]; then
echo "usage: restic-review [OFFSET]" 1>&2
return 1
fi
restic snapshots --group-by host --host $(hostname -s) | \
grep -Eo "^[a-f0-9]{8,}" | \
tail -$(( 2 + $offset )) | \
head -2 | \
xargs -r restic diff
}
# Fuzzy-finding wrapper for brew install, info and uninstall
function brew-fzf {
case "$1" in
uninstall)
cat <(brew leaves) <(brew list --cask | sed "s/^/--cask /") | fzf --multi | xargs -r brew "$1"
;;
info|install)
cat <(brew formulae) <(brew casks | sed "s/^/--cask /") | fzf --multi | xargs -r brew "$1"
;;
*)
echo "usage: brew-fzf [ info | install | uninstall ]" 1>&2
return 1
;;
esac
}
# Use colors in diff output when supported
if diff --color=auto /dev/null /dev/null 2> /dev/null; then
alias diff="diff --color=auto"
fi
# Alias ls
ls_opts="--group-directories-first --color=auto"
case "$OSTYPE" in
darwin*|freebsd*)
if (( $+commands[gls] )); then
alias ls="gls ${ls_opts}"
alias ll="gls ${ls_opts} -lh"
elif (( $+commands[gnuls] )); then
alias ls="gnuls ${ls_opts}"
alias ll="gnuls ${ls_opts} -lh"
else
alias ls="ls -G"
alias ll="ls -Glh"
fi
;;
*)
alias ls="ls ${ls_opts}"
alias ll="ls ${ls_opts} -lh"
;;
esac
unset ls_opts
# Activate or deactivate a virtualenv
function venv {
local -r activate="${1:-.venv}/bin/activate"
if [[ -n "$VIRTUAL_ENV" ]]; then
echo "venv: deactivating $VIRTUAL_ENV" 1>&2
deactivate
elif [[ -f "$activate" ]]; then
echo "venv: activating $(realpath $activate/../..)" 1>&2
source "$activate"
else
echo "venv: $activate not found" 1>&2
return 1
fi
}
# A shell variant of the locate-dominating-file function found in Emacs
function locate-dominating-file {
local -r file="$1"
local -r name="$2"
local dir="$file"
# Resolve parent if we're not given a directory directly
if [[ ! -d "$dir" ]]; then
dir="${dir:h}" # h is dirname
if [[ ! -d "$dir" ]]; then
echo "locate-dominating-file: $dir is not a directory" 1>&2
return 1
fi
fi
local cur_dir="$dir"
while true; do
cur_dir="${cur_dir:P}" # P converts to realpath
if [[ -e "$cur_dir/$name" ]]; then
echo "$cur_dir"
break
elif [[ "$cur_dir" == "/" ]]; then
echo "locate-dominating-file: $name not found in $dir or any of its parents" 1>&2
return 1
fi
cur_dir="$cur_dir/.."
done
}
# Change directory to the nearest one containing the given file or directory
#
# Example:
#
# ~/project/some/deep/path$ cdn .git # or README.md, go.mod etc.
# ~/project$
#
function cdn {
cd "$(locate-dominating-file "$PWD" "$1")"
}
# Adjust monitor brightness using ddcutil
function brightness {
local -r level="${1:-}"
if ! command -v ddcutil > /dev/null; then
echo "$0: ddcutil not found in path" 1>&2
return 1
fi
if [[ -z "$level" ]]; then
local current_level
current_level="$(ddcutil getvcp 10 | sed -E 's/.* current value = *([0-9]+),.*/\1/')"
echo "$0: current level: $current_level"
elif [[ ! "$level" =~ ^([0-9]|[1-9][0-9]+)$ || "$level" -gt 100 ]]; then
echo "$0: level must be a number between 0 and 100" 1>&2
return 1
else
echo "$0: changing level to $level"
ddcutil setvcp 10 "$level"
fi
}
#
# Local configuration
#
source "$HOME/.zshrc.local" 2> /dev/null
#
# Extensions
#
# zsh-syntax-highlighting should be initialized as late as possible because it
# wraps ZLE widgets. Paths are tried in this order: Homebrew on macOS, dpkg on
# Debian and home directory.
source "$HOMEBREW_PREFIX/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" 2> /dev/null || \
source "/usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" 2> /dev/null || \
source "$HOME/.local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" 2> /dev/null
# Set highlight colors
if [[ -n "$ZSH_HIGHLIGHT_STYLES" ]]; then
# Do not colorize comments. Default is too dark
ZSH_HIGHLIGHT_STYLES[comment]="fg=none"
fi
# Load fzf keybindings and completion. E.g. C-r searches history using fzf.
# Paths are tried in this order: Homebrew on macOS, dpkg on Debian and rpm on
# Fedora.
source "$HOMEBREW_PREFIX/opt/fzf/shell/key-bindings.zsh" 2> /dev/null || \
source "/usr/share/doc/fzf/examples/key-bindings.zsh" 2> /dev/null || \
source "/usr/share/fzf/shell/key-bindings.zsh" 2> /dev/null
source "$HOMEBREW_PREFIX/opt/fzf/shell/completion.zsh" 2> /dev/null || \
source "/usr/share/doc/fzf/examples/completion.zsh" 2> /dev/null || \
source "/usr/share/zsh/site-functions/fzf" 2> /dev/null
# Cleanup
(( $+commands[brew] )) || unfunction brew-fzf
unfunction load-prompt set-terminal-title cond-alias