feat(browser): set_chrome_profile_appearance v1.1.0 — color tiñe el tema del navegador

Antes --color solo escribía los campos de color en Local State (info_cache), que
únicamente tiñen el círculo del avatar en el selector de perfiles. Ahora --color
aplica además el tema del navegador (toolbar, frame/bordes, barra de pestañas y
omnibox), que es lo que permite identificar un perfil de un vistazo.

El tema vive en el Preferences del perfil, no en Local State. La función ahora
escribe browser.theme.user_color2 (SkColor ARGB con signo), browser_color_variant
y is_grayscale2, y fuerza extensions.theme.system_theme=0. Escribe también las
claves legacy sin sufijo "2" por compatibilidad de versiones. Nuevo flag
--variant <0..4> (default 3 vibrant) para la intensidad del tinte. Backup y
validación del Preferences con el mismo patrón que Local State.

Claves verificadas empíricamente con captura de pantalla en Chromium 148: un
perfil lanzado con estas claves muestra la toolbar y el frame teñidos del color.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 10:12:37 +02:00
parent 25054ff64e
commit e2c073b8b7
2 changed files with 180 additions and 49 deletions
@@ -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 <dir> --profile <dir-name>
[--avatar <N|ruta.png>] [--color <#rrggbb>] [--dry-run]
[--avatar <N|ruta.png>] [--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 <dir> --profile <dir-name>
--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
)"