From a44f109dc743d8ab7f3fbec93b321e07e2d2a592 Mon Sep 17 00:00:00 2001 From: Fabian Ising Date: Mon, 27 Jul 2026 10:55:58 +0200 Subject: [PATCH] [ZSH] Extract shared startup modules Keep the server entrypoint focused on server policy while moving update checks, Antidote loading, interactive defaults, and prompt sourcing into reusable modules. --- zsh/.zshrc | 210 +++++----------------------------------- zsh/lib/antidote.zsh | 12 +++ zsh/lib/interactive.zsh | 43 ++++++++ zsh/lib/prompt.zsh | 3 + zsh/lib/updates.zsh | 61 ++++++++++++ 5 files changed, 142 insertions(+), 187 deletions(-) create mode 100644 zsh/lib/antidote.zsh create mode 100644 zsh/lib/interactive.zsh create mode 100644 zsh/lib/prompt.zsh create mode 100644 zsh/lib/updates.zsh diff --git a/zsh/.zshrc b/zsh/.zshrc index d6d8bc1..f9e560b 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -1,18 +1,16 @@ export PATH="/snap/bin:$HOME/.local/bin:$HOME/dotfiles/scripts:$PATH" -export XDG_CONFIG_HOME="$HOME/.config" # Fix nvim bug - +export XDG_CONFIG_HOME="$HOME/.config" [[ -z $SSH_AUTH_SOCK ]] || export FORWARD_SOCK=$SSH_AUTH_SOCK - [[ -f ~/.zsh/.powerline_config ]] && source ~/.zsh/.powerline_config -if [[ -f ~/.zsh/.shared_config ]] && [[ "$SUDO_USER" != "fabian" ]] ; then +if [[ -f ~/.zsh/.shared_config && "$SUDO_USER" != "fabian" ]]; then shared_config=1 else shared_config=0 fi -# Workaround for HOME directory in sudo nvim (caused by snap) +# Work around snap's HOME handling for root. if [[ "$USER" == "root" ]]; then if [[ ! -e /home/root ]]; then ln -s /root/ /home/root @@ -20,214 +18,52 @@ if [[ "$USER" == "root" ]]; then ln -s /root/.config /home/root/.config fi fi +[[ -f ~/.zsh/.shared_config ]] && touch ~/.config/nvim/.shared_config -if [[ -f ~/.zsh/.shared_config ]] ; then - touch ~/.config/nvim/.shared_config -fi +source "${ZDOTDIR:-$HOME}/.zsh/lib/updates.zsh" -# On shell start, bring ~/dotfiles up to date by fast-forwarding the CURRENT -# branch to its upstream only. Never rebase or stash in the background: if the -# branch diverged or local edits block it, warn and leave it for a manual pull. -# Per-branch, so `server` on each device fast-forwards to origin/server. -update_dotfiles() { - local repo="$HOME/dotfiles" - local cache_dir="${ZDOTDIR:-$HOME}/.cache/zsh" - local notice_file="$cache_dir/dotfiles-update-notice" - local notice_tmp="$notice_file.$$.tmp" - # Bound the network call so a stalled fetch can't wedge this backgrounded - # (&!) job into a days-long orphan holding unreaped children. `timeout` is - # absent on macOS, so only use it when present. Disable ssh multiplexing so - # no daemonized mux master can inherit and keep our fds open past git's exit. - local -a TO; (( $+commands[timeout] )) && TO=(timeout -k 5 15) - GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=5 -o ControlMaster=no -o ControlPath=none' \ - $TO git -C "$repo" fetch --quiet /dev/null || return - git -C "$repo" rev-parse --abbrev-ref '@{u}' >/dev/null 2>&1 || return # no upstream - git -C "$repo" merge-base --is-ancestor '@{u}' HEAD 2>/dev/null && return # already current - git -C "$repo" merge --ff-only --quiet '@{u}' 2>/dev/null && return - local notice="⚠ dotfiles: $(git -C "$repo" symbolic-ref --short HEAD) can't fast-forward to @{u} — run: git -C ~/dotfiles pull" - mkdir -p "$cache_dir" || return - print -r -- "$notice" >| "$notice_tmp" || return - mv -f "$notice_tmp" "$notice_file" -} - -update_dotfiles &! - -if [ "$shared_config" -eq 0 ]; then +if (( ! shared_config )); then export GIT_AUTHOR_NAME="Fabian Ising" export GIT_AUTHOR_EMAIL="f.ising@fh-muenster.de" export GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME export GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL fi -# On shell start, check the security tools repo for upstream updates and queue a -# one-line notice if the checkout is behind. A precmd hook prints it between -# commands, rather than letting this background job write across other startup -# output or a prompt that is already being edited. Notify only (never auto-pull). -# The check stays read-only so it works even in root-owned /opt/tools: -# - `safe.directory=*` sidesteps git's "dubious ownership" refusal on a repo -# owned by root, which would otherwise fail every git command. -# - `ls-remote` queries the remote without writing into .git (a normal user -# can't write a root-owned .git, so `fetch` would fail there). -# Uses the first candidate that is a git checkout; covers global and per-user layouts. -check_tools_repo() { - local repo branch remote mergeref remote_sha local_sha cmd notice - local cache_dir="${ZDOTDIR:-$HOME}/.cache/zsh" - local notice_file="$cache_dir/tools-update-notice" - local notice_tmp="$notice_file.$$.tmp" - # See update_dotfiles: bound the network probe (timeout, absent on macOS) and - # disable ssh multiplexing so a stalled ls-remote can't orphan this &! job. - local -a TO; (( $+commands[timeout] )) && TO=(timeout -k 5 10) - for repo in /opt/tools "$HOME/tools" "$HOME/tools/tools-repo"; do - local -a g=(git -c 'safe.directory=*' -C "$repo") - $g rev-parse --is-inside-work-tree >/dev/null 2>&1 || continue - branch=$($g symbolic-ref --short HEAD 2>/dev/null) || return - remote=$($g config "branch.$branch.remote" 2>/dev/null) || return # no upstream - mergeref=$($g config "branch.$branch.merge" 2>/dev/null) || return - remote_sha=$(GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=5 -o ControlMaster=no -o ControlPath=none' \ - $TO $g ls-remote "$remote" "$mergeref" /dev/null | awk '{print $1}') - local_sha=$($g rev-parse HEAD 2>/dev/null) - [[ -n "$remote_sha" && -n "$local_sha" ]] || return # remote unreachable - [[ "$remote_sha" == "$local_sha" ]] && return # already current - $g merge-base --is-ancestor "$remote_sha" HEAD 2>/dev/null && return # remote is an ancestor: ahead/equal (also true if we lack the object -> falls through to "behind") - [[ -w "$repo/.git" ]] && cmd="git -C $repo pull" || cmd="sudo git -C $repo pull" - notice="⬆ tools: updates available in $repo — run: $cmd" - mkdir -p "$cache_dir" || return - print -r -- "$notice" >| "$notice_tmp" || return - mv -f "$notice_tmp" "$notice_file" - return - done -} -check_tools_repo &! - -show_update_notices() { - local cache_dir="${ZDOTDIR:-$HOME}/.cache/zsh" - local notice_file claimed_notice - for notice_file in "$cache_dir"/{dotfiles,tools}-update-notice; do - [[ -s "$notice_file" ]] || continue - claimed_notice="$notice_file.$$.show" - mv "$notice_file" "$claimed_notice" 2>/dev/null || continue - command cat "$claimed_notice" - command rm -f "$claimed_notice" - done -} -autoload -Uz add-zsh-hook -add-zsh-hook precmd show_update_notices - -# Load Antidote -mkdir -p ${ZDOTDIR:-~}/.cache/zsh -static_file=${ZDOTDIR:-~}/.cache/zsh/.zsh_plugins.zsh -if [ $shared_config -eq 0 ]; then - plugins_txt=${ZDOTDIR:-~}/.zsh/.zsh_plugins.txt - # Vi mode +static_file=${ZDOTDIR:-$HOME}/.cache/zsh/.zsh_plugins.zsh +if (( ! shared_config )); then + plugins_txt=${ZDOTDIR:-$HOME}/.zsh/.zsh_plugins.txt bindkey -v VI_MODE_SET_CURSOR=true else - plugins_txt=${ZDOTDIR:-~}/.zsh/.zsh_plugins_shared.txt - static_file=${ZDOTDIR:-~}/.cache/zsh/.zsh_shared_plugins.zsh -fi -# clone antidote if necessary -if ! [[ -e ${ZDOTDIR:-~}/.antidote ]]; then - git clone https://github.com/mattmc3/antidote.git ${ZDOTDIR:-~}/.antidote + plugins_txt=${ZDOTDIR:-$HOME}/.zsh/.zsh_plugins_shared.txt + static_file=${ZDOTDIR:-$HOME}/.cache/zsh/.zsh_shared_plugins.zsh fi +source "${ZDOTDIR:-$HOME}/.zsh/lib/antidote.zsh" -#zstyle ':omz:plugins:docker' legacy-completion yes -zstyle ':completion:*:ssh:*' hosts off -zstyle ':completion:*:scp:*' hosts off - -# Run rehash for external commands -zstyle ":completion:*:commands" rehash 1 -# source antidote and load plugins from `${ZDOTDIR:-~}/.zsh_plugins.txt` -source ${ZDOTDIR:-~}/.antidote/antidote.zsh -antidote load ${plugins_txt} ${static_file} export SSH_REAL_SOCK=$SSH_AUTH_SOCK [[ -z $FORWARD_SOCK ]] || export SSH_AUTH_SOCK=$FORWARD_SOCK -setopt interactivecomments +source "${ZDOTDIR:-$HOME}/.zsh/lib/interactive.zsh" -# History options -HISTSIZE=100000 # Set the amount of lines you want saved -SAVEHIST=100000 # This is required to actually save them, needs to match with HISTSIZE -setopt EXTENDED_HISTORY # Write the history file in the ":start:elapsed;command" format. -setopt INC_APPEND_HISTORY # Write to the history file immediately, not when the shell exits. -setopt SHARE_HISTORY # Share history between all sessions. -setopt HIST_EXPIRE_DUPS_FIRST # Expire duplicate entries first when trimming history. -setopt HIST_IGNORE_DUPS # Don\'t record an entry that was just recorded again. -setopt HIST_IGNORE_ALL_DUPS # Delete old recorded entry if new entry is a duplicate. -setopt HIST_FIND_NO_DUPS # Do not display a line previously found. -setopt HIST_IGNORE_SPACE # Don\'t record an entry starting with a space. -setopt HIST_SAVE_NO_DUPS # Don\'t write duplicate entries in the history file. -setopt HIST_REDUCE_BLANKS # Remove superfluous blanks before recording entry. -# Clear screen by ctrl+q -bindkey '^q' clear-screen - -(( $+commands[nvim] )) && alias vim=nvim -alias sudo='sudo ' -alias cgrep="grep --color=always" -alias cdiff="git diff --color-words --no-index" -(( $+commands[nvim] )) && export EDITOR='nvim' -(( $+commands[nvim] )) && export VISUAL='nvim' - -# Allow access to all libvirt vms export LIBVIRT_DEFAULT_URI="qemu:///system" - -alias ls="ls --color=always" - -if [ "$shared_config" -eq 0 ]; then -# Use safecp/safemv from the tools-repo when available -(( $+commands[safecp] )) && alias cp=safecp -(( $+commands[safemv] )) && alias mv=safemv -(( $+commands[safescp] )) && alias scp=safescp +if (( ! shared_config )); then + (( $+commands[safecp] )) && alias cp=safecp + (( $+commands[safemv] )) && alias mv=safemv + (( $+commands[safescp] )) && alias scp=safescp fi -delzip() { - unzip -Z -1 "$@" | xargs -I{} rm -rf {} -} -# mitmproxy -export MITMPROXY_SSLKEYLOGFILE="~/.mitmproxy/sslkeylogfile.txt" - [[ -f ~/.zsh/.mac_config ]] && source ~/.zsh/.mac_config -# Workaround for async issues https://github.com/romkatv/powerlevel10k/issues/1554 -unset ZSH_AUTOSUGGEST_USE_ASYNC - -# Powerlevel 10k -# Remove padding on right side -ZLE_RPROMPT_INDENT=0 - -# To customize prompt, run `p10k configure` or edit ~/dotfiles/zsh/.p10k.zsh. -function load_p10k() { - if zmodload zsh/terminfo && (( terminfo[colors] >= 256 )) && [ $shared_config -eq 0 ]; then - [[ ! -f ~/dotfiles/zsh/.p10k.zsh ]] || source ~/dotfiles/zsh/.p10k.zsh - else - [[ ! -f ~/dotfiles/zsh/.p10k_shared.zsh ]] || source ~/dotfiles/zsh/.p10k_shared.zsh - fi -} -load_p10k -# -# This speeds up pasting w/ autosuggest -# https://github.com/zsh-users/zsh-autosuggestions/issues/238 -pasteinit() { - OLD_SELF_INSERT=${${(s.:.)widgets[self-insert]}[2,3]} - zle -N self-insert url-quote-magic # I wonder if you'd need `.url-quote-magic`? -} - -pastefinish() { - zle -N self-insert $OLD_SELF_INSERT -} -zstyle :bracketed-paste-magic paste-init pasteinit -zstyle :bracketed-paste-magic paste-finish pastefinish -# https://github.com/zsh-users/zsh-autosuggestions/issues/351 -ZSH_AUTOSUGGEST_CLEAR_WIDGETS+=(bracketed-paste) - -export LANG="en_US.UTF-8" -export LC_CTYPE="en_US.UTF-8" -export TIME_STYLE="long-iso" +if zmodload zsh/terminfo && (( terminfo[colors] >= 256 )) && (( ! shared_config )); then + p10k_config=~/dotfiles/zsh/.p10k.zsh +else + p10k_config=~/dotfiles/zsh/.p10k_shared.zsh +fi +source "${ZDOTDIR:-$HOME}/.zsh/lib/prompt.zsh" [[ -f ~/.zsh/.user_config ]] && source ~/.zsh/.user_config [[ -f ~/.zsh/.virtual_env_config.zsh ]] && source ~/.zsh/.virtual_env_config.zsh [[ -f ~/.zsh/.local_config ]] && source ~/.zsh/.local_config [[ -f ~/.zsh/.os_config.zsh ]] && source ~/.zsh/.os_config.zsh -# Remove duplicate entries added by inherited and machine-specific PATH config -# while preserving the first occurrence and therefore command precedence. typeset -U path PATH diff --git a/zsh/lib/antidote.zsh b/zsh/lib/antidote.zsh new file mode 100644 index 0000000..085e8aa --- /dev/null +++ b/zsh/lib/antidote.zsh @@ -0,0 +1,12 @@ +# Callers set plugins_txt and static_file before sourcing this module. +mkdir -p "${static_file:h}" +if [[ ! -e ${ZDOTDIR:-$HOME}/.antidote ]]; then + git clone https://github.com/mattmc3/antidote.git "${ZDOTDIR:-$HOME}/.antidote" +fi + +zstyle ':completion:*:ssh:*' hosts off +zstyle ':completion:*:scp:*' hosts off +zstyle ':completion:*:commands' rehash 1 + +source "${ZDOTDIR:-$HOME}/.antidote/antidote.zsh" +antidote load "$plugins_txt" "$static_file" diff --git a/zsh/lib/interactive.zsh b/zsh/lib/interactive.zsh new file mode 100644 index 0000000..3702bf5 --- /dev/null +++ b/zsh/lib/interactive.zsh @@ -0,0 +1,43 @@ +setopt interactivecomments + +HISTSIZE=100000 +SAVEHIST=100000 +setopt EXTENDED_HISTORY +setopt INC_APPEND_HISTORY +setopt SHARE_HISTORY +setopt HIST_EXPIRE_DUPS_FIRST +setopt HIST_IGNORE_DUPS +setopt HIST_IGNORE_ALL_DUPS +setopt HIST_FIND_NO_DUPS +setopt HIST_IGNORE_SPACE +setopt HIST_SAVE_NO_DUPS +setopt HIST_REDUCE_BLANKS + +bindkey '^q' clear-screen +(( $+commands[nvim] )) && alias vim=nvim +(( $+commands[nvim] )) && export EDITOR=nvim VISUAL=nvim +alias sudo='sudo ' +alias ls='ls --color=always' +alias cgrep='grep --color=always' +alias cdiff='git diff --color-words --no-index' + +delzip() { + unzip -Z -1 "$@" | xargs -I{} rm -rf {} +} +export MITMPROXY_SSLKEYLOGFILE='~/.mitmproxy/sslkeylogfile.txt' + +unset ZSH_AUTOSUGGEST_USE_ASYNC +export LANG='en_US.UTF-8' +export LC_CTYPE='en_US.UTF-8' +export TIME_STYLE='long-iso' + +pasteinit() { + OLD_SELF_INSERT=${${(s.:.)widgets[self-insert]}[2,3]} + zle -N self-insert url-quote-magic +} +pastefinish() { + zle -N self-insert "$OLD_SELF_INSERT" +} +zstyle :bracketed-paste-magic paste-init pasteinit +zstyle :bracketed-paste-magic paste-finish pastefinish +ZSH_AUTOSUGGEST_CLEAR_WIDGETS+=(bracketed-paste) diff --git a/zsh/lib/prompt.zsh b/zsh/lib/prompt.zsh new file mode 100644 index 0000000..2963795 --- /dev/null +++ b/zsh/lib/prompt.zsh @@ -0,0 +1,3 @@ +# Callers select p10k_config before sourcing this module. +ZLE_RPROMPT_INDENT=0 +[[ -z "$p10k_config" || ! -f "$p10k_config" ]] || source "$p10k_config" diff --git a/zsh/lib/updates.zsh b/zsh/lib/updates.zsh new file mode 100644 index 0000000..4021a4f --- /dev/null +++ b/zsh/lib/updates.zsh @@ -0,0 +1,61 @@ +# Background repository maintenance shared by desktop and server shells. + +update_dotfiles() { + local repo="$HOME/dotfiles" + local cache_dir="${ZDOTDIR:-$HOME}/.cache/zsh" + local notice_file="$cache_dir/dotfiles-update-notice" + local notice_tmp="$notice_file.$$.tmp" + local -a TO; (( $+commands[timeout] )) && TO=(timeout -k 5 15) + GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=5 -o ControlMaster=no -o ControlPath=none' \ + $TO git -C "$repo" fetch --quiet /dev/null || return + git -C "$repo" rev-parse --abbrev-ref '@{u}' >/dev/null 2>&1 || return + git -C "$repo" merge-base --is-ancestor '@{u}' HEAD 2>/dev/null && return + git -C "$repo" merge --ff-only --quiet '@{u}' 2>/dev/null && return + local notice="⚠ dotfiles: $(git -C "$repo" symbolic-ref --short HEAD) can't fast-forward to @{u} — run: git -C ~/dotfiles pull" + mkdir -p "$cache_dir" || return + print -r -- "$notice" >| "$notice_tmp" || return + mv -f "$notice_tmp" "$notice_file" +} +update_dotfiles &! + +check_tools_repo() { + local repo branch remote mergeref remote_sha local_sha cmd notice + local cache_dir="${ZDOTDIR:-$HOME}/.cache/zsh" + local notice_file="$cache_dir/tools-update-notice" + local notice_tmp="$notice_file.$$.tmp" + local -a TO; (( $+commands[timeout] )) && TO=(timeout -k 5 10) + for repo in /opt/tools "$HOME/tools" "$HOME/tools/tools-repo"; do + local -a g=(git -c 'safe.directory=*' -C "$repo") + $g rev-parse --is-inside-work-tree >/dev/null 2>&1 || continue + branch=$($g symbolic-ref --short HEAD 2>/dev/null) || return + remote=$($g config "branch.$branch.remote" 2>/dev/null) || return + mergeref=$($g config "branch.$branch.merge" 2>/dev/null) || return + remote_sha=$(GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=5 -o ControlMaster=no -o ControlPath=none' \ + $TO $g ls-remote "$remote" "$mergeref" /dev/null | awk '{print $1}') + local_sha=$($g rev-parse HEAD 2>/dev/null) + [[ -n "$remote_sha" && -n "$local_sha" ]] || return + [[ "$remote_sha" == "$local_sha" ]] && return + $g merge-base --is-ancestor "$remote_sha" HEAD 2>/dev/null && return + [[ -w "$repo/.git" ]] && cmd="git -C $repo pull" || cmd="sudo git -C $repo pull" + notice="⬆ tools: updates available in $repo — run: $cmd" + mkdir -p "$cache_dir" || return + print -r -- "$notice" >| "$notice_tmp" || return + mv -f "$notice_tmp" "$notice_file" + return + done +} +check_tools_repo &! + +show_update_notices() { + local cache_dir="${ZDOTDIR:-$HOME}/.cache/zsh" + local notice_file claimed_notice + for notice_file in "$cache_dir"/{dotfiles,tools}-update-notice; do + [[ -s "$notice_file" ]] || continue + claimed_notice="$notice_file.$$.show" + mv "$notice_file" "$claimed_notice" 2>/dev/null || continue + command cat "$claimed_notice" + command rm -f "$claimed_notice" + done +} +autoload -Uz add-zsh-hook +add-zsh-hook precmd show_update_notices