25054ff64e
Nueva función Bash del dominio browser para personalizar la apariencia de un perfil Chrome/Chromium y diferenciarlo de un vistazo. Edita `profile.info_cache.<perfil>` en el Local State del user-data-dir: - `--avatar <N>`: avatar built-in de Chrome (índice 0..55) vía `avatar_icon = chrome://theme/IDR_PROFILE_AVATAR_<N>`. Camino robusto. - `--avatar <ruta.png>`: avatar custom best-effort (copia la imagen al perfil y marca `is_using_default_avatar=false`); ver gotchas del .md. - `--color <#rrggbb>`: color del perfil. Convierte el hex a int32 con signo en formato ARGB (0xAARRGGBB) y lo aplica a `profile_highlight_color`, `profile_color_seed` y `default_avatar_fill_color`. Sigue el patrón de create/delete_chrome_profile: backup del Local State antes de escribir, validación del JSON resultante con restauración del backup si queda inválido, guard de SingletonLock (chromium debe estar cerrado), idempotente y con --dry-run. No crea perfiles (eso es create_chrome_profile); requiere que el perfil ya exista. Probada con --avatar 26 --color #1f6feb y casos edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
299 lines
13 KiB
Bash
299 lines
13 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.
|
|
|
|
set -euo pipefail
|
|
|
|
set_chrome_profile_appearance() {
|
|
# ── defaults ──────────────────────────────────────────────────────────────
|
|
local _udd=""
|
|
local _profile_dir=""
|
|
local _avatar=""
|
|
local _color=""
|
|
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]
|
|
|
|
--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).
|
|
--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 (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 ;;
|
|
--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
|
|
|
|
# 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 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 " profile_highlight_color, profile_color_seed, default_avatar_fill_color" >&2
|
|
fi
|
|
echo " archivo : ${_local_state}" >&2
|
|
printf '{"profile":"%s","avatar_applied":%s,"color_applied":%s,"dry_run":true}\n' \
|
|
"$_profile_dir" \
|
|
"$([[ -n "$_avatar" ]] && echo 'true' || echo 'false')" \
|
|
"$([[ -n "$_color_hex" ]] && echo 'true' || echo 'false')"
|
|
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 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
|
|
|
|
# ── 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], {})
|
|
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),
|
|
"default_avatar_fill_color": entry.get("default_avatar_fill_color", 0),
|
|
"backup": "Local State.bak." + __import__("datetime").date.today().strftime("%Y%m%d"),
|
|
}
|
|
print(json.dumps(out, separators=(",",":")))
|
|
PY
|
|
)"
|
|
|
|
echo "$_result_json"
|
|
}
|
|
|
|
# ── auto-ejecución ────────────────────────────────────────────────────────────
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
set_chrome_profile_appearance "$@"
|
|
fi
|