diff --git a/bash/functions/browser/set_chrome_profile_appearance.md b/bash/functions/browser/set_chrome_profile_appearance.md new file mode 100644 index 00000000..a9062d94 --- /dev/null +++ b/bash/functions/browser/set_chrome_profile_appearance.md @@ -0,0 +1,97 @@ +--- +name: set_chrome_profile_appearance +kind: function +lang: bash +domain: browser +version: "1.0.0" +purity: impure +signature: "set_chrome_profile_appearance --user-data-dir --profile [--avatar ] [--color <#rrggbb>] [--dry-run]" +description: "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 los campos avatar_icon, is_using_default_avatar, profile_highlight_color, profile_color_seed y default_avatar_fill_color en profile.info_cache de Local State. Requiere que Chromium esté cerrado sobre el user-data-dir. Hace backup de Local State antes de escribir y valida el JSON resultante." +tags: [navegator, chromium, profile, browser, cdp, scraping, appearance, avatar, color] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/browser/set_chrome_profile_appearance.sh" +params: + - name: --user-data-dir + desc: "Raíz del user-data-dir de Chrome/Chromium donde vive el perfil. El directorio y Local State deben existir. Obligatorio." + - name: --profile + desc: "Nombre de la carpeta del perfil dentro de user-data-dir, por ejemplo: Default, Automation, \"Profile 1\". El perfil debe existir previamente en info_cache de Local State. Obligatorio." + - name: --avatar + desc: "Índice entero 0..55 del avatar built-in de Chrome (56 avatares: animales, objetos, personas) o ruta absoluta/relativa a un archivo PNG/JPG para avatar custom. Con índice: sets avatar_icon=IDR_PROFILE_AVATAR_ e is_using_default_avatar=true. Con imagen: copia el archivo al perfil como 'Google Profile Picture.png' y sets is_using_default_avatar=false. Opcional; al menos uno de --avatar o --color debe darse." + - name: --color + desc: "Color de acento del perfil en hex #rrggbb, con o sin el '#' inicial. Se convierte internamente a int32 con signo en formato ARGB 0xFFRRGGBB y se aplica a profile_highlight_color, profile_color_seed y default_avatar_fill_color. Opcional; al menos uno de --avatar o --color debe darse." + - name: --dry-run + desc: "Describe las acciones que se ejecutarían (campos a modificar, conversión de color) sin escribir nada ni verificar si Chromium está corriendo. Emite JSON de resultado con dry_run:true." +output: "JSON en stdout con los campos resultantes del perfil: {\"profile\":\"\",\"avatar_icon\":\"...\",\"is_using_default_avatar\":true|false,\"profile_highlight_color\":,\"profile_color_seed\":,\"default_avatar_fill_color\":,\"backup\":\"Local State.bak.YYYYMMDD\"}. En dry-run: {\"profile\":\"...\",\"avatar_applied\":true|false,\"color_applied\":true|false,\"dry_run\":true}. Mensajes de diagnóstico a stderr. Exit 0 en éxito." +--- + +## Ejemplo + +```bash +source $HOME/fn_registry/bash/functions/browser/set_chrome_profile_appearance.sh + +# Asignar avatar built-in #26 (oso panda) y color azul GitHub al perfil Automation +set_chrome_profile_appearance \ + --user-data-dir ~/.config/chromium-cdp \ + --profile Automation \ + --avatar 26 \ + --color "#1f6feb" +# Salida JSON con los valores aplicados + +# Solo cambiar color (sin tocar avatar) +set_chrome_profile_appearance \ + --user-data-dir ~/.config/chromium-cdp \ + --profile Default \ + --color "16a34a" + +# Solo cambiar avatar built-in +set_chrome_profile_appearance \ + --user-data-dir ~/.config/chromium-cdp \ + --profile "Profile 1" \ + --avatar 5 + +# Avatar custom desde imagen +set_chrome_profile_appearance \ + --user-data-dir ~/.config/chromium-cdp \ + --profile Personal \ + --avatar /tmp/foto.png \ + --color "#0ea5e9" + +# Dry-run: ver qué se aplicaría sin escribir +set_chrome_profile_appearance \ + --user-data-dir ~/.config/chromium-cdp \ + --profile Automation \ + --avatar 26 \ + --color "#1f6feb" \ + --dry-run +``` + +## Cuando usarla + +Úsala para diferenciar visualmente los perfiles de un user-data-dir de automatización — un color y avatar distintos por perfil hacen inmediata la identificación en el selector de Chrome. Ejecútala justo después de `create_chrome_profile` (con `--no-launch`) o como paso independiente de personalización batch antes de lanzar sesiones CDP. + +## Gotchas + +- **Chromium debe estar cerrado**: Chrome reescribe `Local State` completo desde memoria al cerrar; si se ejecuta mientras hay un proceso chromium vivo sobre el mismo user-data-dir, Chrome sobreescribirá los cambios al salir. La función detecta esto con `pgrep -x chromium` filtrando por cmdline y sale con exit 2 antes de modificar nada. Usa `pkill -TERM chromium` para cerrar y espera unos segundos. +- **El perfil debe existir en info_cache**: esta función personaliza perfiles existentes; no los crea. Usa `create_chrome_profile` primero (con `--no-launch` basta para que aparezca en Local State) y luego `set_chrome_profile_appearance`. +- **color es int32 con signo en ARGB**: Chrome almacena el color como entero con signo de 32 bits en formato `0xAARRGGBB`. Un color como `#1f6feb` (azul) da ARGB `0xFF1F6FEB` → signed int32 `-14713877`. La función hace la conversión internamente; tú pasas siempre hex `#rrggbb`. +- **Avatar custom (imagen) es best-effort**: el campo `gaia_picture_file_name` y `is_using_default_avatar=false` se aplican correctamente en Local State y la imagen se copia al directorio del perfil. Sin embargo, Chrome puede ignorar la foto de perfil en perfiles sin sesión Google activa (Chromium sin cuenta). El camino robusto y garantizado es usar el índice built-in (`--avatar 0..55`): 56 avatares (animales, objetos, personas) son más que suficientes para diferenciar perfiles de automatización. +- **Backup diario**: se crea `Local State.bak.YYYYMMDD` en el user-data-dir antes de cualquier escritura. Si ya existe el backup del día no se sobreescribe. Si el JSON resultante es inválido, se restaura automáticamente el backup. +- **is_using_default_avatar con índice built-in**: Chrome considera los avatares IDR_PROFILE_AVATAR_* como "avatares por defecto" del sistema, por eso `is_using_default_avatar` permanece `true` con índice numérico. Esto es correcto y es lo que Chrome haría internamente. + +## Exit codes + +| Código | Significado | +|--------|------------| +| 0 | Éxito | +| 1 | Argumento obligatorio faltante, rango inválido o archivo de imagen no encontrado | +| 2 | Lock: hay un chromium usando el mismo user-data-dir | +| 3 | El perfil no existe en info_cache de Local State | +| 4 | Error editando Local State (JSON inválido tras escritura, restaurado backup) | diff --git a/bash/functions/browser/set_chrome_profile_appearance.sh b/bash/functions/browser/set_chrome_profile_appearance.sh new file mode 100644 index 00000000..a07c485c --- /dev/null +++ b/bash/functions/browser/set_chrome_profile_appearance.sh @@ -0,0 +1,298 @@ +#!/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 --profile + [--avatar ] [--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_ + 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