Last active
June 3, 2026 19:00
-
-
Save 0GiS0/645b62ccd33add7aff550d86cf60f95a to your computer and use it in GitHub Desktop.
Copilot CLI macOS statusline
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "logLevel": "all", | |
| "footer": { | |
| "showModelEffort": true, | |
| "showDirectory": true, | |
| "showBranch": true, | |
| "showContextWindow": true, | |
| "showQuota": true, | |
| "showAiUsed": true, | |
| "showAgent": true, | |
| "showCodeChanges": true, | |
| "showUsername": true, | |
| "showSandbox": true, | |
| "showCustom": true | |
| }, | |
| "experimental": true, | |
| "statusLine": { | |
| "type": "command", | |
| "command": "~/.copilot/statusline.sh", | |
| "padding": 1 | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ───────────────────────────────────────────────────────── | |
| # Copilot CLI Statusline — Métricas del sistema macOS 🖥️ | |
| # Muestra: Batería, CPU, RAM, Temperatura CPU, Disco y Spotify | |
| # | |
| # Copilot CLI ejecuta este script en cada "render tick" | |
| # (cada vez que refresca la interfaz) y muestra por pantalla | |
| # lo que el script imprima por stdout. | |
| # ───────────────────────────────────────────────────────── | |
| # ── LOCALE ─────────────────────────────────────────────── | |
| # Forzamos locale C para que los números usen punto decimal | |
| # (no coma). Sin esto, en sistemas con locale español (es_ES), | |
| # awk/printf producen "34,5" en vez de "34.5" y los porcentajes | |
| # y formatos se rompen. | |
| export LC_ALL=C | |
| # ── PERSONALIZACIÓN ────────────────────────────────────── | |
| # TTL del cache en segundos. Las métricas del sistema se cachean | |
| # para evitar ejecutar top/vm_stat en cada tick (son lentos). | |
| # Spotify NO se cachea — se consulta siempre en tiempo real. | |
| # El tiempo meteorológico se cachea más tiempo (10 min) porque | |
| # no cambia con frecuencia y la petición HTTP es más lenta. | |
| SYSTEM_CACHE_TTL="${SYSTEM_CACHE_TTL:-5}" | |
| WEATHER_CACHE_TTL="${WEATHER_CACHE_TTL:-600}" | |
| STATUSLINE_WIDTH="${STATUSLINE_WIDTH:-160}" | |
| CACHE_FILE="${HOME}/.copilot/.system-statusline.cache" | |
| WEATHER_CACHE_FILE="${HOME}/.copilot/.weather-statusline.cache" | |
| # ───────────────────────────────────────────────────────── | |
| # ── SENTINEL anti-residuos ─────────────────────────────── | |
| # Copilot CLI hace `output.trim()` sobre lo que imprime este script. | |
| # Eso ELIMINA los espacios de relleno finales que usamos con | |
| # `printf "%-160s"` para limpiar restos de renders anteriores, así que | |
| # sin esto el relleno no sirve de nada y quedan caracteres residuales | |
| # (p.ej. el nombre de la canción anterior, o dígitos del CPU previo | |
| # que hacen que "19%" parezca ">100%"). | |
| # | |
| # U+200B (ZERO WIDTH SPACE) NO es whitespace para .trim(), así que al | |
| # ponerlo DESPUÉS del relleno, los espacios quedan "protegidos" y | |
| # sobreviven al trim. Es invisible (ancho cero) en el terminal. | |
| # Se construye con bytes octales para ser compatible con Bash 3.2 (macOS). | |
| SENTINEL=$(printf '\342\200\213') | |
| # Imprime la línea final rellenada a ancho fijo (constante entre renders) | |
| # y termina con el SENTINEL para que el relleno sobreviva al trim del CLI. | |
| render_line() { | |
| printf "%-*s%s\n" "$STATUSLINE_WIDTH" "$1" "$SENTINEL" | |
| } | |
| cache_is_fresh() { | |
| local file="$1" ttl="$2" now mod | |
| [[ -f "$file" ]] || return 1 | |
| now=$(date +%s) | |
| mod=$(stat -f %m "$file" 2>/dev/null) || return 1 | |
| (( now - mod < ttl )) | |
| } | |
| # Copilot CLI envía un JSON por stdin con info de la sesión. | |
| # No lo necesitamos, pero hay que drenarlo para que no bloquee. | |
| # Solo lo hacemos si stdin viene de un pipe (no al ejecutar manual). | |
| if [[ ! -t 0 ]]; then | |
| cat > /dev/null | |
| fi | |
| # ── Spotify ────────────────────────────────────────────── | |
| # Definida al principio del script porque se usa en dos sitios: | |
| # 1) En el path cacheado (para mostrar la canción actual en vivo) | |
| # 2) En el path fresco (cuando se recalculan todas las métricas) | |
| # Usa AppleScript (osascript) para consultar Spotify — no necesita | |
| # API keys ni tokens, funciona nativamente en macOS. | |
| get_spotify() { | |
| # Primero comprobamos si Spotify está corriendo, para no lanzarlo | |
| # accidentalmente (osascript abriría la app si no hacemos esta comprobación) | |
| local running | |
| running=$(osascript -e 'tell application "System Events" to (name of processes) contains "Spotify"' 2>/dev/null) | |
| [[ "$running" != "true" ]] && return 1 | |
| local info state track artist rest | |
| info=$(osascript <<'APPLESCRIPT' 2>/dev/null | |
| tell application "Spotify" | |
| set playbackState to player state as string | |
| if playbackState is not "playing" and playbackState is not "paused" then return "" | |
| return playbackState & linefeed & (name of current track) & linefeed & (artist of current track) | |
| end tell | |
| APPLESCRIPT | |
| ) || return 1 | |
| [[ -z "$info" ]] && return 1 | |
| state=${info%%$'\n'*} | |
| rest=${info#*$'\n'} | |
| track=${rest%%$'\n'*} | |
| artist=${rest#*$'\n'} | |
| [[ "$state" != "playing" && "$state" != "paused" ]] && return 1 | |
| [[ -z "$track" ]] && return 1 | |
| # Truncar nombres largos para que el statusline quepa en pantalla | |
| (( ${#track} > 25 )) && track="${track:0:22}..." | |
| (( ${#artist} > 20 )) && artist="${artist:0:17}..." | |
| # 🎵 si está sonando, ⏸️ si está en pausa | |
| local icon="🎵" | |
| [[ "$state" == "paused" ]] && icon="⏸️" | |
| echo "${icon} ${track} – ${artist}" | |
| } | |
| add_live_spotify() { | |
| local line="$1" spotify | |
| spotify=$(get_spotify 2>/dev/null) | |
| [[ -n "$spotify" ]] && line="${line} │ ${spotify}" | |
| echo "$line" | |
| } | |
| # ── Comprobación de cache ──────────────────────────────── | |
| # Si el cache existe y tiene menos de SYSTEM_CACHE_TTL segundos, | |
| # devolvemos las métricas cacheadas + Spotify en vivo. | |
| # Esto evita ejecutar top/vm_stat/osx-cpu-temp en cada tick. | |
| if cache_is_fresh "$CACHE_FILE" "$SYSTEM_CACHE_TTL"; then | |
| render_line "$(add_live_spotify "$(cat "$CACHE_FILE")")" | |
| exit 0 | |
| fi | |
| # ── Función auxiliar ───────────────────────────────────── | |
| # Si una métrica falla, mostramos "—" en su lugar | |
| safe() { echo "${1:-—}"; } | |
| # ── Batería ────────────────────────────────────────────── | |
| # Usa pmset para leer el estado de energía. | |
| # En un Mac de sobremesa (sin batería) muestra "🔌 AC". | |
| # En un portátil muestra el porcentaje + icono de carga. | |
| get_battery() { | |
| local raw | |
| raw=$(pmset -g batt 2>/dev/null) || { safe; return; } | |
| local pct | |
| pct=$(echo "$raw" | grep -oE '[0-9]+%' | head -1 | tr -d '%') | |
| # Mac de sobremesa o sin información de batería | |
| if [[ -z "$pct" ]]; then | |
| if echo "$raw" | grep -qi "AC Power"; then | |
| echo "🔌 AC" | |
| else | |
| safe | |
| fi | |
| return | |
| fi | |
| # Icono según nivel de batería | |
| local icon state_icon | |
| if (( pct >= 50 )); then icon="🔋" | |
| elif (( pct >= 20 )); then icon="🪫" | |
| else icon="🪫" | |
| fi | |
| # ⚡ si está cargando o conectado a corriente | |
| if echo "$raw" | grep -qi "charging\|AC Power"; then | |
| state_icon="⚡" | |
| else | |
| state_icon="" | |
| fi | |
| echo "${icon} ${pct}%${state_icon:+ ${state_icon}}" | |
| } | |
| # ── Uso de CPU ─────────────────────────────────────────── | |
| # Usa top con dos muestras (-l 2) y sin procesos (-n 0). | |
| # La primera muestra de top es imprecisa (mide desde el boot), | |
| # la segunda es un delta real de ~1s — mucho más fiel a lo que | |
| # muestra Monitor de Actividad. | |
| # Método: extraemos el % idle y restamos de 100 (más robusto | |
| # que intentar parsear user + sys por separado). | |
| # Indicadores de color: 🟢 < 50%, 🟡 50-80%, 🔴 > 80% | |
| get_cpu() { | |
| local cpu_line | |
| cpu_line=$(top -l 2 -n 0 2>/dev/null | grep "CPU usage" | tail -1) || { safe; return; } | |
| # Extraer el % idle (siempre es el último valor antes de "idle") | |
| local idle total | |
| idle=$(echo "$cpu_line" | awk '{for(i=1;i<=NF;i++) if($i=="idle") print $(i-1)}' | tr -d '%') | |
| [[ -z "$idle" ]] && { safe; return; } | |
| total=$(awk "BEGIN { t=100-${idle}; if(t<0) t=0; if(t>100) t=100; printf \"%.0f\", t }") | |
| local icon | |
| if (( total >= 80 )); then icon="🔴" | |
| elif (( total >= 50 )); then icon="🟡" | |
| else icon="🟢" | |
| fi | |
| # Ancho fijo (3 caracteres) en el porcentaje: " 9%", " 19%", "100%". | |
| # Mantiene constante el número de dígitos para que al pasar de "100%" | |
| # a "19%" no queden dígitos residuales del render anterior. | |
| printf '⚙️ %3d%% %s' "$total" "$icon" | |
| } | |
| # ── RAM ────────────────────────────────────────────────── | |
| # vm_stat devuelve páginas de memoria (no bytes directamente). | |
| # Multiplicamos por el tamaño de página (4096 bytes en macOS) | |
| # y sumamos: activa + wired + comprimida = RAM "en uso real". | |
| get_ram() { | |
| local total_bytes vm page_size stats active wired compressed | |
| total_bytes=$(sysctl -n hw.memsize 2>/dev/null) || { safe; return; } | |
| vm=$(vm_stat 2>/dev/null) || { safe; return; } | |
| page_size=$(echo "$vm" | awk '/page size of/ { for (i=1; i<=NF; i++) if ($i == "of") { print $(i+1); exit } }') | |
| [[ -z "$page_size" ]] && { safe; return; } | |
| stats=$(echo "$vm" | awk ' | |
| /Pages active/ { gsub(/\./, "", $NF); active=$NF } | |
| /Pages wired/ { gsub(/\./, "", $NF); wired=$NF } | |
| /Pages occupied by compressor/ { gsub(/\./, "", $NF); compressed=$NF } | |
| END { | |
| if (active == "") exit 1 | |
| printf "%d %d %d\n", active, wired, compressed | |
| } | |
| ') || { safe; return; } | |
| read -r active wired compressed <<< "$stats" | |
| [[ -z "$active" ]] && { safe; return; } | |
| wired=${wired:-0} | |
| compressed=${compressed:-0} | |
| # Convertir páginas a GB: (páginas * tamaño_página) / 1024^3 | |
| local used_gb total_gb | |
| used_gb=$(awk "BEGIN { printf \"%.1f\", (${active} + ${wired} + ${compressed}) * ${page_size} / 1073741824 }") | |
| total_gb=$(awk "BEGIN { printf \"%.0f\", ${total_bytes} / 1073741824 }") | |
| echo "🧠 ${used_gb}/${total_gb}GB" | |
| } | |
| # ── Tiempo meteorológico ───────────────────────────────── | |
| # Usa wttr.in para obtener la temperatura de tu ciudad. | |
| # No necesita API keys ni instalar nada — se auto-detecta por IP. | |
| # Se cachea 10 minutos porque el tiempo no cambia tan rápido | |
| # y la petición HTTP es más lenta que leer sensores locales. | |
| get_weather() { | |
| # Comprobar si hay cache válido del tiempo | |
| if cache_is_fresh "$WEATHER_CACHE_FILE" "$WEATHER_CACHE_TTL"; then | |
| cat "$WEATHER_CACHE_FILE" | |
| return | |
| fi | |
| # Consultar wttr.in en una sola petición (más rápido) | |
| # %c = emoji del tiempo (wttr.in lo elige según condición + día/noche) | |
| # %t = temperatura, %l = localización | |
| local raw | |
| raw=$(curl -s --max-time 3 "wttr.in/?format=%c|%t|%l" 2>/dev/null) | |
| [[ -z "$raw" ]] && { echo "🌡️ —"; return; } | |
| local icon temp city | |
| icon=$(echo "$raw" | cut -d'|' -f1 | tr -d ' ') | |
| temp=$(echo "$raw" | cut -d'|' -f2) | |
| city=$(echo "$raw" | cut -d'|' -f3 | sed 's/,.*//' | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}') | |
| [[ -z "$temp" ]] && { echo "🌡️ —"; return; } | |
| local result="${icon} ${temp} ${city}" | |
| # Guardar en cache del tiempo | |
| mkdir -p "$(dirname "$WEATHER_CACHE_FILE")" | |
| echo "$result" > "$WEATHER_CACHE_FILE" | |
| echo "$result" | |
| } | |
| # ── Disco ──────────────────────────────────────────────── | |
| # Espacio libre en el disco principal (/) | |
| get_disk() { | |
| local avail | |
| avail=$(df -h / 2>/dev/null | awk 'NR==2 { print $4 }') || { safe; return; } | |
| [[ -z "$avail" ]] && { safe; return; } | |
| echo "💽 ${avail} free" | |
| } | |
| # ── Construir el statusline ────────────────────────────── | |
| battery=$(get_battery) | |
| cpu=$(get_cpu) | |
| ram=$(get_ram) | |
| weather=$(get_weather) | |
| disk=$(get_disk) | |
| system_line="${battery} │ ${cpu} │ ${ram} │ ${disk} │ ${weather}" | |
| # Guardar en cache solo las métricas del sistema (sin Spotify) | |
| mkdir -p "$(dirname "$CACHE_FILE")" | |
| echo "$system_line" > "$CACHE_FILE" | |
| # Añadir Spotify en vivo (nunca cacheado — las canciones cambian) | |
| system_line=$(add_live_spotify "$system_line") | |
| # Rellenar a ancho fijo + SENTINEL invisible al final. El relleno limpia | |
| # los restos de renders anteriores (canción más larga, CPU anterior, etc.) | |
| # y el SENTINEL evita que Copilot CLI elimine ese relleno con su .trim(). | |
| render_line "$system_line" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment