Guest User

Untitled

a guest
Feb 7th, 2026
697
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 16.11 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. import sys
  4. import json
  5. import os
  6. import re
  7. import time
  8. from datetime import datetime
  9. from pathlib import Path
  10. import urllib.request
  11.  
  12. # ANSI colors
  13. E = "\033"
  14. BLACK   = f"{E}[38;2;0;0;0m"
  15. BLUE    = f"{E}[38;2;0;153;255m"
  16. ORANGE  = f"{E}[38;2;255;176;85m"
  17. GREEN   = f"{E}[38;2;0;160;0m"
  18. CYAN    = f"{E}[38;2;100;200;200m"
  19. RED     = f"{E}[38;2;255;85;85m"
  20. YELLOW  = f"{E}[38;2;230;200;0m"
  21. WHITE   = f"{E}[38;2;220;220;220m"
  22. GRAY    = f"{E}[38;2;180;180;180m"
  23. PURPLE  = f"{E}[38;2;167;107;206m"
  24. PEACH   = f"{E}[38;2;253;202;162m"
  25. PEACH2  = f"{E}[38;2;252;169;133m"
  26. DKPEACH = f"{E}[38;2;189;127;100m"
  27. DKGREEN = f"{E}[38;2;0;120;0m"
  28. MUSTARD = f"{E}[38;2;255;219;88m"
  29. DKYELLOW = f"{E}[38;2;80;80;0m"
  30. DKRED   = f"{E}[38;2;180;50;50m"
  31. BG_RED  = f"{E}[48;2;150;0;0m"
  32. BG_YELLOW = f"{E}[48;2;225;225;0m"
  33. BG_AZURE = f"{E}[48;2;0;128;255m"
  34. DIM     = f"{E}[2m"
  35. RST     = f"{E}[0m"
  36. SEP     = f" {DIM}|{RST} "
  37.  
  38. RAINBOW = [
  39.     f"{E}[38;2;255;85;85m",
  40.     f"{E}[38;2;255;176;85m",
  41.     f"{E}[38;2;230;200;0m",
  42.     f"{E}[38;2;0;200;0m",
  43.     f"{E}[38;2;46;149;153m",
  44.     f"{E}[38;2;0;153;255m",
  45.     f"{E}[38;2;180;100;255m",
  46. ]
  47.  
  48.  
  49. def rainbow(text):
  50.     return "".join(RAINBOW[i % len(RAINBOW)] + c for i, c in enumerate(text)) + RST
  51.  
  52.  
  53. def format_tokens(num):
  54.     if num >= 1_000_000:
  55.         return f"{num / 1_000_000:.1f}m"
  56.     if num >= 1_000:
  57.         return f"{round(num / 1000)}k"
  58.     return str(num)
  59.  
  60.  
  61. def build_bar(pct, width=10):
  62.     pct = max(0, min(100, pct))
  63.     filled = round(pct * width / 100)
  64.     empty = width - filled
  65.     if pct >= 75:
  66.         c = RED
  67.     elif pct >= 50:
  68.         c = YELLOW
  69.     else:
  70.         c = GREEN
  71.     return f"{c}{'\u25cf' * filled}{DIM}{'\u25cb' * empty}{RST}"
  72.  
  73.  
  74. def pad(text, visible_len, col_width):
  75.     gap = col_width - visible_len
  76.     return text + " " * gap if gap > 0 else text
  77.  
  78.  
  79. def vlen(s):
  80.     return len(re.sub(r'\033\[[^m]*m', '', s))
  81.  
  82.  
  83. def format_reset(iso, style="time"):
  84.     if not iso:
  85.         return ""
  86.     try:
  87.         utc = datetime.fromisoformat(iso.replace("Z", "+00:00"))
  88.         local = utc.astimezone()
  89.         if style == "time":
  90.             return local.strftime("%-I:%M%p").lower()
  91.         if style == "datetime":
  92.             return local.strftime("%b %-d, %-I:%M%p").lower()
  93.         return local.strftime("%b %-d").lower()
  94.     except Exception:
  95.         return ""
  96.  
  97.  
  98. def fetch_usage():
  99.     """Return usage dict, using a file cache to avoid hammering the API."""
  100.     cache_file = "/tmp/claude-statusline-usage-cache.json"
  101.     cache_max_age = 60
  102.  
  103.     usage_data = None
  104.     needs_refresh = True
  105.  
  106.     if os.path.exists(cache_file):
  107.         age = time.time() - os.path.getmtime(cache_file)
  108.         if age < cache_max_age:
  109.             needs_refresh = False
  110.             try:
  111.                 with open(cache_file) as f:
  112.                     usage_data = json.load(f)
  113.             except Exception:
  114.                 needs_refresh = True
  115.  
  116.     if needs_refresh:
  117.         try:
  118.             creds_path = Path.home() / ".claude" / ".credentials.json"
  119.             if creds_path.exists():
  120.                 creds = json.loads(creds_path.read_text())
  121.                 token = creds.get("claudeAiOauth", {}).get("accessToken", "")
  122.                 if token:
  123.                     req = urllib.request.Request(
  124.                         "https://api.anthropic.com/api/oauth/usage",
  125.                         headers={
  126.                             "Accept": "application/json",
  127.                             "Content-Type": "application/json",
  128.                             "Authorization": f"Bearer {token}",
  129.                             "anthropic-beta": "oauth-2025-04-20",
  130.                             "User-Agent": "claude-code/2.1.34",
  131.                         },
  132.                     )
  133.                     with urllib.request.urlopen(req, timeout=5) as resp:
  134.                         usage_data = json.loads(resp.read())
  135.                     with open(cache_file, "w") as f:
  136.                         json.dump(usage_data, f)
  137.         except Exception:
  138.             if os.path.exists(cache_file):
  139.                 try:
  140.                     with open(cache_file) as f:
  141.                         usage_data = json.load(f)
  142.                 except Exception:
  143.                     pass
  144.  
  145.     return usage_data
  146.  
  147.  
  148. WEATHER_EMOJI = {
  149.     "Sunny": "☀️",
  150.     "Clear": "☀️",
  151.     "Partly Cloudy": "⛅",
  152.     "Cloudy": "☁️",
  153.     "Overcast": "☁️",
  154.     "Mist": "🌫️",
  155.     "Fog": "🌫️",
  156.     "Freezing fog": "🌫️🧊",
  157.     "Patchy rain nearby": "💧",
  158.     "Patchy rain possible": "💧",
  159.     "Patchy light drizzle": "💧",
  160.     "Light drizzle": "💧",
  161.     "Patchy light rain": "💧",
  162.     "Light rain": "💧",
  163.     "Light rain shower": "💧",
  164.     "Moderate rain at times": "💧",
  165.     "Moderate rain": "💧",
  166.     "Heavy rain at times": "💧💧",
  167.     "Heavy rain": "💧💧",
  168.     "Moderate or heavy rain shower": "💧💧",
  169.     "Torrential rain shower": "💧💧",
  170.     "Freezing drizzle": "💧🧊",
  171.     "Heavy freezing drizzle": "💧💧🧊",
  172.     "Light freezing rain": "💧🧊",
  173.     "Moderate or heavy freezing rain": "💧💧🧊",
  174.     "Thundery outbreaks in nearby": "⚡💧",
  175.     "Patchy light rain in area with thunder": "⚡💧",
  176.     "Moderate or heavy rain in area with thunder": "⚡💧💧",
  177.     "Patchy snow nearby": "❄️",
  178.     "Patchy light snow": "❄️",
  179.     "Light snow": "❄️",
  180.     "Light snow showers": "❄️",
  181.     "Patchy moderate snow": "❄️",
  182.     "Moderate snow": "❄️",
  183.     "Patchy heavy snow": "❄️❄️",
  184.     "Heavy snow": "❄️❄️",
  185.     "Moderate or heavy snow showers": "❄️❄️",
  186.     "Blowing snow": "💨❄️",
  187.     "Blizzard": "💨❄️❄️",
  188.     "Patchy sleet nearby": "💧🧊",
  189.     "Light sleet": "💧🧊",
  190.     "Light sleet showers": "💧🧊",
  191.     "Moderate or heavy sleet": "💧💧🧊",
  192.     "Moderate or heavy sleet showers": "💧💧🧊",
  193.     "Ice pellets": "🧊",
  194.     "Light showers of ice pellets": "🧊",
  195.     "Moderate or heavy showers of ice pellets": "🧊🧊",
  196.     "Patchy freezing drizzle nearby": "💧🧊",
  197.     "Patchy light snow in area with thunder": "⚡❄️",
  198.     "Moderate or heavy snow in area with thunder": "⚡❄️❄️",
  199. }
  200.  
  201.  
  202. def fetch_weather():
  203.     """Return weather dict, cached for 30 minutes."""
  204.     cache_file = "/tmp/claude-statusline-weather-cache.json"
  205.     cache_max_age = 1800
  206.  
  207.     weather = None
  208.     needs_refresh = True
  209.  
  210.     if os.path.exists(cache_file):
  211.         age = time.time() - os.path.getmtime(cache_file)
  212.         if age < cache_max_age:
  213.             needs_refresh = False
  214.             try:
  215.                 with open(cache_file) as f:
  216.                     weather = json.load(f)
  217.             except Exception:
  218.                 needs_refresh = True
  219.  
  220.     if needs_refresh:
  221.         try:
  222.             req = urllib.request.Request(
  223.                 "https://wttr.in/?format=j1",
  224.                 headers={"User-Agent": "curl/7.68.0"},
  225.             )
  226.             with urllib.request.urlopen(req, timeout=5) as resp:
  227.                 raw = json.loads(resp.read())
  228.             cc = raw.get("current_condition", [{}])[0]
  229.             today = (raw.get("weather") or [{}])[0]
  230.             area = (raw.get("nearest_area") or [{}])[0]
  231.             weather = {
  232.                 "temp": cc.get("temp_F", ""),
  233.                 "humidity": cc.get("humidity", ""),
  234.                 "desc": (cc.get("weatherDesc") or [{}])[0].get("value", ""),
  235.                 "high": today.get("maxtempF", ""),
  236.                 "low": today.get("mintempF", ""),
  237.                 "lat": area.get("latitude", ""),
  238.                 "lon": area.get("longitude", ""),
  239.             }
  240.             with open(cache_file, "w") as f:
  241.                 json.dump(weather, f)
  242.         except Exception:
  243.             if os.path.exists(cache_file):
  244.                 try:
  245.                     with open(cache_file) as f:
  246.                         weather = json.load(f)
  247.                 except Exception:
  248.                     pass
  249.  
  250.     return weather
  251.  
  252.  
  253. def fetch_alerts(lat, lon):
  254.     """Return list of active alerts, cached for 15 minutes."""
  255.     cache_file = "/tmp/claude-statusline-alerts-cache.json"
  256.     cache_max_age = 900
  257.  
  258.     alerts = None
  259.     needs_refresh = True
  260.  
  261.     if os.path.exists(cache_file):
  262.         age = time.time() - os.path.getmtime(cache_file)
  263.         if age < cache_max_age:
  264.             needs_refresh = False
  265.             try:
  266.                 with open(cache_file) as f:
  267.                     alerts = json.load(f)
  268.             except Exception:
  269.                 needs_refresh = True
  270.  
  271.     if needs_refresh:
  272.         try:
  273.             req = urllib.request.Request(
  274.                 f"https://api.weather.gov/alerts/active?point={lat},{lon}",
  275.                 headers={
  276.                     "User-Agent": "claude-statusline/1.0",
  277.                     "Accept": "application/geo+json",
  278.                 },
  279.             )
  280.             with urllib.request.urlopen(req, timeout=5) as resp:
  281.                 raw = json.loads(resp.read())
  282.             alerts = []
  283.             for feat in raw.get("features") or []:
  284.                 props = feat.get("properties") or {}
  285.                 event = props.get("event", "")
  286.                 if event:
  287.                     alerts.append(event)
  288.             with open(cache_file, "w") as f:
  289.                 json.dump(alerts, f)
  290.         except Exception:
  291.             if os.path.exists(cache_file):
  292.                 try:
  293.                     with open(cache_file) as f:
  294.                         alerts = json.load(f)
  295.                 except Exception:
  296.                     pass
  297.  
  298.     return alerts or []
  299.  
  300.  
  301. def main():
  302.     try:
  303.         input_text = sys.stdin.read().strip()
  304.         if not input_text:
  305.             sys.stdout.write("Claude")
  306.             return
  307.  
  308.         data = json.loads(input_text)
  309.  
  310.         # --- Model name ---
  311.         model_name = (data.get("model") or {}).get("display_name") or "Claude"
  312.  
  313.         # --- Token calculations ---
  314.         cw = data.get("context_window") or {}
  315.         size = int(cw.get("context_window_size") or 0) or 200_000
  316.         usage = cw.get("current_usage") or {}
  317.         current = (
  318.             int(usage.get("input_tokens") or 0)
  319.             + int(usage.get("cache_creation_input_tokens") or 0)
  320.             + int(usage.get("cache_read_input_tokens") or 0)
  321.         )
  322.         pct_used = round((current / size) * 100) if size else 0
  323.         pct_remain = 100 - pct_used
  324.  
  325.         # --- Thinking status & effort level ---
  326.         thinking_on = False
  327.         effort_level = ""
  328.         settings_path = Path.home() / ".claude" / "settings.json"
  329.         if settings_path.exists():
  330.             try:
  331.                 settings = json.loads(settings_path.read_text())
  332.                 thinking_on = settings.get("alwaysThinkingEnabled", False) is True
  333.                 effort_level = settings.get("effortLevel", "")
  334.             except Exception:
  335.                 pass
  336.  
  337.         # Column widths (shared between all lines)
  338.         bw = 15       # bar width
  339.         c1w = 30      # column 1 visible width
  340.         c2w = 30      # column 2 visible width
  341.  
  342.         # ===== LINE 1 =====
  343.         thinking_str = rainbow("ultrathink") if thinking_on else f"{DKRED}dumb{RST}"
  344.         ctx_bar = build_bar(pct_used, bw)
  345.         l1_c1_vis = f"context: {'x' * bw} {pct_used}%"
  346.         l1_c1 = f"{WHITE}context:{RST} {ctx_bar} {CYAN}{pct_used}%{RST}"
  347.         l1_c1 = pad(l1_c1, len(l1_c1_vis), c1w)
  348.         l1_left = l1_c1 + SEP + f"{PURPLE}{size - current:,} left{RST}"
  349.         l1_right = f"{BLUE}{model_name}{RST} {thinking_str}"
  350.         model_col = c1w + 3 + 16 + 5  # col1 + sep + max datetime + gap
  351.         gap = model_col - vlen(l1_left)
  352.         line1 = l1_left + " " * max(gap, 1) + l1_right
  353.  
  354.         # ===== LINES 2 & 3 — usage limits =====
  355.         usage_data = fetch_usage()
  356.         line2 = ""
  357.         line3 = ""
  358.  
  359.         if usage_data:
  360.  
  361.             # --- 5-hour (current) ---
  362.             fh = usage_data.get("five_hour") or {}
  363.             fh_pct = round(float(fh.get("utilization") or 0))
  364.             fh_reset = format_reset(fh.get("resets_at"), "time")
  365.             fh_bar = build_bar(fh_pct, bw)
  366.  
  367.             # --- 5-hour (current) — line 2 ---
  368.             fh_c1_vis = f"current: {'x' * bw} {fh_pct}%"
  369.             fh_c1 = f"{WHITE}current:{RST} {fh_bar} {CYAN}{fh_pct}%{RST}"
  370.             fh_c1 = pad(fh_c1, len(fh_c1_vis), c1w)
  371.             fh_c2 = f"{GRAY}{fh_reset}{RST}"
  372.  
  373.             # --- 7-day (weekly) — line 3 ---
  374.             sd = usage_data.get("seven_day") or {}
  375.             sd_pct = round(float(sd.get("utilization") or 0))
  376.             sd_reset = format_reset(sd.get("resets_at"), "datetime")
  377.             sd_bar = build_bar(sd_pct, bw)
  378.  
  379.             sd_c1_vis = f"weekly:  {'x' * bw} {sd_pct}%"
  380.             sd_c1 = f"{WHITE}weekly:{RST}  {sd_bar} {CYAN}{sd_pct}%{RST}"
  381.             sd_c1 = pad(sd_c1, len(sd_c1_vis), c1w)
  382.             sd_c2 = f"{GRAY}{sd_reset}{RST}"
  383.  
  384.             # --- Extra usage (appended to line 3) ---
  385.             extra_str = ""
  386.             extra = usage_data.get("extra_usage") or {}
  387.             if extra.get("is_enabled"):
  388.                 ex_pct = round(float(extra.get("utilization") or 0))
  389.                 ex_used = round(float(extra.get("used_credits") or 0) / 100, 2)
  390.                 ex_limit = round(float(extra.get("monthly_limit") or 0) / 100, 2)
  391.                 ex_bar = build_bar(ex_pct, bw)
  392.                 extra_str = SEP + f"{WHITE}extra:{RST} {ex_bar} {CYAN}${ex_used}/${ex_limit}{RST}"
  393.  
  394.             l2_left = fh_c1 + SEP + fh_c2
  395.             effort_c = {"high": DKGREEN, "medium": DKYELLOW, "low": DKRED}.get(effort_level, GRAY)
  396.             effort_str = f"{effort_c}{effort_level} effort{RST}" if effort_level else ""
  397.             gap2 = model_col - vlen(l2_left)
  398.             line2 = l2_left + " " * max(gap2, 1) + effort_str
  399.             l3_left = sd_c1 + SEP + sd_c2 + extra_str
  400.             line3 = l3_left
  401.  
  402.         # ===== Weather & Alerts =====
  403.         # TODO: remove fake alerts after testing
  404.         # FAKE_ALERTS = ["Tropical Storm Warning", "Hurricane Watch", "Flood Advisory"]
  405.         FAKE_ALERTS = []
  406.         weather = fetch_weather()
  407.         weather_l3 = ""
  408.         alert_lines = []
  409.         if weather:
  410.             temp = weather.get("temp", "")
  411.             humidity = weather.get("humidity", "")
  412.             desc = weather.get("desc", "")
  413.             high = weather.get("high", "")
  414.             low = weather.get("low", "")
  415.             if temp:
  416.                 emoji = WEATHER_EMOJI.get(desc, "")
  417.                 hilo = f" ({low}°-{high}°)" if high and low else ""
  418.                 weather_l3 = f"{emoji} {WHITE}{temp}°F {humidity}%{hilo}{RST}"
  419.             lat = weather.get("lat", "")
  420.             lon = weather.get("lon", "")
  421.             if lat and lon:
  422.                 alerts = fetch_alerts(lat, lon)
  423.             else:
  424.                 alerts = []
  425.         else:
  426.             alerts = []
  427.         alerts += FAKE_ALERTS
  428.         seen = set()
  429.         alert_parts = []
  430.         for event in alerts:
  431.             if event in seen:
  432.                 continue
  433.             seen.add(event)
  434.             evt_lower = event.lower()
  435.             if "warning" in evt_lower:
  436.                 alert_parts.append(f"{WHITE}{BG_RED} {event} {RST}")
  437.             elif "watch" in evt_lower:
  438.                 alert_parts.append(f"{BLACK}{BG_YELLOW} {event} {RST}")
  439.             elif "advisory" in evt_lower or "alert" in evt_lower or "statement" in evt_lower:
  440.                 alert_parts.append(f"{BLACK}{BG_AZURE} {event} {RST}")
  441.  
  442.         # ===== Last line — current path =====
  443.         cwd = data.get("cwd") or ""
  444.  
  445.         if alert_parts:
  446.             sys.stdout.write(" ".join(alert_parts) + "\n")
  447.         sys.stdout.write(line1)
  448.         if line2:
  449.             sys.stdout.write(f"\n{line2}")
  450.         if line3:
  451.             if weather_l3:
  452.                 gap3 = model_col - vlen(line3)
  453.                 line3 = line3 + " " * max(gap3, 1) + weather_l3
  454.             sys.stdout.write(f"\n{line3}")
  455.         if cwd:
  456.             sys.stdout.write(f"\n{DKPEACH}{cwd}{RST}")
  457.  
  458.     except Exception as e:
  459.         sys.stdout.write(f"Claude | Error: {e}")
  460.  
  461.  
  462. if __name__ == "__main__":
  463.     main()
  464.  
Add Comment
Please, Sign In to add comment