feat: split zshrc, add mouse scrolling to vim/delta
This commit is contained in:
@@ -19,6 +19,20 @@
|
|||||||
|
|
||||||
[core]
|
[core]
|
||||||
autocrlf = input
|
autocrlf = input
|
||||||
|
pager = delta
|
||||||
|
|
||||||
|
[interactive]
|
||||||
|
diffFilter = delta --color-only
|
||||||
|
|
||||||
|
[delta]
|
||||||
|
navigate = true
|
||||||
|
side-by-side = false
|
||||||
|
line-numbers = true
|
||||||
|
paging = always
|
||||||
|
pager = less --mouse -RFX
|
||||||
|
|
||||||
|
[merge]
|
||||||
|
conflictstyle = zdiff3
|
||||||
|
|
||||||
[color]
|
[color]
|
||||||
ui = auto
|
ui = auto
|
||||||
@@ -66,5 +80,5 @@
|
|||||||
m = commit --amend --verbose
|
m = commit --amend --verbose
|
||||||
st = stash
|
st = stash
|
||||||
sa = stash apply
|
sa = stash apply
|
||||||
stl = stash list
|
cc = copilot --continue
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ set number relativenumber
|
|||||||
set ignorecase smartcase incsearch hlsearch
|
set ignorecase smartcase incsearch hlsearch
|
||||||
set autoindent expandtab tabstop=4 shiftwidth=4 softtabstop=4
|
set autoindent expandtab tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
set scrolloff=8 wildmenu showcmd laststatus=2
|
set scrolloff=8 wildmenu showcmd laststatus=2
|
||||||
|
set mouse=a
|
||||||
set hidden
|
set hidden
|
||||||
set noswapfile nobackup undofile
|
set noswapfile nobackup undofile
|
||||||
let s:undo_dir = expand('~/.vim/undodir')
|
let s:undo_dir = expand('~/.vim/undodir')
|
||||||
|
|||||||
280
files/home/.zsh/prompt.zsh
Normal file
280
files/home/.zsh/prompt.zsh
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
# Prompt
|
||||||
|
(( ${+PROMPT_MIN_DURATION} )) || typeset -gi PROMPT_MIN_DURATION=2 # show duration after N seconds
|
||||||
|
(( ${+PROMPT_FLASH_DELAY} )) || typeset -gi PROMPT_FLASH_DELAY=4 # flash prompt for N centiseconds
|
||||||
|
|
||||||
|
typeset -gi _dots_prompt_cmd_start=0
|
||||||
|
typeset -gi _dots_prompt_cmd_ran=0
|
||||||
|
typeset -gi _dots_prompt_flashing=0
|
||||||
|
typeset -g _dots_prompt_base=""
|
||||||
|
typeset -gA _dots_pc
|
||||||
|
|
||||||
|
_dots_init_colors() {
|
||||||
|
if [[ "$COLORTERM" == (truecolor|24bit) ]]; then
|
||||||
|
_dots_pc=(
|
||||||
|
teal $'%{\e[38;2;44;180;148m%}'
|
||||||
|
orange $'%{\e[38;2;248;140;20m%}'
|
||||||
|
red $'%{\e[38;2;244;4;4m%}'
|
||||||
|
grey $'%{\e[38;2;114;144;184m%}'
|
||||||
|
)
|
||||||
|
elif [[ "$TERM" == *256color* ]]; then
|
||||||
|
_dots_pc=(
|
||||||
|
teal $'%{\e[38;5;43m%}'
|
||||||
|
orange $'%{\e[38;5;208m%}'
|
||||||
|
red $'%{\e[38;5;196m%}'
|
||||||
|
grey $'%{\e[38;5;103m%}'
|
||||||
|
)
|
||||||
|
else
|
||||||
|
_dots_pc=(
|
||||||
|
teal $'%{\e[36m%}'
|
||||||
|
orange $'%{\e[33m%}'
|
||||||
|
red $'%{\e[31m%}'
|
||||||
|
grey $'%{\e[34m%}'
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
_dots_pc[reset]=$'%{\e[0m%}'
|
||||||
|
_dots_pc[bold]=$'%{\e[1m%}'
|
||||||
|
}
|
||||||
|
|
||||||
|
_dots_abbrev_path() {
|
||||||
|
local dir="${PWD/#$HOME/~}"
|
||||||
|
local -a parts=( "${(@s:/:)dir}" )
|
||||||
|
local count=${#parts[@]}
|
||||||
|
|
||||||
|
(( count <= 3 )) && { print -r -- "$dir"; return }
|
||||||
|
|
||||||
|
local result=""
|
||||||
|
local i
|
||||||
|
for (( i=1; i <= count-3; i++ )); do
|
||||||
|
result+="${parts[i][1]}/"
|
||||||
|
done
|
||||||
|
print -r -- "${result}${parts[-3]}/${parts[-2]}/${parts[-1]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
_dots_session() {
|
||||||
|
[[ -n "$CODESPACE_NAME" ]] && { print -r -- "$CODESPACE_NAME"; return }
|
||||||
|
[[ -n "$SSH_CONNECTION" || -n "$SSH_CLIENT" || -n "$SSH_TTY" ]] && { print -r -- "%n@%m"; return }
|
||||||
|
[[ -f /.dockerenv ]] && { print -r -- "${DEVCONTAINER_ID:-$(</etc/hostname)}"; return }
|
||||||
|
}
|
||||||
|
|
||||||
|
_dots_git_info_sync() {
|
||||||
|
# Use git status --short with awk for better performance
|
||||||
|
# Avoids shell loops and uses compiled awk for parsing
|
||||||
|
local output
|
||||||
|
output=$(git status --short --branch --ignore-submodules=dirty 2>/dev/null) || return
|
||||||
|
|
||||||
|
local branch="" ahead=0 behind=0 staged=0 unstaged=0 untracked=0
|
||||||
|
|
||||||
|
# Parse efficiently: branch info from first line, counts from rest
|
||||||
|
local first_line="${output%%$'\n'*}"
|
||||||
|
|
||||||
|
# Extract branch from "## branch...remote [ahead N, behind M]"
|
||||||
|
if [[ "$first_line" == "## "* ]]; then
|
||||||
|
branch="${first_line#\#\# }"
|
||||||
|
# Handle detached HEAD
|
||||||
|
if [[ "$branch" == "HEAD (no branch)" || "$branch" == [0-9a-f]##* ]]; then
|
||||||
|
branch="${branch:0:7}"
|
||||||
|
else
|
||||||
|
# Remove tracking info
|
||||||
|
branch="${branch%%...*}"
|
||||||
|
branch="${branch%% *}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract ahead/behind
|
||||||
|
[[ "$first_line" =~ "ahead ([0-9]+)" ]] && ahead="${match[1]}"
|
||||||
|
[[ "$first_line" =~ "behind ([0-9]+)" ]] && behind="${match[1]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -z "$branch" ]] && return
|
||||||
|
|
||||||
|
# Count file status with awk (fast, no shell loops)
|
||||||
|
local -a counts
|
||||||
|
counts=( $(awk '
|
||||||
|
NR == 1 { next } # Skip branch line
|
||||||
|
/^\?\?/ { untracked++; next }
|
||||||
|
{
|
||||||
|
x = substr($0, 1, 1)
|
||||||
|
y = substr($0, 2, 1)
|
||||||
|
if (x != " " && x != "?") staged++
|
||||||
|
if (y != " " && y != "?") unstaged++
|
||||||
|
}
|
||||||
|
END { print staged+0, unstaged+0, untracked+0 }
|
||||||
|
' <<< "$output") )
|
||||||
|
|
||||||
|
staged=${counts[1]:-0}
|
||||||
|
unstaged=${counts[2]:-0}
|
||||||
|
untracked=${counts[3]:-0}
|
||||||
|
|
||||||
|
local info="${_dots_pc[grey]}(${branch})${_dots_pc[reset]}"
|
||||||
|
|
||||||
|
local dirty=""
|
||||||
|
local sep=""
|
||||||
|
if (( staged )); then
|
||||||
|
dirty+="${_dots_pc[teal]}+${staged}${_dots_pc[reset]}"
|
||||||
|
sep=" "
|
||||||
|
fi
|
||||||
|
if (( unstaged )); then
|
||||||
|
dirty+="${sep}${_dots_pc[orange]}~${unstaged}${_dots_pc[reset]}"
|
||||||
|
sep=" "
|
||||||
|
fi
|
||||||
|
if (( untracked )); then
|
||||||
|
dirty+="${sep}${_dots_pc[grey]}?${untracked}${_dots_pc[reset]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local arrows=""
|
||||||
|
sep=""
|
||||||
|
if (( ahead )); then
|
||||||
|
arrows+="${_dots_pc[teal]}↑${ahead}${_dots_pc[reset]}"
|
||||||
|
sep=" "
|
||||||
|
fi
|
||||||
|
if (( behind )); then
|
||||||
|
arrows+="${sep}${_dots_pc[orange]}↓${behind}${_dots_pc[reset]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$dirty" || -n "$arrows" ]]; then
|
||||||
|
info+=" "
|
||||||
|
[[ -n "$dirty" ]] && info+="$dirty"
|
||||||
|
[[ -n "$dirty" && -n "$arrows" ]] && info+=" "
|
||||||
|
[[ -n "$arrows" ]] && info+="$arrows"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print -r -- "$info"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Async git info
|
||||||
|
typeset -g _dots_git_info_result=""
|
||||||
|
typeset -g _dots_git_info_pwd=""
|
||||||
|
typeset -g _dots_git_async_fd=""
|
||||||
|
|
||||||
|
_dots_git_async_callback() {
|
||||||
|
local fd=$1
|
||||||
|
local result=""
|
||||||
|
# Use sysread for efficient non-blocking read from fd
|
||||||
|
if [[ -n "$fd" ]] && sysread -i "$fd" result 2>/dev/null; then
|
||||||
|
result="${result%$'\n'}" # trim trailing newline
|
||||||
|
_dots_git_info_result="$result"
|
||||||
|
_dots_build_dots_prompt_base
|
||||||
|
PROMPT="$_dots_prompt_base"
|
||||||
|
zle && zle reset-prompt
|
||||||
|
fi
|
||||||
|
# Clean up
|
||||||
|
exec {fd}<&-
|
||||||
|
zle -F "$fd" 2>/dev/null
|
||||||
|
_dots_git_async_fd=""
|
||||||
|
}
|
||||||
|
|
||||||
|
_dots_git_async_start() {
|
||||||
|
# Check if we're in a git repo
|
||||||
|
local git_dir
|
||||||
|
git_dir=$(git rev-parse --git-dir 2>/dev/null) || return
|
||||||
|
|
||||||
|
# Cancel any pending async job (reuse single worker)
|
||||||
|
if [[ -n "$_dots_git_async_fd" ]]; then
|
||||||
|
exec {_dots_git_async_fd}<&- 2>/dev/null
|
||||||
|
zle -F "$_dots_git_async_fd" 2>/dev/null
|
||||||
|
_dots_git_async_fd=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start background job
|
||||||
|
exec {_dots_git_async_fd}< <(
|
||||||
|
_dots_git_info_sync
|
||||||
|
)
|
||||||
|
zle -F "$_dots_git_async_fd" _dots_git_async_callback
|
||||||
|
}
|
||||||
|
|
||||||
|
_dots_build_dots_prompt_base() {
|
||||||
|
local dir_path="$(_dots_abbrev_path)"
|
||||||
|
local symbol="${_dots_pc[grey]}>${_dots_pc[reset]}"
|
||||||
|
(( EUID == 0 )) && symbol="${_dots_pc[orange]}${_dots_pc[bold]}#${_dots_pc[reset]}"
|
||||||
|
|
||||||
|
local line1="${_dots_pc[teal]}${dir_path}${_dots_pc[reset]}"
|
||||||
|
[[ -n "$_dots_git_info_result" ]] && line1+=" ${_dots_git_info_result}"
|
||||||
|
|
||||||
|
_dots_prompt_base=$'\n'"${line1}"$'\n'"${symbol} "
|
||||||
|
}
|
||||||
|
|
||||||
|
_dots_preexec() {
|
||||||
|
_dots_prompt_cmd_start=$EPOCHSECONDS
|
||||||
|
_dots_prompt_cmd_ran=1
|
||||||
|
}
|
||||||
|
|
||||||
|
_dots_precmd() {
|
||||||
|
local e=$? d=0
|
||||||
|
# Only show exit code if a command actually ran
|
||||||
|
(( _dots_prompt_cmd_ran )) || e=0
|
||||||
|
_dots_prompt_cmd_ran=0
|
||||||
|
# First prompt should never show error from shell init
|
||||||
|
[[ -z "$_dots_first_prompt" ]] && { _dots_first_prompt=1; e=0; }
|
||||||
|
|
||||||
|
# Build RPROMPT: time, error, host
|
||||||
|
local rp_parts=()
|
||||||
|
|
||||||
|
if (( _dots_prompt_cmd_start )); then
|
||||||
|
d=$(( EPOCHSECONDS - _dots_prompt_cmd_start ))
|
||||||
|
_dots_prompt_cmd_start=0
|
||||||
|
if (( d >= PROMPT_MIN_DURATION )); then
|
||||||
|
(( d >= 60 )) && rp_parts+=("${_dots_pc[grey]}($(( d/60 ))m$(( d%60 ))s)${_dots_pc[reset]}") \
|
||||||
|
|| rp_parts+=("${_dots_pc[grey]}(${d}s)${_dots_pc[reset]}")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
(( e )) && rp_parts+=("${_dots_pc[red]}[${e}]${_dots_pc[reset]}")
|
||||||
|
|
||||||
|
local session="$(_dots_session)"
|
||||||
|
[[ -n "$session" ]] && rp_parts+=("${_dots_pc[orange]}[${session}]${_dots_pc[reset]}")
|
||||||
|
|
||||||
|
RPROMPT="${(j: :)rp_parts}"
|
||||||
|
|
||||||
|
# On directory change, clear git info until async refreshes
|
||||||
|
if [[ "$PWD" != "$_dots_git_info_pwd" ]]; then
|
||||||
|
_dots_git_info_pwd="$PWD"
|
||||||
|
_dots_git_info_result=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
_dots_build_dots_prompt_base
|
||||||
|
_dots_git_async_start
|
||||||
|
PROMPT="$_dots_prompt_base"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
TRAPINT() {
|
||||||
|
# Only customize when ZLE is active (at prompt, not during command)
|
||||||
|
if [[ -o zle ]] && [[ -o interactive ]] && (( ${+WIDGET} )); then
|
||||||
|
if [[ -z "$BUFFER" ]] && (( ! _dots_prompt_flashing )); then
|
||||||
|
# Empty buffer: flash the prompt symbol
|
||||||
|
_dots_prompt_flashing=1
|
||||||
|
local git_part=""
|
||||||
|
[[ -n "$_dots_git_info_result" ]] && git_part=" ${_dots_git_info_result}"
|
||||||
|
local flash_prompt=$'\n'"${_dots_pc[teal]}$(_dots_abbrev_path)${_dots_pc[reset]}${git_part}"$'\n'$'%{\e[48;2;248;140;20m\e[30m%}> %{\e[0m%}'
|
||||||
|
PROMPT="$flash_prompt"
|
||||||
|
zle reset-prompt
|
||||||
|
zselect -t $PROMPT_FLASH_DELAY
|
||||||
|
_dots_prompt_flashing=0
|
||||||
|
PROMPT="$_dots_prompt_base"
|
||||||
|
zle reset-prompt
|
||||||
|
return 0
|
||||||
|
elif [[ -n "$BUFFER" ]]; then
|
||||||
|
# Buffer has content: clear autosuggest, then default behavior
|
||||||
|
zle autosuggest-clear 2>/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Propagate signal: use special return code -1 to let zsh handle normally
|
||||||
|
return $((128 + ${1:-2}))
|
||||||
|
}
|
||||||
|
|
||||||
|
_dots_prompt_init() {
|
||||||
|
zmodload zsh/datetime 2>/dev/null
|
||||||
|
zmodload zsh/zselect 2>/dev/null
|
||||||
|
zmodload zsh/system 2>/dev/null
|
||||||
|
_dots_init_colors
|
||||||
|
_dots_build_dots_prompt_base
|
||||||
|
|
||||||
|
setopt PROMPT_SUBST EXTENDED_HISTORY INC_APPEND_HISTORY_TIME
|
||||||
|
autoload -Uz add-zsh-hook
|
||||||
|
add-zsh-hook preexec _dots_preexec
|
||||||
|
add-zsh-hook precmd _dots_precmd
|
||||||
|
add-zsh-hook chpwd _dots_build_dots_prompt_base
|
||||||
|
|
||||||
|
PROMPT="$_dots_prompt_base" RPROMPT=""
|
||||||
|
}
|
||||||
|
_dots_prompt_init
|
||||||
235
files/home/.zsh/widgets.zsh
Normal file
235
files/home/.zsh/widgets.zsh
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
_dots_load_keybindings() {
|
||||||
|
bindkey -e
|
||||||
|
stty -ixon 2>/dev/null
|
||||||
|
|
||||||
|
# Ctrl+J: zoxide jump
|
||||||
|
_dots_zoxide_widget() {
|
||||||
|
local result
|
||||||
|
result="$(zoxide query -i -- 2>&1)" || { zle reset-prompt; return; }
|
||||||
|
BUFFER="cd ${(q)result}"
|
||||||
|
zle reset-prompt
|
||||||
|
zle accept-line
|
||||||
|
}
|
||||||
|
zle -N _dots_zoxide_widget
|
||||||
|
bindkey '^J' _dots_zoxide_widget
|
||||||
|
|
||||||
|
# Ctrl+B: git branch checkout
|
||||||
|
_dots_git_branch_widget() {
|
||||||
|
local branch
|
||||||
|
branch="$(git branch --all --sort=-committerdate --format='%(refname:short)' 2>/dev/null \
|
||||||
|
| fzf --preview 'git log --oneline --color -20 {}')" || { zle reset-prompt; return; }
|
||||||
|
branch="${branch#origin/}"
|
||||||
|
BUFFER="git checkout ${(q)branch}"
|
||||||
|
zle reset-prompt
|
||||||
|
zle accept-line
|
||||||
|
}
|
||||||
|
zle -N _dots_git_branch_widget
|
||||||
|
bindkey '^B' _dots_git_branch_widget
|
||||||
|
|
||||||
|
# Ctrl+E: edit file
|
||||||
|
_dots_edit_widget() {
|
||||||
|
local file
|
||||||
|
file="$({ rg --files --hidden --glob '!.git' 2>/dev/null || find . -type f -not -path '*/.git/*'; } \
|
||||||
|
| fzf --preview 'head -100 {}')" || { zle reset-prompt; return; }
|
||||||
|
${EDITOR:-vim} "$file" </dev/tty
|
||||||
|
zle reset-prompt
|
||||||
|
}
|
||||||
|
zle -N _dots_edit_widget
|
||||||
|
bindkey '^E' _dots_edit_widget
|
||||||
|
|
||||||
|
# Ctrl+G: remote connect
|
||||||
|
if [[ -z "${CODESPACES:-}" ]]; then
|
||||||
|
_dots_ssh_hosts() {
|
||||||
|
local ssh_log="${XDG_DATA_HOME:-$HOME/.local/share}/ssh/log"
|
||||||
|
local cs_cache="$_dots_cache_dir/codespaces"
|
||||||
|
|
||||||
|
# Refresh codespace cache in background if stale (>5 min)
|
||||||
|
if [[ ! -f "$cs_cache" ]] || [[ -n "$(find "$cs_cache" -mmin +5 2>/dev/null)" ]]; then
|
||||||
|
{ gh cs list --json name -q '.[].name' 2>/dev/null | sed 's/^/cs:/' > "$cs_cache.tmp" && mv "$cs_cache.tmp" "$cs_cache"; } &!
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
if [[ -f "$ssh_log" ]]; then
|
||||||
|
awk '{c[$2]++; t[$2]=$1} END {for(h in c) print c[h]*1000+t[h], h}' "$ssh_log" | sort -rn | awk '{print $2}'
|
||||||
|
fi
|
||||||
|
awk '/^Host / && !/\*/ {print $2}' ~/.ssh/config ~/.ssh/config.d/* 2>/dev/null
|
||||||
|
awk '{print $1}' ~/.ssh/known_hosts 2>/dev/null | tr ',' '\n' | sed 's/\[//;s/\]:.*//'
|
||||||
|
[[ -f "$cs_cache" ]] && cat "$cs_cache"
|
||||||
|
} | awk '!seen[$0]++'
|
||||||
|
}
|
||||||
|
_dots_ssh_widget() {
|
||||||
|
local target
|
||||||
|
target="$(_dots_ssh_hosts | fzf)" || { zle reset-prompt; return; }
|
||||||
|
if [[ "$target" == cs:* ]]; then
|
||||||
|
BUFFER="cs ${target#cs:}"
|
||||||
|
else
|
||||||
|
BUFFER="ssh $target"
|
||||||
|
fi
|
||||||
|
zle reset-prompt
|
||||||
|
zle accept-line
|
||||||
|
}
|
||||||
|
zle -N _dots_ssh_widget
|
||||||
|
bindkey '^G' _dots_ssh_widget
|
||||||
|
else
|
||||||
|
bindkey -r '^G'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ctrl+F: find in files
|
||||||
|
_dots_find_in_files_widget() {
|
||||||
|
local selection
|
||||||
|
selection="$(rg --color=always --line-number --no-heading '' 2>/dev/null \
|
||||||
|
| fzf --ansi --delimiter=: \
|
||||||
|
--preview 'head -n $((({2}+30))) {1} | tail -n 60' \
|
||||||
|
--preview-window='right:60%')" || { zle reset-prompt; return; }
|
||||||
|
local file="${selection%%:*}"
|
||||||
|
local line="${${selection#*:}%%:*}"
|
||||||
|
${EDITOR:-vim} "+$line" "$file" </dev/tty
|
||||||
|
zle reset-prompt
|
||||||
|
}
|
||||||
|
zle -N _dots_find_in_files_widget
|
||||||
|
bindkey '^F' _dots_find_in_files_widget
|
||||||
|
|
||||||
|
# Ctrl+N: tmux session
|
||||||
|
_dots_tmux_widget() {
|
||||||
|
if [[ -z "$TMUX" ]]; then
|
||||||
|
tmux new-session </dev/tty
|
||||||
|
else
|
||||||
|
local session
|
||||||
|
session="$(tmux list-sessions -F '#{session_name}' 2>/dev/null \
|
||||||
|
| fzf --preview 'tmux list-windows -t {} -F " #{window_index}: #{window_name} #{pane_current_command}"')" \
|
||||||
|
|| { zle reset-prompt; return; }
|
||||||
|
tmux switch-client -t "$session"
|
||||||
|
fi
|
||||||
|
zle reset-prompt
|
||||||
|
}
|
||||||
|
zle -N _dots_tmux_widget
|
||||||
|
bindkey '^N' _dots_tmux_widget
|
||||||
|
|
||||||
|
# Ctrl+O: open in browser
|
||||||
|
_dots_open_widget() {
|
||||||
|
local choice
|
||||||
|
choice="$(printf 'repo\npr\nissues\nactions' | fzf)" || { zle reset-prompt; return; }
|
||||||
|
case "$choice" in
|
||||||
|
repo) BUFFER="gh browse" ;;
|
||||||
|
pr) BUFFER="gh pr view --web" ;;
|
||||||
|
issues) BUFFER="gh browse --issues" ;;
|
||||||
|
actions) BUFFER="gh browse --actions" ;;
|
||||||
|
esac
|
||||||
|
zle reset-prompt
|
||||||
|
zle accept-line
|
||||||
|
}
|
||||||
|
zle -N _dots_open_widget
|
||||||
|
bindkey '^O' _dots_open_widget
|
||||||
|
|
||||||
|
# Ctrl+P: project switch
|
||||||
|
_dots_project_widget() {
|
||||||
|
local result
|
||||||
|
result="$(zoxide query -l 2>/dev/null | grep "${WORKSPACE:-$HOME/Workspace}" \
|
||||||
|
| fzf --preview 'ls -1 {}')" || { zle reset-prompt; return; }
|
||||||
|
BUFFER="cd ${(q)result}"
|
||||||
|
zle reset-prompt
|
||||||
|
zle accept-line
|
||||||
|
}
|
||||||
|
zle -N _dots_project_widget
|
||||||
|
bindkey '^P' _dots_project_widget
|
||||||
|
|
||||||
|
# Ctrl+S: copilot sessions
|
||||||
|
_dots_copilot_session_widget() {
|
||||||
|
local session_dir="$HOME/.copilot/session-state"
|
||||||
|
[[ -d "$session_dir" ]] || { zle reset-prompt; return; }
|
||||||
|
local session
|
||||||
|
session="$(python3 -c "
|
||||||
|
import os, json, glob
|
||||||
|
sd = os.path.expanduser('~/.copilot/session-state')
|
||||||
|
home = os.path.expanduser('~')
|
||||||
|
entries = []
|
||||||
|
for ws in glob.glob(os.path.join(sd, '*/workspace.yaml')):
|
||||||
|
try:
|
||||||
|
d = {}
|
||||||
|
with open(ws) as f:
|
||||||
|
for l in f:
|
||||||
|
for k in ('updated_at:','cwd:','id:','summary:','repository:'):
|
||||||
|
if l.startswith(k): d[k[:-1]] = l.split(': ',1)[1].strip()
|
||||||
|
sid = d.get('id','')
|
||||||
|
if not sid: continue
|
||||||
|
ts = d.get('updated_at','?')[:16]
|
||||||
|
repo = d.get('repository','').split('/')[-1] if d.get('repository') else ''
|
||||||
|
summary = d.get('summary','')
|
||||||
|
msg = ''
|
||||||
|
ev = os.path.join(os.path.dirname(ws), 'events.jsonl')
|
||||||
|
if os.path.exists(ev):
|
||||||
|
with open(ev) as f:
|
||||||
|
for l in f:
|
||||||
|
if '\"user.message\"' in l:
|
||||||
|
try: msg = json.loads(l)['data']['content'].strip().split(chr(10))[0][:60]
|
||||||
|
except: pass
|
||||||
|
break
|
||||||
|
if not msg: continue
|
||||||
|
ctx = repo or d.get('cwd','?').replace(home,'~')
|
||||||
|
label = f'{ctx} \u00b7 {summary}' if summary else f'{ctx} \u00b7 {msg}'
|
||||||
|
entries.append((ts, sid, label))
|
||||||
|
except: pass
|
||||||
|
for jf in glob.glob(os.path.join(sd, '*.jsonl')):
|
||||||
|
try:
|
||||||
|
sid = os.path.basename(jf).replace('.jsonl','')
|
||||||
|
with open(jf) as f:
|
||||||
|
ts = json.loads(f.readline())['data']['startTime'][:16]
|
||||||
|
msg = ''
|
||||||
|
with open(jf) as f:
|
||||||
|
for l in f:
|
||||||
|
if '\"user.message\"' in l:
|
||||||
|
try: msg = json.loads(l)['data']['content'].strip().split(chr(10))[0][:60]
|
||||||
|
except: pass
|
||||||
|
break
|
||||||
|
if not msg: continue
|
||||||
|
entries.append((ts, sid, msg))
|
||||||
|
except: pass
|
||||||
|
entries.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
for ts, sid, label in entries:
|
||||||
|
print(f'{ts} | {sid} | {label}')
|
||||||
|
" 2>/dev/null | fzf --preview '
|
||||||
|
id=$(echo {} | cut -d"|" -f2 | tr -d " ")
|
||||||
|
sd="'"$session_dir"'"
|
||||||
|
f="$sd/$id/events.jsonl"
|
||||||
|
[[ -f "$f" ]] || f="$sd/${id}.jsonl"
|
||||||
|
[[ -f "$f" ]] || exit 0
|
||||||
|
grep "\"user.message\"" "$f" | python3 -c "
|
||||||
|
import sys,json
|
||||||
|
for line in sys.stdin:
|
||||||
|
try:
|
||||||
|
msg=json.loads(line)[\"data\"][\"content\"].strip().split(chr(10))[0][:100]
|
||||||
|
print(\">\", msg)
|
||||||
|
except: pass
|
||||||
|
" 2>/dev/null
|
||||||
|
' --header 'enter=colby | ctrl-r=restricted' \
|
||||||
|
--expect=ctrl-r)" || { zle reset-prompt; return; }
|
||||||
|
local key=$(echo "$session" | head -1)
|
||||||
|
local line=$(echo "$session" | tail -1)
|
||||||
|
local id=$(echo "$line" | cut -d'|' -f2 | tr -d ' ')
|
||||||
|
if [[ "$key" == "ctrl-r" ]]; then
|
||||||
|
BUFFER="gh copilot --resume $id"
|
||||||
|
else
|
||||||
|
BUFFER="copilot --allow-all-tools --allow-all-paths --resume $id"
|
||||||
|
fi
|
||||||
|
zle reset-prompt
|
||||||
|
zle accept-line
|
||||||
|
}
|
||||||
|
zle -N _dots_copilot_session_widget
|
||||||
|
bindkey '^S' _dots_copilot_session_widget
|
||||||
|
|
||||||
|
# Ctrl+Y: git stash browser
|
||||||
|
_dots_stash_widget() {
|
||||||
|
local stash
|
||||||
|
stash="$(git stash list --color=always 2>/dev/null \
|
||||||
|
| fzf --ansi --no-sort \
|
||||||
|
--preview 'git stash show -p --color=always $(echo {} | cut -d: -f1)' \
|
||||||
|
--preview-window='right:60%')" || { zle reset-prompt; return; }
|
||||||
|
local ref="${stash%%:*}"
|
||||||
|
BUFFER="git stash apply $ref"
|
||||||
|
zle reset-prompt
|
||||||
|
zle accept-line
|
||||||
|
}
|
||||||
|
zle -N _dots_stash_widget
|
||||||
|
bindkey '^Y' _dots_stash_widget
|
||||||
|
}
|
||||||
|
_dots_load_keybindings
|
||||||
@@ -72,241 +72,9 @@ _dots_load_history() {
|
|||||||
}
|
}
|
||||||
_dots_load_history
|
_dots_load_history
|
||||||
|
|
||||||
_dots_load_keybindings() {
|
setopt IGNORE_EOF
|
||||||
bindkey -e
|
|
||||||
stty -ixon 2>/dev/null
|
|
||||||
|
|
||||||
# Ctrl+J: zoxide jump
|
source "$HOME/.zsh/widgets.zsh"
|
||||||
_dots_zoxide_widget() {
|
|
||||||
local result
|
|
||||||
result="$(zoxide query -i -- 2>&1)" || { zle reset-prompt; return; }
|
|
||||||
BUFFER="cd ${(q)result}"
|
|
||||||
zle reset-prompt
|
|
||||||
zle accept-line
|
|
||||||
}
|
|
||||||
zle -N _dots_zoxide_widget
|
|
||||||
bindkey '^J' _dots_zoxide_widget
|
|
||||||
|
|
||||||
# Ctrl+B: git branch checkout
|
|
||||||
_dots_git_branch_widget() {
|
|
||||||
local branch
|
|
||||||
branch="$(git branch --all --sort=-committerdate --format='%(refname:short)' 2>/dev/null \
|
|
||||||
| fzf --preview 'git log --oneline --color -20 {}')" || { zle reset-prompt; return; }
|
|
||||||
branch="${branch#origin/}"
|
|
||||||
BUFFER="git checkout ${(q)branch}"
|
|
||||||
zle reset-prompt
|
|
||||||
zle accept-line
|
|
||||||
}
|
|
||||||
zle -N _dots_git_branch_widget
|
|
||||||
bindkey '^B' _dots_git_branch_widget
|
|
||||||
|
|
||||||
# Ctrl+E: edit file
|
|
||||||
_dots_edit_widget() {
|
|
||||||
local file
|
|
||||||
file="$({ rg --files --hidden --glob '!.git' 2>/dev/null || find . -type f -not -path '*/.git/*'; } \
|
|
||||||
| fzf --preview 'head -100 {}')" || { zle reset-prompt; return; }
|
|
||||||
${EDITOR:-vim} "$file" </dev/tty
|
|
||||||
zle reset-prompt
|
|
||||||
}
|
|
||||||
zle -N _dots_edit_widget
|
|
||||||
bindkey '^E' _dots_edit_widget
|
|
||||||
|
|
||||||
# Ctrl+G: remote connect
|
|
||||||
if [[ -z "${CODESPACES:-}" ]]; then
|
|
||||||
_dots_ssh_hosts() {
|
|
||||||
local ssh_log="${XDG_DATA_HOME:-$HOME/.local/share}/ssh/log"
|
|
||||||
local cs_cache="$_dots_cache_dir/codespaces"
|
|
||||||
|
|
||||||
# Refresh codespace cache in background if stale (>5 min)
|
|
||||||
if [[ ! -f "$cs_cache" ]] || [[ -n "$(find "$cs_cache" -mmin +5 2>/dev/null)" ]]; then
|
|
||||||
{ gh cs list --json name -q '.[].name' 2>/dev/null | sed 's/^/cs:/' > "$cs_cache.tmp" && mv "$cs_cache.tmp" "$cs_cache"; } &!
|
|
||||||
fi
|
|
||||||
|
|
||||||
{
|
|
||||||
if [[ -f "$ssh_log" ]]; then
|
|
||||||
awk '{c[$2]++; t[$2]=$1} END {for(h in c) print c[h]*1000+t[h], h}' "$ssh_log" | sort -rn | awk '{print $2}'
|
|
||||||
fi
|
|
||||||
awk '/^Host / && !/\*/ {print $2}' ~/.ssh/config ~/.ssh/config.d/* 2>/dev/null
|
|
||||||
awk '{print $1}' ~/.ssh/known_hosts 2>/dev/null | tr ',' '\n' | sed 's/\[//;s/\]:.*//'
|
|
||||||
[[ -f "$cs_cache" ]] && cat "$cs_cache"
|
|
||||||
} | awk '!seen[$0]++'
|
|
||||||
}
|
|
||||||
_dots_ssh_widget() {
|
|
||||||
local target
|
|
||||||
target="$(_dots_ssh_hosts | fzf)" || { zle reset-prompt; return; }
|
|
||||||
if [[ "$target" == cs:* ]]; then
|
|
||||||
BUFFER="cs ${target#cs:}"
|
|
||||||
else
|
|
||||||
BUFFER="ssh $target"
|
|
||||||
fi
|
|
||||||
zle reset-prompt
|
|
||||||
zle accept-line
|
|
||||||
}
|
|
||||||
zle -N _dots_ssh_widget
|
|
||||||
bindkey '^G' _dots_ssh_widget
|
|
||||||
else
|
|
||||||
bindkey -r '^G'
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ctrl+F: find in files
|
|
||||||
_dots_find_in_files_widget() {
|
|
||||||
local selection
|
|
||||||
selection="$(rg --color=always --line-number --no-heading '' 2>/dev/null \
|
|
||||||
| fzf --ansi --delimiter=: \
|
|
||||||
--preview 'head -n $((({2}+30))) {1} | tail -n 60' \
|
|
||||||
--preview-window='right:60%')" || { zle reset-prompt; return; }
|
|
||||||
local file="${selection%%:*}"
|
|
||||||
local line="${${selection#*:}%%:*}"
|
|
||||||
${EDITOR:-vim} "+$line" "$file" </dev/tty
|
|
||||||
zle reset-prompt
|
|
||||||
}
|
|
||||||
zle -N _dots_find_in_files_widget
|
|
||||||
bindkey '^F' _dots_find_in_files_widget
|
|
||||||
|
|
||||||
# Ctrl+N: tmux session
|
|
||||||
_dots_tmux_widget() {
|
|
||||||
if [[ -z "$TMUX" ]]; then
|
|
||||||
tmux new-session </dev/tty
|
|
||||||
else
|
|
||||||
local session
|
|
||||||
session="$(tmux list-sessions -F '#{session_name}' 2>/dev/null \
|
|
||||||
| fzf --preview 'tmux list-windows -t {} -F " #{window_index}: #{window_name} #{pane_current_command}"')" \
|
|
||||||
|| { zle reset-prompt; return; }
|
|
||||||
tmux switch-client -t "$session"
|
|
||||||
fi
|
|
||||||
zle reset-prompt
|
|
||||||
}
|
|
||||||
zle -N _dots_tmux_widget
|
|
||||||
bindkey '^N' _dots_tmux_widget
|
|
||||||
|
|
||||||
# Ctrl+O: open in browser
|
|
||||||
_dots_open_widget() {
|
|
||||||
local choice
|
|
||||||
choice="$(printf 'repo\npr\nissues\nactions' | fzf)" || { zle reset-prompt; return; }
|
|
||||||
case "$choice" in
|
|
||||||
repo) BUFFER="gh browse" ;;
|
|
||||||
pr) BUFFER="gh pr view --web" ;;
|
|
||||||
issues) BUFFER="gh browse --issues" ;;
|
|
||||||
actions) BUFFER="gh browse --actions" ;;
|
|
||||||
esac
|
|
||||||
zle reset-prompt
|
|
||||||
zle accept-line
|
|
||||||
}
|
|
||||||
zle -N _dots_open_widget
|
|
||||||
bindkey '^O' _dots_open_widget
|
|
||||||
|
|
||||||
# Ctrl+P: project switch
|
|
||||||
_dots_project_widget() {
|
|
||||||
local result
|
|
||||||
result="$(zoxide query -l 2>/dev/null | grep "${WORKSPACE:-$HOME/Workspace}" \
|
|
||||||
| fzf --preview 'ls -1 {}')" || { zle reset-prompt; return; }
|
|
||||||
BUFFER="cd ${(q)result}"
|
|
||||||
zle reset-prompt
|
|
||||||
zle accept-line
|
|
||||||
}
|
|
||||||
zle -N _dots_project_widget
|
|
||||||
bindkey '^P' _dots_project_widget
|
|
||||||
|
|
||||||
# Ctrl+S: copilot sessions
|
|
||||||
_dots_copilot_session_widget() {
|
|
||||||
local session_dir="$HOME/.copilot/session-state"
|
|
||||||
[[ -d "$session_dir" ]] || { zle reset-prompt; return; }
|
|
||||||
local session
|
|
||||||
session="$(python3 -c "
|
|
||||||
import os, json, glob
|
|
||||||
sd = os.path.expanduser('~/.copilot/session-state')
|
|
||||||
home = os.path.expanduser('~')
|
|
||||||
entries = []
|
|
||||||
for ws in glob.glob(os.path.join(sd, '*/workspace.yaml')):
|
|
||||||
try:
|
|
||||||
d = {}
|
|
||||||
with open(ws) as f:
|
|
||||||
for l in f:
|
|
||||||
for k in ('updated_at:','cwd:','id:','summary:','repository:'):
|
|
||||||
if l.startswith(k): d[k[:-1]] = l.split(': ',1)[1].strip()
|
|
||||||
sid = d.get('id','')
|
|
||||||
if not sid: continue
|
|
||||||
ts = d.get('updated_at','?')[:16]
|
|
||||||
repo = d.get('repository','').split('/')[-1] if d.get('repository') else ''
|
|
||||||
summary = d.get('summary','')
|
|
||||||
msg = ''
|
|
||||||
ev = os.path.join(os.path.dirname(ws), 'events.jsonl')
|
|
||||||
if os.path.exists(ev):
|
|
||||||
with open(ev) as f:
|
|
||||||
for l in f:
|
|
||||||
if '\"user.message\"' in l:
|
|
||||||
try: msg = json.loads(l)['data']['content'].strip().split(chr(10))[0][:60]
|
|
||||||
except: pass
|
|
||||||
break
|
|
||||||
if not msg: continue
|
|
||||||
ctx = repo or d.get('cwd','?').replace(home,'~')
|
|
||||||
label = f'{ctx} \u00b7 {summary}' if summary else f'{ctx} \u00b7 {msg}'
|
|
||||||
entries.append((ts, sid, label))
|
|
||||||
except: pass
|
|
||||||
for jf in glob.glob(os.path.join(sd, '*.jsonl')):
|
|
||||||
try:
|
|
||||||
sid = os.path.basename(jf).replace('.jsonl','')
|
|
||||||
with open(jf) as f:
|
|
||||||
ts = json.loads(f.readline())['data']['startTime'][:16]
|
|
||||||
msg = ''
|
|
||||||
with open(jf) as f:
|
|
||||||
for l in f:
|
|
||||||
if '\"user.message\"' in l:
|
|
||||||
try: msg = json.loads(l)['data']['content'].strip().split(chr(10))[0][:60]
|
|
||||||
except: pass
|
|
||||||
break
|
|
||||||
if not msg: continue
|
|
||||||
entries.append((ts, sid, msg))
|
|
||||||
except: pass
|
|
||||||
entries.sort(key=lambda x: x[0], reverse=True)
|
|
||||||
for ts, sid, label in entries:
|
|
||||||
print(f'{ts} | {sid} | {label}')
|
|
||||||
" 2>/dev/null | fzf --preview '
|
|
||||||
id=$(echo {} | cut -d"|" -f2 | tr -d " ")
|
|
||||||
sd="'"$session_dir"'"
|
|
||||||
f="$sd/$id/events.jsonl"
|
|
||||||
[[ -f "$f" ]] || f="$sd/${id}.jsonl"
|
|
||||||
[[ -f "$f" ]] || exit 0
|
|
||||||
grep "\"user.message\"" "$f" | python3 -c "
|
|
||||||
import sys,json
|
|
||||||
for line in sys.stdin:
|
|
||||||
try:
|
|
||||||
msg=json.loads(line)[\"data\"][\"content\"].strip().split(chr(10))[0][:100]
|
|
||||||
print(\">\", msg)
|
|
||||||
except: pass
|
|
||||||
" 2>/dev/null
|
|
||||||
' --header 'enter=colby | ctrl-r=restricted' \
|
|
||||||
--expect=ctrl-r)" || { zle reset-prompt; return; }
|
|
||||||
local key=$(echo "$session" | head -1)
|
|
||||||
local line=$(echo "$session" | tail -1)
|
|
||||||
local id=$(echo "$line" | cut -d'|' -f2 | tr -d ' ')
|
|
||||||
if [[ "$key" == "ctrl-r" ]]; then
|
|
||||||
BUFFER="gh copilot --resume $id"
|
|
||||||
else
|
|
||||||
BUFFER="copilot --allow-all-tools --allow-all-paths --resume $id"
|
|
||||||
fi
|
|
||||||
zle reset-prompt
|
|
||||||
zle accept-line
|
|
||||||
}
|
|
||||||
zle -N _dots_copilot_session_widget
|
|
||||||
bindkey '^S' _dots_copilot_session_widget
|
|
||||||
|
|
||||||
# Ctrl+Y: git stash browser
|
|
||||||
_dots_stash_widget() {
|
|
||||||
local stash
|
|
||||||
stash="$(git stash list --color=always 2>/dev/null \
|
|
||||||
| fzf --ansi --no-sort \
|
|
||||||
--preview 'git stash show -p --color=always $(echo {} | cut -d: -f1)' \
|
|
||||||
--preview-window='right:60%')" || { zle reset-prompt; return; }
|
|
||||||
local ref="${stash%%:*}"
|
|
||||||
BUFFER="git stash apply $ref"
|
|
||||||
zle reset-prompt
|
|
||||||
zle accept-line
|
|
||||||
}
|
|
||||||
zle -N _dots_stash_widget
|
|
||||||
bindkey '^Y' _dots_stash_widget
|
|
||||||
}
|
|
||||||
_dots_load_keybindings
|
|
||||||
|
|
||||||
_dots_load_fzf() {
|
_dots_load_fzf() {
|
||||||
command -v fzf &>/dev/null || return
|
command -v fzf &>/dev/null || return
|
||||||
@@ -338,286 +106,7 @@ _dots_load_zoxide() {
|
|||||||
eval "$(zoxide init zsh)"
|
eval "$(zoxide init zsh)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Prompt
|
source "$HOME/.zsh/prompt.zsh"
|
||||||
(( ${+PROMPT_MIN_DURATION} )) || typeset -gi PROMPT_MIN_DURATION=2 # show duration after N seconds
|
|
||||||
(( ${+PROMPT_FLASH_DELAY} )) || typeset -gi PROMPT_FLASH_DELAY=4 # flash prompt for N centiseconds
|
|
||||||
|
|
||||||
typeset -gi _dots_prompt_cmd_start=0
|
|
||||||
typeset -gi _dots_prompt_cmd_ran=0
|
|
||||||
typeset -gi _dots_prompt_flashing=0
|
|
||||||
typeset -g _dots_prompt_base=""
|
|
||||||
typeset -gA _dots_pc
|
|
||||||
|
|
||||||
_dots_init_colors() {
|
|
||||||
if [[ "$COLORTERM" == (truecolor|24bit) ]]; then
|
|
||||||
_dots_pc=(
|
|
||||||
teal $'%{\e[38;2;44;180;148m%}'
|
|
||||||
orange $'%{\e[38;2;248;140;20m%}'
|
|
||||||
red $'%{\e[38;2;244;4;4m%}'
|
|
||||||
grey $'%{\e[38;2;114;144;184m%}'
|
|
||||||
)
|
|
||||||
elif [[ "$TERM" == *256color* ]]; then
|
|
||||||
_dots_pc=(
|
|
||||||
teal $'%{\e[38;5;43m%}'
|
|
||||||
orange $'%{\e[38;5;208m%}'
|
|
||||||
red $'%{\e[38;5;196m%}'
|
|
||||||
grey $'%{\e[38;5;103m%}'
|
|
||||||
)
|
|
||||||
else
|
|
||||||
_dots_pc=(
|
|
||||||
teal $'%{\e[36m%}'
|
|
||||||
orange $'%{\e[33m%}'
|
|
||||||
red $'%{\e[31m%}'
|
|
||||||
grey $'%{\e[34m%}'
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
_dots_pc[reset]=$'%{\e[0m%}'
|
|
||||||
_dots_pc[bold]=$'%{\e[1m%}'
|
|
||||||
}
|
|
||||||
|
|
||||||
_dots_abbrev_path() {
|
|
||||||
local dir="${PWD/#$HOME/~}"
|
|
||||||
local -a parts=( "${(@s:/:)dir}" )
|
|
||||||
local count=${#parts[@]}
|
|
||||||
|
|
||||||
(( count <= 3 )) && { print -r -- "$dir"; return }
|
|
||||||
|
|
||||||
local result=""
|
|
||||||
local i
|
|
||||||
for (( i=1; i <= count-3; i++ )); do
|
|
||||||
result+="${parts[i][1]}/"
|
|
||||||
done
|
|
||||||
print -r -- "${result}${parts[-3]}/${parts[-2]}/${parts[-1]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
_dots_session() {
|
|
||||||
[[ -n "$CODESPACE_NAME" ]] && { print -r -- "$CODESPACE_NAME"; return }
|
|
||||||
[[ -n "$SSH_CONNECTION" || -n "$SSH_CLIENT" || -n "$SSH_TTY" ]] && { print -r -- "%n@%m"; return }
|
|
||||||
[[ -f /.dockerenv ]] && { print -r -- "${DEVCONTAINER_ID:-$(</etc/hostname)}"; return }
|
|
||||||
}
|
|
||||||
|
|
||||||
_dots_git_info_sync() {
|
|
||||||
# Use git status --short with awk for better performance
|
|
||||||
# Avoids shell loops and uses compiled awk for parsing
|
|
||||||
local output
|
|
||||||
output=$(git status --short --branch --ignore-submodules=dirty 2>/dev/null) || return
|
|
||||||
|
|
||||||
local branch="" ahead=0 behind=0 staged=0 unstaged=0 untracked=0
|
|
||||||
|
|
||||||
# Parse efficiently: branch info from first line, counts from rest
|
|
||||||
local first_line="${output%%$'\n'*}"
|
|
||||||
|
|
||||||
# Extract branch from "## branch...remote [ahead N, behind M]"
|
|
||||||
if [[ "$first_line" == "## "* ]]; then
|
|
||||||
branch="${first_line#\#\# }"
|
|
||||||
# Handle detached HEAD
|
|
||||||
if [[ "$branch" == "HEAD (no branch)" || "$branch" == [0-9a-f]##* ]]; then
|
|
||||||
branch="${branch:0:7}"
|
|
||||||
else
|
|
||||||
# Remove tracking info
|
|
||||||
branch="${branch%%...*}"
|
|
||||||
branch="${branch%% *}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract ahead/behind
|
|
||||||
[[ "$first_line" =~ "ahead ([0-9]+)" ]] && ahead="${match[1]}"
|
|
||||||
[[ "$first_line" =~ "behind ([0-9]+)" ]] && behind="${match[1]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ -z "$branch" ]] && return
|
|
||||||
|
|
||||||
# Count file status with awk (fast, no shell loops)
|
|
||||||
local -a counts
|
|
||||||
counts=( $(awk '
|
|
||||||
NR == 1 { next } # Skip branch line
|
|
||||||
/^\?\?/ { untracked++; next }
|
|
||||||
{
|
|
||||||
x = substr($0, 1, 1)
|
|
||||||
y = substr($0, 2, 1)
|
|
||||||
if (x != " " && x != "?") staged++
|
|
||||||
if (y != " " && y != "?") unstaged++
|
|
||||||
}
|
|
||||||
END { print staged+0, unstaged+0, untracked+0 }
|
|
||||||
' <<< "$output") )
|
|
||||||
|
|
||||||
staged=${counts[1]:-0}
|
|
||||||
unstaged=${counts[2]:-0}
|
|
||||||
untracked=${counts[3]:-0}
|
|
||||||
|
|
||||||
local info="${_dots_pc[grey]}(${branch})${_dots_pc[reset]}"
|
|
||||||
|
|
||||||
local dirty=""
|
|
||||||
local sep=""
|
|
||||||
if (( staged )); then
|
|
||||||
dirty+="${_dots_pc[teal]}+${staged}${_dots_pc[reset]}"
|
|
||||||
sep=" "
|
|
||||||
fi
|
|
||||||
if (( unstaged )); then
|
|
||||||
dirty+="${sep}${_dots_pc[orange]}~${unstaged}${_dots_pc[reset]}"
|
|
||||||
sep=" "
|
|
||||||
fi
|
|
||||||
if (( untracked )); then
|
|
||||||
dirty+="${sep}${_dots_pc[grey]}?${untracked}${_dots_pc[reset]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local arrows=""
|
|
||||||
sep=""
|
|
||||||
if (( ahead )); then
|
|
||||||
arrows+="${_dots_pc[teal]}↑${ahead}${_dots_pc[reset]}"
|
|
||||||
sep=" "
|
|
||||||
fi
|
|
||||||
if (( behind )); then
|
|
||||||
arrows+="${sep}${_dots_pc[orange]}↓${behind}${_dots_pc[reset]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$dirty" || -n "$arrows" ]]; then
|
|
||||||
info+=" "
|
|
||||||
[[ -n "$dirty" ]] && info+="$dirty"
|
|
||||||
[[ -n "$dirty" && -n "$arrows" ]] && info+=" "
|
|
||||||
[[ -n "$arrows" ]] && info+="$arrows"
|
|
||||||
fi
|
|
||||||
|
|
||||||
print -r -- "$info"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Async git info
|
|
||||||
typeset -g _dots_git_info_result=""
|
|
||||||
typeset -g _dots_git_info_pwd=""
|
|
||||||
typeset -g _dots_git_async_fd=""
|
|
||||||
|
|
||||||
_dots_git_async_callback() {
|
|
||||||
local fd=$1
|
|
||||||
local result=""
|
|
||||||
# Use sysread for efficient non-blocking read from fd
|
|
||||||
if [[ -n "$fd" ]] && sysread -i "$fd" result 2>/dev/null; then
|
|
||||||
result="${result%$'\n'}" # trim trailing newline
|
|
||||||
_dots_git_info_result="$result"
|
|
||||||
_dots_build_dots_prompt_base
|
|
||||||
PROMPT="$_dots_prompt_base"
|
|
||||||
zle && zle reset-prompt
|
|
||||||
fi
|
|
||||||
# Clean up
|
|
||||||
exec {fd}<&-
|
|
||||||
zle -F "$fd" 2>/dev/null
|
|
||||||
_dots_git_async_fd=""
|
|
||||||
}
|
|
||||||
|
|
||||||
_dots_git_async_start() {
|
|
||||||
# Check if we're in a git repo
|
|
||||||
local git_dir
|
|
||||||
git_dir=$(git rev-parse --git-dir 2>/dev/null) || return
|
|
||||||
|
|
||||||
# Cancel any pending async job (reuse single worker)
|
|
||||||
if [[ -n "$_dots_git_async_fd" ]]; then
|
|
||||||
exec {_dots_git_async_fd}<&- 2>/dev/null
|
|
||||||
zle -F "$_dots_git_async_fd" 2>/dev/null
|
|
||||||
_dots_git_async_fd=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start background job
|
|
||||||
exec {_dots_git_async_fd}< <(
|
|
||||||
_dots_git_info_sync
|
|
||||||
)
|
|
||||||
zle -F "$_dots_git_async_fd" _dots_git_async_callback
|
|
||||||
}
|
|
||||||
|
|
||||||
_dots_build_dots_prompt_base() {
|
|
||||||
local dir_path="$(_dots_abbrev_path)"
|
|
||||||
local symbol="${_dots_pc[grey]}>${_dots_pc[reset]}"
|
|
||||||
(( EUID == 0 )) && symbol="${_dots_pc[orange]}${_dots_pc[bold]}#${_dots_pc[reset]}"
|
|
||||||
|
|
||||||
local line1="${_dots_pc[teal]}${dir_path}${_dots_pc[reset]}"
|
|
||||||
[[ -n "$_dots_git_info_result" ]] && line1+=" ${_dots_git_info_result}"
|
|
||||||
|
|
||||||
_dots_prompt_base=$'\n'"${line1}"$'\n'"${symbol} "
|
|
||||||
}
|
|
||||||
|
|
||||||
_dots_preexec() {
|
|
||||||
_dots_prompt_cmd_start=$EPOCHSECONDS
|
|
||||||
_dots_prompt_cmd_ran=1
|
|
||||||
}
|
|
||||||
|
|
||||||
_dots_precmd() {
|
|
||||||
local e=$? d=0
|
|
||||||
# Only show exit code if a command actually ran
|
|
||||||
(( _dots_prompt_cmd_ran )) || e=0
|
|
||||||
_dots_prompt_cmd_ran=0
|
|
||||||
# First prompt should never show error from shell init
|
|
||||||
[[ -z "$_dots_first_prompt" ]] && { _dots_first_prompt=1; e=0; }
|
|
||||||
|
|
||||||
# Build RPROMPT: time, error, host
|
|
||||||
local rp_parts=()
|
|
||||||
|
|
||||||
if (( _dots_prompt_cmd_start )); then
|
|
||||||
d=$(( EPOCHSECONDS - _dots_prompt_cmd_start ))
|
|
||||||
_dots_prompt_cmd_start=0
|
|
||||||
if (( d >= PROMPT_MIN_DURATION )); then
|
|
||||||
(( d >= 60 )) && rp_parts+=("${_dots_pc[grey]}($(( d/60 ))m$(( d%60 ))s)${_dots_pc[reset]}") \
|
|
||||||
|| rp_parts+=("${_dots_pc[grey]}(${d}s)${_dots_pc[reset]}")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
(( e )) && rp_parts+=("${_dots_pc[red]}[${e}]${_dots_pc[reset]}")
|
|
||||||
|
|
||||||
local session="$(_dots_session)"
|
|
||||||
[[ -n "$session" ]] && rp_parts+=("${_dots_pc[orange]}[${session}]${_dots_pc[reset]}")
|
|
||||||
|
|
||||||
RPROMPT="${(j: :)rp_parts}"
|
|
||||||
|
|
||||||
# On directory change, clear git info until async refreshes
|
|
||||||
if [[ "$PWD" != "$_dots_git_info_pwd" ]]; then
|
|
||||||
_dots_git_info_pwd="$PWD"
|
|
||||||
_dots_git_info_result=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
_dots_build_dots_prompt_base
|
|
||||||
_dots_git_async_start
|
|
||||||
PROMPT="$_dots_prompt_base"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
TRAPINT() {
|
|
||||||
# Only customize when ZLE is active (at prompt, not during command)
|
|
||||||
if [[ -o zle ]] && [[ -o interactive ]] && (( ${+WIDGET} )); then
|
|
||||||
if [[ -z "$BUFFER" ]] && (( ! _dots_prompt_flashing )); then
|
|
||||||
# Empty buffer: flash the prompt symbol
|
|
||||||
_dots_prompt_flashing=1
|
|
||||||
local git_part=""
|
|
||||||
[[ -n "$_dots_git_info_result" ]] && git_part=" ${_dots_git_info_result}"
|
|
||||||
local flash_prompt=$'\n'"${_dots_pc[teal]}$(_dots_abbrev_path)${_dots_pc[reset]}${git_part}"$'\n'$'%{\e[48;2;248;140;20m\e[30m%}> %{\e[0m%}'
|
|
||||||
PROMPT="$flash_prompt"
|
|
||||||
zle reset-prompt
|
|
||||||
zselect -t $PROMPT_FLASH_DELAY
|
|
||||||
_dots_prompt_flashing=0
|
|
||||||
PROMPT="$_dots_prompt_base"
|
|
||||||
zle reset-prompt
|
|
||||||
return 0
|
|
||||||
elif [[ -n "$BUFFER" ]]; then
|
|
||||||
# Buffer has content: clear autosuggest, then default behavior
|
|
||||||
zle autosuggest-clear 2>/dev/null
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# Propagate signal: use special return code -1 to let zsh handle normally
|
|
||||||
return $((128 + ${1:-2}))
|
|
||||||
}
|
|
||||||
|
|
||||||
_dots_prompt_init() {
|
|
||||||
zmodload zsh/datetime 2>/dev/null
|
|
||||||
zmodload zsh/zselect 2>/dev/null
|
|
||||||
zmodload zsh/system 2>/dev/null
|
|
||||||
_dots_init_colors
|
|
||||||
_dots_build_dots_prompt_base
|
|
||||||
|
|
||||||
setopt PROMPT_SUBST EXTENDED_HISTORY INC_APPEND_HISTORY_TIME
|
|
||||||
autoload -Uz add-zsh-hook
|
|
||||||
add-zsh-hook preexec _dots_preexec
|
|
||||||
add-zsh-hook precmd _dots_precmd
|
|
||||||
add-zsh-hook chpwd _dots_build_dots_prompt_base
|
|
||||||
|
|
||||||
PROMPT="$_dots_prompt_base" RPROMPT=""
|
|
||||||
}
|
|
||||||
_dots_prompt_init
|
|
||||||
|
|
||||||
_dots_load_mise() {
|
_dots_load_mise() {
|
||||||
command -v mise &>/dev/null && eval "$(mise activate zsh)"
|
command -v mise &>/dev/null && eval "$(mise activate zsh)"
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ typeset -a MISE_APPS=(
|
|||||||
"fzf@latest"
|
"fzf@latest"
|
||||||
"zoxide@latest"
|
"zoxide@latest"
|
||||||
"ripgrep@latest"
|
"ripgrep@latest"
|
||||||
|
"delta@latest"
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ "$DOTS_ENV" != "codespaces" ]]; then
|
if [[ "$DOTS_ENV" != "codespaces" ]]; then
|
||||||
@@ -101,4 +102,5 @@ fi
|
|||||||
fzf --version
|
fzf --version
|
||||||
zoxide --version
|
zoxide --version
|
||||||
rg --version | head -1
|
rg --version | head -1
|
||||||
|
delta --version | head -1
|
||||||
log_pass "mise tools installed"
|
log_pass "mise tools installed"
|
||||||
|
|||||||
Reference in New Issue
Block a user