feat(claude): статусная строка переписана и устанавливается install.sh

- Зачем:
  - statusLine ссылался на ~/.claude/statusline-command.sh, которого install.sh
    никогда не создавал, а сам скрипт не округлял проценты и резал путь по числу
    сегментов вместо длины.
- Что:
  - скрипт переписан: округление процентов до целых, порог длины пути в
    переменной MAX_PATH_LEN (28), резервный разбор JSON без jq, USER/USERNAME и
    HOSTNAME вместо hostname -s, нормализация обратных слэшей — для Git Bash.
  - install.sh копирует statusline-command.sh в ~/.claude с правами 755.
- Проверка:
  - printf '%s' "$json" | bash claude/.claude/statusline-command.sh на JSON с
    дробными процентами, null в rate_limits и без блока rate_limits.
This commit is contained in:
ddadmin committed 2026-07-26 17:12:56 +03:00
1 parent ab4c7c7342
commit 571334d67b
2 files changed
+106 -69

No files matched your search

+98 -69
View File
@@ -1,87 +1,116 @@
#!/usr/bin/env bash
# Статусная строка Claude Code. JSON приходит на stdin.
#
# user@host:.../dir (branch) | Model Name (NM context) ctx:N% | 5h:N% 7d:N%
#
# Работает в Linux и в Git Bash на Windows: без `hostname -s`, jq не обязателен.
# Читаем и парсим JSON одним вызовом jq
IFS=$'\t' read -r cwd display_name ctx_pct rate5h rate7d < <(jq -r '[
(.workspace.current_dir // ""),
(.model.display_name // ""),
(.context_window.used_percentage // ""),
(.rate_limits.five_hour.used_percentage // ""),
(.rate_limits.seven_day.used_percentage // "")
] | @tsv')
# Порог, после которого путь сокращается до .../<последний каталог>
MAX_PATH_LEN="${STATUSLINE_MAX_PATH_LEN:-28}"
# Путь: $HOME → ~, длинные пути → .../<last_dir>
path="$cwd"
home="$HOME"
if [[ "$path" == "$home" ]]; then
path="~"
elif [[ "$path" == "$home"/* ]]; then
path="~${path#$home}"
input="$(cat)"
# ── Разбор JSON ───────────────────────────────────────────────────────
# Числовое поле $2 внутри объекта $1 (резервный разбор, без jq).
json_num() {
printf '%s' "$input" \
| tr -d ' \t\n\r' \
| grep -o "\"$1\":{[^}]*}" \
| grep -o "\"$2\":[0-9.eE+-]*" \
| head -1 \
| cut -d: -f2
}
# Строковое поле $2 внутри объекта $1 (резервный разбор, без jq).
json_str() {
printf '%s' "$input" \
| tr -d '\n\r' \
| grep -o "\"$1\"[[:space:]]*:[[:space:]]*{[^}]*}" \
| grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \
| head -1 \
| sed 's/.*"[[:space:]]*:[[:space:]]*"//; s/"$//'
}
if command -v jq >/dev/null 2>&1; then
IFS=$'\t' read -r cwd display_name ctx_pct rate5h rate7d <<EOF
$(printf '%s' "$input" | jq -r '
def num: if type == "number" then tostring else "" end;
[ (.workspace.current_dir // ""),
(.model.display_name // ""),
(.context_window.used_percentage | num),
(.rate_limits.five_hour.used_percentage | num),
(.rate_limits.seven_day.used_percentage | num)
] | @tsv' 2>/dev/null)
EOF
else
cwd="$(json_str workspace current_dir)"
display_name="$(json_str model display_name)"
ctx_pct="$(json_num context_window used_percentage)"
rate5h="$(json_num five_hour used_percentage)"
rate7d="$(json_num seven_day used_percentage)"
fi
# Считаем сегменты (кроме ведущего ~)
IFS='/' read -ra segs <<< "${path#\~}"
segs=("${segs[@]/#/}") # убираем пустые
real_segs=()
for s in "${segs[@]}"; do [[ -n "$s" ]] && real_segs+=("$s"); done
# Проценты в JSON дробные — округляем до целых.
round_pct() {
case "$1" in
''|null) return 0 ;;
esac
awk -v v="$1" 'BEGIN { printf "%d", (v < 0 ? -int(-v + 0.5) : int(v + 0.5)) }' 2>/dev/null
}
if (( ${#real_segs[@]} > 2 )); then
last="${real_segs[-1]}"
if [[ "$path" == ~* ]]; then
path="~/.../$last"
else
path=".../$last"
fi
ctx_pct="$(round_pct "$ctx_pct")"
rate5h="$(round_pct "$rate5h")"
rate7d="$(round_pct "$rate7d")"
# ── Путь ──────────────────────────────────────────────────────────────
# В Git Bash current_dir может прийти с обратными слэшами.
path="${cwd//\\//}"
home="${HOME//\\//}"
if [ -n "$home" ]; then
if [ "$path" = "$home" ]; then
path="~"
else
case "$path" in
"$home"/*) path="~${path#"$home"}" ;;
esac
fi
fi
# Git branch
if [ "${#path}" -gt "$MAX_PATH_LEN" ]; then
path=".../${path##*/}"
fi
# ── Остальные секции ──────────────────────────────────────────────────
branch=""
if [[ -n "$cwd" ]]; then
branch=$(git -C "$cwd" rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -n "$cwd" ]; then
branch="$(git -C "$cwd" rev-parse --abbrev-ref HEAD 2>/dev/null)"
fi
# Модель: убрать префикс "Claude "
# Размер контекста уже внутри display_name, убираем только префикс «Claude ».
model="${display_name#Claude }"
# ANSI цвета
GREEN='\033[32m'
YELLOW='\033[33m'
PURPLE='\033[35m'
CYAN='\033[36m'
RESET='\033[0m'
user="${USER:-${USERNAME:-$(id -un 2>/dev/null)}}"
host="${HOSTNAME:-$(hostname 2>/dev/null)}"
host="${host%%.*}"
# user@hostname
user_host="${GREEN}${USER}@$(hostname -s)${RESET}"
GREEN=$'\033[32m'
YELLOW=$'\033[33m'
PURPLE=$'\033[35m'
CYAN=$'\033[36m'
RESET=$'\033[0m'
# путь
colored_path="${YELLOW}${path}${RESET}"
out="${GREEN}${user}@${host}${RESET}:${YELLOW}${path}${RESET}"
[ -n "$branch" ] && out="${out} ${PURPLE}(${branch})${RESET}"
out="${out} | ${CYAN}${model}${RESET}"
[ -n "$ctx_pct" ] && out="${out} ctx:${ctx_pct}%"
# ветка
colored_branch=""
if [[ -n "$branch" ]]; then
colored_branch=" ${PURPLE}(${branch})${RESET}"
fi
# модель
colored_model="${CYAN}${model}${RESET}"
# Собираем секции
out="${user_host}:${colored_path}${colored_branch} | ${colored_model}"
# ctx: только если не пустое и не null
if [[ -n "$ctx_pct" && "$ctx_pct" != "null" ]]; then
out+=" ctx:${ctx_pct}%"
fi
# rate limits: только если не пустые и не null
rates=""
if [[ -n "$rate5h" && "$rate5h" != "null" ]]; then
rates+=" 5h:${rate5h}%"
fi
if [[ -n "$rate7d" && "$rate7d" != "null" ]]; then
rates+=" 7d:${rate7d}%"
fi
if [[ -n "$rates" ]]; then
out+=" |${rates}"
fi
[ -n "$rate5h" ] && rates="${rates} 5h:${rate5h}%"
[ -n "$rate7d" ] && rates="${rates} 7d:${rate7d}%"
[ -n "$rates" ] && out="${out} |${rates}"
printf '%b\n' "$out"
printf '%s\n' "$out"