e2c073b8b7
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>
427 lines
19 KiB
Bash
427 lines
19 KiB
Bash
#!/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 <dir> --profile <dir-name>
|
|
[--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,
|
|
"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_<N>
|
|
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
|