Files
fn_registry/bash/functions/browser/set_chrome_profile_appearance.sh
T
egutierrez 25054ff64e feat(browser): set_chrome_profile_appearance — avatar + color de perfiles Chrome
Nueva función Bash del dominio browser para personalizar la apariencia de un
perfil Chrome/Chromium y diferenciarlo de un vistazo. Edita
`profile.info_cache.<perfil>` en el Local State del user-data-dir:

- `--avatar <N>`: avatar built-in de Chrome (índice 0..55) vía
  `avatar_icon = chrome://theme/IDR_PROFILE_AVATAR_<N>`. Camino robusto.
- `--avatar <ruta.png>`: avatar custom best-effort (copia la imagen al perfil y
  marca `is_using_default_avatar=false`); ver gotchas del .md.
- `--color <#rrggbb>`: color del perfil. Convierte el hex a int32 con signo en
  formato ARGB (0xAARRGGBB) y lo aplica a `profile_highlight_color`,
  `profile_color_seed` y `default_avatar_fill_color`.

Sigue el patrón de create/delete_chrome_profile: backup del Local State antes de
escribir, validación del JSON resultante con restauración del backup si queda
inválido, guard de SingletonLock (chromium debe estar cerrado), idempotente y
con --dry-run. No crea perfiles (eso es create_chrome_profile); requiere que el
perfil ya exista. Probada con --avatar 26 --color #1f6feb y casos edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 09:57:12 +02:00

299 lines
13 KiB
Bash

