statuslin.es

Kanji Rain

CyberPunk/BladeRunner styling. Two lines with kanji field labels: repo 庫, branch 枝, worktree 樹, and model 脳 on top; context 文, 5-hour 時, and weekly 週 meters below. The rate-limit bars carry a ┃ pace cursor marking how far through the reset window you are — cyan when your usage trails the cursor (on track), amber/red when it outruns it.

Updated 2026-07-10

Preview

Clean repo
app · main · OPUS-4.8 ▓▓░░░░░░ 22% · ▓▓░░░░░ 26% · ▓░░░░░░ 7%
New session
app · main · OPUS-4.8 ░░░░░░░░ -- · ░░░░░░░░ -- · ░░░░░░░░ --
Dirty branch
app · feat/auth · feat-auth · SONNET-4.6 ▓▓▓▓░░░░ 48% · ▓▓▓░░░░ 40% · ▓░░░░░░ 18%
Near-full
app · main · OPUS-4.8 ▓▓▓▓▓▓▓░ 91% · ▓▓▓▓▓▓▓ 88% · ▓▓▓▓▓░░ 61%
1M context
app · main · FABLE-5 ▓▓▓▓▓░░░ 64% · ▓░░░░░ 33% · ░░░░░░ 12%
Post-compact
app · main · HAIKU-4.5 ░░░░░░░░ -- · ▓▓░░░░ 52% · ░░░░░░░░ --
Worktree
app · worktree-feature · feature · OPUS-4.8 ▓▓▓░░░░░ 37% · ▓▓▓░░░░ 44% · ▓▓░░░░░ 20%
Non-git
OPUS-4.8 ▓▓░░░░░░ 22% · ░░░░░░░░ -- · ░░░░░░░░ --

Source

#!/usr/bin/env bash
# Kanji Rain — statusline for Claude Code.
#
# Claude Code pipes session JSON on stdin (see ~/.claude/settings.json
# "statusLine"); this prints two ANSI-colored lines:
#
#   庫 repo · 枝 branch · 樹 worktree · 脳 model
#   文 context ▓▓▓░░░┃░ 42% · 時 5h · 週 weekly
#
# Rate-limit meters (時/週) carry a ┃ pace cursor marking how far through the
# reset window you are: fill behind the cursor means you're consuming slower
# than the window refills (on track, cyan); fill past it means you're outrunning
# it (amber, then red for way over). Absolute floors still apply — >= 85% used
# is always red no matter the pace. The context meter (文) has no window, so it
# uses plain 60/85 thresholds. Fields with no data (rate limits before the
# first response, context at session start) render as a dim empty bar. The 樹
# worktree segment only appears inside a linked worktree; 庫/枝/樹 all
# disappear outside a git repo.
#
# Run it from a terminal (no piped stdin) for a preview of the
# normal / critical / no-data states. Requires jq; git is optional.

# ── Palette (truecolor) ───────────────────────────────────────────────────────
MAG=$'\033[38;2;255;79;208m'   # kanji field icons
CYN=$'\033[38;2;77;240;224m'   # values, healthy meters
AMB=$'\033[38;2;255;180;84m'   # meters >= 60%
RED=$'\033[38;2;255;75;87m'    # meters >= 85%
WHT=$'\033[38;2;232;238;250m'  # model name
DIM=$'\033[38;2;74;88;113m'    # separators, missing data
RST=$'\033[0m'

SEP=" ${DIM}·${RST} "
BAR_CELLS=8
# ▓/░ render one column wide in every monospace font; fancier glyphs like ▰/▱
# are East-Asian-ambiguous width and overlap in many terminal fonts.
FILL="▓"
EMPTY="░"
MARK="┃"

# ── Meter: "▓▓▓░░░┃░ 42%" ─────────────────────────────────────────────────────
# $1 usage pct (-1 = no data), $2 elapsed pct of the reset window (-1 = none).
# With a window, the ┃ cursor marks "now" and color keys off usage - elapsed:
# on pace cyan, > 5 over amber, > 20 over red, with absolute floors at 70/85.
# Without one, plain 60/85 thresholds.
meter() {
  local pct=${1:--1} elapsed=${2:--1} color filled mark=0 i bar=""
  if (( pct < 0 )); then
    for (( i = 1; i <= BAR_CELLS; i++ )); do bar+="$EMPTY"; done
    printf '%s' "${DIM}${bar} --${RST}"
    return
  fi
  (( pct > 100 )) && pct=100

  local sev=0  # 0 cyan · 1 amber · 2 red
  if (( elapsed >= 0 )); then
    local over=$(( pct - elapsed ))
    if   (( over > 20 )); then sev=2
    elif (( over > 5  )); then sev=1
    fi
    (( pct >= 70 && sev < 1 )) && sev=1
    (( pct >= 85 )) && sev=2
    mark=$(( (elapsed * BAR_CELLS + 50) / 100 ))
    (( mark < 1 )) && mark=1
    (( mark > BAR_CELLS )) && mark=BAR_CELLS
  else
    (( pct >= 60 )) && sev=1
    (( pct >= 85 )) && sev=2
  fi
  case $sev in
    2) color=$RED ;;
    1) color=$AMB ;;
    *) color=$CYN ;;
  esac

  filled=$(( (pct * BAR_CELLS + 50) / 100 ))
  for (( i = 1; i <= BAR_CELLS; i++ )); do
    if   (( i == mark ));   then bar+="${WHT}${MARK}${color}"
    elif (( i <= filled )); then bar+="$FILL"
    else                         bar+="$EMPTY"
    fi
  done
  printf '%s' "${color}${bar} ${pct}%${RST}"
}

