#
# ~/.zshrc
# ZSH configuration file
#
# Credits:
# [1] https://github.com/spicycode/ze-best-zsh-config
# [2] Zsh Configuration from the Ground Up
#     http://zanshin.net/2013/02/02/zsh-configuration-from-the-ground-up/
# [3] GRML Zsh
#     http://grml.org/zsh/
#
# References:
# [1] The Unix Shell's Humble if
#     https://robots.thoughtbot.com/the-unix-shells-humble-if
#

#
# OS checks
#

IS_LINUX=false
IS_MAC=false
IS_BSD=false
OS=$(uname -s)
if [[ "${OS}" = 'Linux' ]]; then
    IS_LINUX=true
elif [[ "${OS}" = 'Darwin' ]]; then
    IS_MAC=true
elif [[ "${OS}" = 'DragonFly' ]] || [[ "${OS}" =~ '.*BSD' ]]; then
    IS_BSD=true
fi

#
# Zsh Parameters
# See zshparam(1)
#

# Ask only if the top of the listing would scroll off the screen
LISTMAX=0
# Report consuming time statistics if user+system greater than 60 seconds
REPORTTIME=60
# Format of process time reports with the `time' keyword
TIMEFMT="%J  %U user %S system %P cpu %MM memory %*E total"

# History
HISTFILE=~/.zsh_history
HISTSIZE=9000
SAVEHIST=9000
# Ignore these commands without arguments
HISTIGNORE="cd:ls:ll"

#
# Zsh Options
# See zshoptions(1)
#

## Basics
setopt NO_BEEP
setopt AUTO_CD
# make cd push the old directory onto the directory stack
setopt AUTO_PUSHD
# don't push multiple copies of the same directory into the directory stack
setopt PUSHD_IGNORE_DUPS
# exchange meanings of `+` and `-` when specifying a directory in the stack
setopt PUSHD_MINUS
# do not print the directory stack after `pushd` or `popd`
setopt PUSHD_SILENT
# treat #, ~, and ^ as part of patterns for filename generation
setopt EXTENDED_GLOB
# allow comments even in interactive shells (especially for Muness)
setopt INTERACTIVE_COMMENTS
# display PID when suspending processes as well
setopt LONG_LIST_JOBS
# disable output flow control via start/stop characters (^S/^Q)
unsetopt FLOW_CONTROL

## History
# allow multiple terminal sessions to all append to one zsh command history
setopt APPEND_HISTORY
# include timestamp of command and duration to history
setopt EXTENDED_HISTORY
# add comamnds as they are typed, don't wait until shell exit
setopt INC_APPEND_HISTORY
# do not write events to history that are duplicates of previous events
setopt HIST_IGNORE_DUPS
# remove command line from history list when it begins a space
setopt HIST_IGNORE_SPACE
# when searching history don't display results already cycled through twice
setopt HIST_FIND_NO_DUPS
# remove extra blanks from each command line being added to history
setopt HIST_REDUCE_BLANKS
# don't execute, just expand history
setopt HIST_VERIFY

## Completion
# `*' shouldn't match dotfiles. ever.
setopt NO_GLOB_DOTS
# allow completion from within a word/phrase
setopt COMPLETE_IN_WORD
# when completing from middle of a word, move cursor to the end of the word
setopt ALWAYS_TO_END
# show completion menu on successive tab press (needs 'unsetopt MENU_COMPLETE')
setopt AUTO_MENU
unsetopt MENU_COMPLETE
# make the completion list compact
setopt LIST_PACKED

## Correction
# spelling correction for commands
setopt CORRECT
# spelling correction for arguments
#setopt CORRECTALL

#
# Prompt
# Credit: http://chneukirchen.org/dotfiles/.zshrc
#

# enable parameter expansion, command substitution, and arithmetic expansion
setopt PROMPT_SUBST
# remove any right prompt from display when accepting a command line
setopt TRANSIENT_RPROMPT