#!/usr/bin/env bash
# set_chrome_profile_appearance — personaliza la apariencia visual de un perfil
# Chrome/Chromium existente: asigna un avatar built-in (índice 0..55) o una imagen
# PNG/JPG custom, y/o un color de acento (hex #rrggbb). Edita Local State.
set -euo pipefail
set_chrome_profile_appearance() {
# ── defaults ──────────────────────────────────────────────────────────────
local _udd=""
local _profile_dir=""
local _avatar=""
local _color=""
local _dry_run=0
# ── parse args ─────────────────────────────────────────────────────────────
_usage() {
cat >&2 <<'EOF'
Usage: set_chrome_profile_appearance --user-data-dir <dir> --profile <dir-name>
[--avatar <N|ruta.png>] [--color <#rrggbb>] [--dry-run]
--user-data-dir Raíz del user-data-dir de Chrome/Chromium (obligatorio).
--profile Nombre de la carpeta del perfil, ej: Default, Automation,
"Profile 1" (obligatorio). El perfil debe existir.
--avatar Índice entero 0..55 del avatar built-in de Chrome, o ruta a
un archivo PNG/JPG para avatar custom (opcional).
--color Color de acento del perfil en formato hex #rrggbb, con o sin
el '#' inicial (opcional).
--dry-run Describe las acciones sin modificar nada.
Al menos uno de --avatar o --color debe indicarse.
Exit codes:
0 éxito
1 error de argumento o validación
2 lock: hay un chromium corriendo con este user-data-dir
3 el perfil no existe en info_cache de Local State
4 error editando Local State (JSON inválido tras escritura)
EOF
return 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--user-data-dir) _udd="$2"; shift 2 ;;
--profile) _profile_dir="$2"; shift 2 ;;
--avatar) _avatar="$2"; shift 2 ;;
--color) _color="$2"; shift 2 ;;
--dry-run) _dry_run=1; shift ;;
-h|--help) _usage; return 0 ;;
*) echo "set_chrome_profile_appearance: argumento desconocido: $1" >&2; return 1 ;;
esac
done
# ── validaciones obligatorias ──────────────────────────────────────────────
if [[ -z "$_udd" ]]; then
echo "set_chrome_profile_appearance: --user-data-dir es obligatorio" >&2
return 1
fi
if [[ -z "$_profile_dir" ]]; then
echo "set_chrome_profile_appearance: --profile es obligatorio" >&2
return 1
fi
if [[ -z "$_avatar" && -z "$_color" ]]; then
echo "set_chrome_profile_appearance: al menos --avatar o --color debe indicarse" >&2
return 1
fi
# Expandir ~ en el user-data-dir
_udd="${_udd/#\~/$HOME}"
local _local_state="${_udd}/Local State"
# Verificar que user-data-dir y Local State existen
if [[ ! -d "$_udd" ]]; then
echo "set_chrome_profile_appearance: user-data-dir no encontrado: ${_udd}" >&2
return 1
fi
if [[ ! -f "$_local_state" ]]; then
echo "set_chrome_profile_appearance: Local State no encontrado: ${_local_state}" >&2
return 1
fi
# ── validar --avatar ──────────────────────────────────────────────────────
local _avatar_index=-1
local _avatar_image_path=""
if [[ -n "$_avatar" ]]; then
if [[ "$_avatar" =~ ^[0-9]+$ ]]; then
# Índice built-in
_avatar_index=$(( _avatar ))
if [[ $_avatar_index -lt 0 || $_avatar_index -gt 55 ]]; then
echo "set_chrome_profile_appearance: índice de avatar fuera de rango (0..55): ${_avatar}" >&2
return 1
fi
else
# Ruta a imagen custom
local _img_path="${_avatar/#\~/$HOME}"
if [[ ! -f "$_img_path" ]]; then
echo "set_chrome_profile_appearance: archivo de imagen no encontrado: ${_img_path}" >&2
return 1
fi
_avatar_image_path="$_img_path"
fi
fi
# ── validar --color ───────────────────────────────────────────────────────
local _color_hex=""
if [[ -n "$_color" ]]; then
_color_hex="${_color/#\#/}" # quitar # inicial si lo hay
if ! [[ "$_color_hex" =~ ^[0-9a-fA-F]{6}$ ]]; then
echo "set_chrome_profile_appearance: color hex inválido (espera rrggbb): ${_color}" >&2
return 1
fi
fi
# ── guard: ningún chromium debe tener ESTE user-data-dir abierto ──────────
# pgrep -x chromium lista solo procesos cuyo comm es exactamente "chromium",
# nunca grep/pgrep/bash. Así evitamos auto-matchear el propio script cuando
# el path del udd contiene "chromium" (p.ej. ~/.config/chromium-cdp).
if [[ $_dry_run -eq 0 ]]; then
local _p _busy=0
for _p in $(pgrep -x chromium 2>/dev/null); do
if tr '\0' ' ' < "/proc/$_p/cmdline" 2>/dev/null | grep -qF -- "$_udd"; then
_busy=1; break
fi
done
if [[ $_busy -eq 1 ]]; then
echo "set_chrome_profile_appearance: hay un chromium corriendo con este user-data-dir — ciérralo primero:" >&2
echo " pkill -TERM chromium" >&2
echo " (Chrome reescribe Local State al cerrar y pierde los cambios)" >&2
return 2
fi
fi
# ── verificar que el perfil existe en info_cache ──────────────────────────
if [[ $_dry_run -eq 0 ]]; then
local _profile_exists
_profile_exists="$(python3 -c "
import json, sys
data = json.load(open(sys.argv[1]))
ic = data.get('profile', {}).get('info_cache', {})
print('yes' if sys.argv[2] in ic else 'no')
" "$_local_state" "$_profile_dir" 2>/dev/null || echo "no")"
if [[ "$_profile_exists" != "yes" ]]; then
echo "set_chrome_profile_appearance: perfil '${_profile_dir}' no existe en info_cache de Local State" >&2
echo " Perfiles disponibles:" >&2
python3 -c "
import json, sys
data = json.load(open(sys.argv[1]))
ic = data.get('profile', {}).get('info_cache', {})
for k in ic: print(' ', k)
" "$_local_state" >&2 2>/dev/null || true
return 3
fi
fi
# ── modo dry-run ──────────────────────────────────────────────────────────
if [[ $_dry_run -eq 1 ]]; then
echo "=== set_chrome_profile_appearance DRY-RUN ===" >&2
echo " user-data-dir : ${_udd}" >&2
echo " profile : ${_profile_dir}" >&2
if [[ $_avatar_index -ge 0 ]]; then
echo " avatar : built-in #${_avatar_index} → avatar_icon=chrome://theme/IDR_PROFILE_AVATAR_${_avatar_index}" >&2
echo " is_using_default_avatar=true" >&2
elif [[ -n "$_avatar_image_path" ]]; then
local _dest_img="${_udd}/${_profile_dir}/Google Profile Picture.png"
echo " avatar : imagen custom ${_avatar_image_path}" >&2
echo " copiaría a ${_dest_img}" >&2
echo " is_using_default_avatar=false" >&2
echo " gaia_picture_file_name=Google Profile Picture.png" >&2
fi
if [[ -n "$_color_hex" ]]; then
local _signed_preview
_signed_preview="$(python3 -c "
rgb = int('${_color_hex}', 16)
argb = 0xFF000000 | rgb
signed = argb - 0x100000000 if argb >= 0x80000000 else argb
print(signed)
" 2>/dev/null || echo '?')"
echo " color : #${_color_hex} → signed int32 ${_signed_preview}" >&2
echo " profile_highlight_color, profile_color_seed, default_avatar_fill_color" >&2
fi
echo " archivo : ${_local_state}" >&2
printf '{"profile":"%s","avatar_applied":%s,"color_applied":%s,"dry_run":true}\n' \
"$_profile_dir" \
"$([[ -n "$_avatar" ]] && echo 'true' || echo 'false')" \
"$([[ -n "$_color_hex" ]] && echo 'true' || echo 'false')"
return 0
fi
# ── backup de Local State (no sobreescribir el del mismo día) ────────────
local _today
_today="$(date +%Y%m%d)"
local _backup="${_local_state}.bak.${_today}"
if [[ ! -f "$_backup" ]]; then
cp "$_local_state" "$_backup"
fi
# ── copiar imagen custom si es necesario ──────────────────────────────────
local _copy_image_done=false
if [[ -n "$_avatar_image_path" ]]; then
local _profile_path="${_udd}/${_profile_dir}"
mkdir -p "$_profile_path"
cp "$_avatar_image_path" "${_profile_path}/Google Profile Picture.png"
_copy_image_done=true
fi
# ── editar Local State con python3 ────────────────────────────────────────
if ! python3 - \
"$_local_state" \
"$_profile_dir" \
"${_avatar_index}" \
"${_avatar_image_path}" \
"${_color_hex}" <<'PY'; then
import sys, json
ls_path = sys.argv[1]
prof_dir = sys.argv[2]
avatar_index = int(sys.argv[3]) # -1 = no cambiar avatar
avatar_img = sys.argv[4] # "" = no usar imagen
color_hex = sys.argv[5] # "" = no cambiar color
with open(ls_path, "r", encoding="utf-8") as f:
data = json.load(f)
profile_section = data.setdefault("profile", {})
info_cache = profile_section.setdefault("info_cache", {})
# El perfil debe existir (ya validado en bash, pero doble check)
if prof_dir not in info_cache:
print(f"error: perfil '{prof_dir}' no existe en info_cache", file=sys.stderr)
sys.exit(1)
entry = info_cache[prof_dir]
# ── Avatar ────────────────────────────────────────────────────────────────────
if avatar_index >= 0:
# Avatar built-in: IDR_PROFILE_AVATAR_<N>
entry["avatar_icon"] = f"chrome://theme/IDR_PROFILE_AVATAR_{avatar_index}"
entry["is_using_default_avatar"] = True
elif avatar_img:
# Avatar custom imagen: Chrome necesita gaia_picture_file_name
entry["avatar_icon"] = "chrome://theme/IDR_PROFILE_AVATAR_0"
entry["is_using_default_avatar"] = False
entry["gaia_picture_file_name"] = "Google Profile Picture.png"
# ── Color ─────────────────────────────────────────────────────────────────────
if color_hex:
rgb = int(color_hex, 16) # 0xRRGGBB
argb = 0xFF000000 | rgb # alpha=FF opaco → 0xFFRRGGBB
# Convertir a int32 con signo (Python usa enteros arbitrarios)
signed = argb - 0x100000000 if argb >= 0x80000000 else argb
entry["profile_highlight_color"] = signed
entry["profile_color_seed"] = signed
entry["default_avatar_fill_color"] = signed
with open(ls_path, "w", encoding="utf-8") as f:
json.dump(data, f, separators=(",", ":"))
PY
echo "set_chrome_profile_appearance: error editando Local State con python3" >&2
return 4
fi
# ── validar JSON tras escritura ───────────────────────────────────────────
if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$_local_state" 2>/dev/null; then
echo "set_chrome_profile_appearance: JSON inválido tras escribir Local State; restaurando backup" >&2
cp "$_backup" "$_local_state"
return 4
fi
# ── leer valores resultantes para el JSON de salida ───────────────────────
local _result_json
_result_json="$(python3 - "$_local_state" "$_profile_dir" <<'PY'
import json, sys
data = json.load(open(sys.argv[1]))
entry = data.get("profile", {}).get("info_cache", {}).get(sys.argv[2], {})
out = {
"profile": sys.argv[2],
"avatar_icon": entry.get("avatar_icon", ""),
"is_using_default_avatar": entry.get("is_using_default_avatar", True),
"profile_highlight_color": entry.get("profile_highlight_color", 0),
"profile_color_seed": entry.get("profile_color_seed", 0),
"default_avatar_fill_color": entry.get("default_avatar_fill_color", 0),
"backup": "Local State.bak." + __import__("datetime").date.today().strftime("%Y%m%d"),
}
print(json.dumps(out, separators=(",",":")))
PY
)"
echo "$_result_json"
}
# ── auto-ejecución ────────────────────────────────────────────────────────────
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
set_chrome_profile_appearance "$@"
fi