feat(canvas): revive Themelings color tracking (raw/semantic) for canvas branch

This commit is contained in:
lvwmwm 2026-08-28 13:36:10 -03:00
parent 3fa53ae111
commit 7bb5e3364c
6 changed files with 136 additions and 38 deletions

View file

@ -5,14 +5,12 @@
"": {
"name": "themelings",
"dependencies": {
"@napi-rs/canvas": "^0.1.97",
"color": "^5.0.3",
"color": "^5.0.2",
"discord-api-types": "^0.38.46",
},
"devDependencies": {
"@biomejs/biome": "^2.4.11",
"@types/bun": "^1.3.12",
"@types/color": "^4.2.1",
},
"peerDependencies": {
"typescript": "^6.0.2",
@ -41,38 +39,8 @@
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.11", "", { "os": "win32", "cpu": "x64" }, "sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A=="],
"@napi-rs/canvas": ["@napi-rs/canvas@0.1.97", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.97", "@napi-rs/canvas-darwin-arm64": "0.1.97", "@napi-rs/canvas-darwin-x64": "0.1.97", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.97", "@napi-rs/canvas-linux-arm64-gnu": "0.1.97", "@napi-rs/canvas-linux-arm64-musl": "0.1.97", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.97", "@napi-rs/canvas-linux-x64-gnu": "0.1.97", "@napi-rs/canvas-linux-x64-musl": "0.1.97", "@napi-rs/canvas-win32-arm64-msvc": "0.1.97", "@napi-rs/canvas-win32-x64-msvc": "0.1.97" } }, "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ=="],
"@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.97", "", { "os": "android", "cpu": "arm64" }, "sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ=="],
"@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.97", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw=="],
"@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.97", "", { "os": "darwin", "cpu": "x64" }, "sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA=="],
"@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.97", "", { "os": "linux", "cpu": "arm" }, "sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw=="],
"@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.97", "", { "os": "linux", "cpu": "arm64" }, "sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w=="],
"@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.97", "", { "os": "linux", "cpu": "arm64" }, "sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ=="],
"@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.97", "", { "os": "linux", "cpu": "none" }, "sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA=="],
"@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.97", "", { "os": "linux", "cpu": "x64" }, "sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg=="],
"@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.97", "", { "os": "linux", "cpu": "x64" }, "sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA=="],
"@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@0.1.97", "", { "os": "win32", "cpu": "arm64" }, "sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A=="],
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.97", "", { "os": "win32", "cpu": "x64" }, "sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ=="],
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
"@types/color": ["@types/color@4.2.1", "", { "dependencies": { "@types/color-convert": "2" } }, "sha512-ResWeDLy1vozIMbD6JLRKuNBbIcIlBkjTIxVHHd5Cqtm77T+ahH3BWE/PWv1OhFd1HAwcn8no4ig2uTaRXpYQQ=="],
"@types/color-convert": ["@types/color-convert@2.0.3", "", { "dependencies": { "@types/color-name": "*" } }, "sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg=="],
"@types/color-name": ["@types/color-name@1.1.4", "", {}, "sha512-hulKeREDdLFesGQjl96+4aoJSHY5b2GRjagzzcqCfIrWhe5vkCqIvrLbqzBaI1q94Vg8DNJZZqTR5ocdWmWclg=="],
"@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="],
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],

View file

@ -10,6 +10,7 @@
"format": "biome check --fix ."
},
"dependencies": {
"color": "^5.0.2",
"discord-api-types": "^0.38.46"
},
"devDependencies": {

View file

@ -16,6 +16,22 @@ export type CodeDiff =
size: number;
};
export type RawColors = Record<string, string>;
export type Icons = Record<
string,
{
file: string;
hash: string;
scales: number[];
width: number | null;
height: number | null;
}
>;
export type Semantic = Record<string, [string, { raw: string; opacity: number }]>;
export type SemanticColors = Record<string, Semantic>;
export type Differs = {
code: Map<string, CodeDiff> | undefined;
};

