네이티브 statusline 스크립트 및 자동 설치/업데이트 도구 추가

Claude Code 네이티브 statusLine 방식의 4줄 상태바 스크립트와
어느 머신에서든 한 줄로 설치·업데이트할 수 있는 스크립트 추가.
크로스플랫폼(Linux/macOS) stat 호환 처리 포함.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zaksal58 2026-02-21 00:26:02 +09:00
parent 584c4dabb1
commit 9168133823
4 changed files with 468 additions and 22 deletions

View File

@ -1,39 +1,85 @@
# ccstatusline-config
Claude Code [ccstatusline](https://github.com/sirmalloc/ccstatusline) 커스텀 설정.
Claude Code 커스텀 statusline 중앙 관리 저장소.
## 구성
```
ccstatusline/settings.json -- 위젯 배치/색상/이모지 설정
scripts/ctx-bar.sh -- 컨텍스트 사용량 progress bar
scripts/ctx-debug.sh -- 디버그용 (컨텍스트 raw 데이터 출력)
```
여러 머신에서 동일한 statusline을 사용하기 위한 설정과 설치 스크립트.
## 상태바 레이아웃
```
🎨 Explanatory | 🖥️ OY-Mac | 🧠 [████████░░░░] 70% | 💰 $4.10
🤖 Opus 4.6 | 🔑 session-id
📁 /Users/zaksal58 | ⚡ MCP [ATL:O|SRN:O] | 🕐 11:15
🎨 Explanatory │ 🖥️ odroidhc4 │ 🧠 [░░░░░░░░░░░░] 0% │ 💰 $0.00 0 tokens
🤖 ◆ Opus 4.6 │ 🔑 e6271b19 │ 📟 v2.1.49
📁 /home/user │ 🌿 main │ 📊 ▲+0 ▼-0 │ ⏱️ 9s
⏵⏵ Explanatory
```
## 설치 (복원)
### 표시 항목
| 라인 | 항목 |
|------|------|
| 1 | Output Style, 호스트명, 컨텍스트 사용률 (색상 변화), 비용, 토큰 수 |
| 2 | 모델 (티어별 심볼: ◆Opus/◇Sonnet/○Haiku), 세션 ID, 버전 |
| 3 | 작업 디렉토리, Git 브랜치, 추가/삭제 라인 수, 경과 시간 |
| 4 | 현재 모드, Vim 모드 (사용 시), 에이전트 이름 (팀 모드 시) |
## 설치
### 방법 1: 클론 후 설치
```bash
# 1. ccstatusline 설정 복사
cp ccstatusline/settings.json ~/.config/ccstatusline/settings.json
git clone https://git.scrutineer.co.kr/zaksal58/ccstatusline-config.git
cd ccstatusline-config
./install.sh
```
# 2. 커스텀 스크립트 복사
cp scripts/ctx-bar.sh ~/.claude/ctx-bar.sh
cp scripts/ctx-debug.sh ~/.claude/ctx-debug.sh
chmod +x ~/.claude/ctx-bar.sh ~/.claude/ctx-debug.sh
### 방법 2: 원격 설치 (한 줄)
# 3. ctx-bar.sh 경로 수정 (username이 다를 경우)
# settings.json 내 commandPath를 본인 경로로 변경
```bash
curl -sSL https://git.scrutineer.co.kr/zaksal58/ccstatusline-config/raw/branch/main/install.sh | bash
```
## 업데이트
```bash
# 방법 1: 클론된 디렉토리에서
cd ccstatusline-config && git pull && ./install.sh
# 방법 2: 원격 업데이트 (한 줄)
curl -sSL https://git.scrutineer.co.kr/zaksal58/ccstatusline-config/raw/branch/main/update.sh | bash
```
## 구조
```
statusline.sh -- 메인 statusline 스크립트 (Claude Code 네이티브)
install.sh -- 자동 설치 스크립트
update.sh -- 원격 업데이트 스크립트
ccstatusline/ -- (레거시) ccstatusline npm 패키지용 설정
settings.json -- 위젯 배치/색상 설정
scripts/ -- (레거시) ccstatusline 커스텀 커맨드 스크립트
ctx-bar.sh -- 컨텍스트 사용량 progress bar
ctx-debug.sh -- 디버그용 raw 데이터 출력
```
## 요구사항
- [ccstatusline](https://github.com/sirmalloc/ccstatusline) (npx로 자동 실행)
- `jq` (context bar에 필요)
- `jq` (JSON 파싱에 필요)
- `git` (설치/업데이트에 필요)
- Claude Code v1.0+
## 동작 방식
Claude Code의 네이티브 `statusLine` 설정을 사용합니다.
`~/.claude/settings.json`에 다음이 추가됩니다:
```json
{
"statusLine": {
"type": "command",
"command": "/home/<user>/.claude/statusline.sh",
"padding": 0
}
}
```
스크립트는 Claude Code에서 JSON을 stdin으로 받아 ANSI 색상이 적용된 4줄 상태바를 출력합니다.

151
install.sh Executable file
View File

@ -0,0 +1,151 @@
#!/usr/bin/env bash
# ccstatusline-config installer
# 어느 머신에서든 한 줄로 설치:
# git clone https://git.scrutineer.co.kr/zaksal58/ccstatusline-config.git && cd ccstatusline-config && ./install.sh
#
# 또는 원격 설치:
# curl -sSL https://git.scrutineer.co.kr/zaksal58/ccstatusline-config/raw/branch/main/install.sh | bash
set -euo pipefail
REPO_URL="https://git.scrutineer.co.kr/zaksal58/ccstatusline-config.git"
CLAUDE_DIR="${HOME}/.claude"
STATUSLINE_SCRIPT="${CLAUDE_DIR}/statusline.sh"
SETTINGS_FILE="${CLAUDE_DIR}/settings.json"
# --- Colors ---
RED=$'\033[91m'
GREEN=$'\033[92m'
YELLOW=$'\033[93m'
CYAN=$'\033[96m'
BOLD=$'\033[1m'
RESET=$'\033[0m'
info() { echo "${CYAN}[INFO]${RESET} $*"; }
ok() { echo "${GREEN}[OK]${RESET} $*"; }
warn() { echo "${YELLOW}[WARN]${RESET} $*"; }
err() { echo "${RED}[ERROR]${RESET} $*" >&2; }
# --- Dependency check ---
check_deps() {
local missing=()
for cmd in jq git; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing+=("$cmd")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
err "필수 의존성이 없습니다: ${missing[*]}"
err "설치 후 다시 실행해주세요."
exit 1
fi
}
# --- Determine script source directory ---
get_script_dir() {
# If run from a cloned repo, use local files
local dir
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "${dir}/statusline.sh" ]; then
echo "$dir"
return
fi
# If run via curl pipe, clone to temp dir
local tmpdir
tmpdir=$(mktemp -d)
info "저장소 클론 중..."
git clone --depth 1 "$REPO_URL" "$tmpdir" >/dev/null 2>&1
echo "$tmpdir"
}
# --- Install statusline script ---
install_statusline() {
local src_dir="$1"
mkdir -p "$CLAUDE_DIR"
# Backup existing statusline if different
if [ -f "$STATUSLINE_SCRIPT" ]; then
if ! diff -q "${src_dir}/statusline.sh" "$STATUSLINE_SCRIPT" >/dev/null 2>&1; then
local backup="${STATUSLINE_SCRIPT}.bak.$(date +%Y%m%d%H%M%S)"
cp "$STATUSLINE_SCRIPT" "$backup"
warn "기존 statusline.sh 백업: ${backup}"
fi
fi
cp "${src_dir}/statusline.sh" "$STATUSLINE_SCRIPT"
chmod +x "$STATUSLINE_SCRIPT"
ok "statusline.sh 설치 완료: ${STATUSLINE_SCRIPT}"
}
# --- Update settings.json ---
update_settings() {
local statusline_entry
statusline_entry=$(cat <<JSONEOF
{
"type": "command",
"command": "${STATUSLINE_SCRIPT}",
"padding": 0
}
JSONEOF
)
if [ ! -f "$SETTINGS_FILE" ]; then
# Create new settings.json with statusLine
cat > "$SETTINGS_FILE" <<JSONEOF
{
"statusLine": {
"type": "command",
"command": "${STATUSLINE_SCRIPT}",
"padding": 0
}
}
JSONEOF
ok "settings.json 생성 완료 (statusLine 설정 포함)"
return
fi
# Check if statusLine is already configured
local existing
existing=$(jq -r '.statusLine.command // ""' "$SETTINGS_FILE" 2>/dev/null || echo "")
if [ "$existing" = "$STATUSLINE_SCRIPT" ]; then
ok "settings.json에 statusLine이 이미 올바르게 설정되어 있습니다."
return
fi
# Update or add statusLine entry
local tmpfile
tmpfile=$(mktemp)
jq --arg cmd "$STATUSLINE_SCRIPT" '.statusLine = {"type": "command", "command": $cmd, "padding": 0}' "$SETTINGS_FILE" > "$tmpfile"
mv "$tmpfile" "$SETTINGS_FILE"
ok "settings.json 업데이트 완료 (statusLine 경로: ${STATUSLINE_SCRIPT})"
}
# --- Main ---
main() {
echo ""
echo "${BOLD}${CYAN}╔══════════════════════════════════════╗${RESET}"
echo "${BOLD}${CYAN}║ Claude Code Statusline Installer ║${RESET}"
echo "${BOLD}${CYAN}╚══════════════════════════════════════╝${RESET}"
echo ""
check_deps
local src_dir
src_dir=$(get_script_dir)
install_statusline "$src_dir"
update_settings
echo ""
echo "${GREEN}${BOLD}설치 완료!${RESET}"
echo ""
echo " Claude Code를 다시 시작하면 새 statusline이 적용됩니다."
echo ""
echo " 업데이트하려면:"
echo " ${CYAN}cd ${src_dir} && git pull && ./install.sh${RESET}"
echo ""
}
main "$@"

208
statusline.sh Executable file
View File

@ -0,0 +1,208 @@
#!/usr/bin/env bash
# Claude Code 4-line statusline
# Reads JSON from stdin, outputs ANSI-colored 4-line status bar
# --- Color definitions (bright variants for dark backgrounds) ---
RED=$'\033[91m'
GREEN=$'\033[92m'
YELLOW=$'\033[93m'
BLUE=$'\033[94m'
MAGENTA=$'\033[95m'
CYAN=$'\033[96m'
GRAY=$'\033[37m'
WHITE=$'\033[97m'
BOLD=$'\033[1m'
DIM=$'\033[2m'
RESET=$'\033[0m'
SEP="${GRAY}${RESET}"
# --- Read all JSON input ---
input=$(cat)
# --- Single jq call: extract all 16 fields newline-separated ---
# Using newlines instead of tabs to avoid bash IFS merging consecutive delimiters
mapfile -t _fields < <(echo "$input" | jq -r '
(.model.display_name // .model.id // "?"),
(.model.id // "?"),
(.context_window.used_percentage // 0 | floor | tostring),
(.context_window.context_window_size // 0 | tostring),
(.context_window.total_input_tokens // 0 | tostring),
(.context_window.total_output_tokens // 0 | tostring),
(.cost.total_cost_usd // 0 | . * 100 | floor | . / 100 | tostring),
(.cost.total_duration_ms // 0 | tostring),
(.cost.total_lines_added // 0 | tostring),
(.cost.total_lines_removed // 0 | tostring),
(if .session_id == null or .session_id == "" then "--------" else .session_id end),
(.version // "?.?.?"),
(if .output_style.name == null or .output_style.name == "" then "default" else .output_style.name end),
(.workspace.current_dir // "~"),
(.vim.mode // ""),
(.agent.name // "")
')
model_name="${_fields[0]}"
model_id="${_fields[1]}"
ctx_pct="${_fields[2]}"
ctx_size="${_fields[3]}"
total_in_tok="${_fields[4]}"
total_out_tok="${_fields[5]}"
cost="${_fields[6]}"
dur_ms="${_fields[7]}"
lines_add="${_fields[8]}"
lines_rm="${_fields[9]}"
session_id="${_fields[10]}"
version="${_fields[11]}"
style="${_fields[12]}"
cwd="${_fields[13]}"
vim_mode="${_fields[14]}"
agent_name="${_fields[15]}"
# --- Hostname (cached in variable, never changes) ---
HOSTNAME_SHORT=$(hostname -s 2>/dev/null || echo "unknown")
# --- Context progress bar (12 chars wide) ---
pct=${ctx_pct:-0}
filled=$(( pct * 12 / 100 ))
empty=$(( 12 - filled ))
if [ "$pct" -ge 70 ]; then
bar_color="$RED"
elif [ "$pct" -ge 50 ]; then
bar_color="$YELLOW"
else
bar_color="$GREEN"
fi
bar_filled=""
bar_empty=""
for (( i=0; i<filled; i++ )); do bar_filled+="█"; done
for (( i=0; i<empty; i++ )); do bar_empty+="░"; done
progress_bar="${bar_color}${bar_filled}${GRAY}${bar_empty}${RESET}"
pct_display="${bar_color}${pct}%${RESET}"
# --- Cost color ---
cost_val=${cost:-0}
# Compare using awk for floating-point
cost_display=$(awk -v c="$cost_val" '
BEGIN {
if (c+0 >= 5) col="\033[91m";
else if (c+0 >= 2) col="\033[93m";
else col="\033[92m";
printf "%s$%.2f\033[0m", col, c+0
}')
# --- Token formatting ---
format_tokens() {
local tok=$1
if [ "$tok" -ge 1000000 ] 2>/dev/null; then
echo "$(( tok / 1000000 ))M"
elif [ "$tok" -ge 1000 ] 2>/dev/null; then
echo "$(( tok / 1000 ))k"
else
echo "${tok}"
fi
}
in_tok_display=$(format_tokens "${total_in_tok:-0}")
# --- Model tier symbol ---
case "$model_id" in
*opus*) tier_sym="${MAGENTA}${BOLD}${RESET}"; model_color="${MAGENTA}${BOLD}" ;;
*sonnet*) tier_sym="${BLUE}${BOLD}${RESET}"; model_color="${BLUE}${BOLD}" ;;
*haiku*) tier_sym="${GREEN}${BOLD}${RESET}"; model_color="${GREEN}${BOLD}" ;;
*) tier_sym="${GRAY}${RESET}"; model_color="${GRAY}" ;;
esac
# --- Session ID (first 8 chars) ---
session_short="${session_id:0:8}"
# --- Duration formatting ---
dur_total_sec=$(( ${dur_ms:-0} / 1000 ))
if [ "$dur_total_sec" -ge 3600 ]; then
dur_h=$(( dur_total_sec / 3600 ))
dur_m=$(( (dur_total_sec % 3600) / 60 ))
dur_display="${dur_h}h ${dur_m}m"
elif [ "$dur_total_sec" -ge 60 ]; then
dur_m=$(( dur_total_sec / 60 ))
dur_s=$(( dur_total_sec % 60 ))
dur_display="${dur_m}m ${dur_s}s"
else
dur_display="${dur_total_sec}s"
fi
# --- Git branch (cached with 5s TTL) ---
CACHE_FILE="/tmp/claude-statusline-git-cache"
NOW=$(date +%s)
GIT_BRANCH=""
if [ -f "$CACHE_FILE" ]; then
CACHE_AGE=$(( NOW - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0) ))
if [ "$CACHE_AGE" -lt 5 ]; then
GIT_BRANCH=$(cat "$CACHE_FILE")
fi
fi
if [ -z "$GIT_BRANCH" ]; then
GIT_BRANCH=$(cd "${cwd:-$HOME}" 2>/dev/null && git symbolic-ref --short HEAD 2>/dev/null || echo "")
echo "$GIT_BRANCH" > "$CACHE_FILE"
fi
# ============================================================
# LINE 1: Identity & Resources
# ============================================================
line1="\xf0\x9f\x8e\xa8 ${WHITE}${style}${RESET}"
line1+="${SEP}"
line1+="\xf0\x9f\x96\xa5\xef\xb8\x8f ${CYAN}${BOLD}${HOSTNAME_SHORT}${RESET}"
line1+="${SEP}"
line1+="\xf0\x9f\xa7\xa0 [${progress_bar}] ${pct_display}"
line1+="${SEP}"
line1+="\xf0\x9f\x92\xb0 ${cost_display}"
line1+=" ${GRAY}${in_tok_display} tokens${RESET}"
# ============================================================
# LINE 2: Model & Session
# ============================================================
line2="\xf0\x9f\xa4\x96 ${tier_sym} ${model_color}${model_name}${RESET}"
line2+="${SEP}"
line2+="\xf0\x9f\x94\x91 ${GRAY}${session_short}${RESET}"
line2+="${SEP}"
line2+="\xf0\x9f\x93\x9f ${CYAN}v${version}${RESET}"
# ============================================================
# LINE 3: Workspace & Tools
# ============================================================
line3="\xf0\x9f\x93\x81 ${CYAN}${cwd}${RESET}"
if [ -n "$GIT_BRANCH" ]; then
line3+="${SEP}"
line3+="\xf0\x9f\x8c\xbf ${GREEN}${BOLD}${GIT_BRANCH}${RESET}"
fi
line3+="${SEP}"
line3+="\xf0\x9f\x93\x8a ${GREEN}▲+${lines_add}${RESET} ${RED}▼-${lines_rm}${RESET}"
line3+="${SEP}"
line3+="\xe2\x8f\xb1\xef\xb8\x8f ${YELLOW}${dur_display}${RESET}"
# ============================================================
# LINE 4: Mode & Status (conditional)
# ============================================================
line4="\xe2\x8f\xb5\xe2\x8f\xb5 ${BOLD}${style}${RESET}"
if [ -n "$vim_mode" ]; then
vim_upper=$(echo "$vim_mode" | tr '[:lower:]' '[:upper:]')
case "$vim_upper" in
NORMAL) vim_color="$BLUE" ;;
INSERT) vim_color="$GREEN" ;;
*) vim_color="$WHITE" ;;
esac
line4+="${SEP}"
line4+="\xf0\x9f\x94\xb2 ${vim_color}${vim_upper}${RESET}"
fi
if [ -n "$agent_name" ]; then
line4+="${SEP}"
line4+="\xf0\x9f\x8f\xb7\xef\xb8\x8f ${MAGENTA}${agent_name}${RESET}"
fi
# ============================================================
# Output all 4 lines
# ============================================================
printf '%b\n' "$line1"
printf '%b\n' "$line2"
printf '%b\n' "$line3"
printf '%b\n' "$line4"

41
update.sh Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env bash
# ccstatusline-config updater
# 원격 저장소에서 최신 statusline을 가져와 설치합니다.
#
# 사용법:
# ~/.claude/update-statusline.sh
#
# 또는 원격 실행:
# curl -sSL https://git.scrutineer.co.kr/zaksal58/ccstatusline-config/raw/branch/main/update.sh | bash
set -euo pipefail
REPO_URL="https://git.scrutineer.co.kr/zaksal58/ccstatusline-config.git"
CLAUDE_DIR="${HOME}/.claude"
STATUSLINE_SCRIPT="${CLAUDE_DIR}/statusline.sh"
GREEN=$'\033[92m'
CYAN=$'\033[96m'
YELLOW=$'\033[93m'
BOLD=$'\033[1m'
RESET=$'\033[0m'
info() { echo "${CYAN}[INFO]${RESET} $*"; }
ok() { echo "${GREEN}[OK]${RESET} $*"; }
warn() { echo "${YELLOW}[WARN]${RESET} $*"; }
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
info "최신 statusline 가져오는 중..."
git clone --depth 1 "$REPO_URL" "$tmpdir" >/dev/null 2>&1
if diff -q "${tmpdir}/statusline.sh" "$STATUSLINE_SCRIPT" >/dev/null 2>&1; then
ok "이미 최신 버전입니다."
exit 0
fi
cp "${tmpdir}/statusline.sh" "$STATUSLINE_SCRIPT"
chmod +x "$STATUSLINE_SCRIPT"
ok "statusline.sh 업데이트 완료!"
echo " Claude Code를 다시 시작하면 적용됩니다."