Skip to content

Commit 110c9a8

Browse files
berlysiaclaude
andcommitted
feat(prompt): replace starship with native shell prompt
Replace starship (~177ms/prompt) with a self-made native shell prompt (~55ms/prompt, 3.2x faster) supporting both bash and zsh. Features: - Git branch, remote tracking, ahead/behind, file status counts - Merge/rebase/cherry-pick/bisect state detection - Command execution duration (1s+ threshold) - Exit code display with visual indicators - USE_STARSHIP=1 env var to revert to starship File structure: - prompt/git_info.sh: POSIX sh shared git info extraction - prompt/cmd_duration.sh: POSIX sh shared timer helper - prompt/bash.sh: bash-specific prompt (PROMPT_COMMAND + DEBUG trap) - dot_zsh/prompt.zsh: zsh-specific prompt (precmd/preexec hooks) Co-Authored-By: Claude <noreply@anthropic.com> Entire-Checkpoint: b2caf85f25df
1 parent 29dcdb2 commit 110c9a8

6 files changed

Lines changed: 394 additions & 3 deletions

File tree

home/dot_bashrc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ if [[ "$PROMPT_COMMAND" != *"echo -ne"* ]]; then
3030
PROMPT_COMMAND='echo -ne "\033]0;$(basename "$PWD")\007"'
3131
fi
3232

33-
# Load bash-specific prompt (fallback if Starship is not available)
34-
if [ "$HAS_STARSHIP" = "1" ]; then
33+
# Load prompt
34+
if [ "$USE_STARSHIP" = "1" ] && [ "$HAS_STARSHIP" = "1" ]; then
3535
eval "$(starship init bash)"
36+
elif [ -f "$SHELL_COMMON/prompt/bash.sh" ]; then
37+
source "$SHELL_COMMON/prompt/bash.sh"
3638
fi
3739

