#!/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