diff options
| author | srdusr <trevorgray@srdusr.com> | 2025-09-24 00:49:52 +0200 |
|---|---|---|
| committer | srdusr <trevorgray@srdusr.com> | 2025-09-24 00:49:52 +0200 |
| commit | a70909b2057bf8d5923241d53e8ef3daef328458 (patch) | |
| tree | 4e215383912f7d035d61cc10ec06e86fc04deacc /common | |
| parent | 280f7799be30cba8fa893b489c49ac511cefe229 (diff) | |
| download | dotfiles-a70909b2057bf8d5923241d53e8ef3daef328458.tar.gz dotfiles-a70909b2057bf8d5923241d53e8ef3daef328458.zip | |
Zsh config changes
Diffstat (limited to 'common')
| -rw-r--r-- | common/config/zsh/.zshenv | 346 | ||||
| -rw-r--r-- | common/config/zsh/.zshrc | 73 | ||||
| -rw-r--r-- | common/config/zsh/user/aliases.zsh | 208 | ||||
| -rw-r--r-- | common/config/zsh/user/bindings.zsh | 175 | ||||
| -rw-r--r-- | common/config/zsh/user/completion.zsh | 172 | ||||
| -rw-r--r-- | common/config/zsh/user/functions.zsh | 1607 | ||||
| -rw-r--r-- | common/config/zsh/user/options.zsh | 66 | ||||
| -rw-r--r-- | common/config/zsh/user/prompt.zsh | 679 | ||||
| -rw-r--r-- | common/config/zsh/user/prompt_minimal.zsh | 295 | ||||
| -rw-r--r-- | common/config/zsh/user/prompt_new.zsh | 863 | ||||
| -rw-r--r-- | common/config/zsh/user/prompt_simple.zsh | 227 |
11 files changed, 4711 insertions, 0 deletions
diff --git a/common/config/zsh/.zshenv b/common/config/zsh/.zshenv new file mode 100644 index 0000000..cdab5b7 --- /dev/null +++ b/common/config/zsh/.zshenv @@ -0,0 +1,346 @@ +# Load local/system wide binaries and scripts +export PATH=$HOME/.bin:$HOME/.local/bin:$HOME/.scripts:/usr/local/bin:/sbin:/usr/sbin:$PATH +export PATH="/data/data/com.termux/files/usr/local/bin:$PATH" + +# List of directories to ignore (relative to ~/.scripts) +EXCLUDE_DIRS=("assets" "test") + +# Add .scripts to path +if [ -d "$HOME/.scripts" ]; then + while IFS= read -r -d '' dir; do + # Extract relative path + rel_path="${dir#$HOME/.scripts/}" + + # Check if the directory is in the exclude list + skip=false + for exclude in "${EXCLUDE_DIRS[@]}"; do + if [[ "$rel_path" == "$exclude"* ]]; then + skip=true + break + fi + done + + # Add to PATH if not excluded + if [ "$skip" = false ]; then + PATH="$dir:$PATH" + fi + done < <(find "$HOME/.scripts" -type d -print0) +fi + +# Global TERM color +export TERM=xterm-256color + + +# Conditionally set default term +available_terms=("wezterm" "kitty" "alacritty" "xterm") +for term in "${available_terms[@]}"; do + if command -v "$term" &> /dev/null; then + export TERMINAL="$term" + break + fi +done + +# Default Programs: +export EDITOR=$(command -v nvim || echo "vim") +export TEXEDIT="$EDITOR" +export FCEDIT="$EDITOR" +export VISUAL="$EDITOR" +export GIT_EDITOR="$EDITOR" +export COLORTERM="truecolor" +export TERM="xterm-256color" +export READER="zathura" +export BROWSER="firefox" +export OPENER="xdg-open" +if command -v nvim &> /dev/null; then + export MANPAGER="sh -c 'col -b | nvim -c \"set ft=man ts=8 nomod nolist nonu noma\" -c \"autocmd VimEnter * call feedkeys(\\\"\\<CR>q\\\")\" -'" +else + export MANPAGER="bat" +fi +export MANROFFOPT="-c" +export PAGER="less" +export FAQ_STYLE='github' +export VIDEO="mpv" +export IMAGE="phototonic" + +# XDG Paths: +export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} +export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share} +export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache} +export INPUTRC="${XDG_CONFIG_HOME:-$HOME/.config}/inputrc" + +export ZDOTDIR="$XDG_CONFIG_HOME/zsh" +export HISTFILE="$ZDOTDIR/.zhistory" # History filepath +export HISTSIZE=1000000 # Maximum events for internal history +export SAVEHIST=1000000 # Maximum events in history file +export BANG_HIST # Treat the '!' character specially during expansion. +export EXTENDED_HISTORY # Write the history file in the ":start:elapsed;command" format. +export INC_APPEND_HISTORY # Write to the history file immediately, not when the shell exits. +export SHARE_HISTORY # Share history between all sessions. +export HIST_EXPIRE_DUPS_FIRST # Expire duplicate entries first when trimming history. +export HIST_IGNORE_DUPS # Don't record an entry that was just recorded again. +export HIST_IGNORE_ALL_DUPS # Delete old recorded entry if new entry is a duplicate. +export HIST_FIND_NO_DUPS # Do not display a line previously found. +export HIST_IGNORE_SPACE # Don't record an entry starting with a space. +export HIST_SAVE_NO_DUPS # Don't write duplicate entries in the history file. +export HIST_REDUCE_BLANKS # Remove superfluous blanks before recording entry. +export HIST_VERIFY # Don't execute immediately upon history expansion. +export HIST_BEEP # Beep when accessing nonexistent history. +export INC_APPEND_HISTORY + +# Customize `ls` colours +export LSCOLORS=ExGxBxDxCxEgEdxbxgxcxd + +# Other XDG paths: +export RIPGREP_CONFIG_PATH="$XDG_CONFIG_HOME/ripgrep/ripgreprc" +export DOCKER_CONFIG="$XDG_CONFIG_HOME/docker" +export VSCODE_PORTABLE="$XDG_DATA_HOME/vscode" +export GTK2_RC_FILES="$XDG_CONFIG_HOME/gtk-2.0/gtkrc" +export PATH="/usr/bin/cmake:$PATH" +export PATH=$PATH:/opt/google/chrome +export DISCORD_USER_DATA_DIR="$XDG_DATA_HOME" +export LYNX_CFG="$XDG_CONFIG_HOME/.lynxrc" + +# Manage Arch linux build sources +export ASPROOT="${XDG_CACHE_HOME:-$HOME/.cache}/asp" + +# Homebrew +#export PATH=/opt/homebrew/bin:$PATH +export PATH="/opt/homebrew/sbin:$PATH" + +# Nix-profile +export PATH=$HOME/.nix-profile/bin:$PATH + +# GnuPG +export GPG_TTY=$(tty) +#export GNUPGHOME="$XDG_CONFIG_HOME/gnupg" + +# Nvim +export NVIM_TUI_ENABLE_TRUE_COLOR=1 + +# Let FZF use ripgrep by default +if type rg &> /dev/null; then + export FZF_DEFAULT_COMMAND="rg --files --hidden --glob '!{node_modules/*,.git/*}'" + export FZF_DEFAULT_OPTS='-m --height 50% --border' + export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" +fi + +# Zoxide (cd alternative) +if command -v zoxide >/dev/null 2>&1; then + eval "$(zoxide init zsh)" +fi + +export XDG_MENU_PREFIX=gnome- + +# enable git scripts +export DEVELOPMENT_DIRECTORY="$HOME/code" + +# Android Home +export ANDROID_HOME=/opt/android-sdk +export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$PATH +#export PATH=$ANDROID_HOME/cmdline-tools/bin:$PATH +export PATH=$ANDROID_HOME/tools:$PATH +export PATH=$ANDROID_HOME/tools/bin:$PATH +export PATH=$ANDROID_HOME/platform-tools:$PATH +# Android emulator PATH +export PATH=$ANDROID_HOME/emulator:$PATH +# Android SDK ROOT PATH +export ANDROID_SDK_ROOT=/opt/android-sdk +export PATH=$ANDROID_SDK_ROOT:$PATH +#export ANDROID_SDK_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/android" + +# Programming Environment Variables: + +# Rust +export RUSTUP_HOME=${XDG_DATA_HOME:-$HOME/.local/share}/rustup +export CARGO_HOME=${XDG_DATA_HOME:-$HOME/.local/share}/cargo +export PATH="${CARGO_HOME}/bin:${RUSTUP_HOME}/bin:$PATH" +#export PATH="$PATH:$CARGO_HOME/bin" +#[[ -d $CARGO_HOME/bin ]] && path=($CARGO_HOME/bin $path) +if which rustc > /dev/null; then export RUST_BACKTRACE=1; fi +#export PATH="$HOME/.cargo/bin:$PATH" +#export CARGO_HOME=${XDG_DATA_HOME}/cargo +#export RUSTUP_HOME=${XDG_DATA_HOME}/rustup + + +# Dotnet +# # Currently dotnet does not support XDG ( https://github.com/dotnet/sdk/issues/10390 ) +#export DOTNET_TOOLS_DIR="$HOME/.dotnet/tools" +export DOTNET_HOME=${XDG_DATA_HOME:-$HOME/.local/share}/dotnet +export DOTNET_CLI_HOME="$XDG_CONFIG_HOME/dotnet" +#mkdir -p "$DOTNET_CLI_HOME"; +export PATH="$PATH":"$DOTNET_HOME"/tools +export DOTNET_ROOT=/opt/dotnet +# Disable telemetry for dotnet apps +export DOTNET_CLI_TELEMETRY_OPTOUT=1 + + +# Java +#export JAVA_HOME=/usr/lib/jvm/default-java +#export JAVA_HOME='/usr/lib/jvm/java-8-openjdk' +#export JAVA_HOME='/usr/lib/jvm/java-10-openjdk' +#export JAVA_HOME='/usr/lib/jvm/java-11-openjdk' +#export JAVA_HOME='/usr/lib/jvm/java-17-openjdk' +export JAVA_HOME='/usr/lib/jvm/java-20-openjdk' +#export PATH=$JAVA_HOME/bin:$PATH +export _JAVA_OPTIONS=-Djava.util.prefs.userRoot="$XDG_CONFIG_HOME"/java +#export DEFAULT_JVM_OPTS='"-Dcom.android.sdklib.toolsdir=$APP_HOME" -XX:+IgnoreUnrecognizedVMOptions' +#export _JAVA_AWT_WM_NONREPARENTING=1 +#export JAVA_OPTS='-XX:+IgnoreUnrecognizedVMOptions --add-modules java.se.ee' +#export JAVA_OPTS='-XX:+IgnoreUnrecognizedVMOptions --add-modules java.xml.bind' +#Windows: +#set JAVA_OPTS=-XX:+IgnoreUnrecognizedVMOptions --add-modules java.se.ee + + +# Dart/Flutter +export PATH="/opt/flutter/bin:/usr/lib/dart/bin:$PATH" + + +# Go +export GO_PATH=${XDG_DATA_HOME}/go +export GOPATH="${XDG_DATA_HOME:-$HOME/.local/share}/go" + + +# Javascript +# NVM +export NVM_DIR="$HOME/.config/nvm" +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" +#[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm + +# global node installs (gross) +[[ -d "$XDG_DATA_HOME/node/bin" ]] && path=($XDG_DATA_HOME/node/bin $path) +export NODE_REPL_HISTORY="$XDG_DATA_HOME"/node_repl_history +export NPM_CONFIG_USERCONFIG=$XDG_CONFIG_HOME/npm/npmrc +#export NPM_CONFIG_INIT_AUTHOR_NAME='srdusr' +#export NPM_CONFIG_INIT_AUTHOR_EMAIL='trevorgray@srdusr.com' +#export NPM_CONFIG_INIT_AUTHOR_URL='https://srdusr.com' +#export NPM_CONFIG_INIT_LICENSE='GPL-3.0' +#export NPM_CONFIG_INIT_VERSION='0.0.0' +#export NPM_CONFIG_SIGN_GIT_TAG='true' + +export BUN_INSTALL="$HOME/.bun" +export PATH="$BUN_INSTALL/bin:$PATH" + +# Register Bun completion +#fpath=("$HOME/.bun" $fpath) + +# Yarn +#if command -v yarn >/dev/null 2>&1; then +# export PATH="$PATH:`yarn global bin`" +#fi +#export PATH="$(yarn global bin):$PATH" +#YARN_PATH="$HOME/.yarn/bin" +#YARN_BIN_EXPORT="$HOME/.config/yarn/global/node_modules/.bin" + +# Ruby +export GEM_PATH="$XDG_DATA_HOME/ruby/gems" +export GEM_SPEC_CACHE="$XDG_DATA_HOME/ruby/specs" +export GEM_HOME="$XDG_DATA_HOME/ruby/gems" +#if [[ -d ~/.gem/ruby ]]; then +# ver=$(find ~/.gem/ruby/* -maxdepth 0 | sort -rV | head -n 1) +# export PATH="$PATH:${ver}/bin" +#fi + + +# Python +# lazy load pyenv +#export PYENV_ROOT=${PYENV_ROOT:-$HOME/.pyenv} +#[[ -a $PYENV_ROOT/bin/pyenv ]] && path=($PYENV_ROOT/bin $path) +#if type pyenv &> /dev/null || [[ -a $PYENV_ROOT/bin/pyenv ]]; then +# function pyenv() { +# unset pyenv +# path=($PYENV_ROOT/shims $path) +# eval "$(command pyenv init -)" +# if which pyenv-virtualenv-init > /dev/null; then +# eval "$(pyenv virtualenv-init -)" +# export PYENV_VIRTUALENV_DISABLE_PROMPT=1 +# fi +# pyenv $@ +# } +#fi +#export WORKON_HOME="$XDG_DATA_HOME/virtualenvs" +#export WORKON_HOME=$HOME/.virtualenvs +#export VIRTUALENVWRAPPER_PYTHON=`which python3` +#export VIRTUALENVWRAPPER_PYTHON=$(which python3) +#export VIRTUALENVWRAPPER_VIRTUALENV=`which virtualenv` +#source /usr/local/bin/virtualenvwrapper.sh + +# Check if virtualenvwrapper.sh exists before sourcing +if command -v virtualenvwrapper.sh >/dev/null 2>&1; then + export WORKON_HOME="$HOME/.virtualenvs" + export VIRTUALENVWRAPPER_PYTHON="$(command -v python3)" + export VIRTUALENVWRAPPER_VIRTUALENV="$(command -v virtualenv)" + source "$(command -v virtualenvwrapper.sh)" +fi + +export VIRTUAL_ENV_DISABLE_PROMPT=false +export JUPYTER_CONFIG_DIR="$XDG_CONFIG_HOME/jupyter" +export IPYTHONDIR="$XDG_CONFIG_HOME/jupyter" + +# Python +[[ "$(uname)" == "Darwin" ]] && export PYTHON_CONFIGURE_OPTS="--enable-framework" +[[ "$(uname)" == "Linux" ]] && export PYTHON_CONFIGURE_OPTS="--enable-shared" + +export PYENV_ROOT="$HOME/.pyenv" +export PATH="$PYENV_ROOT/bin:$PATH" + +# PHP +PATH="$HOME/.config/composer/vendor/bin:$PATH" + + +# Lua +export PATH=$PATH:/usr/local/luarocks/bin +#export PATH="$XDG_DATA_HOME/luarocks/bin:$PATH" + +#ver=$(find lua* -maxdepth 0 | sort -rV | head -n 1) +#export LUA_PATH="$LUA_PATH:${ver}/share/lua/5.1/?.lua;${ver}/share/lua/5.1/?/init.lua;;" +#export LUA_CPATH="$LUA_CPATH:${ver}/lib/lua/5.1/?.so;;" + +#LUAROCKS_PREFIX=/usr/local +#export LUA_PATH="$LUAROCKS_PREFIX/share/lua/5.1/?.lua;$LUAROCKS_PREFIX/share/lua/5.1/?/init.lua;;" +#export LUA_CPATH="$LUAROCKS_PREFIX/lib/lua/5.1/?.so;;" + +#export LUA_PATH="<path-to-add>;;" +#export LUA_CPATH="./?.so;/usr/local/lib/lua/5.3/?.so; +# /usr/local/share/lua/5.3/?.so;<path-to-add>" + + +# Program settings +#export MOZ_USE_XINPUT2="1" # Mozilla smooth scrolling/touchpads. +# Pixel-perfect Firefox touchpad scrolling +export MOZ_USE_XINPUT2=1 + +# Cmake +export PKG_CONFIG_PATH="/usr/local/lib64/pkgconfig:$PKG_CONFIG_PATH" + + +# Scaling +#export QT_AUTO_SCREEN_SCALE_FACTOR=0 +#export QT_SCALE_FACTOR=1 +#export QT_SCREEN_SCALE_FACTORS="1;1;1" +#export GDK_SCALE=1 +#export GDK_DPI_SCALE=1 + +## Prevent duplicate paths +#typeset -U PATH path +# +## Default most programs to use fcitx global keyboard configurations +#export GTK_IM_MODULE='fcitx' +#export QT_IM_MODULE='fcitx' +#export SDL_IM_MODULE='fcitx' +#export XMODIFIERS='@im=fcitx' + + +# Start blinking +export LESS_TERMCAP_mb=$(tput bold; tput setaf 2) # green +# Start bold +export LESS_TERMCAP_md=$(tput bold; tput setaf 2) # green +# Start stand out +export LESS_TERMCAP_so=$(tput bold; tput setaf 3) # yellow +# End standout +export LESS_TERMCAP_se=$(tput rmso; tput sgr0) +# Start underline +export LESS_TERMCAP_us=$(tput smul; tput bold; tput setaf 1) # red +# End Underline +export LESS_TERMCAP_ue=$(tput sgr0) +# End bold, blinking, standout, underline +export LESS_TERMCAP_me=$(tput sgr0). diff --git a/common/config/zsh/.zshrc b/common/config/zsh/.zshrc new file mode 100644 index 0000000..0ada4f5 --- /dev/null +++ b/common/config/zsh/.zshrc @@ -0,0 +1,73 @@ +# ███████╗███████╗██╗ ██╗██████╗ ██████╗ +# ╚══███╔╝██╔════╝██║ ██║██╔══██╗██╔════╝ +# ███╔╝ ███████╗███████║██████╔╝██║ +# ███╔╝ ╚════██║██╔══██║██╔══██╗██║ +# ███████╗███████║██║ ██║██║ ██║╚██████╗ +# ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ + +# Profile zsh time +#zmodload zsh/zprof + +# If not running interactively, and not being sourced, don’t do anything +[[ $- != *i* ]] && [[ "${BASH_SOURCE[0]:-${(%):-%N}}" == "$0" ]] && return + +# Terminal key bindings +#stty intr '^q' # Free Ctrl+C for copy use Ctrl+Q instead for Interrupt +stty lnext '^-' # Free Ctrl+V for paste use Ctrl+- instead for Literal next +stty stop undef # Disable Ctrl+S to freeze terminal +stty start undef # Disable Ctrl+Q nfreeze terminal + +# Set the current prompt file (e.g., prompt, or prompt_minimal) +ZSH_PROMPT="${ZSH_PROMPT:-prompt}" +#ZSH_PROMPT="${ZSH_PROMPT:-prompt_minimal}" +#ZSH_PROMPT="${ZSH_PROMPT:-prompt_new}" +#ZSH_PROMPT="${ZSH_PROMPT:-prompt_simple}" + +# Source common Zsh files (excluding any that start with 'prompt') +ZSH_SOURCES=() + +for zsh_source in "$HOME"/.config/zsh/user/*.zsh; do + if [[ $(basename "$zsh_source") == prompt* && $(basename "$zsh_source" .zsh) != "$ZSH_PROMPT" ]]; then + continue + fi + ZSH_SOURCES+=("$zsh_source") +done + +# Source ZSH files +for zsh_source in "${ZSH_SOURCES[@]}"; do + source "$zsh_source" +done + +# Faster SSH +if [[ -n "$SSH_CLIENT" ]]; then + export KEYTIMEOUT=10 +else + export KEYTIMEOUT=15 +fi + +# Prevent non-login shell anomalies or toolchain misidentification in VS Code +if [[ "${TERM_PROGRAM:-}" == "vscode" ]]; then + unset ARGV0 +fi + +########## Source Plugins, should be last ########## + +# Load fzf keybindings and completion if fzf is installed +if command -v fzf >/dev/null 2>&1; then + FZF_BASE="/usr/local/bin/fzf/shell" + [[ -f "${FZF_BASE}/key-bindings.zsh" ]] && source "${FZF_BASE}/key-bindings.zsh" + [[ -f "${FZF_BASE}/completion.zsh" ]] && source "${FZF_BASE}/completion.zsh" +fi + +# Source plugins +for plugin in \ + "$HOME/.config/zsh/plugins/zsh-you-should-use/you-should-use.plugin.zsh" \ + "$HOME/.config/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" \ + "$HOME/.config/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh" \ + "$HOME/.config/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh" +do + [ -f "$plugin" ] && source "$plugin" +done + +# Profile zsh time +#zprof # At the end of .zshrc diff --git a/common/config/zsh/user/aliases.zsh b/common/config/zsh/user/aliases.zsh new file mode 100644 index 0000000..c205a9b --- /dev/null +++ b/common/config/zsh/user/aliases.zsh @@ -0,0 +1,208 @@ +########## Aliases ########## + +# Define alias for nvim/vim (fallback to vim) +if command -v nvim > /dev/null; then + alias vi='nvim' +else + alias vi='vim' +fi + +#alias vv='$(history -p !vim)' +alias vv="vim -c 'norm! ^O'" + +# Confirmation # +alias mv='mv -i' +alias cp='cp -i' +alias ln='ln -i' + +# Disable 'rm' +#alias rm='function _rm() { echo -e "\033[0;31mrm\033[0m is disabled, use \033[0;32mtrash\033[0m or \033[0;32mdel \033[0m\033[0;33m$1\033[0m"; }; _rm' +#alias del='/bin/rm' + +# Use lsd for ls if available +if command -v lsd >/dev/null 2>&1; then + alias ls='lsd --color=auto --group-directories-first' +fi + +# ls variants +alias l='ls -FAh --group-directories-first' +alias la='ls -lAFh --group-directories-first' +alias lt='ls -lFAht --group-directories-first' +alias lr='ls -RFAh --group-directories-first' + +# more ls variants +alias ldot='ls -ld .* --group-directories-first' +alias lS='ls -1FASsh --group-directories-first' +alias lart='ls -1Fcart --group-directories-first' +alias lrt='ls -1Fcrt --group-directories-first' + +# ls with different alphabethical sorting +#unalias ll +#ll() { LC_COLLATE=C ls "$@" } + +# suffix aliases +alias -g CP='| xclip -selection clipboard -rmlastnl' +alias -g LL="| less exit 2>1 /dev/null" +alias -g CA="| cat -A" +alias -g KE="2>&1" +alias -g NE="2>/dev/null" +alias -g NUL=">/dev/null 2>&1" + +alias grep='grep --color=auto --exclude-dir={.git,.svn,.hg}' +alias egrep='egrep --color=auto --exclude-dir={.git,.svn,.hg}' +alias egrep='fgrep --color=auto --exclude-dir={.git,.svn,.hg}' + +#alias hist="grep '$1' $HISTFILE" +alias hist="history | grep $1" + + +alias gdb='gdb -q' +alias rust-gdb='rust-gdb -q' + +alias cd="cd-clear-ls" +alias clear='newline_clear' + +# List upto last 10 visited directories using "d" and quickly cd into any specific one +alias d="dirs -v | head -10" + +# Using just a number from "0" to "9" +alias 0="cd +0" +alias 1="cd +1" +alias 2="cd +2" +alias 3="cd +3" +alias 4="cd +4" +alias 5="cd +5" +alias 6="cd +6" +alias 7="cd +7" +alias 8="cd +8" +alias 9="cd +9" + +alias sudo='sudo ' # zsh: elligible for alias expansion/fix syntax highlight +alias sedit='sudoedit' +#alias se='sudoedit' +alias se='sudo -e' +alias :q='exit 2>1 /dev/null' +alias disk-destroyer='$(command -v dd)' +alias dd='echo "Warning use command: disk-destroyer"' +alias sc="systemctl" +alias jc="journalctl" +alias jck="journalctl -k" # Kernel +alias jce='sudo journalctl -b --priority 0..3' # error +alias journalctl-error='sudo journalctl -b --priority 0..3' +alias jcssh="sudo journalctl -u sshd" +alias tunnel='ssh -fNTL' +# tty aliases +#if [[ "$TERM" == 'linux' ]]; then +# alias tmux='/usr/bin/tmux -L linux' +#fi +#alias logout="loginctl kill-user $(whoami)" + +logout() { + local wm + wm="$(windowManagerName)" + if [[ -n "$wm" ]]; then + echo "Logging out by killing window manager: $wm" + pkill "$wm" + else + echo "No window manager detected!" >&2 + fi +} +alias lg="logout" + +#alias suspend='systemctl suspend && betterlockscreen -l' # Suspend(sleep) and lock screen if using systemctl +#alias suspend='systemctl suspend' # Suspend(sleep) and lock screen if using systemctl +alias suspend='loginctl suspend' # Suspend(sleep) and lock screen if using systemctl +#alias shutdown='loginctl poweroff' # Suspend(sleep) and lock screen if using systemctl +#alias shutdown='sudo /sbin/shutdown -h' +#alias poweroff='loginctl poweroff' +#alias reboot='loginctl reboot' +alias reboot='sudo reboot' +#alias hibernate='systemctl hibernate' # Hibernate +alias lock='DISPLAY=:0 xautolock -locknow' # Lock my workstation screen from my phone +alias oports="sudo lsof -i -P -n | grep -i 'listen'" # List open ports +alias keyname="xev | sed -n 's/[ ]*state.* \([^ ]*\)).*/\1/p'" +alias wget=wget --hsts-file="$XDG_CACHE_HOME/wget-hsts" # wget does not support environment variables +alias open="xdg-open" +alias pp='getlast 2>&1 |&tee -a output.txt' +#alias lg='la | grep' +alias pg='ps aux | grep' +alias py='python' +alias py3='python3' +alias activate='source ~/.local/share/venv/bin/activate' +alias sha256='shasum -a 256' +alias rgf='rg -F' +alias weather='curl wttr.in/durban' +alias diary='nvim "$HOME/documents/main/inbox/diary/$(date +'%Y-%m-%d').md"' +alias wifi='nmcli dev wifi show-password' +alias ddg='w3m lite.duckduckgo.com' +alias rss='newsboat' +alias vpn='protonvpn' +alias yt-dl="yt-dlp -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4' --restrict-filename" +#alias com.obsproject.Studio="obs" +#alias obs="com.obsproject.Studio" +#alias obs-stuido="obs" + +# Time aliases +alias utc='TZ=Africa/Johannesburg date' +alias ber='TZ=Europe/Berlin date' +alias nyc='TZ=America/New_York date' +alias sfo='TZ=America/Los_Angeles date' +alias utc='TZ=Etc/UTC date' + +alias src='source $ZDOTDIR/.zshrc' +alias p=proxy + +alias cheat='~/.scripts/cheat.sh ~/documents/notes/cheatsheets' +alias crypto='curl -s rate.sx | head -n -2 | tail -n +10' +#alias todo='glow "$HOME"/documents/main/notes/TODO.md' + +alias todo='$EDITOR "$(find "$HOME"/documents/main -type f -iname "todo.md" | head -n 1)"' +alias android-studio='/opt/android-studio/bin/studio.sh' # android-studio +alias nomachine='/usr/NX/bin/nxplayer' # nomachine +alias firefox="firefox-bin" +alias discord="vesktop-bin" +alias fetch="fastfetch" +alias batt='upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E "state|to full|percentage"' +alias emerge-fetch='sudo tail -f /var/log/emerge-fetch.log' +alias spotify="env LD_PRELOAD=/usr/local/lib/spotify-adblock.so spotify %U" + +alias proofread='firejail --private --private-tmp --net=none --seccomp --caps.drop=all zathura' + +# NVM +if [ -s "$NVM_DIR/nvm.sh" ]; then + nvm_cmds=(nvm node npm yarn) + for cmd in "${nvm_cmds[@]}"; do + alias "$cmd"="unalias ${nvm_cmds[*]} && unset nvm_cmds && . $NVM_DIR/nvm.sh && $cmd" + done +fi + +# Kubernetes +if command -v kubectl > /dev/null; then + replaceNS() { kubectl config view --minify --flatten --context=$(kubectl config current-context) | yq ".contexts[0].context.namespace=\"$1\"" ; } + alias kks='KUBECONFIG=<(replaceNS "kube-system") kubectl' + alias kam='KUBECONFIG=<(replaceNS "authzed-monitoring") kubectl' + alias kas='KUBECONFIG=<(replaceNS "authzed-system") kubectl' + alias kar='KUBECONFIG=<(replaceNS "authzed-region") kubectl' + alias kt='KUBECONFIG=<(replaceNS "tenant") kubectl' + + if command -v kubectl-krew > /dev/null; then + path=($XDG_CONFIG_HOME/krew/bin $path) + fi + + rmfinalizers() { + kubectl get deployment "$1" -o json | jq '.metadata.finalizers = null' | kubectl apply -f - + } +fi + +# Castero +castero() { + if [[ -f ~/.local/share/venv/bin/activate ]]; then + . ~/.local/share/venv/bin/activate + fi + command castero "$@" +} + +# Zoxide (cd alternative) +if command -v zoxide >/dev/null 2>&1; then + eval "$(zoxide init zsh)" +fi diff --git a/common/config/zsh/user/bindings.zsh b/common/config/zsh/user/bindings.zsh new file mode 100644 index 0000000..52cab06 --- /dev/null +++ b/common/config/zsh/user/bindings.zsh @@ -0,0 +1,175 @@ +########## Vi mode ########## +bindkey -v + +local WORDCHARS='*?_-.[]~=&;!#$%^(){}<>' +backward-kill-dir () { + local WORDCHARS=${WORDCHARS/\/} + zle backward-kill-word + zle -f kill +} + +zle -N backward-kill-dir +bindkey '^[^?' backward-kill-dir +bindkey "^W" backward-kill-dir + +bindkey -M viins '^[[3~' delete-char +bindkey -M vicmd '^[[3~' delete-char +bindkey -v '^?' backward-delete-char +bindkey -r '\e/' +bindkey -s jk '\e' +#bindkey "^W" backward-kill-word +bindkey "^H" backward-delete-char # Control-h also deletes the previous char +bindkey "^U" backward-kill-line +bindkey "^[j" history-search-forward # or you can bind it to the down key "^[[B" +bindkey "^[k" history-search-backward # or you can bind it to Up key "^[[A" + +bindkey '^[[D' backward-char # Left arrow +bindkey '^[[C' forward-char # Right arrow +bindkey '^[D' backward-char # Left arrow +bindkey '^[C' forward-char # Right arrow +bindkey '[C' forward-word +bindkey '[D' backward-word +bindkey -M viins '^[[D' backward-char # Left arrow +bindkey -M viins '^[[C' forward-char # Right arrow + +bindkey -M vicmd '^[[D' backward-char # Left arrow +bindkey -M vicmd '^[[C' forward-char # Right arrow + +# Define the 'autosuggest-execute' and 'autosuggest-accept' ZLE widgets +autoload -Uz autosuggest-execute autosuggest-accept +zle -N autosuggest-execute +zle -N autosuggest-accept +bindkey '^X' autosuggest-execute +bindkey '^Y' autosuggest-accept +bindkey '\M-l' accept-and-complete-next-history + +# Accept completion with <tab> or Ctrl+i and go to next/previous suggestions with Vi like keys: Ctrl+n/p +zmodload -i zsh/complist +accept-and-complete-next-history() { + zle expand-or-complete-prefix +} +zle -N accept-and-complete-next-history +#bindkey -M menuselect '^i' accept-and-complete-next-history +bindkey '^n' expand-or-complete +bindkey '^p' reverse-menu-complete +#bindkey '^I' expand-or-complete +#bindkey '^[[Z]]' reverse-menu-complete +bindkey -M menuselect '^[' undo + +# Edit line in vim with alt-e +autoload edit-command-line; zle -N edit-command-line +bindkey '^e' edit-command-line +bindkey '^[e' edit-command-line # alt + e + +# Allow CTRL+D to exit zsh with partial command line (non empty line) +exit_zsh() { exit } +zle -N exit_zsh +bindkey '^D' exit_zsh + +# Copy/Paste +# Safe clipboard copy +smart_copy() { + local text="${LBUFFER}${RBUFFER}" + + # Prefer Wayland, fallback to X11, then others + if command -v wl-copy >/dev/null 2>&1 && [[ "$WAYLAND_DISPLAY" || "$XDG_SESSION_TYPE" == "wayland" ]]; then + echo -n "$text" | wl-copy --foreground --type text/plain 2>/dev/null || true + elif command -v xclip >/dev/null 2>&1 && [[ "$DISPLAY" || "$XDG_SESSION_TYPE" == "x11" || "$XDG_SESSION_TYPE" == "x11-xwayland" ]]; then + echo -n "$text" | xclip -selection clipboard 2>/dev/null || true + elif [[ "$(uname -s)" == "Darwin" ]] && command -v pbcopy >/dev/null 2>&1; then + echo -n "$text" | pbcopy 2>/dev/null || true + elif [[ "$OSTYPE" == "cygwin" || "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + echo -n "$text" | clip.exe 2>/dev/null || true + else + echo "smart_copy: No supported clipboard utility found." >&2 + fi +} + +# Safe clipboard paste +smart_paste() { + local clip="" + if command -v wl-paste >/dev/null 2>&1 && [[ "$WAYLAND_DISPLAY" || "$XDG_SESSION_TYPE" == "wayland" ]]; then + clip=$(wl-paste --no-newline 2>/dev/null) + elif command -v xclip >/dev/null 2>&1 && [[ "$DISPLAY" || "$XDG_SESSION_TYPE" == "x11" || "$XDG_SESSION_TYPE" == "x11-xwayland" ]]; then + clip=$(xclip -selection clipboard -o 2>/dev/null) + elif [[ "$(uname -s)" == "Darwin" ]] && command -v pbpaste >/dev/null 2>&1; then + clip=$(pbpaste 2>/dev/null) + elif [[ "$OSTYPE" == "cygwin" || "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + clip=$(powershell.exe -Command 'Get-Clipboard -Raw' 2>/dev/null | tr -d '\r') + else + echo "smart_paste: No supported clipboard utility found." >&2 + fi + + LBUFFER+="$clip" + zle reset-prompt +} + +# Register widgets +zle -N smart_copy +zle -N smart_paste + +# Bind keys (optional: choose your preferred) +bindkey '^V' smart_paste +bindkey -M viins '^V' smart_paste +bindkey -M vicmd '^V' smart_paste +bindkey -M vicmd 'p' smart_paste + +bindkey '^Y' smart_copy +bindkey -M viins '^Y' smart_copy +bindkey -M vicmd '^Y' smart_copy +bindkey -M vicmd 'y' smart_copy + +# In vi mode, map Alt-H and Alt-L +#bindkey -M viins "^[u" go_up # Alt-H to go up +#bindkey -M viins "^[o" go_into # Alt-L to go into a directory + + +# Newline and clear +function newline_clear() { + printf "\n" + command clear +} + +zle -N newline_clear + +no_tmux_clear() { + zle clear-screen +} +zle -N no_tmux_clear + +# Newline before clear +if [[ -n "$TMUX" ]]; then + # Bind Ctrl-L to send newline and clear screen + bindkey '^L' newline_clear +else + bindkey '^L' no_tmux_clear +fi + +# use ctrl-z to toggle in and out of bg +function toggle_fg_bg() { + if [[ $#BUFFER -eq 0 ]]; then + BUFFER="fg" + zle accept-line + else + BUFFER="" + zle clear-screen + fi +} +zle -N toggle_fg_bg +bindkey '^Z' toggle_fg_bg + + + + +## Custom key bindings to control history behavior +#bindkey -M vicmd '^[[C' vi-forward-char # Right arrow in normal mode - just move cursor +#bindkey -M vicmd '^[[D' vi-backward-char # Left arrow in normal mode - just move cursor +#bindkey -M vicmd '^A' beginning-of-line # Ctrl-A - go to beginning of line +#bindkey -M vicmd '^E' end-of-line # Ctrl-E - go to end of line + +# Disable automatic suggestion accept on right arrow in normal mode + +## Additional vi-mode key bindings to prevent unwanted history completion +## Disable automatic history completion in normal mode +#bindkey -M vicmd '^[[C' vi-forward-char # Right arrow - just move right, don't complete +#bindkey -M vicmd '^[[D' vi-backward-char # Left arrow - just move left diff --git a/common/config/zsh/user/completion.zsh b/common/config/zsh/user/completion.zsh new file mode 100644 index 0000000..2445548 --- /dev/null +++ b/common/config/zsh/user/completion.zsh @@ -0,0 +1,172 @@ +#!/bin/zsh + +########## Completion(s) ########## + +autoload -Uz compinit +_comp_path="${XDG_CACHE_HOME:-$HOME/.cache}/zcompdump" + +# 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 + +# Skip the not really helpful global compinit +skip_global_compinit=0 + +DISABLE_MAGIC_FUNCTIONS=true + + +#zstyle ':completion:*' menu select=1 +#zstyle ':completion:*:directory-stack' list-colors '=(#b) #([0-9]#)*( *)==95=38;5;12' + +# Options +#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. + +## Disable all custom completions +unsetopt COMPLETE_IN_WORD +#unsetopt AUTO_MENU +#unsetopt AUTO_LIST +#unsetopt AUTO_PARAM_SLASH +#unsetopt EXTENDED_GLOB +#unsetopt MENU_COMPLETE # Do not autoselect the first completion entry. + +# Optional: Uncomment to disable waiting dots on completion +# COMPLETION_WAITING_DOTS="false" + +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. + +# Test the behavior with just the basics +compinit +# Variables +LS_COLORS=${LS_COLORS:-'di=34:ln=35:so=32:pi=33:ex=31:bd=36;01:cd=33;01:su=31;40;07:sg=36;40;07:tw=32;40;07:ow=33;40;07:'} + +# Styles +# 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 "${XDG_CACHE_HOME:-$HOME/.cache}/zcompcache" + + +# 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:*' 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:*:*: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. But allow ignoring custom entries from static +# */etc/hosts* which might be uninteresting. +zstyle -a ':completion:*:hosts' etc-host-ignores '_etc_host_ignores' + +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 }:#*\**}:#*\?*}} +)' + +# Don't complete uninteresting users... +zstyle ':completion:*:*:*:users' ignored-patterns \ + adm amanda apache avahi beaglidx bin cacti canna clamav daemon \ + dbus distcache dovecot fax ftp games gdm gkrellmd gopher \ + hacluster haldaemon halt hsqldb ident junkbust ldap lp mail \ + mailman mailnull mldonkey mysql nagios \ + named netdump news nfsnobody nobody nscd ntp nut nx openvpn \ + operator pcap postfix postgres privoxy pulse pvm quagga radvd \ + rpc rpcuser rpm shutdown squid sshd sync uucp vcsa xfs '_*' + +# ... unless we really want to. +zstyle '*' single-ignored show + +# 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 + +# Media Players +zstyle ':completion:*:*:mpg123:*' file-patterns '*.(mp3|MP3):mp3\ files *(-/):directories' +zstyle ':completion:*:*:mpg321:*' file-patterns '*.(mp3|MP3):mp3\ files *(-/):directories' +zstyle ':completion:*:*:ogg123:*' file-patterns '*.(ogg|OGG|flac):ogg\ files *(-/):directories' +zstyle ':completion:*:*:mocp:*' file-patterns '*.(wav|WAV|mp3|MP3|ogg|OGG|flac):ogg\ files *(-/):directories' + +# Mutt +if [[ -s "$HOME/.mutt/aliases" ]]; then + zstyle ':completion:*:*:mutt:*' menu yes select + zstyle ':completion:*:mutt:*' users ${${${(f)"$(<"$HOME/.mutt/aliases")"}#alias[[:space:]]}%%[[:space:]]*} +fi + +# 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::*' diff --git a/common/config/zsh/user/functions.zsh b/common/config/zsh/user/functions.zsh new file mode 100644 index 0000000..56f0ca1 --- /dev/null +++ b/common/config/zsh/user/functions.zsh @@ -0,0 +1,1607 @@ +# Dotfiles Management System +if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then + # Core git wrapper with repository as work-tree + _config() { + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" "$@" + } + + # Detect OS + case "$(uname -s)" in + Linux) CFG_OS="linux" ;; + Darwin) CFG_OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) CFG_OS="windows" ;; + *) CFG_OS="other" ;; + esac + + # Map system path to repository path + _repo_path() { + local f="$1" + + # If it's an absolute path that's not in HOME, handle it specially + if [[ "$f" == /* && "$f" != "$HOME/"* ]]; then + echo "$CFG_OS/${f#/}" + return + fi + + # Check for paths that should go to the repository root + case "$f" in + common/*|linux/*|macos/*|windows/*|profile/*|README.md) + echo "$f" + return + ;; + "$HOME/"*) + f="${f#$HOME/}" + ;; + esac + + # Default: put under OS-specific home + echo "$CFG_OS/home/$f" + } + + _sys_path() { + local repo_path="$1" + local os_path_pattern="$CFG_OS/" + + # Handle OS-specific files that are not in the home subdirectory + if [[ "$repo_path" == "$os_path_pattern"* && "$repo_path" != */home/* ]]; then + echo "/${repo_path#$os_path_pattern}" + return + fi + + case "$repo_path" in + # Common configs → OS-specific config dirs + common/config/*) + case "$CFG_OS" in + linux) + local base="${XDG_CONFIG_HOME:-$HOME/.config}" + echo "$base/${repo_path#common/config/}" + ;; + macos) + echo "$HOME/Library/Application Support/${repo_path#common/config/}" + ;; + windows) + echo "$LOCALAPPDATA\\${repo_path#common/config/}" + ;; + *) + echo "$HOME/.config/${repo_path#common/config/}" + ;; + esac + ;; + + # Common assets → stay in repo + common/assets/*) + echo "$HOME/.cfg/$repo_path" + ;; + + # Other common files (dotfiles like .bashrc, .gitconfig, etc.) → $HOME + common/*) + echo "$HOME/${repo_path#common/}" + ;; + + # OS-specific home + */home/*) + echo "$HOME/${repo_path#*/home/}" + ;; + + # Profile configs and README → stay in repo + profile/*|README.md) + echo "$HOME/.cfg/$repo_path" + ;; + + # Default fallback + *) + echo "$HOME/.cfg/$repo_path" + ;; + + esac + } + + # Prompts for sudo if needed and runs the command + _sudo_prompt() { + if [[ $EUID -eq 0 ]]; then + "$@" + else + if command -v sudo >/dev/null; then + sudo "$@" + elif command -v doas >/dev/null; then + doas "$@" + elif command -v pkexec >/dev/null; then + pkexec "$@" + else + echo "Error: No privilege escalation tool found." + return 1 + fi + fi + } + + # Main config command + config() { + local cmd="$1"; shift + local target_dir="" + # Parse optional --target flag for add + if [[ "$cmd" == "add" ]]; then + while [[ "$1" == --* ]]; do + case "$1" in + --target|-t) + target_dir="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + return 1 + ;; + esac + done + fi + + case "$cmd" in + add) + local file_path + for file_path in "$@"; do + local repo_path + if [[ -n "$target_dir" ]]; then + local rel_path + if [[ "$file_path" == /* ]]; then + rel_path="$(basename "$file_path")" + else + rel_path="$file_path" + fi + repo_path="$target_dir/$rel_path" + else + repo_path="$(_repo_path "$file_path")" + fi + + local full_repo_path="$HOME/.cfg/$repo_path" + mkdir -p "$(dirname "$full_repo_path")" + cp -a "$file_path" "$full_repo_path" + + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" add "$repo_path" + + echo "Added: $file_path -> $repo_path" + done + ;; + rm) + local rm_opts="" + local file_path_list=() + + for arg in "$@"; do + if [[ "$arg" == "-"* ]]; then + rm_opts+=" $arg" + else + file_path_list+=("$arg") + fi + done + + for file_path in "${file_path_list[@]}"; do + local repo_path="$(_repo_path "$file_path")" + + if [[ "$rm_opts" == *"-r"* ]]; then + _config rm --cached -r "$repo_path" + else + _config rm --cached "$repo_path" + fi + + eval "rm $rm_opts \"$file_path\"" + echo "Removed: $file_path" + done + ;; + sync) + local direction="${1:-to-repo}"; shift + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ "$direction" == "to-repo" ]]; then + if [[ -e "$sys_file" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then + cp -a "$sys_file" "$full_repo_path" + echo "Synced to repo: $sys_file" + fi + elif [[ "$direction" == "from-repo" ]]; then + if [[ -e "$full_repo_path" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then + local dest_dir="$(dirname "$sys_file")" + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + echo "Synced from repo: $sys_file" + fi + fi + done + ;; + status) + local auto_synced=() + while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ -e "$sys_file" && -e "$full_repo_path" ]]; then + if ! diff -q "$full_repo_path" "$sys_file" >/dev/null 2>&1; then + cp -fa "$sys_file" "$full_repo_path" + auto_synced+=("$repo_file") + fi + fi + done < <(_config ls-files) + if [[ ${#auto_synced[@]} -gt 0 ]]; then + echo "=== Auto-synced Files ===" + for repo_file in "${auto_synced[@]}"; do + echo "synced: $(_sys_path "$repo_file") -> $repo_file" + done + echo + fi + _config status + echo + ;; + deploy) + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" # destination only + + # Only continue if the source exists + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if needed + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Deployed: $repo_file -> $sys_file" + fi + done + ;; + checkout) + echo "Checking out dotfiles from .cfg..." + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" + + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if it doesn't exist + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Checked out: $repo_file -> $sys_file" + fi + done + ;; + backup) + local timestamp=$(date +%Y%m%d%H%M%S) + local backup_dir="$HOME/.dotfiles_backup/$timestamp" + echo "Backing up existing dotfiles to $backup_dir..." + + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + if [[ -e "$sys_file" ]]; then + local dest_dir_full="$backup_dir/$(dirname "$repo_file")" + mkdir -p "$dest_dir_full" + cp -a "$sys_file" "$backup_dir/$repo_file" + fi + done + echo "Backup complete. To restore, copy files from $backup_dir to their original locations." + ;; + *) + _config "$cmd" "$@" + ;; + esac + } +fi + +# Make SUDO_ASKPASS agnostic: pick the first available askpass binary. +# You can predefine SUDO_ASKPASS env var to force a particular path. +: "${SUDO_ASKPASS:=""}" + +# list of common askpass binaries (order: preferred -> fallback) +_askpass_candidates=( + "$SUDO_ASKPASS" # user-specified (if absolute path) + "/usr/lib/ssh/x11-ssh-askpass" + "/usr/libexec/openssh/ssh-askpass" + "/usr/lib/ssh/ssh-askpass" + "/usr/bin/ssh-askpass" + "/usr/bin/ssh-askpass-gtk" + "/usr/bin/ssh-askpass-gnome" + "/usr/bin/ssh-askpass-qt" + "/usr/bin/ksshaskpass" + "/usr/bin/zenity" # use zenity --entry as wrapper (see below) + "/usr/bin/mate-ssh-askpass" + "/usr/bin/xdg-open" # last-resort GUI helper (not ideal) +) + +find_askpass() { + for p in "${_askpass_candidates[@]}"; do + [ -z "$p" ] && continue + # if user gave a path in SUDO_ASKPASS we accept it only if it's executable + if [ -n "$SUDO_ASKPASS" ] && [ "$p" = "$SUDO_ASKPASS" ]; then + [ -x "$p" ] && { printf '%s\n' "$p"; return 0; } + continue + fi + + # if candidate is an absolute path, test directly + if [ "${p#/}" != "$p" ]; then + [ -x "$p" ] && { printf '%s\n' "$p"; return 0; } + continue + fi + + # otherwise try to resolve via PATH + if command -v "$p" >/dev/null 2>&1; then + # For zenity, we will use a small wrapper (see below) + printf '%s\n' "$(command -v "$p")" + return 0 + fi + done + + return 1 +} + +# If zenity is chosen, use a thin wrapper script so sudo -A can call it like an askpass binary. +# This wrapper will be created in $XDG_RUNTIME_DIR or /tmp (non-persistent). +create_zenity_wrapper() { + local wrapper + wrapper="${XDG_RUNTIME_DIR:-/tmp}/.sudo_askpass_zenity.sh" + cat >"$wrapper" <<'EOF' +#!/bin/sh +# simple zenity askpass wrapper for sudo +# prints password to stdout so sudo -A works +zenity --entry --title="Authentication" --text="Elevated privileges are required" --hide-text 2>/dev/null || exit 1 +EOF + chmod 700 "$wrapper" + printf '%s\n' "$wrapper" +} + +# Set askpass +if [ -z "$SUDO_ASKPASS" ]; then + candidate="$(find_askpass || true)" + if [ -n "$candidate" ]; then + if command -v zenity >/dev/null 2>&1 && [ "$(command -v zenity)" = "$candidate" ]; then + # create the wrapper and export it + wrapper="$(create_zenity_wrapper)" + export SUDO_ASKPASS="$wrapper" + else + export SUDO_ASKPASS="$candidate" + fi + else + # optional: leave unset or set to empty to avoid mistakes + unset SUDO_ASKPASS + fi +fi +# debug: (uncomment to print what was chosen) +# printf 'SUDO_ASKPASS -> %s\n' "${SUDO_ASKPASS:-<none>}" + + +# Git +# No arguments: `git status` +# With arguments: acts like `git` +g() { + if [ $# -gt 0 ]; then + git "$@" # If arguments are provided, pass them to git + else + git status # Otherwise, show git status + fi +} + +# Complete g like git +compdef g=git + +# Git alias commands +ga() { g add "$@"; } # ga: Add files to the staging area +gaw() { g add -A && g diff --cached -w | g apply --cached -R; } # gaw: Add all changes to the staging area and unstage whitespace changes +grm() { g rm "$@"; } +gb() { g branch "$@"; } # gb: List branches +gbl() { g branch -l "$@"; } # gbl: List local branches +gbD() { g branch -D "$@"; } # gbD: Delete a branch +gbu() { g branch -u "$@"; } # gbu: Set upstream branch +ge() { g clone "$@"; } +gc() { g commit "$@"; } # gc: Commit changes +gcm() { g commit -m "$@"; } # gcm: Commit with a message +gca() { g commit -a "$@"; } # gca: Commit all changes +gcaa() { g commit -a --amend "$@"; } # gcaa: Amend the last commit +gcam() { g commit -a -m "$@"; } # gcam: Commit all changes with a message +gce() { g commit -e "$@"; } # gce: Commit with message and allow editing +gcfu() { g commit --fixup "$@"; } # gcfu: Commit fixes in the context of the previous commit +gco() { g checkout "$@"; } # gco: Checkout a branch or file +gcob() { g checkout -b "$@"; } # gcob: Checkout a new branch +gcoB() { g checkout -B "$@"; } # gcoB: Checkout a new branch, even if it exists +gcp() { g cherry-pick "$@"; } # gcp: Cherry-pick a commit +gcpc() { g cherry-pick --continue "$@"; } # gcpc: Continue cherry-picking after resolving conflicts +gd() { g diff "$@"; } # gd: Show changes +#gd^() { g diff HEAD^ HEAD "$@"; } # gd^: Show changes between HEAD^ and HEAD +gds() { g diff --staged "$@"; } # gds: Show staged changes +gl() { g lg "$@"; } # gl: Show a customized log +glg() { g log --graph --decorate --all "$@"; } # glg: Show a customized log with graph +gls() { # Query `glog` with regex query. + query="$1" + shift + glog --pickaxe-regex "-S$query" "$@" +} +gdc() { g diff --cached "$@"; } # gdc: Show changes between the working directory and the index +gu() { g pull "$@"} # gu: Pull +gp() { g push "$@"} # gp: Push +gpom() { g push origin main "$@"; } # gpom: Push changes to origin main +gr() { g remote "$@"; } # gr: Show remote +gra() { g rebase --abort "$@"; } # gra: Abort a rebase +grb() { g rebase --committer-date-is-author-date "$@"; } # grb: Rebase with the author date preserved +grbom() { grb --onto master "$@"; } # grbom: Rebase onto master +grbasi() { g rebase --autosquash --interactive "$@"; } # grbasi: Interactive rebase with autosquash +grc() { g rebase --continue "$@"; } # grc: Continue a rebase +grs() { g restore --staged "$@"; } # grs: Restore changes staged for the next commit +grv() { g remote -v "$@"; } # grv: Show remote URLs after each name +grh() { g reset --hard "$@"; } # grh: Reset the repository and the working directory +grH() { g reset HEAD "$@"; } # grH: Reset the index but not the working directory +#grH^() { g reset HEAD^ "$@"; } # grH^: Reset the index and working directory to the state of the HEAD's first parent +gs() { g status -sb "$@"; } # gs: Show the status of the working directory and the index +gsd() { g stash drop "$@"; } # gsd: Drop a stash +gsl() { g stash list --date=relative "$@"; } # gsl: List all stashes +gsp() { g stash pop "$@"; } # gsp: Apply and remove a single stash +gss() { g stash show "$@"; } # gss: Show changes recorded in the stash as a diff +gst() { g status "$@"; } # gst: Show the status of the working directory and the index +gsu() { g standup "$@"; } # gsu: Customized standup command +gforgotrecursive() { g submodule update --init --recursive --remote "$@"; } # gforgotrecursive: Update submodules recursively +gfp() { g commit --amend --no-edit && g push --force-with-lease "$@"; } # gfp: Amending the last commit and force-pushing + +# Temporarily unset GIT_WORK_TREE +function git_without_work_tree() { + # Only proceed if a git command is being run + if [ "$1" = "git" ]; then + shift + # Check if the current directory is inside a Git work tree + if git rev-parse --is-inside-work-tree &>/dev/null; then + # If inside a work tree, temporarily unset GIT_WORK_TREE + GIT_WORK_TREE_OLD="$GIT_WORK_TREE" + unset GIT_WORK_TREE + git "$@" + export GIT_WORK_TREE="$GIT_WORK_TREE_OLD" + else + # If not inside a work tree, call git command directly + git "$@" + fi + else + # If it's not a git command, just execute it normally + command "$@" + fi +} + +# Set alias conditionally +#alias git='git_without_work_tree git' + +# Set bare dotfiles repository git environment variables dynamically +function set_git_env_vars() { + # Do nothing unless ~/.cfg exists and is a bare git repo + [[ -d "$HOME/.cfg" ]] || return + git --git-dir="$HOME/.cfg" rev-parse --is-bare-repository &>/dev/null || return + + # Skip if last command was a package manager + if [[ "${(%)${(z)history[1]}}" =~ ^(pacman|yay|apt|dnf|brew|npm|pip|gem|go|cargo) ]]; then + return + fi + + # Only set env vars if not already inside another Git repo + if ! git rev-parse --is-inside-work-tree &>/dev/null; then + export GIT_DIR="$HOME/.cfg" + export GIT_WORK_TREE="$(realpath ~)" + else + unset GIT_DIR + unset GIT_WORK_TREE + fi +} + +# Hook and initial call +function chpwd() { set_git_env_vars } +set_git_env_vars + +# Git Subtrees +function gsp() { + # Config file for subtrees + # + # Format: + # <prefix>;<remote address>;<remote branch> + # # Lines starting with '#' will be ignored + GIT_SUBTREE_FILE="$PWD/.gitsubtrees" + + if [ ! -f "$GIT_SUBTREE_FILE" ]; then + echo "Nothing to do - file <$GIT_SUBTREE_FILE> does not exist." + return + fi + + if ! command -v config &> /dev/null; then + echo "Error: 'config' command not found. Make sure it's available in your PATH." + return + fi + + OLD_IFS=$IFS + IFS=$'\n' + for LINE in $(cat "$GIT_SUBTREE_FILE"); do + + # Skip lines starting with '#'. + if [[ $LINE = \#* ]]; then + continue + fi + + # Parse the current line. + PREFIX=$(echo "$LINE" | cut -d';' -f 1) + REMOTE=$(echo "$LINE" | cut -d';' -f 2) + BRANCH=$(echo "$LINE" | cut -d';' -f 3) + + # Pull from the remote. + echo "Executing: git subtree pull --prefix=$PREFIX $REMOTE $BRANCH" + if git subtree pull --prefix="$PREFIX" "$REMOTE" "$BRANCH"; then + echo "Subtree pull successful for $PREFIX." + else + echo "Error: Subtree pull failed for $PREFIX." + fi + done + + IFS=$OLD_IFS +} + +# Print previous command into a file +getlast () { + fc -nl $((HISTCMD - 1)) +} + +# Copy the current command to a file +copy_command_to_file() { + # Only write the last command if BUFFER is not empty + if [[ -n "$BUFFER" ]]; then + echo "$BUFFER" > ~/command_log.txt # Overwrite with the latest command + else + # If the buffer is empty, remove the previous log file + command rm -f ~/command_log.txt # Optionally remove the log if no command is present + fi +} + +# Display the latest command from the log in the user input +display_latest_command() { + if [[ -f ~/command_log.txt ]]; then + # Read the last command from the log + local last_command + last_command=$(< ~/command_log.txt) + + # Only display if the last command is not empty + if [[ -n "$last_command" ]]; then + BUFFER="$last_command" # Set the BUFFER to the last command + CURSOR=${#BUFFER} # Set the cursor to the end of the command + fi + fi + zle reset-prompt # Refresh the prompt +} + +# Go up a directory +go_up() { + copy_command_to_file # Copy the current command to a file + BUFFER="" # Clear the current command line + cd .. || return # Change directory and return if it fails + display_latest_command # Display the latest command in the user input +} + +# Initialize a variable to store the previous directory +previous_dir="" + +# Function to change directories +go_into() { + copy_command_to_file # Copy the current command to a file + + # Use fzf or another tool to choose the directory + local dir + dir=$( (ls -d */; echo "Go Last directory:") | fzf --height 40% --reverse --tac) # Include previous directory as an option + + if [[ -n "$dir" ]]; then + # Check if the user selected the previous directory + if [[ "$dir" == Previous:* ]]; then + cd - || return # Change to the previous directory + else + cd "${dir%/}" || return # Change directory if a selection is made (remove trailing slash) + fi + + # Save the current directory to previous_dir + previous_dir=$(pwd) # Update previous_dir to current directory after changing + BUFFER="" # Clear the current command line + display_latest_command # Display the last command if available + fi +} + +# Register functions as ZLE widgets +zle -N go_up +zle -N go_into + + +# XDG_GAMES_DIR: +# Path to user-dirs config +USER_DIRS_FILE="$HOME/.config/user-dirs.dirs" + +if [ -f "$USER_DIRS_FILE" ]; then + # Extract directory names from user-dirs config + _dirs=( + ${(f)"$(grep '^XDG_.*_DIR=' "$USER_DIRS_FILE" \ + | cut -d= -f2 \ + | tr -d '"' \ + | sed "s|^\$HOME/||")"} + ) + + _lowercase_count=0 + _total_count=0 + for d in "${_dirs[@]}"; do + [ -n "$d" ] || continue + _total_count=$(( _total_count + 1 )) + case "$d" in + [a-z0-9]*) + _lowercase_count=$(( _lowercase_count + 1 )) + ;; + esac + done + + # Require majority lowercase (≥70%) + if [ "$_total_count" -gt 0 ]; then + _percent=$(( 100 * _lowercase_count / _total_count )) + if [ "$_percent" -ge 70 ]; then + # Ensure the lowercase games directory exists + if [ ! -d "$HOME/games" ]; then + mkdir -p "$HOME/games" + fi + + # Create symbolic link if it doesn't already exist + if [ ! -L "$HOME/Games" ] && [ ! -d "$HOME/Games" ]; then + ln -s "$HOME/games" "$HOME/Games" + fi + + export XDG_GAMES_DIR="$HOME/games" + fi + fi + + unset _dirs _lowercase_count _total_count _percent +fi + + +# Enter directory and list contents +function cd-clear-ls() { + if [ -n "$1" ]; then + builtin cd "$@" 2>/dev/null || { echo "cd: no such file or directory: $1"; return 1; } + else + builtin cd ~ || return 1 + fi + + echo -e "\033[H\033[J" # Clear screen but keep scroll buffer + + if [ "$PWD" != "$HOME" ] && git rev-parse --is-inside-work-tree &>/dev/null; then + ls -a + else + ls + fi +} + +# cd using "up n" as a command up as many directories, example "up 3" +up() { + # default parameter to 1 if non provided + declare -i d=${@:-1} + # ensure given parameter is non-negative. Print error and return if it is + (( $d < 0 )) && (>&2 echo "up: Error: negative value provided") && return 1; + # remove last d directories from pwd, append "/" in case result is empty + cd "$(pwd | sed -E 's;(/[^/]*){0,'$d'}$;;')/"; +} + +# cd into $XDG_CONFIG_HOME/$1 directory +c() { + local root=${XDG_CONFIG_HOME:-~/.config} + local dname="$root/$1" + if [[ ! -d "$dname" ]]; then + return + fi + cd "$dname" +} + +# Make and cd into directory and any parent directories +mkcd () { + if [[ -z "$1" ]]; then + echo "Usage: mkcd <dir>" 1>&2 + return 1 + fi + mkdir -p "$1" + cd "$1" +} + +bak() { + if [[ -e "$1" ]]; then + echo "Found: $1" + mv "${1%.*}"{,.bak} + elif [[ -e "$1.bak" ]]; then + echo "Found: $1.bak" + mv "$1"{.bak,} + fi +} + +back() { + for file in "$@"; do + cp -r "$file" "$file".bak + done +} + +# tre is a shorthand for tree +tre() { + tree -aC -I \ + '.git|.hg|.svn|.tmux|.backup|.vim-backup|.swap|.vim-swap|.undo|.vim-undo|*.bak|tags' \ + --dirsfirst "$@" \ + | less +} + +# switch from/to project/package dir +pkg() { + if [ "$#" -eq 2 ]; then + ln -s "$(readlink -f $1)" "$(readlink -f $2)"/._pkg + ln -s "$(readlink -f $2)" "$(readlink -f $1)"/._pkg + else + cd "$(readlink -f ./._pkg)" + fi +} + +# Prepare C/C++ project for Language Server Protoco +lsp-prep() { + (cd build && cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON) \ + && ln -sf build/compile_commands.json +} + +reposize() { + url=`echo $1 \ + | perl -pe 's#(?:https?://github.com/)([\w\d.-]+\/[\w\d.-]+).*#\1#g' \ + | perl -pe 's#git\@github.com:([\w\d.-]+\/[\w\d.-]+)\.git#\1#g' + ` + printf "https://github.com/$url => " + curl -s https://api.github.com/repos/$url \ + | jq '.size' \ + | numfmt --to=iec --from-unit=1024 +} + +# Launch a program in a terminal without getting any output, +# and detach the process from terminal +# (can then close the terminal without terminating process) +-echo() { + "$@" &> /dev/null & disown +} + +# Reload shell +function reload() { + local compdump_files="$ZDOTDIR/.zcompdump*" + + if ls $compdump_files &> /dev/null; then + rm -f $compdump_files + fi + + exec $SHELL -l +} + +#pom() { +# local -r HOURS=${1:?} +# local -r MINUTES=${2:-0} +# local -r POMODORO_DURATION=${3:-25} +# +# bc <<< "(($HOURS * 60) + $MINUTES) / $POMODORO_DURATION" +#} + +#mnt() { +# local FILE="/mnt/external" +# if [ ! -z $2 ]; then +# FILE=$2 +# fi +# +# if [ ! -z $1 ]; then +# sudo mount "$1" "$FILE" -o rw +# echo "Device in read/write mounted in $FILE" +# fi +# +# if [ $# = 0 ]; then +# echo "You need to provide the device (/dev/sd*) - use lsblk" +# fi +#} +# +#umnt() { +# local DIRECTORY="/mnt" +# if [ ! -z $1 ]; then +# DIRECTORY=$1 +# fi +# MOUNTED=$(grep $DIRECTORY /proc/mounts | cut -f2 -d" " | sort -r) +# cd "/mnt" +# sudo umount $MOUNTED +# echo "$MOUNTED unmounted" +#} + +mntmtp() { + local DIRECTORY="$HOME/mnt" + if [ ! -z $2 ]; then + local DIRECTORY=$2 + fi + if [ ! -d $DIRECTORY ]; then + mkdir $DIRECTORY + fi + + if [ ! -z $1 ]; then + simple-mtpfs --device "$1" "$DIRECTORY" + echo "MTPFS device in read/write mounted in $DIRECTORY" + fi + + if [ $# = 0 ]; then + echo "You need to provide the device number - use simple-mtpfs -l" + fi +} + +umntmtp() { + local DIRECTORY="$HOME/mnt" + if ; then + DIRECTORY=$1 + fi + cd $HOME + umount $DIRECTORY + echo "$DIRECTORY with mtp filesystem unmounted" +} +duckduckgo() { + lynx -vikeys -accept_all_cookies "https://lite.duckduckgo.com/lite/?q=$@" +} + +wikipedia() { + lynx -vikeys -accept_all_cookies "https://en.wikipedia.org/wiki?search=$@" +} + +#function filesize() { +# # Check if 'du' supports the -b option, which provides sizes in bytes. +# if du -b /dev/null > /dev/null 2>&1; then +# local arg=-sbh; # If supported, use -sbh options for 'du'. +# else +# local arg=-sh; # If not supported, use -sh options for 'du'. +# fi +# +# # Check if no arguments are provided. +# if [ "$#" -eq 0 ]; then +# # Calculate and display sizes for all files and directories in cwd. +# du $arg ./* +# else +# # Calculate and display sizes for the specified files and directories. +# du $arg -- "$@" +# fi +#} +# + +fgl() { + git log --graph --color=always \ + --format="%C(auto)%h%d %s %C(black)%C(bold)%cr" "$@" | + fzf --ansi --no-sort --reverse --tiebreak=index --bind=ctrl-s:toggle-sort \ + --bind "ctrl-m:execute: + (grep -o '[a-f0-9]\{7\}' | head -1 | + xargs -I % sh -c 'git show --color=always % | less -R') << 'FZF-EOF' + {} +FZF-EOF" +} + +fgb() { + local branches branch + branches=$(git --no-pager branch -vv) && + branch=$(echo "$branches" | fzf +m) && + git checkout $(echo "$branch" | awk '{print $1}' | sed "s/.* //") +} + +# +--------+ +# | Pacman | +# +--------+ + +# TODO can improve that with a bind to switch to what was installed +fpac() { + pacman -Slq | fzf --multi --reverse --preview 'pacman -Si {1}' | xargs -ro sudo pacman -S +} + +fyay() { + yay -Slq | fzf --multi --reverse --preview 'yay -Si {1}' | xargs -ro yay -S +} + +# +------+ +# | tmux | +# +------+ + +fmux() { + prj=$(find $XDG_CONFIG_HOME/tmuxp/ -execdir bash -c 'basename "${0%.*}"' {} ';' | sort | uniq | nl | fzf | cut -f 2) + echo $prj + [ -n "$prj" ] && tmuxp load $prj +} + +# ftmuxp - propose every possible tmuxp session +ftmuxp() { + if [[ -n $TMUX ]]; then + return + fi + + # get the IDs + ID="$(ls $XDG_CONFIG_HOME/tmuxp | sed -e 's/\.yml$//')" + if [[ -z "$ID" ]]; then + tmux new-session + fi + + create_new_session="Create New Session" + + ID="${create_new_session}\n$ID" + ID="$(echo $ID | fzf | cut -d: -f1)" + + if [[ "$ID" = "${create_new_session}" ]]; then + tmux new-session + elif [[ -n "$ID" ]]; then + # Change name of urxvt tab to session name + printf '\033]777;tabbedx;set_tab_name;%s\007' "$ID" + tmuxp load "$ID" + fi +} + +# ftmux - help you choose tmux sessions +ftmux() { + if [[ ! -n $TMUX ]]; then + # get the IDs + ID="`tmux list-sessions`" + if [[ -z "$ID" ]]; then + tmux new-session + fi + create_new_session="Create New Session" + ID="$ID\n${create_new_session}:" + ID="`echo $ID | fzf | cut -d: -f1`" + if [[ "$ID" = "${create_new_session}" ]]; then + tmux new-session + elif [[ -n "$ID" ]]; then + printf '\033]777;tabbedx;set_tab_name;%s\007' "$ID" + tmux attach-session -t "$ID" + else + : # Start terminal normally + fi + fi +} + +# +-------+ +# | Other | +# +-------+ + +# List install files for dotfiles +fdot() { + file=$(find "$DOTFILES/install" -exec basename {} ';' | sort | uniq | nl | fzf | cut -f 2) + [ -n "$file" ] && "$EDITOR" "$DOTFILES/install/$file" +} + +# List projects +fwork() { + result=$(find ~/workspace/* -type d -prune -exec basename {} ';' | sort | uniq | nl | fzf | cut -f 2) + [ -n "$result" ] && cd ~/workspace/$result +} + +# Open pdf with Zathura +fpdf() { + result=$(find -type f -name '*.pdf' | fzf --bind "ctrl-r:reload(find -type f -name '*.pdf')" --preview "pdftotext {} - | less") + [ -n "$result" ] && nohup zathura "$result" &> /dev/null & disown +} + +# Open epubs with Zathura +fepub() { + result=$(find -type f -name '*.epub' | fzf --bind "ctrl-r:reload(find -type f -name '*.epub')") + [ -n "$result" ] && nohup zathura "$result" &> /dev/null & disown +} + +# Search and find directories in the dir stack +fpop() { + # Only work with alias d defined as: + + # alias d='dirs -v' + # for index ({1..9}) alias "$index"="cd +${index}"; unset index + + d | fzf --height="20%" | cut -f 1 | source /dev/stdin +} + +#ip() { +# emulate -LR zsh +# +# if [[ $1 == 'get' ]]; then +# res=$(curl -s ipinfo.io/ip) +# echo -n $res | xsel --clipboard +# echo "copied $res to clipboard" +# # only run ip if it exists +# elif (( $+commands[ip] )); then +# command ip $* +# fi +#} + +ssh-create() { + if [ ! -z "$1" ]; then + ssh-keygen -f $HOME/.ssh/$1 -t rsa -N '' -C "$1" + chmod 700 $HOME/.ssh/$1* + fi +} + +guest() { + local guest="$1" + shift + + local port + if [[ "$#" -ge 2 && "${@: -1}" =~ ^[0-9]+$ ]]; then + port="${@: -1}" + set -- "${@:1:$(($#-1))}" + fi + + if [[ -z "$guest" || "$#" -lt 1 ]]; then + echo "Send file(s) or directories to remote machine" + echo "Usage: guest <guest-alias> <file-or-directory>... [port]" + return 1 + fi + + # Auto-detect port + if [[ -z "$port" ]]; then + if nc -z localhost 22220 2>/dev/null; then + port=22220 + elif nc -z localhost 22 2>/dev/null; then + port=22 + else + echo "No known SSH port (22220 or 22) is open. Specify a port manually." + return 1 + fi + fi + + for src in "$@"; do + src="${src/#\~/$HOME}" + if [[ ! -e "$src" ]]; then + echo "Error: '$src' does not exist." + continue + fi + + local abs_path dest_dir rel_dir rsync_src rsync_dest + + abs_path=$(realpath "$src") + rel_dir="${abs_path#$HOME/}" + dest_dir=$(dirname "$rel_dir") + + # Ensure target dir exists remotely + ssh -p "$port" "$guest" "mkdir -p ~/$dest_dir" + + if [[ -d "$src" ]]; then + # Add trailing slash to copy contents instead of nesting the dir + rsync_src="${src%/}/" + rsync_dest="~/$rel_dir/" + else + rsync_src="$src" + rsync_dest="~/$dest_dir/" + fi + + echo "Sending '$src' to '$guest:$rsync_dest'..." + rsync -avz -e "ssh -p $port" "$rsync_src" "$guest:$rsync_dest" + done +} +historystat() { + history 0 | awk '{print $2}' | sort | uniq -c | sort -n -r | head +} + +promptspeed() { + for i in $(seq 1 10); do /usr/bin/time zsh -i -c exit; done +} + + +matrix() { + local lines=$(tput lines) + cols=$(tput cols) + + # Check if tmux is available + if command -v tmux > /dev/null; then + # Save the current status setting + local status_setting=$(tmux show -g -w -v status) + + # Turn off tmux status + tmux set -g status off + else + echo "tmux is not available. Exiting." + return 1 + fi + + # Function to restore terminal state + restore_terminal() { + # Clear the screen + clear + + # Bring back tmux status to its original setting + if command -v tmux > /dev/null; then + tmux set -g status "$status_setting" + fi + } + + trap 'restore_terminal' INT + + awkscript=' + { + letters="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*()" + lines=$1 + random_col=$3 + c=$4 + letter=substr(letters,c,1) + cols[random_col]=0; + for (col in cols) { + line=cols[col]; + cols[col]=cols[col]+1; + printf "\033[%s;%sH\033[2;32m%s", line, col, letter; + printf "\033[%s;%sH\033[1;37m%s\033[0;0H", cols[col], col, letter; + if (cols[col] >= lines) { + cols[col]=0; + } + } + } + ' + + echo -e "\e[1;40m" + clear + + while :; do + echo $lines $cols $(( $RANDOM % $cols)) $(( $RANDOM % 72 )) + sleep 0.05 + done | awk "$awkscript" + + # Restore terminal state + restore_terminal +} + +## Reload shell +function reload() { + local compdump_files="$ZDOTDIR/.zcompdump*" + + if ls $compdump_files &> /dev/null; then + rm -f $compdump_files + fi + + exec $SHELL -l +} +## Generate a secure password +function passgen() { + LC_ALL=C tr -dc ${1:-"[:graph:]"} < /dev/urandom | head -c ${2:-20} +} +## Encode/Decode string using base64 +function b64e() { + echo "$@" | base64 +} + +function b64d() { + echo "$@" | base64 -D +} +# Search through all man pages +function fman() { + man -k . | fzf -q "$1" --prompt='man> ' --preview $'echo {} | tr -d \'()\' | awk \'{printf "%s ", $2} {print $1}\' | xargs -r man' | tr -d '()' | awk '{printf "%s ", $2} {print $1}' | xargs -r man +} +# Back up a file. Usage "backupthis <filename>" +backupthis() { + cp -riv $1 ${1}-$(date +%Y%m%d%H%M).backup; +} + +# Spawn a clone of current terminal +putstate () { + declare +x >~/environment.tmp + declare -x >>~/environment.tmp + echo cd "$PWD" >>~/environment.tmp +} + +getstate () { + . ~/environment.tmp +} + + +# Tmux layout +openSession () { + tmux split-window -h -t + tmux split-window -v -t + tmux resize-pane -U 5 +} + +# archive compress +compress() { + if [[ -n "$1" ]]; then + local file=$1 + shift + case "$file" in + *.tar ) tar cf "$file" "$*" ;; + *.tar.bz2 ) tar cjf "$file" "$*" ;; + *.tar.gz ) tar czf "$file" "$*" ;; + *.tgz ) tar czf "$file" "$*" ;; + *.zip ) zip "$file" "$*" ;; + *.rar ) rar "$file" "$*" ;; + * ) tar zcvf "$file.tar.gz" "$*" ;; + esac + else + echo 'usage: compress <foo.tar.gz> ./foo ./bar' + fi +} + +extract() { + if [[ -f "$1" ]] ; then + local filename + filename=$(basename "$1") + local foldername="${filename%%.*}" + local fullpath + fullpath=$(perl -e 'use Cwd "abs_path";print abs_path(shift)' "$1") + local didfolderexist=false + + if [[ -d "$foldername" ]]; then + didfolderexist=true + read -p "$foldername already exists, do you want to overwrite it? (y/n) " -n 1 + echo + if [[ "$REPLY" =~ ^[Nn]$ ]]; then + return + fi + fi + + mkdir -p "$foldername" && cd "$foldername" || return + + case "$1" in + *.tar.bz2) tar xjf "$fullpath" ;; + *.tar.gz) tar xzf "$fullpath" ;; + *.tar.xz) tar Jxf "$fullpath" ;; + *.tar.Z) tar xzf "$fullpath" ;; + *.tar) tar xf "$fullpath" ;; + *.taz) tar xzf "$fullpath" ;; + *.tb2) tar xjf "$fullpath" ;; + *.tbz) tar xjf "$fullpath" ;; + *.tbz2) tar xjf "$fullpath" ;; + *.tgz) tar xzf "$fullpath" ;; + *.txz) tar Jxf "$fullpath" ;; + *.rar) unrar x -o+ "$fullpath" >/dev/null ;; + *.zip) unzip -o "$fullpath" ;; + *) + echo "'$1' cannot be extracted via extract()" \ + && cd .. \ + && ! "$didfolderexist" \ + && rm -r "$foldername" + ;; + esac + else + echo "'$1' is not a valid file" + fi +} + +ports() { + local result + result=$(sudo netstat -tulpn | grep LISTEN) + echo "$result" | fzf +} + +trash() { + case "$1" in + --list) + ls -A1 ~/.local/share/Trash/files/ + ;; + --empty) + ls -A1 ~/.local/share/Trash/files/ && \rm -rfv ~/.local/share/Trash/files/* + ;; + --restore) + gio trash --restore "$(gio trash --list | fzf | cut -f 1)" + ;; + --delete) + trash_files=$(ls -A ~/.local/share/Trash/files/ | fzf --multi); echo $trash_files | xargs -I {} rm -rf ~/.local/share/Trash/files/{} + ;; + *) + gio trash "$@" + ;; + esac +} + +what() { + type "$1" + echo "$PATH" +} + +shutdown() { + if [ "$#" -eq 0 ]; then + sudo /sbin/shutdown -h now + else + sudo /sbin/shutdown -h "$@" + fi +} + +windowManagerName () { + local window=$( + xprop -root -notype + ) + + local identifier=$( + echo "${window}" | + awk '$1=="_NET_SUPPORTING_WM_CHECK:"{print $5}' + ) + + local attributes=$( + xprop -id "${identifier}" -notype -f _NET_WM_NAME 8t + ) + + local name=$( + echo "${attributes}" | + grep "_NET_WM_NAME = " | + cut --delimiter=' ' --fields=3 | + cut --delimiter='"' --fields=2 + ) + + echo "${name}" +} + +logout() { + local wm + wm="$(windowManagerName)" + if [[ -n "$wm" ]]; then + echo "Logging out by killing window manager: $wm" + pkill "$wm" + else + echo "No window manager detected!" >&2 + fi +} + +# Gentoo +emg() { + if [[ -z "$1" ]]; then + echo "Usage: emg [USE_FLAGS] package [package...]" + return 1 + fi + + if [[ "$1" =~ ^[^-].* ]]; then + local use_flags="$1" + shift + sudo USE="$use_flags" emerge -av "$@" + else + sudo emerge -av "$@" + fi +} + +# Remove command from history +forget () { # Accepts one history line number as argument or search term + if [[ -z "$1" ]]; then + echo "Usage: hist <history_number> | hist -s <search_term>" + return 1 + fi + + if [[ "$1" == "-s" ]]; then + if [[ -z "$2" ]]; then + echo "Usage: hist -s <search_term>" + return 1 + fi + + local search_term="$2" + + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + LC_ALL=C sed -i "/${search_term}/d" "$HISTFILE" # GNU sed + else + LC_ALL=C sed -i '' "/${search_term}/d" "$HISTFILE" # BSD/macOS sed + fi + + fc -R "$HISTFILE" + echo "Deleted all history entries matching '$search_term'." + else + local num=$1 + local cmd=$(fc -ln $num $num 2>/dev/null) + + if [[ -z "$cmd" ]]; then + echo "No history entry found for index $num" + return 1 + fi + + history -d $num + + local escaped_cmd=$(echo "$cmd" | sed 's/[\/&]/\\&/g') + + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + LC_ALL=C sed -i "/${escaped_cmd}/d" "$HISTFILE" + else + LC_ALL=C sed -i '' "/${escaped_cmd}/d" "$HISTFILE" + fi + + fc -R "$HISTFILE" + echo "Deleted '$cmd' from history." + fi +} + +# Remove hist command itself +remove_hist_command() { + [[ $1 != 'hist '* ]] +} + +remove_hist_command + + +search() { + # Search for a pattern in the specified directory (non-recursive). + dir="${1:-.}" + ls -1 "$dir" | grep -i "$2" +} + +deepsearch() { + # Perform a recursive search for a pattern in the specified directory. + dir="${1:-.}" + find "$dir" -iname "$2" +} + +notes() { + local base_dir="$HOME/documents/main/" + + if [[ -z "$1" ]]; then + # No argument → cd to notes directory + cd "$base_dir" || return + return + fi + + local target="$1" # The argument itself + + # Use find to check if the file exists anywhere in the base directory + local found_files=($(find "$base_dir" -type f -name "$target")) + + if [[ ${#found_files[@]} -eq 1 ]]; then + # Only one match found, open it directly + $EDITOR "${found_files[0]}" + elif [[ ${#found_files[@]} -gt 1 ]]; then + # Multiple files found, prompt the user to select one + echo "Multiple files found for '$target'. Please choose one:" + PS3="Please enter a number to select a file (1-${#found_files[@]}): " + select selected_file in "${found_files[@]}"; do + if [[ -n "$selected_file" ]]; then + $EDITOR "$selected_file" + break + else + echo "Invalid selection, try again." + fi + done + else + # If no match found, search for a directory + local found_dir=$(find "$base_dir" -type d -name "$target" -print -quit) + + if [[ -n "$found_dir" ]]; then + # Directory found, cd into it + cd "$found_dir" || return + else + # If no match found, create the file and open it + local full_target="$base_dir/$target" + mkdir -p "$(dirname "$full_target")" + $EDITOR "$full_target" + fi + fi +} + +# Enable tab completion for files and directories +_notes_complete() { + local base_dir="$HOME/documents/main" + compadd -o nospace -- $(find "$base_dir" -type f -o -type d -printf '%P\n') +} + +compdef _notes_complete notes + + +ship() { + local binary_dir="$HOME/.local/share" + local bin_symlink_dir="$HOME/.local/bin" + local project_dirs=( + "$HOME/projects/" + "$HOME/src/" + "$HOME/src/site/" + ) + + mkdir -p "$binary_dir" "$bin_symlink_dir" + + local project_dir="" + + if [[ -n "$1" ]]; then + # Project name specified + for dir in "${project_dirs[@]}"; do + if [[ -d "$dir/$1" ]]; then + project_dir="$dir/$1" + break + fi + done + + if [[ -z "$project_dir" ]]; then + echo "Project '$1' not found." + return 1 + fi + else + # No argument: pick latest edited + local bin_file + bin_file=$(find "${project_dirs[@]}" -type f -name "Cargo.toml" -exec stat --format="%Y %n" {} \; 2>/dev/null | sort -nr | head -n1 | cut -d' ' -f2-) + + if [[ -z "$bin_file" ]]; then + echo "No Cargo.toml found." + return 1 + fi + + project_dir=$(dirname "$bin_file") + fi + + cd "$project_dir" || return + echo "Building project in $project_dir..." + + # Build it + cargo build --release || { echo "Build failed"; return 1; } + + # Assume binary has same name as project dir + local binary_name + binary_name=$(basename "$project_dir") + local built_binary="target/release/$binary_name" + + if [[ -x "$built_binary" ]]; then + echo "Copying $built_binary to $binary_dir/$binary_name" + cp "$built_binary" "$binary_dir/$binary_name" + + # Create/Update symlink + local symlink_path="$bin_symlink_dir/$binary_name" + ln -sf "$binary_dir/$binary_name" "$symlink_path" + + echo "Binary is now at: $binary_dir/$binary_name" + echo "Symlink created at: $symlink_path" + else + echo "Built binary not found: $built_binary" + echo "You may need to manually specify the output binary." + fi +} + + +forge() { + + local install=no + local usage="Usage: forge [--install]" + + # Handle --install flag + if [[ "$1" == "--install" ]]; then + install=yes + shift + elif [[ "$1" == "-h" || "$1" == "--help" ]]; then + echo "$usage" + return 0 + fi + + if [[ -f "CMakeLists.txt" ]]; then + echo "📦 CMake project detected" + [[ ! -d build ]] && mkdir build + cmake -B build -DCMAKE_BUILD_TYPE=Release || return 1 + cmake --build build || return 1 + [[ "$install" == "yes" ]] && sudo cmake --install build + + elif [[ -f "meson.build" ]]; then + echo "📦 Meson project detected" + if [[ ! -d build ]]; then + meson setup build || return 1 + fi + ninja -C build || return 1 + [[ "$install" == "yes" ]] && sudo ninja -C build install + + elif [[ -f "Makefile" ]]; then + echo "📦 Makefile project detected" + # Try `make all`, fallback to `make` if `all` fails + if make -q all 2>/dev/null; then + make all || return 1 + else + make || return 1 + fi + [[ "$install" == "yes" ]] && sudo make install + + else + echo "❌ No supported build system found." + return 1 + fi +} + +# Windows Path: +windows_home() { + for dir in /mnt/windows/Users/*(N); do + base=${dir:t} # `:t` is zsh's "tail" = basename + if [[ -d $dir && ! $base =~ ^(All Users|Default|Default User|Public|nx|desktop.ini)$ ]]; then + echo "$dir" + return 0 + fi + done + return 1 +} + +if winhome_path=$(windows_home); then + hash -d winhome="$winhome_path" +fi + +# Allow nnn filemanager to cd on quit +nnn() { + declare -x +g NNN_TMPFILE=$(mktemp --tmpdir $0.XXXX) + trap "command rm -f $NNN_TMPFILE" EXIT + =nnn $@ + [ -s $NNN_TMPFILE ] && source $NNN_TMPFILE +} diff --git a/common/config/zsh/user/options.zsh b/common/config/zsh/user/options.zsh new file mode 100644 index 0000000..99840d7 --- /dev/null +++ b/common/config/zsh/user/options.zsh @@ -0,0 +1,66 @@ +# Recursion limits +FUNCNEST=999 + +DISABLE_MAGIC_FUNCTIONS=true + +# Enable various options for Zsh behavior +setopt interactive_comments # Allow comments to appear in interactive mode +unsetopt BEEP # Disable the system beep (to prevent annoying beeps) +setopt extendedglob # Enable extended globbing for complex pattern matching +setopt nomatch # Prevent errors when a glob pattern doesn't match any files +setopt notify # Notify when background jobs complete +setopt completeinword # Allow tab completion within words +setopt prompt_subst # Allow prompt variables to be substituted + +# Enable automatic directory navigation +setopt autocd # Automatically change to a directory if the directory name is typed alone +setopt AUTO_PUSHD # Save more directory history, and use "cd -" with tab completion + +# Hide history of commands starting with a space +setopt histignorespace # Do not save commands that start with a space in the history + + +# --- Detect terminal control characters and behavior --- + +# Set these to true/false to run on every new tmux/terms +: ${CHECK_ON_TMUX_CHANGES:=false} +: ${CHECK_ON_NEW_INSTANCES:=false} + +# Fast terminal fingerprinting for optimizing prompt rendering +function initialize_terminal_fingerprint() { + # --- Fast terminal fingerprint --- + TERM_BASIC="$TERM-$COLORTERM" + TERM_TMUX="" + [[ "$CHECK_ON_TMUX_CHANGES" == "true" && -n "$TMUX" ]] && TERM_TMUX="-tmux$TMUX_PANE" + TERM_INSTANCE="" + [[ "$CHECK_ON_NEW_INSTANCES" == "true" ]] && TERM_INSTANCE="-$$" + # Combine fingerprint parts only if they're non-empty (faster than function call) + CURRENT_TERM_FINGERPRINT="${TERM_BASIC}${TERM_TMUX}${TERM_INSTANCE}" + # Only run detection if terminal has changed (single comparison) + if [[ "$CURRENT_TERM_FINGERPRINT" != "$LAST_TERM_FINGERPRINT" ]]; then + export LAST_TERM_FINGERPRINT="$CURRENT_TERM_FINGERPRINT" + # Fast reset + export CTRL_C_SIGINT=false + export CTRL_V_PASTE=false + # Fast SIGINT check (single command, no pipes) + INTR_CHAR=$(stty -a 2>/dev/null | sed -n 's/.*intr = \([^;]*\);.*/\1/p' | tr -d ' ') + [[ "$INTR_CHAR" == "^C" ]] && export CTRL_C_SIGINT=true + # Check if Ctrl+V is bound to lnext terminal function + LNEXT_CHAR=$(stty -a 2>/dev/null | sed -n 's/.*lnext = \([^;]*\);.*/\1/p' | tr -d ' ') + # If lnext is NOT ^V, then Ctrl+V might work as paste + if [[ "$LNEXT_CHAR" != "^V" ]]; then + # Check if clipboard tools exist + if [[ -n "$WAYLAND_DISPLAY" && -x "$(command -v wl-paste)" ]]; then + export CTRL_V_PASTE=true + elif [[ -n "$DISPLAY" && -x "$(command -v xclip)" ]]; then + export CTRL_V_PASTE=true + fi + fi + # Print status only if debug is enabled + [[ -n "$DEBUG_TERM_DETECT" ]] && echo "Terminal: CTRL_C_SIGINT=$CTRL_C_SIGINT CTRL_V_PASTE=$CTRL_V_PASTE" + fi +} + +# Initialize terminal fingerprint on startup +initialize_terminal_fingerprint + diff --git a/common/config/zsh/user/prompt.zsh b/common/config/zsh/user/prompt.zsh new file mode 100644 index 0000000..c55a835 --- /dev/null +++ b/common/config/zsh/user/prompt.zsh @@ -0,0 +1,679 @@ +#!/bin/zsh + +########## Prompt(s) ########## + +# Autoload necessary functions for vcs_info and coloring +autoload -Uz vcs_info +autoload -Uz add-zsh-hook +autoload -U colors && colors + +# Enable prompt substitution +setopt prompt_subst + +# Display git branch status and color +precmd_vcs_info() { vcs_info } + +# Add vcs_info to precmd functions +precmd_functions+=( precmd_vcs_info ) + +# Manipulates cursor position: moves down by 2 lines, saves position, and restores cursor after an operation. +terminfo_down_sc=$terminfo[cud1]$terminfo[cuu1]$terminfo[sc]$terminfo[cud1] + +# Track last executed command for exit code display +typeset -g _last_executed_command="" +typeset -g _cmd_start_time=0 +typeset -g _cmd_end_time=0 +typeset -g _cmd_duration=0 +typeset -g _spinner_idx=0 +typeset -ga _spinner_frames=('⣾' '⣽' '⣻' '⢿' '⡿' '⣟' '⣯' '⣷') +typeset -g _cmd_is_running=0 +typeset -g _show_spinner=0 +typeset -g _SPINNER_DELAY=5 # Only show spinner after 5 seconds +typeset -g _FINISHED_DELAY=10 # Only show finished message after 10 seconds + +# Register the ZLE widget for spinner updates - do this early +zle -N update_spinner + +# Cache git information to avoid repeated expensive operations +typeset -g _git_cached_info="" +typeset -g _git_cache_timestamp=0 +typeset -g _git_cache_lifetime=2 # seconds before cache expires + +# Calculate how much space is available for the prompt components +function available_space() { + local width=${COLUMNS:-80} + echo $width +} + +# Check if we need to abbreviate git info +function need_to_abbreviate_git() { + local available=$(available_space) + local vi_mode_len=13 # Length of "-- INSERT --" + local prompt_base_len=20 # Base prompt elements length + local path_len=${#${PWD/#$HOME/\~}} + local git_full_len=0 + + # Try to estimate git info length if available + if git rev-parse --is-inside-work-tree &>/dev/null; then + local branch=$(git symbolic-ref --short HEAD 2>/dev/null) + git_full_len=${#branch} + + # Add length for status indicators + if [[ -n "$(git status --porcelain)" ]]; then + # Rough estimate for status text + git_full_len=$((git_full_len + 20)) + fi + fi + + # Calculate total space needed + local total_needed=$((vi_mode_len + prompt_base_len + path_len + git_full_len)) + + # Determine if we need to abbreviate + if [[ $total_needed -gt $available ]]; then + return 0 # Need to abbreviate + else + return 1 # Don't need to abbreviate + fi +} + +# Custom git branch coloring based on status +git_branch_test_color() { + local now=$(date +%s) + local cache_age=$((now - _git_cache_timestamp)) + + # Use cached value if available and not expired + if [[ -n "$_git_cached_info" && $cache_age -lt $_git_cache_lifetime ]]; then + echo "$_git_cached_info" + return + fi + + local ref=$(git symbolic-ref --short HEAD 2> /dev/null) + if [ -n "${ref}" ]; then + if [ -n "$(git status --porcelain)" ]; then + local gitstatuscolor='%F{green}' + else + local gitstatuscolor='%F{82}' + fi + _git_cached_info="${gitstatuscolor}${ref}" + _git_cache_timestamp=$now + echo "$_git_cached_info" + else + _git_cached_info="" + _git_cache_timestamp=$now + echo "" + fi +} + +# Git branch with dynamic abbreviation +git_branch_dynamic() { + local now=$(date +%s) + local cache_age=$((now - _git_cache_timestamp)) + + # Only query git if cache is expired + if [[ $cache_age -ge $_git_cache_lifetime ]]; then + local ref=$(git symbolic-ref --short HEAD 2> /dev/null) + if [ -n "${ref}" ]; then + if need_to_abbreviate_git; then + # Abbreviated version for small terminals + case "${ref}" in + "main") _git_cached_info="m" ;; + "master") _git_cached_info="m" ;; + "development") _git_cached_info="d" ;; + "develop") _git_cached_info="d" ;; + "feature/"*) _git_cached_info="f/${ref#feature/}" | cut -c 1-4 ;; + "release/"*) _git_cached_info="r/${ref#release/}" | cut -c 1-4 ;; + "hotfix/"*) _git_cached_info="h/${ref#hotfix/}" | cut -c 1-4 ;; + *) _git_cached_info="${ref}" | cut -c 1-5 ;; # Truncate to first 5 chars for other branches + esac + else + # Full branch name when there's room + _git_cached_info="${ref}" + fi + _git_cache_timestamp=$now + echo "$_git_cached_info" + else + _git_cached_info="" + _git_cache_timestamp=$now + echo "" + fi + else + echo "$_git_cached_info" + fi +} + +# VCS info styles (e.g., git) +zstyle ':vcs_info:*' check-for-changes true +zstyle ':vcs_info:*' enable git + +# Dynamically configure vcs_info formats based on available space +function configure_vcs_styles() { + if need_to_abbreviate_git; then + # Abbreviated versions + zstyle ':vcs_info:*' stagedstr ' +%F{15}s%f' + zstyle ':vcs_info:*' unstagedstr ' -%F{15}u%f' + else + # Full versions + zstyle ':vcs_info:*' stagedstr ' +%F{15}staged%f' + zstyle ':vcs_info:*' unstagedstr ' -%F{15}unstaged%f' + fi + + zstyle ':vcs_info:*' actionformats '%F{5}%F{2}%b%F{3}|%F{1}%a%F{5}%f ' + zstyle ':vcs_info:*' formats '%F{208} '$'\uE0A0'' %f$(git_branch_test_color)%f%F{76}%c%F{3}%u%f ' + zstyle ':vcs_info:git*+set-message:*' hooks git-untracked git-dynamic +} + +# Show "untracked" status in git - with conditional abbreviation ++vi-git-untracked() { + if [[ $(git rev-parse --is-inside-work-tree 2> /dev/null) == 'true' ]] && \ + git status --porcelain | grep '??' &> /dev/null ; then + + if need_to_abbreviate_git; then + hook_com[unstaged]+='%F{196} !%f%F{15}u%f' + else + hook_com[unstaged]+='%F{196} !%f%F{15}untracked%f' + fi + fi +} + +# Dynamic git branch hook ++vi-git-dynamic() { + hook_com[branch]=$(git_branch_dynamic) +} + +# SSH info with conditional abbreviation +ssh_name() { + if [[ -n $SSH_CONNECTION ]]; then + local ssh_info + + if need_to_abbreviate_git; then + # Abbreviated SSH info + ssh_info="ssh:%F{green}%n$nc%f" + else + ssh_info="ssh:%F{green}%n$nc%f" + if [[ -n $SSH_CONNECTION ]]; then + local ip_address + ip_address=$(echo $SSH_CONNECTION | awk '{print $3}') + ssh_info="$ssh_info@%F{green}$ip_address%f" + fi + fi + echo " ${ssh_info}" + fi +} + +# Job names (for job control) with conditional abbreviation +function job_name() { + job_name="" + job_length=0 + local available=$(available_space) + + # Only show jobs if we have reasonable space + if [ "${available}" -gt 60 ]; then + local job_count=$(jobs | wc -l) + if [ "${job_count}" -gt 0 ]; then + if need_to_abbreviate_git; then + job_name+="%F{green}j:${job_count}%f" + else + local title_jobs="jobs:" + job_name="${title_jobs}" + job_length=$((${available}-70)) + [ "${job_length}" -lt "0" ] && job_length=0 + + if [ "${job_length}" -gt 0 ]; then + job_name+="%F{green}$(jobs | grep + | tr -s " " | cut -d " " -f 4- | cut -b 1-${job_length} | sed "s/\(.*\)/\1/")%f" + else + job_name+="%F{green}${job_count}%f" + fi + fi + fi + fi + + echo "${job_name}" +} + +# Check if we should show the spinner based on elapsed time +function should_show_spinner() { + if [[ $_cmd_is_running -eq 1 ]]; then + local current_time=$(date +%s) + local elapsed=$((current_time - _cmd_start_time)) + + # Show spinner only after delay threshold + if [[ $elapsed -ge $_SPINNER_DELAY ]]; then + _show_spinner=1 + return 0 # Yes, show spinner + fi + fi + + _show_spinner=0 + return 1 # No, don't show spinner +} + +# Update spinner animation - simplified version +function update_spinner() { + # This function is now just a ZLE widget placeholder + # The actual spinner updates happen in the TRAPALRM handler + : +} + +# Start spinner timer when command runs longer than threshold +function start_spinner_timer() { + _spinner_idx=0 + _cmd_is_running=1 + _show_spinner=0 # Start with spinner hidden until delay passes + + # Set up the TRAPALRM for periodic updates - CRITICAL FIX + TMOUT=0.5 # Update spinner every 0.5 seconds + + # Define TRAPALRM function - this is key to the spinner working + TRAPALRM() { + if [[ $_cmd_is_running -eq 1 ]]; then + local current_time=$(date +%s) + local elapsed=$((current_time - _cmd_start_time)) + + # Show spinner only after delay threshold + if [[ $elapsed -ge $_SPINNER_DELAY ]]; then + _show_spinner=1 + _spinner_idx=$(( (_spinner_idx + 1) % ${#_spinner_frames[@]} )) + + # Force prompt refresh - critical for updating the spinner + if [[ -o zle ]]; then + zle reset-prompt 2>/dev/null || true + zle -R + fi + fi + fi + } +} + +# Stop spinner when command finishes +function stop_spinner_timer() { + _cmd_is_running=0 + _show_spinner=0 + + # Disable the alarm trap and timer + TRAPALRM() { : } + TMOUT=0 + + # Force prompt refresh to clear spinner + if [[ -o zle ]]; then + zle reset-prompt 2>/dev/null || true + zle -R + fi +} + +# Format time in a human-readable way +function format_time() { + local seconds=$1 + local result="" + + # Format time as hours:minutes:seconds for long durations + if [[ $seconds -ge 3600 ]]; then + local hours=$((seconds / 3600)) + local minutes=$(( (seconds % 3600) / 60 )) + local secs=$((seconds % 60)) + result="${hours}h${minutes}m${secs}s" + elif [[ $seconds -ge 60 ]]; then + local minutes=$((seconds / 60)) + local secs=$((seconds % 60)) + result="${minutes}m${secs}s" + else + result="${seconds}s" + fi + + echo "$result" +} + +# Error code display for RPROMPT with spinner - fixed version +function exit_code_info() { + local exit_code=$? + + # If a command is running and we should show spinner + if [[ $_cmd_is_running -eq 1 && $_show_spinner -eq 1 ]]; then + local spinner=${_spinner_frames[$_spinner_idx]} + local current_time=$(date +%s) + local elapsed=$((current_time - _cmd_start_time)) + echo "%F{yellow}${spinner} ${elapsed}s%f" + return + fi + + # Don't show error code when line editor is active (user is typing) + if [[ -o zle ]]; then + echo "" + return + fi + + # Show command finished message for completed commands that took longer than threshold + if [[ -n "$_last_executed_command" && $_cmd_duration -ge $_FINISHED_DELAY ]]; then + local duration_formatted=$(format_time $_cmd_duration) + + # Show error code along with finished message if there was an error + if [[ $exit_code -ne 0 ]]; then + # Show TSTP (148) as a suspension indicator instead of error + if [[ $exit_code -eq 148 ]]; then + echo "%F{cyan}finished ${duration_formatted}%f %F{yellow}⏸ TSTP%f" + return + fi + + local signal_name="" + # Check if it's a signal + if [[ $exit_code -gt 128 && $exit_code -le 165 ]]; then + local signal_num=$((exit_code - 128)) + signal_name=$(kill -l $signal_num 2>/dev/null) + if [[ -n "$signal_name" ]]; then + signal_name=" ($signal_name)" + fi + fi + + # Return formatted error code with finished message + echo "%F{cyan}finished ${duration_formatted}%f %F{red}✘ $exit_code$signal_name%f" + else + echo "%F{cyan}finished ${duration_formatted}%f %F{green}✓%f" + fi + return + fi + + # Don't show anything for exit code 0 (success) if this is first command + if [[ -z "$_last_executed_command" && $exit_code -eq 0 ]]; then + echo "" + return + fi + + # Show TSTP (148) as a suspension indicator instead of error + if [[ $exit_code -eq 148 ]]; then + echo "%F{yellow}⏸ TSTP%f" + return + fi + + if [[ $exit_code -ne 0 ]]; then + local signal_name="" + + # Check if it's a signal + if [[ $exit_code -gt 128 && $exit_code -le 165 ]]; then + local signal_num=$((exit_code - 128)) + signal_name=$(kill -l $signal_num 2>/dev/null) + if [[ -n "$signal_name" ]]; then + signal_name=" ($signal_name)" + fi + fi + + # Return formatted error code + echo "%F{red}✘ $exit_code$signal_name%f" + else + echo "%F{green}✓%f" # Success indicator + fi +} + +abbreviated_path() { + local full_path="${PWD/#$HOME/~}" # Replace $HOME with ~ + local available=$(available_space) + + # If path is root + if [[ "$full_path" == "/" ]]; then + echo "%F{4}/%f" + return + fi + + # If path is just ~ + if [[ "$full_path" == "~" ]]; then + echo "%F{4}~%f" + return + fi + + # If extremely small terminal, show nothing to avoid breaking prompt + if (( available < 20 )); then + echo "" + return + fi + + # For very narrow terminals, just show the current dir + if (( available < 30 )); then + echo "%F{4}%1~%f" + return + fi + + # For moderately narrow terminals, show last two components + if (( available < 40 )); then + echo "%F{4}%2~%f" + return + fi + + # For wide terminals, show full path + if (( available > 70 )); then + echo "%F{4}${full_path}%f" + return + fi + + # Otherwise, show abbreviated path (e.g. ~/d/p/n) + local parts=("${(s:/:)full_path}") + local result="" + local last_index=${#parts[@]} + + for i in {1..$((last_index - 1))}; do + [[ -n ${parts[i]} ]] && result+="/${parts[i]:0:1}" + done + + result+="/${parts[last_index]}" + echo "%F{4}${result}%f" +} + + +# Prompt variables +user="%n" +at="%F{15}at%{$reset_color%}" +machine="%F{4}%m%{$reset_color%}" +relative_home="%F{4}%~%{$reset_color%}" +carriage_return=""$'\n'"" +empty_line_bottom="" +chevron_right="" +color_reset="%{$(tput sgr0)%}" +color_yellow="%{$(tput setaf 226)%}" +color_blink="%{$(tput blink)%}" +prompt_symbol="$" +dollar_sign="${color_yellow}${color_blink}${prompt_symbol}${color_reset}" +dollar="%(?:%F{2}${dollar_sign}:%F{1}${dollar_sign})" +space=" " +#thin_space=$'\u2009' +thin_space=$'\u202F' +cmd_prompt="%(?:%F{2}${chevron_right} :%F{1}${chevron_right} )" +git_info="\$vcs_info_msg_0_" +v1="%{┌─[%}" +v2="%{]%}" +v3="└──[" +v4="]" +newline=$'\n' + +# Indicate INSERT mode for vi - NEVER truncate this +function insert-mode () { + echo "-- INSERT --" +} + +# Indicate NORMAL mode for vi - NEVER truncate this +function normal-mode () { + echo "-- NORMAL --" +} + +# Vi mode indicator +vi-mode-indicator () { + if [[ ${KEYMAP} == vicmd || ${KEYMAP} == vi-cmd-mode ]]; then + echo -ne '\e[1 q' + vi_mode=$(normal-mode) + elif [[ ${KEYMAP} == main || ${KEYMAP} == viins || ${KEYMAP} == '' ]]; then + echo -ne '\e[5 q' + vi_mode=$(insert-mode) + fi +} + +# Prompt function to ensure the prompt stays on one line, even in narrow terminals +function set-prompt() { + vi-mode-indicator + configure_vcs_styles # Dynamically set vcs styles based on available space + vcs_info # Refresh vcs info with new styles + + local available=$(available_space) + if (( available < 14 )); then + # Extremely narrow terminal — use minimal prompt + PS1="${carriage_return}${dollar}${space}${empty_line_bottom}" + RPROMPT='$(exit_code_info)' + + else + # Path display - always show something for path, but adapt based on space + local path_display="$(abbreviated_path)" + + # Git info - omit entirely if not enough space + local gitinfo="" + if [[ $available -gt 40 ]]; then + gitinfo="${vcs_info_msg_0_}" + fi + + # Jobs info + local jobs=" $(job_name)" + + # SSH info + local sshinfo="$(ssh_name)" + + # Vi mode is priority 1 - ALWAYS show it + mode="%F{145}%{$terminfo_down_sc$vi_mode$terminfo[rc]%f%}" + + # Right prompt for error codes or spinner + RPROMPT='$(exit_code_info)' + + PS1="${newline}${v1}${user}${v2} ${path_display}${gitinfo}${jobs}${sshinfo}${carriage_return}${mode}${v3}${dollar}${v4}${empty_line_bottom}" + fi +} + +# Pre-command hook to set prompt +my_precmd() { + # Calculate command duration if a command was run + if [[ -n "$_last_executed_command" && $_cmd_start_time -gt 0 ]]; then + _cmd_end_time=$(date +%s) + _cmd_duration=$((_cmd_end_time - _cmd_start_time)) + else + _cmd_duration=0 + fi + + stop_spinner_timer # Make sure spinner is stopped + vcs_info + set-prompt + vi-mode-indicator +} + +add-zsh-hook precmd my_precmd + +# Update mode file based on current mode +update-mode-file() { + set-prompt + local current_mode=$(cat ~/.vi-mode 2>/dev/null || echo "") + local new_mode="$vi_mode" + + if [[ "$new_mode" != "$current_mode" ]]; then + echo "$new_mode" >| ~/.vi-mode + fi + + # Ensure we're in an interactive shell and ZLE is active + if [[ -o zle ]] && zle -l &>/dev/null; then + zle reset-prompt 2>/dev/null || true + else + # If ZLE is not active, fallback and print the prompt manually + set-prompt + print -Pn "$PS1" + fi + + # Refresh tmux client if tmux is running + if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then + tmux refresh-client -S + fi +} + +# Check if nvim is running and update mode +function check-nvim-running() { + if pgrep -x "nvim" > /dev/null; then + vi_mode="" + update-mode-file + if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then + tmux refresh-client -S + fi + else + if [[ ${KEYMAP} == vicmd || ${KEYMAP} == vi-cmd-mode ]]; then + vi_mode=$(normal-mode) + elif [[ ${KEYMAP} == main || ${KEYMAP} == viins || ${KEYMAP} == '' ]]; then + vi_mode=$(insert-mode) + fi + update-mode-file + if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then + tmux refresh-client -S + fi + fi +} + +# ZLE line initialization hook +function zle-line-init() { + zle reset-prompt + vi-mode-indicator + case "${KEYMAP}" in + vicmd) + echo -ne '\e[1 q' + ;; + main|viins|*) + echo -ne '\e[5 q' + ;; + esac +} + +# ZLE keymap select hook +function zle-keymap-select() { + update-mode-file + zle reset-prompt + zle -R + vi-mode-indicator + case "${KEYMAP}" in + vicmd) + echo -ne '\e[1 q' + ;; + main|viins|*) + echo -ne '\e[5 q' + ;; + esac +} + +# Safer version of zle reset-prompt +function safe_reset_prompt() { + # Only reset if ZLE is active + if [[ -o zle ]] && zle -l &>/dev/null; then + zle reset-prompt 2>/dev/null || true + fi +} + +# Preexec hook for command execution - NO BACKGROUND JOBS VERSION +function preexec() { + # Store the command being executed + _last_executed_command=$1 + _cmd_start_time=$(date +%s) + _cmd_is_running=1 + _show_spinner=0 # Reset spinner flag + + # Start the spinner timer immediately + start_spinner_timer + + print -rn -- $terminfo[el] + echo -ne '\e[5 q' + vi-mode-indicator +} + +# Terminal resizing: resets the prompt if ZLE is active, updates the mode file. +TRAPWINCH() { + if [[ -o zle ]] && zle -l &>/dev/null; then + zle -R + zle reset-prompt 2>/dev/null || true + fi + update-mode-file 2>/dev/null +} + +# Register ZLE hooks +zle -N zle-line-init +zle -N zle-keymap-select +zle -N update_spinner + +# Register hooks +add-zsh-hook preexec preexec +add-zsh-hook precmd my_precmd + +set-prompt diff --git a/common/config/zsh/user/prompt_minimal.zsh b/common/config/zsh/user/prompt_minimal.zsh new file mode 100644 index 0000000..0389e7d --- /dev/null +++ b/common/config/zsh/user/prompt_minimal.zsh @@ -0,0 +1,295 @@ +# vim:ft=zsh ts=2 sw=2 sts=2 +#=#=#= +# simle_is_power theme +# folked from agnoster's Theme - https://gist.github.com/3712874 +# +# In order for this theme to render correctly, you will need a +# [Powerline-patched font](https://github.com/Lokaltog/powerline-fonts). +#=#= +#============================================================================== +# Color setting {{{ +#============================================================================== + +setopt prompt_subst + +bg_dir=240 +bg_dark=237 +fg_red=210 + +#===========================================================================}}} +# Segment drawing {{{ +#============================================================================== +# A few utility functions to make it easy and re-usable to draw segmented prompts + +CURRENT_BG='NONE' +# SEGMENT_SEPARATOR='' +SEGMENT_SEPARATOR='' +# SEGMENT_SEPARATOR='' +# SEGMENT_SEPARATOR='▒' +# SEGMENT_SEPARATOR='▓▒░' + +# Begin a segment +# Takes two arguments, background and foreground. Both can be omitted, +# rendering default background/foreground. +prompt_segment() { + local bg fg + [[ -n $1 ]] && bg="%K{$1}" || bg="%k" + [[ -n $2 ]] && fg="%F{$2}" || fg="%f" + if [[ $CURRENT_BG != 'NONE' && $1 != $CURRENT_BG ]]; then + echo -n " %{$bg%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR%{$fg%} " + else + echo -n "%{$bg%}%{$fg%} " + fi + CURRENT_BG=$1 + [[ -n $3 ]] && echo -n $3 +} + +# End the prompt, closing any open segments +prompt_end() { + if [[ -n $CURRENT_BG ]]; then + echo -n " %{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR" + else + echo -n "%{%k%}" + fi + echo -n "%{%f%}" + CURRENT_BG='' +} + +#===========================================================================}}} +# Prompt components {{{ +#============================================================================== +# Each component will draw itself, and hide itself if no information needs to be shown +#------------------------------------------------------------------------------ +# Init: {{{ +#------------------------------------------------------------------------------ + +prompt_init() { + echo -n "%{%F{240}%K{240}%}" +} + +#---------------------------------------------------------------------------}}} +# Status: {{{ +#------------------------------------------------------------------------------ +# - was there an error +# - am I root +# - are there background jobs? +# - am I in ranger subshell? + +prompt_status() { + local symbols + symbols=() + [[ $RETVAL -ne 0 ]] && symbols+="%{%F{${fg_red}}%}✞" + [[ $UID -eq 0 ]] && symbols+="%{%F{223}%}⚡" + [[ $(jobs -l | wc -l) -gt 0 ]] && symbols+="%{%F{cyan}%}⚙" + [[ -n ${RANGER_LEVEL} ]] && symbols+="%{%F{153}%}®" + + [[ -n "$symbols" ]] && prompt_segment ${bg_dark} NONE "$symbols" +} + +#---------------------------------------------------------------------------}}} +# Virtualenv: current working virtualenv {{{ +#------------------------------------------------------------------------------ + +prompt_virtualenv() { + local virtualenv_path="$VIRTUAL_ENV" + if [[ -n $virtualenv_path && -n $VIRTUAL_ENV_DISABLE_PROMPT ]]; then + prompt_segment green black "(`basename $virtualenv_path`)" + fi +} + +#---------------------------------------------------------------------------}}} +# Dir: current working directory {{{ +#------------------------------------------------------------------------------ + +prompt_dir() { + prompt_segment ${bg_dir} 231 '%~' +} + +#---------------------------------------------------------------------------}}} +# Git: branch/detached head, dirty status {{{ +#------------------------------------------------------------------------------ + +prompt_git() { + local ref dirty mode repo_path + repo_path=$(git rev-parse --git-dir 2>/dev/null) + + if $(git rev-parse --is-inside-work-tree >/dev/null 2>&1); then + # dirty=$(parse_git_dirty) + ref=$(git symbolic-ref HEAD 2> /dev/null) || ref="➔ $(git show-ref --head -s --abbrev |head -n1 2> /dev/null)" + # if [[ -n $dirty ]]; then + # prompt_segment ${bg_dark} 223 + # else + prompt_segment ${bg_dark} 153 + # fi + + if [[ -e "${repo_path}/BISECT_LOG" ]]; then + mode=" <B>" + elif [[ -e "${repo_path}/MERGE_HEAD" ]]; then + mode=" >M<" + elif [[ -e "${repo_path}/rebase" || -e "${repo_path}/rebase-apply" || -e "${repo_path}/rebase-merge" || -e "${repo_path}/../.dotest" ]]; then + mode=" >R>" + fi + + autoload -Uz vcs_info + + zstyle ':vcs_info:*' enable git + zstyle ':vcs_info:*' get-revision true + zstyle ':vcs_info:*' check-for-changes true + zstyle ':vcs_info:*' stagedstr '+' + zstyle ':vcs_info:git:*' unstagedstr '*' + zstyle ':vcs_info:*' formats ' %u%c' + zstyle ':vcs_info:*' actionformats ' %u%c' + vcs_info + echo -n "${ref/refs\/heads\// }${vcs_info_msg_0_%% }${mode}" + fi +} + +#---------------------------------------------------------------------------}}} +# Hg: prompt {{{ +#------------------------------------------------------------------------------ + +prompt_hg() { + local rev status + if $(hg id >/dev/null 2>&1); then + if $(hg prompt >/dev/null 2>&1); then + if [[ $(hg prompt "{status|unknown}") = "?" ]]; then + # if files are not added + prompt_segment ${fg_red} ${bg_dark} + st='±' + elif [[ -n $(hg prompt "{status|modified}") ]]; then + # if any modification + prompt_segment 223 ${bg_dark} + st='±' + else + # if working copy is clean + prompt_segment 153 ${bg_dark} + fi + echo -n $(hg prompt "☿ {rev}@{branch}") $st + else + st="" + rev=$(hg id -n 2>/dev/null | sed 's/[^-0-9]//g') + branch=$(hg id -b 2>/dev/null) + if `hg st | grep -q "^\?"`; then + prompt_segment ${fg_red} ${bg_dark} + st='±' + elif `hg st | grep -q "^(M|A)"`; then + prompt_segment 223 ${bg_dark} + st='±' + else + prompt_segment 153 ${bg_dark} + fi + echo -n "☿ $rev@$branch" $st + fi + fi +} + +#}}}========================================================================}}} +# Build main prompt {{{ +#============================================================================== + + +function vi-mode-indicator() { + local current_mode + current_mode=$(cat ~/.vi-mode 2>/dev/null || echo "") + + if [[ ${KEYMAP} == vicmd || ${KEYMAP} == vi-cmd-mode ]]; then + [[ "$current_mode" != "-- NORMAL --" ]] && echo "-- NORMAL --" >| ~/.vi-mode + elif [[ ${KEYMAP} == main || ${KEYMAP} == viins || ${KEYMAP} == '' ]]; then + [[ "$current_mode" != "-- INSERT --" ]] && echo "-- INSERT --" >| ~/.vi-mode + fi +} + +build_prompt() { + RETVAL=$? + vi-mode-indicator + prompt_init + prompt_virtualenv + prompt_dir + prompt_git + prompt_hg + prompt_status + prompt_end +} + +color_reset="%{$(tput sgr0)%}" +color_yellow="%{$(tput setaf 226)%}" +color_blink="%{$(tput blink)%}" +prompt_symbol="$" +dollar_sign="${color_yellow}${color_blink}${prompt_symbol}${color_reset}" +dollar="%(?:%F{2}${dollar_sign}:%F{1}${dollar_sign})" + +v1="%{┌─[%}" +v2="%{]%}" +v3="└─[" +v4="]" +user="%n" + +PROMPT="${v1}${user}%f%b%k${v2}$(build_prompt)$reset_color +${v3}${dollar}${v4}${empty_line_bottom}$reset_color" +#PROMPT='%n@%m:%~%# ' +#%{%F{240}%}\$ %{$reset_color%}' +#%{${dollar}%} %{$reset_color%}' +RPROMPT='' + +PROMPT2='%{%F{30}%}↪%{$reset_color%} ' +RPROMPT2='%{$fg_bold[green]%}%_%{$reset_color%}' + +function update-mode-file() { + local current_mode=$(cat ~/.vi-mode 2>/dev/null || echo "") + local new_mode="$vi_mode" + + # Check if the mode is different before updating + if [[ "$new_mode" != "$current_mode" ]]; then + echo "$new_mode" >| ~/.vi-mode + fi + + # Only call zle if ZLE is active + if [[ -o zle ]]; then + zle reset-prompt # Force refresh + fi + + # Ensure tmux client refresh only happens if tmux is running + if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then + tmux refresh-client -S + fi +} +function zle-line-init() { + zle reset-prompt + case "${KEYMAP}" in + vicmd) + echo -ne '\e[1 q' + ;; + main|viins|*) + echo -ne '\e[5 q' + ;; + esac +} +function zle-keymap-select() { + local current_keymap + current_keymap="${KEYMAP}" + + update-mode-file + zle reset-prompt + + case "$current_keymap" in + vicmd) + echo -ne '\e[1 q' + ;; + main|viins|*) + echo -ne '\e[5 q' + ;; + esac +} + +precmd () { + print -rP +} + +preexec () { + print -rn -- $terminfo[el] + echo -ne '\e[5 q' # Reset cursor shape +} +zle -N zle-line-init +zle -N zle-keymap-select + +#===========================================================================}}} diff --git a/common/config/zsh/user/prompt_new.zsh b/common/config/zsh/user/prompt_new.zsh new file mode 100644 index 0000000..78791ef --- /dev/null +++ b/common/config/zsh/user/prompt_new.zsh @@ -0,0 +1,863 @@ +# __ _ __ _| | _____ ______ _| | __ +# / _` |/ _` | |/ / _ \_ / _` | |/ / +# | (_| | (_| | < (_) / / (_| | < +# \__,_|\__, |_|\_\___/___\__,_|_|\_\ +# |___/ +# +# An asynchronous, dynamic color prompt for ZSH with Git, vi mode, and exit +# status indicators +# +# +# MIT License +# +# Copyright (c) 2017-2019 Alexandros Kozak +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# +# https://github.com/agkozak/agkozak-zsh-prompt +# + +# shellcheck disable=SC1090,SC2016,SC2034,SC2088,SC2148,SC2154,SC2190 + +# psvar[] Usage +# +# psvar Index Prompt String Equivalent Usage +# +# psvar[1] %1v Hostname/abbreviated hostname (only +# displayed for SSH connections) +# psvar[2] %2v Working directory or abbreviation +# thereof +# psvar[3] %3v Current working Git branch, along +# with indicator of changes made +# psvar[4] %4v Equals 'vicmd' when vi command mode +# is enabled; otherwise empty + +# Set AGKOZAK_PROMPT_DEBUG=1 to see debugging information +AGKOZAK_PROMPT_DEBUG=${AGKOZAK_PROMPT_DEBUG:-0} + +############################################################ +# Display a message on STDERR if debug mode is enabled +# +# Globals: +# AGKOZAK_PROMPT_DEBUG +# Arguments: +# $1 Message to send to STDERR +############################################################ +_agkozak_debug_print() { + (( AGKOZAK_PROMPT_DEBUG )) && print -- "agkozak-zsh-prompt: $1" >&2 +} + +if (( AGKOZAK_PROMPT_DEBUG )); then + autoload -Uz is-at-least + + setopt WARN_CREATE_GLOBAL + + if is-at-least 5.4.0; then + setopt WARN_NESTED_VAR + fi +fi + +# Set AGKOZAK_PROMPT_DIRTRIM to the desired number of directory elements to +# display, or set it to 0 for no directory trimming +typeset -g AGKOZAK_PROMPT_DIRTRIM=${AGKOZAK_PROMPT_DIRTRIM:-2} + +# Set AGKOZAK_MULTILINE to 0 to enable the legacy, single-line prompt +typeset -g AGKOZAK_MULTILINE=${AGKOZAK_MULTILINE:-1} + +# Set AGKOZAK_LEFT_PROMPT_ONLY to have the Git status appear in the left prompt +typeset -g AGKOZAK_LEFT_PROMPT_ONLY=${AGKOZAK_LEFT_PROMPT_ONLY:-0} + +# Set AGKOZAK_COLORS_* variables to any valid color +# AGKOZAK_COLORS_EXIT_STATUS changes the exit status color (default: red) +# AGKOZAK_COLORS_USER_HOST changes the username/hostname color (default: green) +# AGKOZAK_COLORS_PATH changes the path color (default: blue) +# AGKOZAK_COLORS_BRANCH_STATUS changes the branch status color (default: yellow) +# AGKOZAK_COLORS_PROMPT_CHAR changes the prompt character color (default: white) +typeset -g AGKOZAK_COLORS_EXIT_STATUS=${AGKOZAK_COLORS_EXIT_STATUS:-red} +typeset -g AGKOZAK_COLORS_USER_HOST=${AGKOZAK_COLORS_USER_HOST:-green} +typeset -g AGKOZAK_COLORS_PATH=${AGKOZAK_COLORS_PATH:-blue} +typeset -g AGKOZAK_COLORS_BRANCH_STATUS=${AGKOZAK_COLORS_BRANCH_STATUS:-yellow} + +setopt PROMPT_SUBST NO_PROMPT_BANG + +###################################################################### +# GENERAL FUNCTIONS +###################################################################### + +############################################################ +# Are colors available? +# +# Globals: +# AGKOZAK_HAS_COLORS +############################################################ +_agkozak_has_colors() { + if (( $+AGKOZAK_HAS_COLORS )); then + : + else + case $TERM in + *-256color) typeset -g AGKOZAK_HAS_COLORS=1 ;; + vt100|dumb) typeset -g AGKOZAK_HAS_COLORS=0 ;; + *) + local colors + case $OSTYPE in + freebsd*|dragonfly*) colors=$(tput Co) ;; + *) colors=$(tput colors) ;; + esac + typeset -g AGKOZAK_HAS_COLORS=$(( colors >= 8 )) + ;; + esac + fi + (( AGKOZAK_HAS_COLORS )) +} + +############################################################ +# Is the user connected via SSH? +# +# This function works perfectly for regular users. It is +# nearly impossible to detect with accuracy how a superuser +# is connected, so this prompt opts simply to display his or +# her username and hostname in inverse video. +############################################################ +_agkozak_is_ssh() { + [[ -n "${SSH_CONNECTION-}${SSH_CLIENT-}${SSH_TTY-}" ]] +} + +############################################################ +# Emulation of bash's PROMPT_DIRTRIM for ZSH +# +# Take PWD and substitute HOME with `~'. If the rest of PWD +# has more than a certain number of elements in its +# directory tree, keep the number specified by +# AGKOZAK_PROMPT_DIRTRIM (default: 2) and abbreviate the +# rest with `...'. (Set AGKOZAK_PROMPT_DIRTRIM=0 to disable +# directory trimming). For example, +# +# $HOME/dotfiles/polyglot/img +# +# will be displayed as +# +# ~/.../polyglot/img +# +# Named directories will by default be displayed using their +# aliases in the prompt (e.g. `~project'). Set +# AGKOZAK_NAMED_DIRS=0 to have them displayed just like any +# other directory. +# +# Globals: +# AGKOZAK_NAMED_DIRS +# Arguments: +# $@ [Optional] If `-v', store the function's output in +# psvar[2] instead of printing it to STDOUT +# $@ Number of directory elements to display (default: 2) +############################################################ +_agkozak_prompt_dirtrim() { + # Process arguments + local argument + for argument in $@; do + [[ $argument == '-v' ]] && local var=1 + done + until [[ $1 != '-v' ]]; do + shift + done + [[ $1 -ge 0 ]] || set 2 + + # Default behavior (when AGKOZAK_NAMED_DIRS is 1) + typeset -g AGKOZAK_NAMED_DIRS=${AGKOZAK_NAMED_DIRS:-1} + if (( AGKOZAK_NAMED_DIRS )); then + local zsh_pwd + print -Pnz '%~' + + # IF AGKOZAK_PROMPT_DIRTRIM is not 0, trim directory + if (( $1 )); then + read -rz zsh_pwd + case $zsh_pwd in + \~) print -Pnz $zsh_pwd ;; + \~/*) print -Pnz "%($(( $1 + 2 ))~|~/.../%${1}~|%~)" ;; + \~*) print -Pnz "%($(( $1 + 2 ))~|${zsh_pwd%%${zsh_pwd#\~*\/}}.../%${1}~|%~)" ;; + *) print -Pnz "%($(( $1 + 1 ))/|.../%${1}d|%d)" ;; + esac + fi + + # If AGKOZAK_NAMED_DIRS is 0 + else + local dir dir_count + case $HOME in + /) dir=${PWD} ;; + *) dir=${PWD#$HOME} ;; + esac + + # If AGKOZAK_PROMPT_DIRTRIM is not 0, trim the directory + if (( $1 > 0 )); then + + # The number of directory elements is the number of slashes in ${PWD#$HOME} + dir_count=$(( ${#dir} - ${#${dir//\//}} )) + if (( dir_count <= $1 )); then + case $PWD in + ${HOME}) print -nz '~' ;; + ${HOME}*) print -nz "~${dir}" ;; + *) print -nz "$PWD" ;; + esac + else + local lopped_path i + lopped_path=${dir} + i=0 + while (( i != $1 )); do + lopped_path=${lopped_path%\/*} + (( i++ )) + done + case $PWD in + ${HOME}*) print -nz "~/...${dir#${lopped_path}}" ;; + *) print -nz -f '...%s' "${PWD#${lopped_path}}" ;; + esac + fi + + # If AGKOZAK_PROMPT_DIRTRIM is 0 + else + case $PWD in + ${HOME}) print -nz '~' ;; + ${HOME}*) print -nz "~${dir}" ;; + *) print -nz "$PWD" ;; + esac + fi + fi + + local output + read -rz output + + # Argument -v stores the output to psvar[2]; otherwise send to STDOUT + if (( var )); then + psvar[2]=$output + else + print $output + fi +} + +############################################################ +# Display current branch name, followed by symbols +# representing changes to the working copy +############################################################ +_agkozak_branch_status() { + local ref branch + ref=$(command git symbolic-ref --quiet HEAD 2> /dev/null) + case $? in # See what the exit code is. + 0) ;; # $ref contains the name of a checked-out branch. + 128) return ;; # No Git repository here. + # Otherwise, see if HEAD is in detached state. + *) ref=$(command git rev-parse --short HEAD 2> /dev/null) || return ;; + esac + branch=${ref#refs/heads/} + + if [[ -n $branch ]]; then + local git_status symbols i=1 k + git_status="$(LC_ALL=C command git status 2>&1)" + + typeset -A messages + messages=( + '&*' ' have diverged,' + '&' 'Your branch is behind ' + '*' 'Your branch is ahead of ' + '+' 'new file: ' + 'x' 'deleted: ' + '!' 'modified: ' + '>' 'renamed: ' + '?' 'Untracked files:' + ) + + for k in '&*' '&' '*' '+' 'x' '!' '>' '?'; do + case $git_status in + *${messages[$k]}*) symbols+="${AGKOZAK_CUSTOM_SYMBOLS[$i]:-$k}" ;; + esac + (( i++ )) + done + + [[ -n $symbols ]] && symbols=" ${symbols}" + + printf '%s(%s%s)' "${AGKOZAK_BRANCH_STATUS_SEPARATOR- }" "$branch" "$symbols" + fi +} + +############################################################ +# Redraw the prompt when the vi mode changes. When the user +# enters vi command mode, the % or # in the prompt changes +# to a colon +############################################################ +zle-keymap-select() { + [[ $KEYMAP == 'vicmd' ]] && psvar[4]='vicmd' || psvar[4]='' + zle reset-prompt + zle -R +} + +############################################################ +# Redraw prompt when terminal size changes +############################################################ +TRAPWINCH() { + zle && zle -R +} + +############################################################ +# For legacy custom prompts: print a vi mode indicator +############################################################ +_agkozak_vi_mode_indicator() { + case $KEYMAP in + vicmd) print -n ':' ;; + *) print -n '%#' ;; + esac +} + +###################################################################### +# ASYNCHRONOUS FUNCTIONS +###################################################################### + +# Standarized $0 handling +# (See https://github.com/zdharma/Zsh-100-Commits-Club/blob/master/Zsh-Plugin-Standard.adoc) +0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}" +typeset -g AGKOZAK_PROMPT_DIR="${0:A:h}" + +############################################################ +# If zsh-async has not already been loaded, try to load it +# +# Globals: +# AGKOZAK_PROMPT_DEBUG +# AGKOZAK_PROMPT_DIR +############################################################ +_agkozak_load_async_lib() { + if ! whence -w async_init &> /dev/null; then # Don't load zsh-async twice + if (( AGKOZAK_PROMPT_DEBUG )); then + source "${AGKOZAK_PROMPT_DIR}/lib/async.zsh" + else + source "${AGKOZAK_PROMPT_DIR}/lib/async.zsh" &> /dev/null + fi + local success=$? + return $success + fi +} + +############################################################ +# If SIGUSR1 is available and not already in use by ZSH, use +# it; otherwise disable asynchronous mode +############################################################ +_agkozak_has_usr1() { + if whence -w TRAPUSR1 &> /dev/null; then + _agkozak_debug_print 'TRAPUSR1 already defined.' + return 1 + else + case $signals in # Array containing names of available signals + *USR1*) return 0 ;; + *) + _agkozak_debug_print 'SIGUSR1 not available.' + return 1 + ;; + esac + fi +} + +############################################################ +# If AGKOZAK_FORCE_ASYNC_METHOD is set to a valid value, +# set AGKOZAK_ASYNC_METHOD to that; otherwise, determine +# the optimal asynchronous method from the environment (usr1 +# for MSYS2/Cygwin, zsh-async for WSL, subst-async for +# everything else), with fallbacks being available. Define +# the necessary asynchronous functions (loading async.zsh +# when necessary). +# +# Globals: +# AGKOZAK_IS_WSL +# AGKOZAK_ASYNC_METHOD +# AGKOZAK_FORCE_ASYNC_METHOD +# AGKOZAK_TRAPUSR1_FUNCTION +############################################################ +_agkozak_async_init() { + + # WSL should have BG_NICE disabled, since it does not have a Linux kernel + setopt LOCAL_OPTIONS EXTENDED_GLOB + if [[ -e /proc/version ]]; then + if [[ -n ${(M)${(f)"$(</proc/version)"}:#*Microsoft*} ]]; then + unsetopt BG_NICE + typeset -g AGKOZAK_IS_WSL=1 # For later reference + fi + fi + + # If AGKOZAK_FORCE_ASYNC_METHOD is set, force the asynchronous method + [[ $AGKOZAK_FORCE_ASYNC_METHOD == 'zsh-async' ]] && _agkozak_load_async_lib + if [[ $AGKOZAK_FORCE_ASYNC_METHOD == (subst-async|zsh-async|usr1|none) ]]; then + typeset -g AGKOZAK_ASYNC_METHOD=$AGKOZAK_FORCE_ASYNC_METHOD + + # Otherwise, first provide for certain quirky systems + else + + if (( AGKOZAK_IS_WSL )) || [[ $OSTYPE == solaris* ]]; then + if [[ $ZSH_VERSION != '5.0.2' ]] &&_agkozak_load_async_lib; then + typeset -g AGKOZAK_ASYNC_METHOD='zsh-async' + elif _agkozak_has_usr1; then + typeset -g AGKOZAK_ASYNC_METHOD='usr1' + else + typeset -g AGKOZAK_ASYNC_METHOD='subst-async' + fi + + # SIGUSR1 method is still much faster on MSYS2, Cygwin, and ZSH v5.0.2 + elif [[ $ZSH_VERSION == '5.0.2' ]] || [[ $OSTYPE == (msys|cygwin) ]]; then + if _agkozak_has_usr1; then + typeset -g AGKOZAK_ASYNC_METHOD='usr1' + else + typeset -g AGKOZAK_ASYNC_METHOD='subst-async' + fi + + # Asynchronous methods don't work in Emacs shell mode (but they do in term + # and ansi-term) + elif [[ $TERM == 'dumb' ]]; then + typeset -g AGKOZAK_ASYNC_METHOD='none' + + # Otherwise use subst-async + else + typeset -g AGKOZAK_ASYNC_METHOD='subst-async' + fi + fi + + ############################################################ + # Process substitution async method + # + # Fork a background process to fetch the Git status and feed + # it asynchronously to a file descriptor. Install a callback + # handler to process input from the file descriptor. + # + # Globals: + # AGKOZAK_ASYNC_FD + # AGKOZAK_IS_WSL + ############################################################ + _agkozak_subst_async() { + setopt LOCAL_OPTIONS NO_IGNORE_BRACES + typeset -g AGKOZAK_ASYNC_FD=13371 + + # Workaround for buggy behavior in MSYS2, Cygwin, and Solaris + if [[ $OSTYPE == (msys|cygwin|solaris*) ]]; then + exec {AGKOZAK_ASYNC_FD}< <(_agkozak_branch_status; command true) + # Prevent WSL from locking up when using X; also workaround for ZSH v5.0.2 + elif (( AGKOZAK_IS_WSL )) && (( $+DISPLAY )) \ + || [[ $ZSH_VERSION == '5.0.2' ]]; then + exec {AGKOZAK_ASYNC_FD}< <(_agkozak_branch_status) + command sleep 0.01 + else + exec {AGKOZAK_ASYNC_FD}< <(_agkozak_branch_status) + fi + + # Bug workaround; see http://www.zsh.org/mla/workers/2018/msg00966.html + command true + + zle -F "$AGKOZAK_ASYNC_FD" _agkozak_zsh_subst_async_callback + } + + ############################################################ + # ZLE callback handler + # + # Read Git status from file descriptor and set psvar[3] + # + # Arguments: + # $1 File descriptor + ############################################################ + _agkozak_zsh_subst_async_callback() { + setopt LOCAL_OPTIONS NO_IGNORE_BRACES + + local FD="$1" response + + # Read data from $FD descriptor + IFS='' builtin read -rs -d $'\0' -u "$FD" response + + # Withdraw callback and close the file descriptor + zle -F ${FD}; exec {FD}<&- + + # Make the changes visible + psvar[3]="$response" + zle && zle reset-prompt + } + + case $AGKOZAK_ASYNC_METHOD in + + zsh-async) + + ############################################################ + # Create zsh-async worker + ############################################################ + _agkozak_zsh_async() { + async_start_worker agkozak_git_status_worker -n + async_register_callback agkozak_git_status_worker _agkozak_zsh_async_callback + async_job agkozak_git_status_worker _agkozak_branch_status + } + + ############################################################ + # Set RPROMPT and stop worker + ############################################################ + _agkozak_zsh_async_callback() { + psvar[3]=$3 + zle && zle reset-prompt + async_stop_worker agkozak_git_status_worker -n + } + ;; + + usr1) + + ############################################################ + # Launch async workers to calculate Git status. TRAPUSR1 + # actually displays the status; if some other script + # redefines TRAPUSR1, drop the prompt into synchronous mode. + # + # Globals: + # AGKOZAK_TRAPUSR1_FUNCTION + # AGKOZAK_USR1_ASYNC_WORKER + # AGKOZAK_ASYNC_METHOD + ############################################################ + _agkozak_usr1_async() { + if [[ "$(builtin which TRAPUSR1)" = "$AGKOZAK_TRAPUSR1_FUNCTION" ]]; then + # Kill running child process if necessary + if (( AGKOZAK_USR1_ASYNC_WORKER )); then + kill -s HUP "$AGKOZAK_USR1_ASYNC_WORKER" &> /dev/null || : + fi + + # Start background computation of Git status + _agkozak_usr1_async_worker &! + typeset -g AGKOZAK_USR1_ASYNC_WORKER=$! + else + _agkozak_debug_print 'TRAPUSR1 has been redefined. Switching to subst-async mode.' + typeset -g AGKOZAK_ASYNC_METHOD='subst-async' + psvar[3]="$(_agkozak_branch_status)" + fi + } + + ############################################################ + # Calculate Git status and store it in a temporary file; + # then kill own process, sending SIGUSR1 + # + # Globals: + # AGKOZAK_PROMPT_DEBUG + ############################################################ + _agkozak_usr1_async_worker() { + # Save Git branch status to temporary file + _agkozak_branch_status >| /tmp/agkozak_zsh_prompt_$$ + + # Signal parent process + if (( AGKOZAK_PROMPT_DEBUG )); then + kill -s USR1 $$ + else + kill -s USR1 $$ &> /dev/null + fi + } + + ############################################################ + # On SIGUSR1, fetch Git status from temprary file and store + # it in psvar[3]. This function caches its own code in + # AGKOZAK_TRAPUSR1_FUNCTION so that it can tell if it has + # been redefined by another script. + # + # Globals: + # AGKOZAK_USR1_ASYNC_WORKER + # AGKOZAK_TRAPUSR1_FUNCTION + ############################################################ + TRAPUSR1() { + # Set prompt from contents of temporary file + psvar[3]=$(print -n -- "$(< /tmp/agkozak_zsh_prompt_$$)") + + # Reset asynchronous process number + typeset -g AGKOZAK_USR1_ASYNC_WORKER=0 + + # Redraw the prompt + zle && zle reset-prompt + } + + typeset -g AGKOZAK_TRAPUSR1_FUNCTION="$(builtin which TRAPUSR1)" + ;; + esac +} + +###################################################################### +# THE PROMPT +###################################################################### + +############################################################ +# Strip color codes from a prompt string +# +# Arguments: +# $1 The prompt string +############################################################ +_agkozak_strip_colors() { + + local prompt=$1 + local open_braces + + while [[ -n $prompt ]]; do + case $prompt in + %F\{*|%K\{*) + (( open_braces++ )) + prompt=${prompt#%[FK]\{} + while (( open_braces )); do + case ${prompt:0:1} in + \{) (( open_braces++ )) ;; + \}) (( open_braces-- )) ;; + esac + prompt=${prompt#?} + done + ;; + %f*|%k*) prompt=${prompt#%[fk]} ;; + *) + print -n -- "${prompt:0:1}" + prompt=${prompt#?} + ;; + esac + done +} + +############################################################ +# Runs right before each prompt is displayed; hooks into +# precmd +# +# 1) Redisplays path ($psvar[2]) whenever the value of +# AGKOZAK_PROMPT_DIRTRIM or AGKOZAK_NAMED_DIRS changes +# 2) If AGKOZAK_MULTILINE is changed to 0, set +# AGKOZAK_LEFT_PROMPT_ONLY=0 +# 3) If AGKOZAK_LEFT_PROMPT_ONLY is changed, updated both +# prompt strings +# 4) Resets Git status and vi mode display +# 5) Begins to calculate Git status +# 6) Sets AGKOZAK_PROMPT_WHITESPACE based on value of +# AGKOZAK_MULTILINE +# 7) Optionally display a blank line (AGKOZAK_BLANK_LINES), +# while avoiding a blank line when the shell is first +# loaded +# 8) If custom prompts are defined, update the prompt +# strings +# +# TODO: Consider making AGKOZAK_PROMPT_WHITESPACE a psvar +# +# Globals: +# AGKOZAK_PROMPT_DIRTRIM +# AGKOZAK_OLD_PROMPT_DIRTRIM +# AGKOZAK_NAMED_DIRS +# AGKOZAK_OLD_NAMED_DIRS +# AGKOZAK_MULTILINE +# AGKOZAK_OLD_MULTILINE +# AGKOZAK_LEFT_PROMPT_ONLY +# AGKOZAK_OLD_LEFT_PROMPT_ONLY +# AGKOZAK_ASYNC_METHOD +# AGKOZAK_PROMPT_WHITESPACE +# AGKOZAK_BLANK_LINES +# AGKOZAK_FIRST_PROMPT_PRINTED +# AGKOZAK_CUSTOM_PROMPT +# AGKOZAK_CURRENT_CUSTOM_PROMPT +# AGKOZAK_CUSTOM_RPROMPT +# AGKOZAK_CURRENT_CUSTOM_RPROMPT +############################################################ +_agkozak_precmd() { + # Update displayed directory when AGKOZAK_PROMPT_DIRTRIM or AGKOZAK_NAMED_DIRS + # changes or when first sourcing this script + if (( AGKOZAK_PROMPT_DIRTRIM != AGKOZAK_OLD_PROMPT_DIRTRIM )) \ + || (( AGKOZAK_NAMED_DIRS != AGKOZAK_OLD_NAMED_DIRS )) \ + || (( ! $+psvar[2] )); then + _agkozak_prompt_dirtrim -v $AGKOZAK_PROMPT_DIRTRIM + typeset -g AGKOZAK_OLD_PROMPT_DIRTRIM=$AGKOZAK_PROMPT_DIRTRIM + typeset -g AGKOZAK_OLD_NAMED_DIRS=$AGKOZAK_NAMED_DIRS + fi + + if (( AGKOZAK_MULTILINE != AGKOZAK_OLD_MULTILINE )); then + (( AGKOZAK_MULTILINE == 0 )) && AGKOZAK_LEFT_PROMPT_ONLY=0 + typeset -g AGKOZAK_OLD_MULTILINE=$AGKOZAK_MULTILINE + fi + + if (( AGKOZAK_LEFT_PROMPT_ONLY != AGKOZAK_OLD_LEFT_PROMPT_ONLY )); then + unset AGKOZAK_CUSTOM_PROMPT AGKOZAK_CUSTOM_RPROMPT + typeset -g AGKOZAK_OLD_LEFT_PROMPT_ONLY=$AGKOZAK_LEFT_PROMPT_ONLY + _agkozak_prompt_string + fi + + psvar[3]='' + psvar[4]='' + + case $AGKOZAK_ASYNC_METHOD in + 'subst-async') _agkozak_subst_async ;; + 'zsh-async') _agkozak_zsh_async ;; + 'usr1') _agkozak_usr1_async ;; + *) psvar[3]="$(_agkozak_branch_status)" ;; + esac + + if (( AGKOZAK_MULTILINE == 0 )) && (( ! AGKOZAK_LEFT_PROMPT_ONLY )) \ + && [[ -z $INSIDE_EMACS ]]; then + typeset -g AGKOZAK_PROMPT_WHITESPACE=' ' + else + typeset -g AGKOZAK_PROMPT_WHITESPACE=$'\n' + fi + + if (( AGKOZAK_BLANK_LINES )); then + if (( AGKOZAK_FIRST_PROMPT_PRINTED )); then + print + fi + typeset -g AGKOZAK_FIRST_PROMPT_PRINTED=1 + fi + + # If AGKOZAK_CUSTOM_PROMPT or AGKOZAK_CUSTOM_RPROMPT changes, the + # corresponding prompt is updated + + if [[ ${AGKOZAK_CUSTOM_PROMPT} != "${AGKOZAK_CURRENT_CUSTOM_PROMPT}" ]]; then + typeset -g AGKOZAK_CURRENT_CUSTOM_PROMPT=${AGKOZAK_CUSTOM_PROMPT} + PROMPT=${AGKOZAK_CUSTOM_PROMPT} + if ! _agkozak_has_colors; then + PROMPT=$(_agkozak_strip_colors "${PROMPT}") + fi + fi + + if [[ ${AGKOZAK_CUSTOM_RPROMPT} != "${AGKOZAK_CURRENT_CUSTOM_RPROMPT}" ]]; then + typeset -g AGKOZAK_CURRENT_CUSTOM_RPROMPT=${AGKOZAK_CUSTOM_RPROMPT} + RPROMPT=${AGKOZAK_CUSTOM_RPROMPT} + if ! _agkozak_has_colors; then + RPROMPT=$(_agkozak_strip_colors "${RPROMPT}") + fi + fi +} + +############################################################ +# Set the prompt strings +# +# Globals: +# AGKOZAK_CUSTOM_PROMPT +# AGKOZAK_COLORS_EXIT_STATUS +# AGKOZAK_COLORS_USER_HOST +# AGKOZAK_COLORS_PATH +# AGKOZAK_PROMPT_WHITESPACE +# AGKOZAK_COLORS_PROMPT_CHAR +# AGKOZAK_PROMPT_CHAR +# AGKOZAK_CURRENT_CUSTOM_PROMPT +# AGKOZAK_CUSTOM_RPROMPT +# AGKOZAK_COLORS_BRANCH_STATUS +# AGKOZAK_CURRENT_CUSTOM_RPROMPT +############################################################ +_agkozak_prompt_string () { + if (( $+AGKOZAK_CUSTOM_PROMPT )); then + PROMPT=${AGKOZAK_CUSTOM_PROMPT} + else + # The color left prompt + PROMPT='%(?..%B%F{${AGKOZAK_COLORS_EXIT_STATUS}}(%?%)%f%b )' + PROMPT+='%(!.%S%B.%B%F{${AGKOZAK_COLORS_USER_HOST}})%n%1v%(!.%b%s.%f%b) ' + PROMPT+='%B%F{${AGKOZAK_COLORS_PATH}}%2v%f%b' + if (( AGKOZAK_LEFT_PROMPT_ONLY )); then + PROMPT+='%(3V.%F{${AGKOZAK_COLORS_BRANCH_STATUS}}%3v%f.)' + fi + PROMPT+='${AGKOZAK_PROMPT_WHITESPACE}' + PROMPT+='${AGKOZAK_COLORS_PROMPT_CHAR:+%F{${AGKOZAK_COLORS_PROMPT_CHAR}\}}' + PROMPT+='%(4V.${AGKOZAK_PROMPT_CHAR[3]:-:}.%(!.${AGKOZAK_PROMPT_CHAR[2]:-%#}.${AGKOZAK_PROMPT_CHAR[1]:-%#}))' + PROMPT+='${AGKOZAK_COLORS_PROMPT_CHAR:+%f} ' + + typeset -g AGKOZAK_CUSTOM_PROMPT=${PROMPT} + typeset -g AGKOZAK_CURRENT_CUSTOM_PROMPT=${AGKOZAK_CUSTOM_PROMPT} + fi + + if (( $+AGKOZAK_CUSTOM_RPROMPT )); then + RPROMPT=${AGKOZAK_CUSTOM_RPROMPT} + else + # The color right prompt + if (( ! AGKOZAK_LEFT_PROMPT_ONLY )); then + typeset -g RPROMPT='%(3V.%F{${AGKOZAK_COLORS_BRANCH_STATUS}}%3v%f.)' + else + typeset -g RPROMPT='' + fi + + typeset -g AGKOZAK_CUSTOM_RPROMPT=${RPROMPT} + typeset -g AGKOZAK_CURRENT_CUSTOM_RPROMPT=${RPROMPT} + fi + + if ! _agkozak_has_colors; then + PROMPT=$(_agkozak_strip_colors "$PROMPT") + RPROMPT=$(_agkozak_strip_colors "$RPROMPT") + fi +} + +############################################################ +# Prompt setup +# +# Globals: +# AGKOZAK_ASYNC_METHOD +# AGKOZAK_USR1_ASYNC_WORKER +# AGKOZAK_PROMPT_DIRTRIM +############################################################ +() { + + _agkozak_async_init + + case $AGKOZAK_ASYNC_METHOD in + 'subst-async') ;; + 'zsh-async') async_init ;; + 'usr1') typeset -g AGKOZAK_USR1_ASYNC_WORKER=0 ;; + esac + + zle -N zle-keymap-select + + # Don't use ZSH hooks in Emacs classic shell + if (( $+INSIDE_EMACS )) && [[ $TERM == 'dumb' ]]; then + : + else + autoload -Uz add-zsh-hook + add-zsh-hook precmd _agkozak_precmd + + ############################################################ + # Update the displayed directory when the PWD changes + ############################################################ + _agkozak_chpwd() { + _agkozak_prompt_dirtrim -v $AGKOZAK_PROMPT_DIRTRIM + } + + add-zsh-hook chpwd _agkozak_chpwd + fi + + # Only display the HOSTNAME for an SSH connection or for a superuser + if _agkozak_is_ssh || (( EUID == 0 )); then + psvar[1]="@${HOST%%.*}" + else + psvar[1]='' + fi + + # The DragonFly BSD console and Emacs shell can't handle bracketed paste. + # Avoid the ugly ^[[?2004 control sequence. + if [[ $TERM == 'cons25' ]] || [[ $TERM == 'dumb' ]]; then + unset zle_bracketed_paste + fi + + # The Emacs shell has only limited support for some ZSH features, so use a + # more limited prompt. + if [[ $TERM == 'dumb' ]]; then + PROMPT='%(?..(%?%) )' + PROMPT+='%n%1v ' + PROMPT+='$(_agkozak_prompt_dirtrim "$AGKOZAK_PROMPT_DIRTRIM")' + PROMPT+='$(_agkozak_branch_status) ' + PROMPT+='%# ' + else + # Avoid continuation lines in Emacs term and ansi-term + (( $+INSIDE_EMACS )) && ZLE_RPROMPT_INDENT=3 + + # When VSCode is using the DOM renderer, the right prompt overflows off the + # side of the screen + (( $+VSCODE_PID )) && ZLE_RPROMPT_INDENT=6 + + _agkozak_prompt_string + + fi + + _agkozak_debug_print "Using async method: $AGKOZAK_ASYNC_METHOD" +} + +# Clean up environment +unfunction _agkozak_load_async_lib _agkozak_has_usr1 _agkozak_is_ssh \ + _agkozak_async_init + +# vim: ts=2:et:sts=2:sw=2:ROMPT='%~%<< $(git_prompt_info)${PR_BOLD_WHITE}>%{${reset_color}%} ' diff --git a/common/config/zsh/user/prompt_simple.zsh b/common/config/zsh/user/prompt_simple.zsh new file mode 100644 index 0000000..0bbad44 --- /dev/null +++ b/common/config/zsh/user/prompt_simple.zsh @@ -0,0 +1,227 @@ +# vim:ft=zsh ts=2 sw=2 sts=2 +# +### Segment drawing +# A few utility functions to make it easy and re-usable to draw segmented prompts +CURRENT_BG='NONE' + +case ${SOLARIZED_THEME:-dark} in + light) CURRENT_FG='white';; + *) CURRENT_FG='black';; +esac + +# Segments +() { + local LC_ALL="" LC_CTYPE="en_US.UTF-8" + SEGMENT_SEPARATOR= +} + +# Begin a segment +# Takes two arguments, background and foreground. Both can be omitted, +# rendering default background/foreground. +prompt_segment() { + local bg fg + [[ -n $2 ]] && fg="$FG[254]" || fg="%f" + if [[ $CURRENT_BG != 'NONE' && $1 != $CURRENT_BG ]]; then + echo -n " %{$bg%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR%{$fg%} " + else + echo -n "%{$bg%}%{$fg%}" + fi + CURRENT_BG=$1 + [[ -n $3 ]] && echo -n $3 +} + +# End the prompt, closing any open segments +prompt_end() { + if [[ -n $CURRENT_BG ]]; then + echo -n " %{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR" + else + echo -n "%{%k%}" + fi + echo -n "%{%f%}" + CURRENT_BG='' +} + +### Prompt components +# Each component will draw itself, and hide itself if no information needs to be shown + +# Context: user@hostname (who am I and where am I) +prompt_context() { } + +parse_git_dirty() { + local -a git_status + git_status=($(git status --porcelain 2>/dev/null)) + if [[ ${#git_status[@]} -gt 0 ]]; then + echo "±" + fi +} + +# Git: branch/detached head, dirty status +prompt_git() { + (( $+commands[git] )) || return + if [[ "$(git config --get oh-my-zsh.hide-status 2>/dev/null)" = 1 ]]; then + return + fi + local PL_BRANCH_CHAR + () { + local LC_ALL="" LC_CTYPE="en_US.UTF-8" + PL_BRANCH_CHAR=$'' # + } + local ref dirty mode repo_path + + if [[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]]; then + repo_path=$(git rev-parse --git-dir 2>/dev/null) + dirty=$(parse_git_dirty) + ref=$(git symbolic-ref HEAD 2> /dev/null) || ref="➦ $(git rev-parse --short HEAD 2> /dev/null)" + if [[ -n $dirty ]]; then + prompt_segment yellow black + PL_BRANCH_CHAR=%{%F{yellow}%}'' + else + prompt_segment green $CURRENT_FG + fi + + if [[ -e "${repo_path}/BISECT_LOG" ]]; then + mode=" <B>" + elif [[ -e "${repo_path}/MERGE_HEAD" ]]; then + mode=" >M<" + elif [[ -e "${repo_path}/rebase" || -e "${repo_path}/rebase-apply" || -e "${repo_path}/rebase-merge" || -e "${repo_path}/../.dotest" ]]; then + mode=" >R>" + fi + + setopt promptsubst + autoload -Uz vcs_info + + zstyle ':vcs_info:*' enable git + zstyle ':vcs_info:*' get-revision true + zstyle ':vcs_info:*' check-for-changes true + zstyle ':vcs_info:*' stagedstr '✚' + zstyle ':vcs_info:*' unstagedstr '± ' + zstyle ':vcs_info:*' formats ' %u%c' + zstyle ':vcs_info:*' actionformats ' %u%c' + vcs_info + echo -n "${ref/refs\/heads\//$PL_BRANCH_CHAR }${vcs_info_msg_0_%% }${mode}" + fi +} + +prompt_bzr() { + (( $+commands[bzr] )) || return + + # Test if bzr repository in directory hierarchy + local dir="$PWD" + while [[ ! -d "$dir/.bzr" ]]; do + [[ "$dir" = "/" ]] && return + dir="${dir:h}" + done + + local bzr_status status_mod status_all revision + if bzr_status=$(bzr status 2>&1); then + status_mod=$(echo -n "$bzr_status" | head -n1 | grep "modified" | wc -m) + status_all=$(echo -n "$bzr_status" | head -n1 | wc -m) + revision=$(bzr log -r-1 --log-format line | cut -d: -f1) + if [[ $status_mod -gt 0 ]] ; then + prompt_segment yellow black "bzr@$revision ✚" + else + if [[ $status_all -gt 0 ]] ; then + prompt_segment yellow black "bzr@$revision" + else + prompt_segment green black "bzr@$revision" + fi + fi + fi +} + +prompt_hg() { + (( $+commands[hg] )) || return + local rev st branch + if $(hg id >/dev/null 2>&1); then + if $(hg prompt >/dev/null 2>&1); then + if [[ $(hg prompt "{status|unknown}") = "?" ]]; then + # if files are not added + prompt_segment red white + st='±' + elif [[ -n $(hg prompt "{status|modified}") ]]; then + # if any modification + prompt_segment yellow black + st='±' + else + # if working copy is clean + prompt_segment green $CURRENT_FG + fi + echo -n $(hg prompt "☿ {rev}@{branch}") $st + else + st="" + rev=$(hg id -n 2>/dev/null | sed 's/[^-0-9]//g') + branch=$(hg id -b 2>/dev/null) + if `hg st | grep -q "^\?"`; then + prompt_segment red black + st='±' + elif `hg st | grep -q "^[MA]"`; then + prompt_segment yellow black + st='±' + else + prompt_segment green $CURRENT_FG + fi + echo -n "☿ $rev@$branch" $st + fi + fi +} + +# Change prompt for HOME dir +prompt_dir () { + if [[ "$PWD" == "$HOME" ]]; then + prompt_segment blue $CURRENT_FG '' + else + prompt_segment blue CURRENT_FG '%2~' + fi +} + +# Virtualenv: current working virtualenv +prompt_virtualenv() { + local virtualenv_path="$VIRTUAL_ENV" + if [[ -n $virtualenv_path && -n $VIRTUAL_ENV_DISABLE_PROMPT ]]; then + prompt_segment blue black "(`basename $virtualenv_path`)" + fi +} + +# Status: +# - was there an error +# - am I root +# - are there background jobs? +prompt_status() { + local -a symbols + + [[ $RETVAL -ne 0 ]] && symbols+=" %{%F{red}%}" + [[ $UID -eq 0 ]] && symbols+="%{%F{yellow}%}⚡" + [[ $(jobs -l | wc -l) -gt 0 ]] && symbols+="%{%F{cyan}%}⚙" + + [[ -n "$symbols" ]] && prompt_segment black default "$symbols" +} + +#AWS Profile: +# - display current AWS_PROFILE name +# - displays yellow on red if profile name contains 'production' or +# ends in '-prod' +# - displays black on green otherwise +prompt_aws() { + [[ -z "$AWS_PROFILE" || "$SHOW_AWS_PROMPT" = false ]] && return + case "$AWS_PROFILE" in + *-prod|*production*) prompt_segment red yellow "AWS: $AWS_PROFILE" ;; + *) prompt_segment green black "AWS: $AWS_PROFILE" ;; + esac +} + +## Main prompt +build_prompt() { + RETVAL=$? + prompt_status + prompt_virtualenv + prompt_aws + prompt_context + prompt_dir + prompt_git + prompt_bzr + prompt_hg + prompt_end +} + +PROMPT='%{%F{blue}%} %{%f%b%k%}$(build_prompt) ' +bindkey -M vicmd '\e[C' vi-forward-char # ESC + right arrow |
