#!/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 Y el # Preferences del perfil (browser.theme.* para teñir toolbar/frame/omnibox). set -euo pipefail set_chrome_profile_appearance() { # ── defaults ────────────────────────────────────────────────────────────── local _udd="" local _profile_dir="" local _avatar="" local _color="" local _variant=3 local _dry_run=0 # ── parse args ───────────────────────────────────────────────────────────── _usage() { cat >&2 <<'EOF' Usage: set_chrome_profile_appearance --user-data-dir --profile [--avatar ] [--color <#rrggbb>] [--variant <0..4>] [--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). Aplica el color tanto al círculo del avatar (Local State) como al tema del navegador (toolbar/frame/omnibox via Preferences del perfil). --variant Intensidad del tema de color: 0=system, 1=tonal_spot, 2=neutral, 3=vibrant (default), 4=expressive. Solo tiene efecto cuando se usa --color. --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 o Preferences (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 ;; --variant) _variant="$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 # Validar --variant if ! [[ "$_variant" =~ ^[0-4]$ ]]; then echo "set_chrome_profile_appearance: --variant debe ser un entero 0..4, recibido: ${_variant}" >&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 y Preferences 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 " Local State: profile_highlight_color, profile_color_seed, default_avatar_fill_color" >&2 echo " Preferences: browser.theme.user_color2=${_signed_preview}, browser_color_variant=${_variant}, is_grayscale2=false" >&2 echo " Preferences: extensions.theme.system_theme=0" >&2 local _prefs_path="${_udd}/${_profile_dir}/Preferences" echo " Preferences : ${_prefs_path}" >&2 fi echo " Local State : ${_local_state}" >&2 printf '{"profile":"%s","avatar_applied":%s,"color_applied":%s,"theme_applied":%s,"variant":%d,"dry_run":true}\n' \ "$_profile_dir" \ "$([[ -n "$_avatar" ]] && echo 'true' || echo 'false')" \ "$([[ -n "$_color_hex" ]] && echo 'true' || echo 'false')" \ "$([[ -n "$_color_hex" ]] && echo 'true' || echo 'false')" \ "$_variant" 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 de Local State 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 # ── editar Preferences del perfil (browser.theme.*) si hay color ───────── local _prefs_path="${_udd}/${_profile_dir}/Preferences" local _prefs_backup="" local _theme_applied=false if [[ -n "$_color_hex" ]]; then _theme_applied=true # Backup de Preferences antes de escribir (mismo patrón que Local State) if [[ -f "$_prefs_path" ]]; then _prefs_backup="${_prefs_path}.bak.${_today}" if [[ ! -f "$_prefs_backup" ]]; then cp "$_prefs_path" "$_prefs_backup" fi fi # Editar/crear Preferences con python3 if ! python3 - \ "$_prefs_path" \ "${_color_hex}" \ "${_variant}" <<'PY'; then import sys, json, os prefs_path = sys.argv[1] color_hex = sys.argv[2] variant = int(sys.argv[3]) # Calcular el signed int32 ARGB rgb = int(color_hex, 16) argb = 0xFF000000 | rgb signed = argb - 0x100000000 if argb >= 0x80000000 else argb # Cargar Preferences existente o arrancar desde vacío if os.path.isfile(prefs_path): with open(prefs_path, "r", encoding="utf-8") as f: data = json.load(f) else: data = {} # ── browser.theme.* ────────────────────────────────────────────────────────── browser = data.setdefault("browser", {}) theme = browser.setdefault("theme", {}) # Claves modernas (sufijo "2") — verificadas en Chromium 148 theme["user_color2"] = signed theme["browser_color_variant"] = variant theme["is_grayscale2"] = False # Claves legacy (sin sufijo "2") — compatibilidad con versiones anteriores theme["user_color"] = signed theme["color_variant"] = variant theme["is_grayscale"] = False # ── extensions.theme.system_theme = 0 ──────────────────────────────────────── # 0=color propio, 1=GTK, 2=Qt. Forzar 0 para que el user_color tenga efecto. extensions = data.setdefault("extensions", {}) ext_theme = extensions.setdefault("theme", {}) ext_theme["system_theme"] = 0 # Escribir directorio si no existe (perfil recién creado sin arrancar) os.makedirs(os.path.dirname(prefs_path), exist_ok=True) with open(prefs_path, "w", encoding="utf-8") as f: json.dump(data, f, separators=(",", ":")) PY echo "set_chrome_profile_appearance: error editando Preferences con python3" >&2 # Restaurar Preferences si teníamos backup if [[ -n "$_prefs_backup" && -f "$_prefs_backup" ]]; then cp "$_prefs_backup" "$_prefs_path" elif [[ -f "$_prefs_path" ]]; then rm -f "$_prefs_path" fi return 4 fi # Validar JSON de Preferences tras escritura if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$_prefs_path" 2>/dev/null; then echo "set_chrome_profile_appearance: JSON inválido tras escribir Preferences; restaurando backup" >&2 if [[ -n "$_prefs_backup" && -f "$_prefs_backup" ]]; then cp "$_prefs_backup" "$_prefs_path" fi return 4 fi fi # ── leer valores resultantes para el JSON de salida ─────────────────────── local _result_json _result_json="$(python3 - "$_local_state" "$_profile_dir" "$_prefs_path" "$_theme_applied" "$_variant" <<'PY' import json, sys, os ls_path = sys.argv[1] prof_dir = sys.argv[2] prefs_path = sys.argv[3] theme_applied = sys.argv[4] == "true" variant = int(sys.argv[5]) data = json.load(open(ls_path)) entry = data.get("profile", {}).get("info_cache", {}).get(prof_dir, {}) out = { "profile": prof_dir, "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), "theme_applied": theme_applied, "variant": variant, "preferences_path": prefs_path if theme_applied else "", "backup": "Local State.bak." + __import__("datetime").date.today().strftime("%Y%m%d"), } # Añadir valores de theme si se aplicó if theme_applied and os.path.isfile(prefs_path): try: prefs = json.load(open(prefs_path)) bt = prefs.get("browser", {}).get("theme", {}) out["browser_theme_user_color2"] = bt.get("user_color2", 0) out["browser_theme_color_variant"] = bt.get("browser_color_variant", 0) out["extensions_theme_system_theme"] = prefs.get("extensions", {}).get("theme", {}).get("system_theme", -1) except Exception: pass print(json.dumps(out, separators=(",",":"))) PY )" echo "$_result_json" } # ── auto-ejecución ──────────────────────────────────────────────────────────── if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then set_chrome_profile_appearance "$@" fi