diff --git a/bash/functions/browser/set_chrome_profile_appearance.md b/bash/functions/browser/set_chrome_profile_appearance.md index a9062d94..da585df5 100644 --- a/bash/functions/browser/set_chrome_profile_appearance.md +++ b/bash/functions/browser/set_chrome_profile_appearance.md @@ -3,10 +3,10 @@ name: set_chrome_profile_appearance kind: function lang: bash domain: browser -version: "1.0.0" +version: "1.1.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." +signature: "set_chrome_profile_appearance --user-data-dir --profile [--avatar ] [--color <#rrggbb>] [--variant <0..4>] [--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). Con --color aplica el tinte tanto al círculo del avatar en Local State (profile_highlight_color, profile_color_seed, default_avatar_fill_color) como al tema completo del navegador en el Preferences del perfil (browser.theme.user_color2, browser_color_variant, extensions.theme.system_theme), tiñendo toolbar, frame, barra de pestañas y omnibox. Requiere que Chromium esté cerrado sobre el user-data-dir. Hace backup de Local State y Preferences antes de escribir y valida el JSON resultante." tags: [navegator, chromium, profile, browser, cdp, scraping, appearance, avatar, color] uses_functions: [] uses_types: [] @@ -26,10 +26,12 @@ params: - 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." + desc: "Color de acento del perfil en hex #rrggbb, con o sin el '#' inicial. Se convierte a int32 con signo en formato ARGB 0xFFRRGGBB. Aplica el color en dos lugares: (1) Local State info_cache (profile_highlight_color, profile_color_seed, default_avatar_fill_color) para el círculo del avatar; (2) Preferences del perfil (browser.theme.user_color2 + browser_color_variant + extensions.theme.system_theme=0) para teñir toolbar, frame, barra de pestañas y omnibox. Opcional; al menos uno de --avatar o --color debe darse." + - name: --variant + desc: "Intensidad del tema de color aplicado al navegador (browser_color_variant). Entero 0..4: 0=system, 1=tonal_spot, 2=neutral, 3=vibrant (default), 4=expressive. Valores más altos dan tintes más saturados e identificables. Solo tiene efecto cuando se usa --color. Opcional." - 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." + desc: "Describe las acciones que se ejecutarían (campos a modificar en Local State y Preferences, conversión de color, ruta del Preferences) 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\":,\"theme_applied\":true|false,\"variant\":,\"preferences_path\":\"...\",\"browser_theme_user_color2\":,\"browser_theme_color_variant\":,\"extensions_theme_system_theme\":,\"backup\":\"Local State.bak.YYYYMMDD\"}. En dry-run: {\"profile\":\"...\",\"avatar_applied\":true|false,\"color_applied\":true|false,\"theme_applied\":true|false,\"variant\":,\"dry_run\":true}. Mensajes de diagnóstico a stderr. Exit 0 en éxito." --- ## Ejemplo @@ -37,54 +39,51 @@ output: "JSON en stdout con los campos resultantes del perfil: {\"profile\":\"/Local State` (global a todos los perfiles); los cambios de tema del navegador van en `//Preferences` (específico de cada perfil). La función hace backup de ambos archivos por separado antes de tocarlos. - **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`. +- **color es int32 con signo en ARGB**: Chrome almacena el color como entero con signo de 32 bits en formato `0xAARRGGBB`. Un color como `#16a34a` (verde) da ARGB `0xFF16A34A` → signed int32 `-15293622`. La función hace la conversión internamente; tú pasas siempre hex `#rrggbb`. +- **En modo oscuro del sistema el tinte sale más apagado**: en temas oscuros del sistema el color se mezcla con el fondo oscuro y queda menos saturado. Para compensar, usa `--variant 3` (vibrant, default) o `--variant 4` (expressive); valores bajos como 1 o 2 pueden resultar casi imperceptibles en modo oscuro. +- **`extensions.theme.system_theme` se fuerza a 0**: si el perfil usaba el tema GTK del sistema (`system_theme=1`), el GTK puede ignorar el `user_color`. Esta función lo fuerza a 0 (tema propio de Chrome) para que el `user_color2` tenga efecto. Si quieres devolver el perfil al tema del sistema, tendrás que resetear `system_theme` manualmente. - **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. +- **Backup diario**: se crea `Local State.bak.YYYYMMDD` y `Preferences.bak.YYYYMMDD` antes de cualquier escritura. Si ya existen los backups del día no se sobreescriben. Si el JSON resultante es inválido, se restaura automáticamente el backup correspondiente. ## Exit codes @@ -94,4 +93,8 @@ set_chrome_profile_appearance \ | 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) | +| 4 | Error editando Local State o Preferences (JSON inválido tras escritura, restaurado backup) | + +## Capability growth log + +v1.1.0 (2026-06-06) — --color ahora aplica también el tema del navegador (toolbar/frame/omnibox) escribiendo browser.theme.user_color2 + browser_color_variant en el Preferences del perfil, no solo el color del avatar en Local State. Nuevo flag --variant (0..4, default 3 vibrant). Verificado con captura en Chromium 148. diff --git a/bash/functions/browser/set_chrome_profile_appearance.sh b/bash/functions/browser/set_chrome_profile_appearance.sh index a07c485c..8c3f521e 100644 --- a/bash/functions/browser/set_chrome_profile_appearance.sh +++ b/bash/functions/browser/set_chrome_profile_appearance.sh @@ -1,7 +1,8 @@ #!/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. +# 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 @@ -11,13 +12,14 @@ set_chrome_profile_appearance() { 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>] [--dry-run] + [--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, @@ -25,7 +27,12 @@ Usage: set_chrome_profile_appearance --user-data-dir --profile --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). + 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. @@ -35,7 +42,7 @@ Exit codes: 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) + 4 error editando Local State o Preferences (JSON inválido tras escritura) EOF return 1 } @@ -46,6 +53,7 @@ EOF --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 ;; @@ -66,6 +74,12 @@ EOF 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}" @@ -128,7 +142,7 @@ EOF 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 + echo " (Chrome reescribe Local State y Preferences al cerrar y pierde los cambios)" >&2 return 2 fi fi @@ -179,13 +193,19 @@ 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 + 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 " archivo : ${_local_state}" >&2 - printf '{"profile":"%s","avatar_applied":%s,"color_applied":%s,"dry_run":true}\n' \ + 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')" \ + "$([[ -n "$_color_hex" ]] && echo 'true' || echo 'false')" \ + "$_variant" return 0 fi @@ -263,28 +283,136 @@ PY return 4 fi - # ── validar JSON tras escritura ─────────────────────────────────────────── + # ── 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" <<'PY' -import json, sys -data = json.load(open(sys.argv[1])) -entry = data.get("profile", {}).get("info_cache", {}).get(sys.argv[2], {}) + _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": 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), + "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 )"