3840
# Source local configuration if it exists
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/bin/bash
2+
# Bash-specific prompt implementation
3+
4+
# Variables PROMPT_GIT_* and PROMPT_CMD_DURATION are set by sourced helpers
5+
# shellcheck disable=SC2154
6+
7+
# shellcheck source=git_info.sh
8+
[ -f "$HOME/.shell_common/prompt/git_info.sh" ] && . "$HOME/.shell_common/prompt/git_info.sh"
9+
# shellcheck source=cmd_duration.sh
10+
[ -f "$HOME/.shell_common/prompt/cmd_duration.sh" ] && . "$HOME/.shell_common/prompt/cmd_duration.sh"
11+
12+
# --- Command duration via DEBUG trap ---
13+
# Only set if no existing DEBUG trap to avoid conflicts
14+
if [ -z "$(trap -p DEBUG)" ]; then
15+
trap '__prompt_cmd_timer_start' DEBUG
16+
_PROMPT_HAS_DURATION=1
17+
fi
18+
19+
# --- Color helpers for bash (non-printing escape wrappers) ---
20+
_c_reset='\[\033[0m\]'
21+
_c_bold='\[\033[1m\]'
22+
_c_green='\[\033[32m\]'
23+
_c_red='\[\033[31m\]'
24+
_c_cyan='\[\033[36m\]'
25+
_c_blue='\[\033[34m\]'
26+
_c_yellow='\[\033[33m\]'
27+
_c_bold_green='\[\033[1;32m\]'
28+
_c_bold_red='\[\033[1;31m\]'
29+
_c_bold_cyan='\[\033[1;36m\]'
30+
_c_bold_blue='\[\033[1;34m\]'
31+
32+
# --- Main prompt builder ---
33+
__prompt_bash_build() {
34+
local exit_code=$?
35+
36+
# Run shared helpers
37+
__prompt_git_info
38+
if [ "$_PROMPT_HAS_DURATION" = "1" ]; then
39+
__prompt_cmd_timer_stop
40+
fi
41+
42+
# Line 1: directory user@host status [duration]
43+
local dir="${_c_bold_green}\w${_c_reset}"
44+
local user
45+
if [ "$(id -u)" -eq 0 ]; then
46+
user="${_c_bold_red}\u${_c_reset}"
47+
else
48+
user="${_c_bold_cyan}\u${_c_reset}"
49+
fi
50+
local host="${_c_bold_blue}\h${_c_reset}"
51+
52+
local status_icon
53+
if [ "$exit_code" -eq 0 ]; then
54+
status_icon=''
55+
else
56+
status_icon=''" ${_c_red}${exit_code}${_c_reset}"
57+
fi
58+
59+
local duration=""
60+
if [ -n "$PROMPT_CMD_DURATION" ]; then
61+
duration=" ${_c_yellow}took ${PROMPT_CMD_DURATION}${_c_reset}"
62+
fi
63+
64+
local line1="${dir} ${user}${_c_yellow}@${_c_reset}${host} ${status_icon}${duration}"
65+
66+
# Line 2: git info (only in git repos)
67+
local line2=""
68+
if [ "$PROMPT_GIT_ACTIVE" = "1" ]; then
69+
local git_parts="${_c_bold_cyan}${PROMPT_GIT_BRANCH}${_c_reset}"
70+
if [ -n "$PROMPT_GIT_REMOTE" ]; then
71+
git_parts="${git_parts} ${_c_blue}${PROMPT_GIT_REMOTE}${_c_reset}"
72+
fi
73+
74+
# State (merge, rebase, etc.)
75+
if [ -n "$PROMPT_GIT_STATE" ]; then
76+
git_parts="${git_parts} ${_c_bold_red}(${PROMPT_GIT_STATE})${_c_reset}"
77+
fi
78+
79+
# Ahead/behind
80+
local ab=""
81+
if [ -n "$PROMPT_GIT_AHEAD" ] && [ -n "$PROMPT_GIT_BEHIND" ]; then
82+
ab="⇕⇡${PROMPT_GIT_AHEAD}${PROMPT_GIT_BEHIND}"
83+
elif [ -n "$PROMPT_GIT_AHEAD" ]; then
84+
ab="${PROMPT_GIT_AHEAD}"
85+
elif [ -n "$PROMPT_GIT_BEHIND" ]; then
86+
ab="${PROMPT_GIT_BEHIND}"
87+
fi
88+
if [ -n "$ab" ]; then
89+
git_parts="${git_parts} ${_c_cyan}[${ab}]${_c_reset}"
90+
fi
91+
92+
# Clean indicator
93+
if [ "$PROMPT_GIT_STAGED" -eq 0 ] && [ "$PROMPT_GIT_MODIFIED" -eq 0 ] && \
94+
[ "$PROMPT_GIT_UNTRACKED" -eq 0 ] && [ "$PROMPT_GIT_DELETED" -eq 0 ]; then
95+
git_parts="${git_parts}"
96+
fi
97+
98+
# File status counts
99+
if [ "$PROMPT_GIT_STAGED" -gt 0 ]; then
100+
git_parts="${git_parts} ${_c_green}+${PROMPT_GIT_STAGED} staged${_c_reset}"
101+
fi
102+
if [ "$PROMPT_GIT_MODIFIED" -gt 0 ]; then
103+
git_parts="${git_parts} ${_c_yellow}!${PROMPT_GIT_MODIFIED} modified${_c_reset}"
104+
fi
105+
if [ "$PROMPT_GIT_DELETED" -gt 0 ]; then
106+
git_parts="${git_parts} ${_c_red}${PROMPT_GIT_DELETED} deleted${_c_reset}"
107+
fi
108+
if [ "$PROMPT_GIT_UNTRACKED" -gt 0 ]; then
109+
git_parts="${git_parts} ${_c_red}?${PROMPT_GIT_UNTRACKED} untracked${_c_reset}"
110+
fi
111+
112+
line2=$'\n'"${git_parts}"
113+
fi
114+
115+
# Line 3: prompt character
116+
local mark
117+
if [ "$exit_code" -eq 0 ]; then
118+
mark="${_c_bold_green}>${_c_reset} "
119+
else
120+
mark="${_c_bold_red}>${_c_reset} "
121+
fi
122+
123+
# Add newline before prompt (except first prompt)
124+
local newline=""
125+
if [ -z "$_PROMPT_FIRST" ]; then
126+
_PROMPT_FIRST=1
127+
else
128+
newline=$'\n'
129+
fi
130+
131+
PS1="${newline}${line1}${line2}"$'\n'"${mark}"
132+
}
133+
134+
# Append to PROMPT_COMMAND without overwriting existing entries
135+
if [ -n "$PROMPT_COMMAND" ]; then
136+
PROMPT_COMMAND="__prompt_bash_build;${PROMPT_COMMAND}"
137+
else
138+
PROMPT_COMMAND="__prompt_bash_build"
139+
fi
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/sh
2+
# Command duration measurement helper for shell prompts
3+
# POSIX sh compatible - shared between zsh and bash
4+
#
5+
# Usage:
6+
# zsh: add-zsh-hook preexec __prompt_cmd_timer_start
7+
# add-zsh-hook precmd __prompt_cmd_timer_stop
8+
# bash: trap '__prompt_cmd_timer_start' DEBUG (if no existing DEBUG trap)
9+
# call __prompt_cmd_timer_stop in PROMPT_COMMAND
10+
11+
__prompt_cmd_timer_start() {
12+
PROMPT_CMD_TIMER_START="${PROMPT_CMD_TIMER_START:-$SECONDS}"
13+
}
14+
15+
__prompt_cmd_timer_stop() {
16+
PROMPT_CMD_DURATION=""
17+
if [ -n "$PROMPT_CMD_TIMER_START" ]; then
18+
local elapsed=$((SECONDS - PROMPT_CMD_TIMER_START))
19+
if [ "$elapsed" -ge 1 ]; then
20+
if [ "$elapsed" -ge 60 ]; then
21+
local minutes=$((elapsed / 60))
22+
local secs=$((elapsed % 60))
23+
PROMPT_CMD_DURATION="${minutes}m${secs}s"
24+
else
25+
PROMPT_CMD_DURATION="${elapsed}s"
26+
fi
27+
fi
28+
unset PROMPT_CMD_TIMER_START
29+
fi
30+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/bin/sh
2+
# Git information extraction for shell prompts
3+
# Compatible with bash and zsh - shared between both
4+
5+
__prompt_git_info() {
6+
# Reset all variables
7+
PROMPT_GIT_BRANCH=""
8+
PROMPT_GIT_REMOTE=""
9+
PROMPT_GIT_AHEAD=""
10+
PROMPT_GIT_BEHIND=""
11+
PROMPT_GIT_STAGED=0
12+
PROMPT_GIT_MODIFIED=0
13+
PROMPT_GIT_UNTRACKED=0
14+
PROMPT_GIT_DELETED=0
15+
PROMPT_GIT_STATE=""
16+
PROMPT_GIT_ACTIVE=0
17+
18+
# Check if we're in a git repository
19+
local git_dir
20+
git_dir="$(git rev-parse --git-dir 2>/dev/null)" || return
21+
22+
PROMPT_GIT_ACTIVE=1
23+
24+
# Detect special states (merge, rebase, etc.)
25+
if [ -f "$git_dir/MERGE_HEAD" ]; then
26+
PROMPT_GIT_STATE="merge"
27+
elif [ -d "$git_dir/rebase-merge" ]; then
28+
PROMPT_GIT_STATE="rebase"
29+
elif [ -d "$git_dir/rebase-apply" ]; then
30+
PROMPT_GIT_STATE="rebase"
31+
elif [ -f "$git_dir/CHERRY_PICK_HEAD" ]; then
32+
PROMPT_GIT_STATE="cherry-pick"
33+
elif [ -f "$git_dir/BISECT_LOG" ]; then
34+
PROMPT_GIT_STATE="bisect"
35+
elif [ -f "$git_dir/REVERT_HEAD" ]; then
36+
PROMPT_GIT_STATE="revert"
37+
fi
38+
39+
# Get branch and file status in one command
40+
local status_output
41+
status_output=$(git status --porcelain --branch 2>/dev/null) || return
42+
43+
# Parse header line (## branch...remote [ahead N, behind N])
44+
local header
45+
header=$(printf '%s\n' "$status_output" | head -1)
46+
header="${header#\#\# }"
47+
48+
case "$header" in
49+
*...*)
50+
PROMPT_GIT_BRANCH="${header%%...*}"
51+
local tracking="${header#*...}"
52+
# Extract remote name (before any space or [)
53+
local remote_ref="${tracking%% *}"
54+
remote_ref="${remote_ref%%\[*}"
55+
# remote_ref is like "origin/master" or "origin/other-branch"
56+
local remote_name="${remote_ref%%/*}"
57+
local remote_branch="${remote_ref#*/}"
58+
if [ "$remote_branch" = "$PROMPT_GIT_BRANCH" ]; then
59+
# Same name: just show remote (e.g. "origin")
60+
PROMPT_GIT_REMOTE="$remote_name"
61+
else
62+
# Different name: show full ref (e.g. "origin/other-branch")
63+
PROMPT_GIT_REMOTE="$remote_ref"
64+
fi
65+
case "$tracking" in
66+
*'[ahead '*)
67+
local tmp="${tracking#*\[ahead }"
68+
PROMPT_GIT_AHEAD="${tmp%%[],]*}"
69+
;;
70+
esac
71+
case "$tracking" in
72+
*'behind '*)
73+
local tmp="${tracking#*behind }"
74+
PROMPT_GIT_BEHIND="${tmp%%[],]*}"
75+
;;
76+
esac
77+
;;
78+
'No commits yet on '*)
79+
PROMPT_GIT_BRANCH="${header#No commits yet on }"
80+
;;
81+
*)
82+
PROMPT_GIT_BRANCH="$header"
83+
;;
84+
esac
85+
86+
# Count file statuses (skip header line)
87+
local files
88+
files=$(printf '%s\n' "$status_output" | tail -n +2)
89+
90+
if [ -n "$files" ]; then
91+
# Untracked: XY = ??
92+
PROMPT_GIT_UNTRACKED=$(printf '%s\n' "$files" | grep -c '^??' || true)
93+
# Staged: X is one of MADRC (changes in index)
94+
PROMPT_GIT_STAGED=$(printf '%s\n' "$files" | grep -c '^[MADRC]' || true)
95+
# Modified in worktree: Y is M
96+
PROMPT_GIT_MODIFIED=$(printf '%s\n' "$files" | grep -c '^.M' || true)
97+
# Deleted in worktree: Y is D
98+
PROMPT_GIT_DELETED=$(printf '%s\n' "$files" | grep -c '^.D' || true)
99+
fi
100+
}

home/dot_zsh/dot_zshrc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ if [ -z "$SHELL_COMMON" ]; then
4040
fi
4141
[ -f "$SHELL_COMMON/interactive.sh" ] && source "$SHELL_COMMON/interactive.sh"
4242

43-
if [ "$HAS_STARSHIP" = "1" ]; then
43+
if [ "$USE_STARSHIP" = "1" ] && [ "$HAS_STARSHIP" = "1" ]; then
4444
eval "$(starship init zsh)"
45+
else
46+
[ -f "$ZDOTDIR/prompt.zsh" ] && source "$ZDOTDIR/prompt.zsh"
4547
fi
4648

4749
if type fzf &> /dev/null; then

0 commit comments

Comments
 (0)