View file

@ -16,7 +16,7 @@ export async function fetchGitChanged() {
const gitQueue: Promise<void>[] = [];
export async function commit(files: string[], message: string) {
export async function commit(files: string[], message: string, cwd = "../data") {
while (gitQueue[0]) {
await gitQueue[0];
}
@ -27,15 +27,15 @@ export async function commit(files: string[], message: string) {
if (process.env.NODE_ENV === "test" && !commitAnyway) {
await fetchGitChanged();
} else {
await Bun.$`git restore --staged .`.cwd("../data").nothrow().quiet().then(handleShellErr);
await fetchGitChanged();
await Bun.$`git restore --staged .`.cwd(cwd).nothrow().quiet().then(handleShellErr);
if (cwd === "../data") await fetchGitChanged();
await Bun.$`git add ${{ raw: files.map((x) => Bun.$.escape(x)).join(" ") }}`
.cwd("../data")
.cwd(cwd)
.nothrow()
.quiet()
.then(handleShellErr);
await Bun.$`git commit -m ${message}`
.cwd("../data")
.cwd(cwd)
.nothrow()
.quiet()
.then((e) => void e);

View file

@ -5,6 +5,7 @@ import { commit } from "./git";
import { log, makeProgress, wrapPromise } from "./progress";
import { apksFolder, codePath, cuteVersion, isMock, isQuiet, oprevFiles, prevFiles, version } from "./shared";
import codeTask from "./tasks/code";
import colorsTask from "./tasks/colors";
import decompile from "./tasks/decompile";
import diffs from "./tasks/diffs";
import { formatError, handleShellErr, join } from "./utils";
@ -24,6 +25,9 @@ export async function runTasks() {
code_getting: "Finding file imports",
code_remaking: "Recreating code",
code_pushing: "Committing source",
colors: "Colors",
colors_getting: "Extracting colors",
colors_pushing: "Committing colors",
diff: "Diffs",
diff_code: "Diffing code",
},
@ -38,8 +42,12 @@ export async function runTasks() {
progress.start("preinit_discard");
await Bun.$`git reset --hard`.cwd("../data").nothrow().quiet().then(handleShellErr);
await Bun.write("../data/version.txt", version);
// canvas branch may not exist in test
await Bun.$`git reset --hard`.cwd("../canvas").nothrow().quiet().then(() => {});
await Bun.write("../canvas/version.txt", version).catch(() => {});
await Bun.$`git restore --staged .`.cwd("../data").nothrow().quiet().then(handleShellErr);
await Bun.$`git restore --staged .`.cwd("../canvas").nothrow().quiet().then(() => {});
progress.update("preinit_discard", true);
@ -71,6 +79,15 @@ export async function runTasks() {
if (progress.someFailed("code"))
throw new Error(`Failed at parser tasks!\n${progress.prettyErrors("code")}`);
// colors (canvas) - uses same code array
try {
await wrapPromise(colorsTask(code), progress, "colors");
} catch (e: any) {
progress.update("colors", false, formatError(e));
// don't fail whole run if colors fails, just log
console.error("Colors task failed:", formatError(e));
}
while (!progress.isFinished("decompile_gzip")) {
await Bun.sleep(1000);
}
@ -101,4 +118,8 @@ export async function runTasks() {
}
await commit(["version.txt"], `chore: bump app version to ${cuteVersion}`);
// canvas version bump (if canvas checkout exists)
try {
await commit(["version.txt"], `chore: bump app version to ${cuteVersion}`, "../canvas");
} catch {}
}

View file

@ -0,0 +1,92 @@
import { runInNewContext } from "node:vm";
import Color, { type ColorInstance } from "color";
import type { RawColors, SemanticColors } from "../../types";
import { commit } from "../git";
import { cuteVersion } from "../shared";
import { sortObj } from "../utils";
function hex(color: ColorInstance) {
return (color.alpha() === 1 ? color.hex() : color.hexa()).toLowerCase();
}
// Support both old python decompiler (rX[rY] bug) and new Rust
function fixHermesDecIndex(code: string) {
return code.replace(/r(\d+)\[(\d+)\] =/g, "r$1[r$2] =");
}
function findModuleSnippet(code: string[], importPath: string): string | null {
const text = code.join("\n");
const parts = text.split(/(?=\/\/ === Module \d+:)/);
for (const part of parts) if (part.includes(importPath)) return part;
return null;
}
function evalRawModule(snippet: string): any {
let code = snippet;
code = code.replace(/^import set from "set".*$/m, "const __importSet = { fileFinishedImporting: () => {} };");
code = code.replace(/^import.*$/gm, "");
code = code.replace(/set\.fileFinishedImporting/g, "__importSet.fileFinishedImporting");
code = code.replace(/export\s+const\s+_private/, "const _private");
code = code + "\n; _private;";
return runInNewContext(code, {});
}
function evalSemanticModule(snippet: string): any {
let code = snippet;
code = code.replace(/^import set from "set".*$/m, "const __importSet = { fileFinishedImporting: () => {} };");
code = code.replace(/^import items from "items".*$/m, "const items = { _private: { Themes: new Proxy({}, { get(_, k){ return k.toString().toLowerCase(); } }) } };");
code = code.replace(/^import.*$/gm, "");
code = code.replace(/set\.fileFinishedImporting/g, "__importSet.fileFinishedImporting");
code = code.replace(/export\s+const\s+_private/, "const _private");
code = code + "\n; _private;";
code = fixHermesDecIndex(code);
return runInNewContext(code, {});
}
export function getInternalRawColors(code: string[]) {
const snippet = findModuleSnippet(code, "raw-color-definitions.tsx");
if (!snippet) throw new Error("Cannot find raw-color-definitions module");
const mod = evalRawModule(snippet);
const raw: RawColors = {};
for (const [key, color] of Object.entries(mod.RawColors as Record<string, string>)) {
raw[key] = hex(Color(color as string));
}
return raw;
}
export function getInternalSemanticColors(code: string[], raw: Record<string, string>) {
const snippet = findModuleSnippet(code, "native/generated-definitions");
if (!snippet) throw new Error("Cannot find semantic definitions module");
const mod = evalSemanticModule(snippet);
const semantic: SemanticColors = {};
for (const [key, colors] of Object.entries(mod.SemanticColors as Record<string, Record<string, { raw: string; opacity: number }>>)) {
const color: SemanticColors[string] = {};
for (const [theme, val] of Object.entries(colors)) {
if (typeof val !== "object" || !("raw" in val)) continue;
color[theme] = [hex(Color(raw[val.raw]).alpha(val.opacity)), { raw: val.raw, opacity: val.opacity }];
}
semantic[key] = color;
}
return semantic;
}
export function convertSimpleSemantic(semantic: SemanticColors) {
const simpleSemantic: Record<string, Record<string, string>> = {};
for (const [key, colors] of Object.entries(semantic)) {
const color: Record<string, string> = {};
for (const [theme, [clr]] of Object.entries(colors)) color[theme] = clr;
simpleSemantic[key] = color;
}
return simpleSemantic;
}
export default async function colors(code: string[]) {
const raw = getInternalRawColors(code);
await Bun.write("../canvas/raw.json", JSON.stringify(sortObj(raw), null, 4));
const semantic = getInternalSemanticColors(code, raw);
await Bun.write("../canvas/semantic.json", JSON.stringify(sortObj(semantic), null, 4));
await Bun.write("../canvas/semantic_simple.json", JSON.stringify(sortObj(convertSimpleSemantic(semantic)), null, 4));
await commit(["raw.json", "semantic.json", "semantic_simple.json"], `chore: update colors for ${cuteVersion}`, "../canvas");
}