# gitpwd - format the current path with inline git branch for the
#          prompt; the current path is limited to $NDIR segments,
#          meanwhile long segments are shortened to be
#          '<prefix>…<suffix>'.
NDIRS=3
function gitpwd() {
    local -a segs splitprefix
    local prefix branch
    segs=("${(Oas:/:)${(D)PWD}}")
    segs=("${(@)segs/(#b)(?(#c10))??*(?(#c5))/${(j:\u2026:)match}}")

    if gitprefix=$(git rev-parse --show-prefix 2>/dev/null); then
        splitprefix=("${(s:/:)gitprefix}")
        if ! branch=$(git symbolic-ref -q --short HEAD); then
           branch=$(git name-rev --name-only HEAD 2>/dev/null)
           [[ $branch = *\~* ]] || branch+="~0"    # distinguish detached HEAD
        fi
        if (( $#splitprefix > NDIRS )); then
           print -n "${segs[$#splitprefix]}@$branch "
        else
           segs[$#splitprefix]+=@$branch
        fi
    fi

    (( $#segs == NDIRS+1 )) && [[ $segs[-1] == "" ]] && print -n /
    print "${(j:/:)${(@Oa)segs[1,NDIRS]}}"
}

PROMPT='%S%B%F{green}[%m]%s%(?.. %F{red}%??)%(1j. %F{yellow}%j&.)%b%f $(gitpwd)%B%(!.%F{red}.%F{green})%#${SSH_CONNECTION:+%#} %b%f'
RPROMPT=''
SPROMPT='zsh: correct %B%F{red}%R%b%f to %B%F{green}%r%b%f [(y)es (n)o (a)bort (e)dit]? '

#
# Completion
#

fpath=(~/.zsh.completions $fpath)
autoload -U compinit && compinit
zmodload -i zsh/complist

# enable completion caching, use rehash to clear
zstyle ':completion::complete:*' use-cache on
zstyle ':completion::complete:*' cache-path ~/.cache/zsh/${HOST}

# make the selection prompt friendly when there are a lot of choices
zstyle ':completion:*' select-prompt '%SScrolling active: current selection at %p%s'

# list of completers to use
zstyle ':completion:*::::' completer _expand _complete _ignored _approximate
zstyle ':completion:*' menu select=1 _complete _ignored _approximate

# match uppercase from lowercase
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

#
# Functions
#

## Fix terminal
# Credit: https://unix.stackexchange.com/a/299922
fix() {
    reset
    stty sane
    tput rs1
    clear
    echo -e "\033c"
}

## Check the existence/accessibility of a command
# Credit: https://stackoverflow.com/a/677212/4856091
function exists() {
    # 'command' is POSIX-compliant and more portable;
    # 'hash' only searches for commands;
    # while 'type' also considers builtins and keywords.
    type "$1" >/dev/null 2>&1
}

## Check whether the program is running
function is_running() {
    pgrep -x -u "${USER}" "$1" >/dev/null 2>&1
}

## Interactive move/rename: making renaming long filenames less sucks
# Credit: http://chneukirchen.org/dotfiles/.zshrc
function imv() {
    local src dst
    for src; do
        [[ -e "$src" ]] || { print -u2 "$src: does not exist"; continue }
        dst="$src"
        vared dst
        [[ "$src" != "$dst" ]] && mkdir -p ${dst:h} && mv -n $src $dst
    done
}

## Print pre-defined C macros
# Credit: http://chneukirchen.org/dotfiles/.zshrc
ccdef() {
    ${1:-cc} $@[2,-1] -dM -E - </dev/null
}

## Convert IP between dotted-decimal and hexdecimal notation
# Credit: https://stackoverflow.com/a/6149254
ip2hex() {
    printf '%02X' $(echo "$1" | sed 's/\./ /g')
}
hex2ip() {
    printf '%d.%d.%d.%d' $(echo "${1#0[xX]}" | sed 's/\(..\)/0x\1 /g')
}

## Convert IP between dotted-decimal and decimal notation
# Credit: https://stackoverflow.com/a/24136051
ip2dec() {
    echo "$1" | awk 'BEGIN { RS = "." } { d = d * 256 + $1 } END { print d }'
}
# Credit: https://stackoverflow.com/a/31281331
dec2ip() {
    printf '%d.%d.%d.%d' $(echo "obase=256; $1" | bc)
}

## Colorize patch / diff output
# Credit: https://www.moritz.systems/bsd-tips-and-tricks/show-diff-output-in-color/
cpatch() {
    local _c1="$(tput setaf 1)$(tput bold)"  # red bold
    local _c2="$(tput setaf 2)$(tput bold)"  # green bold
    local _clr=$(tput sgr0)
    sed -e 's/^-.*$/'${_c1}'&'${_clr}'/' -e 's/^+.*$/'${_c2}'&'${_clr}'/'
}
cdiff() {
    diff -u "$@" | cpatch
}

## Uncompress zlib data
# Credit: https://unix.stackexchange.com/a/49066
zlibd() {
    # Prepend the gzip magic number and compress method.
    { printf '\x1f\x8b\x08\x00\x00\x00\x00\x00'; cat "$@"; } | gzip -dc
}

#
# Terminal settings
#

# disable sending of start (Ctrl-Q) and stop (Ctrl-S) characters
stty -ixoff
# disable XON/XOFF flow control
stty -ixon

#
# Vi-mode
#
# Credits:
# * https://github.com/robbyrussell/oh-my-zsh/blob/master/plugins/vi-mode/vi-mode.plugin.zsh
# * http://zshwiki.org/home/zle/bindkeys
#

# Updates editor information when the keymap changes.
function zle-keymap-select() {
    zle reset-prompt
    zle -R
}

# Ensure that the prompt is redrawn when the terminal size changes.
TRAPWINCH() {
    zle && { zle reset-prompt; zle -R }
}

zle -N zle-keymap-select
zle -N edit-command-line


# NOTE: This will *reset* previous bindkey settings!
bindkey -v

# allow v to edit the command line (standard behaviour)
autoload -Uz edit-command-line
bindkey -M vicmd 'v' edit-command-line

# allow ctrl-p, ctrl-n for navigate history (standard behaviour)
bindkey '^P' up-history
bindkey '^N' down-history

# allow ctrl-h, ctrl-w, ctrl-? for char and word deletion (standard behaviour)
bindkey '^?' backward-delete-char
bindkey '^h' backward-delete-char
bindkey '^w' backward-kill-word

# allow ctrl-r for incremental history search
bindkey -M viins '^r' history-incremental-search-backward
bindkey -M vicmd '^r' history-incremental-search-backward

# if mode indicator wasn't setup by theme, define default
if [[ "$MODE_INDICATOR" == "" ]]; then
    MODE_INDICATOR="%{$fg_bold[red]%}<%{$fg[red]%}<<%{$reset_color%}"
fi

function vi_mode_prompt_info() {
    echo "${${KEYMAP/vicmd/$MODE_INDICATOR}/(main|viins)/}"
}

# define right prompt, if it wasn't defined by a theme
if [[ "$RPS1" == "" && "$RPROMPT" == "" ]]; then
    RPS1='$(vi_mode_prompt_info)'
fi

#
# Key bindings
# See zshzle(1)
#
# Credit:
# * oh-my-zsh: https://github.com/robbyrussell/oh-my-zsh
#   lib/key-bindings.zsh
#

# Make sure that the terminal is in application mode when zle is active,
# since only then values from $terminfo are valid
if (( ${+terminfo[smkx]} )) && (( ${+terminfo[rmkx]} )); then
    function zle-line-init() {
        echoti smkx
    }
    function zle-line-finish() {
        echoti rmkx
    }
    zle -N zle-line-init
    zle -N zle-line-finish
fi

# [Ctrl-r] - Search backward incrementally for a specified string.
# The string may begin with ^ to anchor the search to the beginning of the line.
bindkey '^r' history-incremental-search-backward

# [PageUp] - Up a line of history
if [[ "${terminfo[kpp]}" != "" ]]; then
    bindkey "${terminfo[kpp]}" up-line-or-history
fi
# [PageDown] - Down a line of history
if [[ "${terminfo[knp]}" != "" ]]; then
    bindkey "${terminfo[knp]}" down-line-or-history
fi

# Start typing + [Up-Arrow] - fuzzy find history forward
if [[ "${terminfo[kcuu1]}" != "" ]]; then
    autoload -U up-line-or-beginning-search
    zle -N up-line-or-beginning-search
    bindkey "${terminfo[kcuu1]}" up-line-or-beginning-search
fi
# Start typing + [Down-Arrow] - fuzzy find history backward
if [[ "${terminfo[kcud1]}" != "" ]]; then
    autoload -U down-line-or-beginning-search
    zle -N down-line-or-beginning-search
    bindkey "${terminfo[kcud1]}" down-line-or-beginning-search
fi

# [Home] - Go to beginning of line
if [[ "${terminfo[khome]}" != "" ]]; then
    bindkey "${terminfo[khome]}" beginning-of-line
fi
# [End] - Go to end of line
if [[ "${terminfo[kend]}" != "" ]]; then
    bindkey "${terminfo[kend]}"  end-of-line
fi

# [Space] - do history expansion
bindkey ' ' magic-space
# [Ctrl-RightArrow] - move forward one word
bindkey '^[[1;5C' forward-word
# [Ctrl-LeftArrow] - move backward one word
bindkey '^[[1;5D' backward-word

# [Shift-Tab] - move through the completion menu backwards
if [[ "${terminfo[kcbt]}" != "" ]]; then
    bindkey "${terminfo[kcbt]}" reverse-menu-complete
fi

# [Backspace] - delete backward
bindkey '^?' backward-delete-char
# [Delete] - delete forward
if [[ "${terminfo[kdch1]}" != "" ]]; then
    bindkey "${terminfo[kdch1]}" delete-char
else
    bindkey "^[[3~" delete-char
    bindkey "^[3;5~" delete-char
    bindkey "\e[3~" delete-char
fi

# Emacs style line editing
bindkey "^K"    kill-whole-line                      # ctrl-k
bindkey "^R"    history-incremental-search-backward  # ctrl-r
bindkey "^A"    beginning-of-line                    # ctrl-a
bindkey "^E"    end-of-line                          # ctrl-e
bindkey "[B"    history-search-forward               # down arrow
bindkey "[A"    history-search-backward              # up arrow
bindkey "^D"    delete-char                          # ctrl-d
bindkey "^F"    forward-char                         # ctrl-f
bindkey "^B"    backward-char                        # ctrl-b

# Bash-style word killing: word characters are alphanumeric characters only
# see zshcontrib(1)
autoload -U select-word-style
select-word-style bash

#
# ZLE generic settings
# See zshzle(1)
#

# Turn off ZLE bracketed paste in dumb and cons25 (DFly default console) term,
# otherwise turn on ZLE bracketed-paste-magic
# Credit: http://zmwangx.github.io/blog/2015-09-21-zsh-51-and-bracketed-paste.html
# See also zshparam(1)
if [[ ${TERM} == dumb ]] || [[ ${TERM} == cons25 ]]; then
    unset zle_bracketed_paste
else
    autoload -Uz bracketed-paste-magic
    zle -N bracketed-paste bracketed-paste-magic
fi

#
# GnuPG integration
#

# NOTE:
# Install both the `pinentry-gtk-2' and `pinentry-curses', and symlink
# `pinentry-gtk-2' to `pinentry' as the default pinentry program, which
# will fallback to the text mode when X11 is not avaiable (i.e.,
# `$DISPLAY' is not set), e.g., through SSH logins.
# `pinentry-gnome3' seems to have problem that cannot fallback to the
# text mode ... (for unkown reasons ...)

# This `GPG_TTY' variable should be set to the correct TTY where the shell
# is running.  See `gpg-agent(1)' for more details.
export GPG_TTY=$(tty)

# Make SSH to use `gpg-agent'.
unset SSH_AGENT_PID
if [ "${gnupg_SSH_AUTH_SOCK_by:-0}" -ne $$ ]; then
    export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)"
fi

# Use curses-based pinentry for SSH logins
# Credit: https://wiki.gentoo.org/wiki/GnuPG
if [ -n "${SSH_CONNECTION}" ] ;then
    export PINENTRY_USER_DATA="USE_CURSES=1"
fi

# Let pinentry know which console to display in for `ssh-agent'.
#
# Since the 'ssh-agent' protocol does not contain a mechanism for telling
# the agent on which terminal/display it is running, gpg-agent's
# ssh-support can just use the TTY or X display when `gpg-agent' has been
# started, which may be before the X session startup.  Therefore, when the
# switched to the X session, or login remotely through SSH, the `pinentry'
# will get popped up on whatever display the `gpg-agent' has been started
# or may just fail.  In this case, a manual update is necessary.
#
# This will set startup TTY and X11 DISPLAY variables to the values of
# this session.
#
# Credits:
# * GnuPG: Commonly Seen Problems
#   https://www.gnupg.org/documentation/manuals/gnupg/Common-Problems.html
# * `gpg-agent(1)': option `--enable-ssh-support'
# * http://blog.mrloop.com/workflow/2017/02/09/pin-entry.html
#
update-gpg-tty() {
    gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1 || true
}
autoload -U add-zsh-hook
add-zsh-hook preexec update-gpg-tty

# Delete all identities from the `gpg-agent', which is similar to
# `ssh-add -D`.
# Credit: http://blog.mrloop.com/workflow/2017/02/09/pin-entry.html
ssh-delete() {
    grep -o '^[A-Z0-9]*' ${HOME}/.gnupg/sshcontrol | \
        xargs -I'%' rm ${HOME}/.gnupg/private-keys-v1.d/'%'.key
    echo "" > ${HOME}/.gnupg/sshcontrol
}

#
# Aliases
#

alias zhelp='run-help'

alias ..='cd ../'
alias ...='cd ../../'
alias ....='cd ../../../'
alias cd..='cd ..'
alias d='dirs -v | head -n 10'
alias po=popd
alias pu=pushd

if ${IS_LINUX} ]]; then
    alias ls='ls --color=auto'
elif ${IS_BSD} || ${IS_MAC}; then
    alias ls='ls -G'
fi
alias l='ls -lah'
alias la='ls -lAh'
alias ll='ls -lh'
alias lsa='ls -lah'

# Do not use `GREP_OPTIONS`
alias grep='grep --color=auto'

exists "vi" || alias vi=vim
exists "safe-rm" && alias rm=safe-rm

#
# Evnironment variables
#

# NOTE:
# Generic environment variables and those needing been set only once
# should go to `~/.profile'.

# colors for BSD ls
if ${IS_BSD}; then
    export CLICOLOR=1
    export LSCOLORS=exfxcxdxbxegedabagacad
fi

# Color setup for `ls': `LS_COLORS'
# NOTE: For unknown reason, the `LS_COLORS' variable get overridden when
#       it is set in `~/.profile'.
if exists dircolors; then
    eval $(dircolors -b)
fi

#
# Local configurations
#

[ -r ~/.zshrc.local ] && source ~/.zshrc.local || true