From 6cb722627302c3c9264ac092e07b2d2a8491ca6b Mon Sep 17 00:00:00 2001 From: Andrejus Date: Thu, 12 Feb 2026 18:35:07 +0000 Subject: [PATCH] feat: split zshrc, add mouse scrolling to vim/delta --- files/home/.gitconfig | 16 +- files/home/.vimrc | 1 + files/home/.zsh/prompt.zsh | 280 +++++++++++++++++++ files/home/.zsh/widgets.zsh | 235 ++++++++++++++++ files/home/.zshrc | 517 +----------------------------------- script/install.d/30-mise.sh | 2 + 6 files changed, 536 insertions(+), 515 deletions(-) create mode 100644 files/home/.zsh/prompt.zsh create mode 100644 files/home/.zsh/widgets.zsh diff --git a/files/home/.gitconfig b/files/home/.gitconfig index b2a3e4f..40b4071 100644 --- a/files/home/.gitconfig +++ b/files/home/.gitconfig @@ -19,6 +19,20 @@ [core] 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] ui = auto @@ -66,5 +80,5 @@ m = commit --amend --verbose st = stash sa = stash apply - stl = stash list + cc = copilot --continue diff --git a/files/home/.vimrc b/files/home/.vimrc index 21b1dca..5c0923f 100644 --- a/files/home/.vimrc +++ b/files/home/.vimrc @@ -4,6 +4,7 @@ set number relativenumber set ignorecase smartcase incsearch hlsearch set autoindent expandtab tabstop=4 shiftwidth=4 softtabstop=4 set scrolloff=8 wildmenu showcmd laststatus=2 +set mouse=a set hidden set noswapfile nobackup undofile let s:undo_dir = expand('~/.vim/undodir') diff --git a/files/home/.zsh/prompt.zsh b/files/home/.zsh/prompt.zsh new file mode 100644 index 0000000..fe70951 --- /dev/null +++ b/files/home/.zsh/prompt.zsh @@ -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:-$(/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 diff --git a/files/home/.zsh/widgets.zsh b/files/home/.zsh/widgets.zsh new file mode 100644 index 0000000..747b302 --- /dev/null +++ b/files/home/.zsh/widgets.zsh @@ -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" 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/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 diff --git a/files/home/.zshrc b/files/home/.zshrc index 7828930..4091c71 100644 --- a/files/home/.zshrc +++ b/files/home/.zshrc @@ -72,241 +72,9 @@ _dots_load_history() { } _dots_load_history -_dots_load_keybindings() { - bindkey -e - stty -ixon 2>/dev/null +setopt IGNORE_EOF - # 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" 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/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 +source "$HOME/.zsh/widgets.zsh" _dots_load_fzf() { command -v fzf &>/dev/null || return @@ -338,286 +106,7 @@ _dots_load_zoxide() { eval "$(zoxide init zsh)" } -# 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:-$(/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 +source "$HOME/.zsh/prompt.zsh" _dots_load_mise() { command -v mise &>/dev/null && eval "$(mise activate zsh)" diff --git a/script/install.d/30-mise.sh b/script/install.d/30-mise.sh index 92574be..713061b 100755 --- a/script/install.d/30-mise.sh +++ b/script/install.d/30-mise.sh @@ -59,6 +59,7 @@ typeset -a MISE_APPS=( "fzf@latest" "zoxide@latest" "ripgrep@latest" + "delta@latest" ) if [[ "$DOTS_ENV" != "codespaces" ]]; then @@ -101,4 +102,5 @@ fi fzf --version zoxide --version rg --version | head -1 +delta --version | head -1 log_pass "mise tools installed"