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
)"