# ── Elapsed % of a reset window, from its resets_at epoch ─────────────────────
# $1 resets_at (-1 = unknown), $2 window length in seconds.
elapsed_pct() {
  local resets_at=${1:--1} window=$2 remaining now=${EPOCHSECONDS:-$(date +%s)}
  if (( resets_at <= 0 )); then printf '%s' -1; return; fi
  remaining=$(( resets_at - now ))
  (( remaining < 0 )) && remaining=0
  (( remaining > window )) && remaining=window
  printf '%s' $(( (window - remaining) * 100 / window ))
}

# ── Render one statusline from a JSON payload ─────────────────────────────────
render() {
  local json="$1"
  local fields model dir wt repo ctx five wk five_reset wk_reset branch

  fields=$(printf '%s' "$json" | jq -r '[
    (.model.display_name? // .model.id? // "claude"),
    (.workspace.current_dir? // .cwd? // ""),
    (.workspace.git_worktree? // .worktree.name? // ""),
    (.workspace.repo.name? // ""),
    ((.context_window.used_percentage? // -1) | round),
    ((.rate_limits.five_hour.used_percentage? // -1) | round),
    ((.rate_limits.seven_day.used_percentage? // -1) | round),
    ((.rate_limits.five_hour.resets_at? // -1) | round),
    ((.rate_limits.seven_day.resets_at? // -1) | round)
  ] | map(tostring) | join("\u001f")' 2>/dev/null) \
    || { printf '%s\n' "${DIM}claude${RST}"; return; }

  # unit separator, not tab: tab is IFS whitespace, so empty fields (e.g. no
  # worktree) would collapse and shift everything after them
  IFS=$'\x1f' read -r model dir wt repo ctx five wk five_reset wk_reset <<< "$fields"

  branch=""
  if [[ -n "$dir" ]]; then
    branch=$(command git -C "$dir" symbolic-ref --short -q HEAD 2>/dev/null) \
      || branch=$(command git -C "$dir" rev-parse --short HEAD 2>/dev/null)
  fi

  if [[ -n "$branch" ]]; then
    local git_dir common_dir
    git_dir=$(command git -C "$dir" rev-parse --absolute-git-dir 2>/dev/null)
    common_dir=$(command git -C "$dir" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
    # the common dir is <repo>/.git even inside a worktree, so its parent names
    # the repo when workspace.repo.name is absent (e.g. no GitHub remote)
    if [[ -z "$repo" && -n "$common_dir" ]]; then
      repo=$(basename "$(dirname "$common_dir")")
    fi
    # Claude Code only fills workspace.git_worktree for worktrees it created;
    # detect hand-made ones (git worktree add) by comparing git dirs.
    if [[ -z "$wt" && -n "$git_dir" && "$git_dir" != "$common_dir" ]]; then
      wt=$(basename "$(command git -C "$dir" rev-parse --show-toplevel 2>/dev/null)")
    fi
  fi

  # tr, not ${model^^}: macOS ships bash 3.2, which lacks case conversion
  model=$(printf '%s' "$model" | tr '[:lower:]' '[:upper:]')
  model=${model// /-}

  local -a parts=() meters=()
  [[ -n "$repo"   ]] && parts+=("${MAG}庫${RST} ${CYN}${repo}${RST}")
  [[ -n "$branch" ]] && parts+=("${MAG}枝${RST} ${CYN}${branch}${RST}")
  [[ -n "$wt"     ]] && parts+=("${MAG}樹${RST} ${CYN}${wt}${RST}")
  parts+=("${MAG}脳${RST} ${WHT}${model}${RST}")
  meters+=("${MAG}文${RST} $(meter "$ctx")")
  meters+=("${MAG}時${RST} $(meter "$five" "$(elapsed_pct "$five_reset" $(( 5 * 3600 )))")")
  meters+=("${MAG}週${RST} $(meter "$wk" "$(elapsed_pct "$wk_reset" $(( 7 * 86400 )))")")

  join_line "${parts[@]}"
  join_line "${meters[@]}"
}

# ── Join segments with the dim separator and print as one line ────────────────
join_line() {
  local line="" part
  for part in "$@"; do
    [[ -n "$line" ]] && line+="$SEP"
    line+="$part"
  done
  printf '%s\n' "$line"
}

# ── Preview mode: sample states, using the real repo for branch info ─────────
preview() {
  local base now=${EPOCHSECONDS:-$(date +%s)}
  base=$(printf '{"model":{"display_name":"Fable 5"},"workspace":{"current_dir":"%s"}}' "$PWD")

  # on track: 5h at 31% used / 75% elapsed, weekly at 68% used / 80% elapsed
  printf 'on track\n'
  render "$(printf '%s' "$base" | jq --argjson now "$now" '. + {
    context_window:{used_percentage:42},
    rate_limits:{five_hour:{used_percentage:31, resets_at:($now + 4500)},
                 seven_day:{used_percentage:68, resets_at:($now + 120960)}}}')"
  # burning hot: 5h at 91% used / 40% elapsed, weekly at 76% used / 50% elapsed
  printf 'critical\n'
  render "$(printf '%s' "$base" | jq --argjson now "$now" '.workspace.git_worktree="feat-statusline" | . + {
    context_window:{used_percentage:74},
    rate_limits:{five_hour:{used_percentage:91, resets_at:($now + 10800)},
                 seven_day:{used_percentage:76, resets_at:($now + 302400)}}}')"
  printf 'no data\n'
  render "$base"
}

if [[ "${1:-}" == "--test" || -t 0 ]]; then
  preview
  exit 0
fi

render "$(cat)"

What it shows

  • Repository name, from workspace.repo.name or the parent directory of the git common dir
  • Current git branch, falling back to a short commit hash when HEAD is detached
  • Git worktree name, shown only when the session is inside a linked worktree
  • The model name in uppercase with spaces turned into hyphens, like OPUS-4.8
  • Context window usage as an 8-cell bar with a percentage
  • Five-hour rate limit usage as a bar and percentage, with a pace cursor marking how far the reset window has elapsed
  • Seven-day rate limit usage with the same bar and pace cursor

Requirements

  • Bash, and it stays compatible with the bash 3.2 that ships on macOS
  • jq, which is required for parsing the session JSON
  • git is optional, without it the repo, branch, and worktree segments just stay empty
  • A terminal with truecolor ANSI support, since all colors are 24-bit escape codes
  • A monospace font that renders the block characters and the kanji labels; no Nerd Font glyphs are used
  • No network access, the script makes no network calls

Behavior notes

  • Outside a git repo the whole top row collapses to just the model segment, as in the scratch-dir scenario
  • The worktree segment appears only in worktree sessions, either from workspace.git_worktree or by detecting a hand-made worktree through differing git dirs
  • Meters with no data (context right after /compact, rate limits before the first response) render as a dim empty bar with -- instead of a number
  • Rate limit bars color by pace, cyan when usage trails the elapsed-time cursor, amber when it runs more than 5 points ahead, red past 20 over, with hard floors that force amber at 70% and red at 85% used
  • The context bar has no reset window, so it switches to amber at 60% and red at 85%, which is why 64% shows amber and 91% shows red in the previews
  • The white pace cursor moves along the rate limit bars as the reset window elapses, sitting near the left early in a window and near the right close to reset
  • In the near-full scenario the five-hour bar goes red at 88% used even though its window has barely elapsed, from the 85% floor
  • When only the five-hour rate limit is reported, the weekly meter alone falls back to the dim no-data bar
  • Run with no piped stdin or with --test, it prints a preview of on-track, critical, and no-data sample states instead of reading a session

More status lines

app · main · Opus-4.8 · effort high · ctx 22% · 5h 26% ↻2h6m · 7d 7% ↻2d0h
███████████▋ 73% 🗝 Ultima Weapon ✦ app ∙ main ████████████████████████ 100% 「SAVE POINT」 ✶ Limit Form ◉ 41
╭─ CONTEXT ────────────────────────────────────────────╮ ████▊░░░░░░░░░░░░░░░░░ 22% of 200K · GOOD ├─ STATS ──────────────────────────────────────────────┤ 44.0K 1.4K $0.41 ⏱ 10m 12s +128/-34 ├─ REPO ───────────────────────────────────────────────┤ Opus 4.8 · main · 17:27 ╰──────────────────────────────────────────────────────╯
Opus 4.8 | main | ██░░░░░░░░ 22.0% | 44k / 200k | 02:07 26% | Wed 7%
Opus 4.8 [high] ~/app main 17:27:05 | ⛁ ██░░░░░░░░ 22% | 5h ●◔○○○ 26% ↻2h7m | 7d ◔○○○○○○ 7% ↻2d1h │ $0.41 ⏱ 10m
Opus 4.8 main app 44k/200k $0.41