diff --git a/README.md b/README.md index 31af658..d9a6d05 100644 --- a/README.md +++ b/README.md @@ -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//.claude/statusline.sh", + "padding": 0 + } +} +``` + +스크립트는 Claude Code에서 JSON을 stdin으로 받아 ANSI 색상이 적용된 4줄 상태바를 출력합니다. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..1feddb4 --- /dev/null +++ b/install.sh @@ -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 < "$SETTINGS_FILE" </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 "$@" diff --git a/statusline.sh b/statusline.sh new file mode 100755 index 0000000..711db50 --- /dev/null +++ b/statusline.sh @@ -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= 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" diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..778941f --- /dev/null +++ b/update.sh @@ -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를 다시 시작하면 적용됩니다."