From 7bb5e3364cd2c2c920c1d1773115468af27eedf7 Mon Sep 17 00:00:00 2001 From: lvwmwm Date: Fri, 28 Aug 2026 13:36:10 -0300 Subject: [PATCH] feat(canvas): revive Themelings color tracking (raw/semantic) for canvas branch --- bun.lock | 34 +------------- package.json | 1 + src/types.ts | 16 +++++++ src/update/git.ts | 10 ++--- src/update/runner.ts | 21 +++++++++ src/update/tasks/colors.ts | 92 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 136 insertions(+), 38 deletions(-) create mode 100644 src/update/tasks/colors.ts diff --git a/bun.lock b/bun.lock index c90719572ee..54a6ca9c533 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index a5bca90f287..ccfe4fcc6b7 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "format": "biome check --fix ." }, "dependencies": { + "color": "^5.0.2", "discord-api-types": "^0.38.46" }, "devDependencies": { diff --git a/src/types.ts b/src/types.ts index 34640cd5a1e..b0bac6187a5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,22 @@ export type CodeDiff = size: number; }; +export type RawColors = Record; + +export type Icons = Record< + string, + { + file: string; + hash: string; + scales: number[]; + width: number | null; + height: number | null; + } +>; + +export type Semantic = Record; +export type SemanticColors = Record; + export type Differs = { code: Map | undefined; }; diff --git a/src/update/git.ts b/src/update/git.ts index f3c4a083a9d..304afd0b8dd 100644 --- a/src/update/git.ts +++ b/src/update/git.ts @@ -16,7 +16,7 @@ export async function fetchGitChanged() { const gitQueue: Promise[] = []; -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); diff --git a/src/update/runner.ts b/src/update/runner.ts index 13f229740a9..037b32b9697 100644 --- a/src/update/runner.ts +++ b/src/update/runner.ts @@ -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 {} } diff --git a/src/update/tasks/colors.ts b/src/update/tasks/colors.ts new file mode 100644 index 00000000000..d23428cbb9c --- /dev/null +++ b/src/update/tasks/colors.ts @@ -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)) { + raw[key] = hex(Color(color as string)); + } + return raw; +} + +export function getInternalSemanticColors(code: string[], raw: Record) { + 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>)) { + 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> = {}; + for (const [key, colors] of Object.entries(semantic)) { + const color: Record = {}; + 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"); +}