Skip to content

Instantly share code, notes, and snippets.

@0GiS0
Last active June 3, 2026 19:00
Show Gist options
  • Select an option

  • Save 0GiS0/645b62ccd33add7aff550d86cf60f95a to your computer and use it in GitHub Desktop.

Select an option

Save 0GiS0/645b62ccd33add7aff550d86cf60f95a to your computer and use it in GitHub Desktop.
Copilot CLI macOS statusline
{
"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
}
}
#!/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