fix(browser): apply clawd theme color
This commit is contained in:
@@ -121,6 +121,93 @@ function setDeep(obj: Record<string, unknown>, keys: string[], value: unknown) {
|
|||||||
node[keys[keys.length - 1] ?? ""] = value;
|
node[keys[keys.length - 1] ?? ""] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseHexRgbToSignedArgbInt(hex: string): number | null {
|
||||||
|
const cleaned = hex.trim().replace(/^#/, "");
|
||||||
|
if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) return null;
|
||||||
|
const rgb = Number.parseInt(cleaned, 16);
|
||||||
|
const argbUnsigned = (0xff << 24) | rgb;
|
||||||
|
// Chrome stores colors as signed 32-bit ints (SkColor).
|
||||||
|
return argbUnsigned > 0x7fffffff
|
||||||
|
? argbUnsigned - 0x1_0000_0000
|
||||||
|
: argbUnsigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isProfileDecorated(
|
||||||
|
userDataDir: string,
|
||||||
|
desiredName: string,
|
||||||
|
desiredColorHex: string,
|
||||||
|
): boolean {
|
||||||
|
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColorHex);
|
||||||
|
|
||||||
|
const localStatePath = path.join(userDataDir, "Local State");
|
||||||
|
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
||||||
|
|
||||||
|
const localState = safeReadJson(localStatePath);
|
||||||
|
const profile = localState?.profile;
|
||||||
|
const infoCache =
|
||||||
|
typeof profile === "object" && profile !== null && !Array.isArray(profile)
|
||||||
|
? (profile as Record<string, unknown>).info_cache
|
||||||
|
: null;
|
||||||
|
const info =
|
||||||
|
typeof infoCache === "object" &&
|
||||||
|
infoCache !== null &&
|
||||||
|
!Array.isArray(infoCache) &&
|
||||||
|
typeof (infoCache as Record<string, unknown>).Default === "object" &&
|
||||||
|
(infoCache as Record<string, unknown>).Default !== null &&
|
||||||
|
!Array.isArray((infoCache as Record<string, unknown>).Default)
|
||||||
|
? ((infoCache as Record<string, unknown>).Default as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const prefs = safeReadJson(preferencesPath);
|
||||||
|
const browserTheme = (() => {
|
||||||
|
const browser = prefs?.browser;
|
||||||
|
const theme =
|
||||||
|
typeof browser === "object" && browser !== null && !Array.isArray(browser)
|
||||||
|
? (browser as Record<string, unknown>).theme
|
||||||
|
: null;
|
||||||
|
return typeof theme === "object" && theme !== null && !Array.isArray(theme)
|
||||||
|
? (theme as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const autogeneratedTheme = (() => {
|
||||||
|
const autogenerated = prefs?.autogenerated;
|
||||||
|
const theme =
|
||||||
|
typeof autogenerated === "object" &&
|
||||||
|
autogenerated !== null &&
|
||||||
|
!Array.isArray(autogenerated)
|
||||||
|
? (autogenerated as Record<string, unknown>).theme
|
||||||
|
: null;
|
||||||
|
return typeof theme === "object" && theme !== null && !Array.isArray(theme)
|
||||||
|
? (theme as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const nameOk =
|
||||||
|
typeof info?.name === "string" ? info.name === desiredName : true;
|
||||||
|
|
||||||
|
if (desiredColorInt == null) {
|
||||||
|
// If the user provided a non-#RRGGBB value, we can only do best-effort.
|
||||||
|
return nameOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localSeedOk =
|
||||||
|
typeof info?.profile_color_seed === "number"
|
||||||
|
? info.profile_color_seed === desiredColorInt
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const prefOk =
|
||||||
|
(typeof browserTheme?.user_color2 === "number" &&
|
||||||
|
browserTheme.user_color2 === desiredColorInt) ||
|
||||||
|
(typeof autogeneratedTheme?.color === "number" &&
|
||||||
|
autogeneratedTheme.color === desiredColorInt);
|
||||||
|
|
||||||
|
return nameOk && localSeedOk && prefOk;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Best-effort profile decoration (name + lobster-orange). Chrome preference keys
|
* Best-effort profile decoration (name + lobster-orange). Chrome preference keys
|
||||||
* vary by version; we keep this conservative and idempotent.
|
* vary by version; we keep this conservative and idempotent.
|
||||||
@@ -133,6 +220,7 @@ export function decorateClawdProfile(
|
|||||||
const desiredColor = (
|
const desiredColor = (
|
||||||
opts?.color ?? DEFAULT_CLAWD_BROWSER_COLOR
|
opts?.color ?? DEFAULT_CLAWD_BROWSER_COLOR
|
||||||
).toUpperCase();
|
).toUpperCase();
|
||||||
|
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColor);
|
||||||
|
|
||||||
const localStatePath = path.join(userDataDir, "Local State");
|
const localStatePath = path.join(userDataDir, "Local State");
|
||||||
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
||||||
@@ -165,12 +253,41 @@ export function decorateClawdProfile(
|
|||||||
["profile", "info_cache", "Default", "user_color"],
|
["profile", "info_cache", "Default", "user_color"],
|
||||||
desiredColor,
|
desiredColor,
|
||||||
);
|
);
|
||||||
|
if (desiredColorInt != null) {
|
||||||
|
// These are the fields Chrome actually uses for profile/avatar tinting.
|
||||||
|
setDeep(
|
||||||
|
localState,
|
||||||
|
["profile", "info_cache", "Default", "profile_color_seed"],
|
||||||
|
desiredColorInt,
|
||||||
|
);
|
||||||
|
setDeep(
|
||||||
|
localState,
|
||||||
|
["profile", "info_cache", "Default", "profile_highlight_color"],
|
||||||
|
desiredColorInt,
|
||||||
|
);
|
||||||
|
setDeep(
|
||||||
|
localState,
|
||||||
|
["profile", "info_cache", "Default", "default_avatar_fill_color"],
|
||||||
|
desiredColorInt,
|
||||||
|
);
|
||||||
|
setDeep(
|
||||||
|
localState,
|
||||||
|
["profile", "info_cache", "Default", "default_avatar_stroke_color"],
|
||||||
|
desiredColorInt,
|
||||||
|
);
|
||||||
|
}
|
||||||
safeWriteJson(localStatePath, localState);
|
safeWriteJson(localStatePath, localState);
|
||||||
|
|
||||||
const prefs = safeReadJson(preferencesPath) ?? {};
|
const prefs = safeReadJson(preferencesPath) ?? {};
|
||||||
setDeep(prefs, ["profile", "name"], desiredName);
|
setDeep(prefs, ["profile", "name"], desiredName);
|
||||||
setDeep(prefs, ["profile", "profile_color"], desiredColor);
|
setDeep(prefs, ["profile", "profile_color"], desiredColor);
|
||||||
setDeep(prefs, ["profile", "user_color"], desiredColor);
|
setDeep(prefs, ["profile", "user_color"], desiredColor);
|
||||||
|
if (desiredColorInt != null) {
|
||||||
|
// Chrome refresh stores the autogenerated theme in these prefs (SkColor ints).
|
||||||
|
setDeep(prefs, ["autogenerated", "theme", "color"], desiredColorInt);
|
||||||
|
// User-selected browser theme color (pref name: browser.theme.user_color2).
|
||||||
|
setDeep(prefs, ["browser", "theme", "user_color2"], desiredColorInt);
|
||||||
|
}
|
||||||
safeWriteJson(preferencesPath, prefs);
|
safeWriteJson(preferencesPath, prefs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -218,8 +335,11 @@ export async function launchClawdChrome(
|
|||||||
const userDataDir = resolveClawdUserDataDir();
|
const userDataDir = resolveClawdUserDataDir();
|
||||||
fs.mkdirSync(userDataDir, { recursive: true });
|
fs.mkdirSync(userDataDir, { recursive: true });
|
||||||
|
|
||||||
const marker = decoratedMarkerPath(userDataDir);
|
const needsDecorate = !isProfileDecorated(
|
||||||
const needsDecorate = !exists(marker);
|
userDataDir,
|
||||||
|
DEFAULT_CLAWD_BROWSER_PROFILE_NAME,
|
||||||
|
(resolved.color ?? DEFAULT_CLAWD_BROWSER_COLOR).toUpperCase(),
|
||||||
|
);
|
||||||
|
|
||||||
// First launch to create preference files if missing, then decorate and relaunch.
|
// First launch to create preference files if missing, then decorate and relaunch.
|
||||||
const spawnOnce = () => {
|
const spawnOnce = () => {
|
||||||
@@ -255,23 +375,33 @@ export async function launchClawdChrome(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
let proc = spawnOnce();
|
|
||||||
|
|
||||||
// If this is the first run, let Chrome create prefs, then decorate + restart.
|
const localStatePath = path.join(userDataDir, "Local State");
|
||||||
if (needsDecorate) {
|
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
||||||
const deadline = Date.now() + 5000;
|
const needsBootstrap = !exists(localStatePath) || !exists(preferencesPath);
|
||||||
|
|
||||||
|
// If the profile doesn't exist yet, bootstrap it once so Chrome creates defaults.
|
||||||
|
// Then decorate (if needed) before the "real" run.
|
||||||
|
if (needsBootstrap) {
|
||||||
|
const bootstrap = spawnOnce();
|
||||||
|
const deadline = Date.now() + 10_000;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const localStatePath = path.join(userDataDir, "Local State");
|
|
||||||
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
||||||
if (exists(localStatePath) && exists(preferencesPath)) break;
|
if (exists(localStatePath) && exists(preferencesPath)) break;
|
||||||
await new Promise((r) => setTimeout(r, 100));
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
proc.kill("SIGTERM");
|
bootstrap.kill("SIGTERM");
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
const exitDeadline = Date.now() + 5000;
|
||||||
|
while (Date.now() < exitDeadline) {
|
||||||
|
if (bootstrap.exitCode != null) break;
|
||||||
|
await new Promise((r) => setTimeout(r, 50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsDecorate) {
|
||||||
try {
|
try {
|
||||||
decorateClawdProfile(userDataDir, { color: resolved.color });
|
decorateClawdProfile(userDataDir, { color: resolved.color });
|
||||||
logInfo(
|
logInfo(
|
||||||
@@ -284,9 +414,10 @@ export async function launchClawdChrome(
|
|||||||
runtime,
|
runtime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
proc = spawnOnce();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const proc = spawnOnce();
|
||||||
|
|
||||||
// Wait for CDP to come up.
|
// Wait for CDP to come up.
|
||||||
const readyDeadline = Date.now() + 15_000;
|
const readyDeadline = Date.now() + 15_000;
|
||||||
while (Date.now() < readyDeadline) {
|
while (Date.now() < readyDeadline) {
|
||||||
|
|||||||
Reference in New Issue
Block a user