From 3d2e127e9c913aec46c89199f975e79653b1c48d Mon Sep 17 00:00:00 2001 From: onempty0814 <3182459C@student.gla.ac.uk> Date: Fri, 24 Apr 2026 21:14:08 +0100 Subject: [PATCH 1/5] fix(themes): handle CC >=2.1.92 assembly format for theme options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In CC >=2.1.92 the theme options are no longer a single flat array [{label:"Dark mode",value:"dark"},…]. Instead only the "auto" option lives in one variable (e.g. HH=[{label:"Auto…",value:"auto"}]) and the remaining built-in themes are individual variables (DH, YH, PH…) that are spread together in a React-memo assembly expression: e=[...HH,DH,YH,PH,t,o,_H,...U.map(kB1),...FH] Two bugs result from the old code on the new format: 1. objArr replacement only swapped HH (the auto option), leaving all built-in theme variables (DH,YH,…) in the spread, so the selector showed tweakcc themes *plus* all built-in themes. 2. obj replacement always emitted `return{…}` as the prefix, turning the module-level variable assignment `eB1={…}` into an invalid return statement at the top level. Fix: - Detect new-assembly format when objArr has exactly 1 item and a matching assembly spread is found. In that case objArr.startIndex and endIndex span the prefix `[...HH,DH,…,themeN,` (everything up to the custom-themes spread `...U.map(…)`). writeThemes emits the tweakcc theme array without a closing `]` so the original suffix `...U.map(kB1),...FH]` completes the expression — preserving CC's own custom-theme and new-theme-creation entries. - Extract and preserve the original obj prefix (`return` or `VAR=`) so the replacement uses the correct syntax for both old and new CC. Closes #665 --- src/patches/themes.ts | 71 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/src/patches/themes.ts b/src/patches/themes.ts index 421a9f67..10cc3a81 100644 --- a/src/patches/themes.ts +++ b/src/patches/themes.ts @@ -3,11 +3,13 @@ import { Theme } from '../types'; import { LocationResult, showDiff } from './index'; -function getThemesLocation(oldFile: string): { +type ThemesLocation = { switchStatement: LocationResult; - objArr: LocationResult; - obj: LocationResult; -} | null { + objArr: LocationResult & { isAssemblyPrefix?: boolean }; + obj: LocationResult & { prefix: string }; +}; + +function getThemesLocation(oldFile: string): ThemesLocation | null { // === Switch Statement === // CC >=2.1.83: switch(A){case"light":return LX9;...default:return CX9} // CC <2.1.83: switch(A){case"light":return{...};...} @@ -70,7 +72,9 @@ function getThemesLocation(oldFile: string): { } // === Theme Options Array === - // Both old and new: [{label:"...",value:"..."}, ...] or [{"label":"...",...] + // Old format: [{label:"Dark mode",value:"dark"},{label:"Light mode",value:"light"},...] + // New format (CC >=2.1.92): HH=[{label:"Auto (match terminal)",value:"auto"}] only, + // with individual vars DH,YH,... spread in assembly: e=[...HH,DH,YH,...,...X.map(kB1),...FH] const objArrPat = /\[(?:\.\.\.\[\],)?(?:\{"?label"?:"(?:Dark|Light|Auto|Monochrome)[^"]*","?value"?:"[^"]+"\},?)+\]/; const objArrMatch = oldFile.match(objArrPat); @@ -80,8 +84,41 @@ function getThemesLocation(oldFile: string): { return null; } + // Check if new assembly format: objArr has only 1 item (just "auto" option) + let objArrLocation: LocationResult & { isAssemblyPrefix?: boolean } = { + startIndex: objArrMatch.index, + endIndex: objArrMatch.index + objArrMatch[0].length, + }; + const objArrItemCount = (objArrMatch[0].match(/"?value"?:/g) || []).length; + if (objArrItemCount === 1) { + // Find the variable name holding this single-item array (e.g. "HH") + const beforeObjArr = oldFile.slice( + Math.max(0, objArrMatch.index - 30), + objArrMatch.index + ); + const varNameMatch = beforeObjArr.match(/([A-Za-z_$][\w$]*)=$/); + if (varNameMatch) { + const autoVarName = varNameMatch[1].replace(/[$]/g, '\\$'); + // Find assembly: [...autoVar, theme1, theme2, ..., ...customThemes.map( + const assemblyPat = new RegExp( + `\\[\\.\\.\\.${autoVarName}(?:,[A-Za-z_$][\\w$]*){2,},\\.\\.\\.` + ); + const assemblyMatch = oldFile.match(assemblyPat); + if (assemblyMatch && assemblyMatch.index != undefined) { + // assemblyMatch[0] ends with ",..." — endIndex is right before "..." + // Replacement "[{theme},..." joins cleanly with remaining "...X.map(kB1),...FH]" + objArrLocation = { + startIndex: assemblyMatch.index, + endIndex: assemblyMatch.index + assemblyMatch[0].length - 3, + isAssemblyPrefix: true, + }; + } + } + } + // === Theme Name Mapping Object === - // {dark:"Dark mode",...} or {"dark":"Dark mode",...} + // Old: return{dark:"Dark mode",...} + // New (CC >=2.1.92): VAR={auto:"Auto...",dark:"Dark mode",...} const objPat = /(?:return|[$\w]+=)\{(?:"?(?:[$\w-]+)"?:"(?:Auto |Dark|Light|Monochrome)[^"]*",?)+\}/; const objMatch = oldFile.match(objPat); @@ -91,19 +128,20 @@ function getThemesLocation(oldFile: string): { return null; } + // Preserve the original prefix (either "return" or "VARNAME=") + const objPrefix = objMatch[0].slice(0, objMatch[0].indexOf('{')); + return { switchStatement: { startIndex: switchStart, endIndex: switchEnd, identifiers: [switchIdent], }, - objArr: { - startIndex: objArrMatch.index, - endIndex: objArrMatch.index + objArrMatch[0].length, - }, + objArr: objArrLocation, obj: { startIndex: objMatch.index, endIndex: objMatch.index + objMatch[0].length, + prefix: objPrefix, }, }; } @@ -126,8 +164,10 @@ export const writeThemes = ( // Process in reverse order to avoid index shifting // Update theme mapping object (obj) + // Preserve the original prefix ("return" or "VARNAME=") to avoid turning a + // module-level variable assignment into an invalid return statement. const obj = - 'return' + + locations.obj.prefix + JSON.stringify( Object.fromEntries(themes.map(theme => [theme.id, theme.name])) ); @@ -145,9 +185,16 @@ export const writeThemes = ( oldFile = newFile; // Update theme options array (objArr) - const objArr = JSON.stringify( + // In new assembly format (CC >=2.1.92), objArr points to the prefix of the + // spread expression "[...auto,theme1,...,themeN," (endIndex is just before + // the custom-themes spread "...U.map(...)"). We emit an open array without + // the closing "]" so the existing suffix "...U.map(kB1),...FH]" completes it. + const themeItems = JSON.stringify( themes.map(theme => ({ label: theme.name, value: theme.id })) ); + const objArr = locations.objArr.isAssemblyPrefix + ? themeItems.slice(0, -1) + ',' // "[{...}," — no closing ], suffix provides it + : themeItems; newFile = newFile.slice(0, locations.objArr.startIndex) + objArr + From 3d1715b11dce6814df7e38de03499cc42222649a Mon Sep 17 00:00:00 2001 From: onempty0814 <3182459C@student.gla.ac.uk> Date: Fri, 24 Apr 2026 21:22:39 +0100 Subject: [PATCH 2/5] refactor(themes): address CodeRabbit review comments - Use capture group in objPat to extract prefix instead of indexOf('{') - Count items with /\{"?label"?:/g to avoid false positives from label text - Return null with diagnostics when varNameMatch/assemblyMatch fail in single-item case, preventing silent fallback to incorrect HH-only patch --- src/patches/themes.ts | 45 ++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/patches/themes.ts b/src/patches/themes.ts index 10cc3a81..087f915c 100644 --- a/src/patches/themes.ts +++ b/src/patches/themes.ts @@ -89,7 +89,9 @@ function getThemesLocation(oldFile: string): ThemesLocation | null { startIndex: objArrMatch.index, endIndex: objArrMatch.index + objArrMatch[0].length, }; - const objArrItemCount = (objArrMatch[0].match(/"?value"?:/g) || []).length; + // Count items by object-opening label key to avoid false positives from + // label text that happens to contain the substring "value:". + const objArrItemCount = (objArrMatch[0].match(/\{"?label"?:/g) || []).length; if (objArrItemCount === 1) { // Find the variable name holding this single-item array (e.g. "HH") const beforeObjArr = oldFile.slice( @@ -97,30 +99,37 @@ function getThemesLocation(oldFile: string): ThemesLocation | null { objArrMatch.index ); const varNameMatch = beforeObjArr.match(/([A-Za-z_$][\w$]*)=$/); - if (varNameMatch) { - const autoVarName = varNameMatch[1].replace(/[$]/g, '\\$'); - // Find assembly: [...autoVar, theme1, theme2, ..., ...customThemes.map( - const assemblyPat = new RegExp( - `\\[\\.\\.\\.${autoVarName}(?:,[A-Za-z_$][\\w$]*){2,},\\.\\.\\.` + if (!varNameMatch) { + console.error('patch: themes: failed to find auto-option variable name'); + return null; + } + const autoVarName = varNameMatch[1].replace(/[$]/g, '\\$'); + // Find assembly: [...autoVar, theme1, theme2, ..., ...customThemes.map( + const assemblyPat = new RegExp( + `\\[\\.\\.\\.${autoVarName}(?:,[A-Za-z_$][\\w$]*){2,},\\.\\.\\.` + ); + const assemblyMatch = oldFile.match(assemblyPat); + if (!assemblyMatch || assemblyMatch.index == undefined) { + console.error( + `patch: themes: failed to find assembly spread for variable "${varNameMatch[1]}"`, ); - const assemblyMatch = oldFile.match(assemblyPat); - if (assemblyMatch && assemblyMatch.index != undefined) { - // assemblyMatch[0] ends with ",..." — endIndex is right before "..." - // Replacement "[{theme},..." joins cleanly with remaining "...X.map(kB1),...FH]" - objArrLocation = { - startIndex: assemblyMatch.index, - endIndex: assemblyMatch.index + assemblyMatch[0].length - 3, - isAssemblyPrefix: true, - }; - } + return null; } + // assemblyMatch[0] ends with ",..." — endIndex is right before "..." + // Replacement "[{theme},..." joins cleanly with remaining "...X.map(kB1),...FH]" + objArrLocation = { + startIndex: assemblyMatch.index, + endIndex: assemblyMatch.index + assemblyMatch[0].length - 3, + isAssemblyPrefix: true, + }; } // === Theme Name Mapping Object === // Old: return{dark:"Dark mode",...} // New (CC >=2.1.92): VAR={auto:"Auto...",dark:"Dark mode",...} + // Capture group 1 holds the prefix so we can preserve it in the replacement. const objPat = - /(?:return|[$\w]+=)\{(?:"?(?:[$\w-]+)"?:"(?:Auto |Dark|Light|Monochrome)[^"]*",?)+\}/; + /(return|[$\w]+=)\{(?:"?(?:[$\w-]+)"?:"(?:Auto |Dark|Light|Monochrome)[^"]*",?)+\}/; const objMatch = oldFile.match(objPat); if (!objMatch || objMatch.index == undefined) { @@ -129,7 +138,7 @@ function getThemesLocation(oldFile: string): ThemesLocation | null { } // Preserve the original prefix (either "return" or "VARNAME=") - const objPrefix = objMatch[0].slice(0, objMatch[0].indexOf('{')); + const objPrefix = objMatch[1]; return { switchStatement: { From 037d1249d310c168a83d6b1be41cc1ba8b3df660 Mon Sep 17 00:00:00 2001 From: onempty0814 <3182459C@student.gla.ac.uk> Date: Fri, 24 Apr 2026 21:35:58 +0100 Subject: [PATCH 3/5] style(themes): fix Prettier trailing comma in error message --- src/patches/themes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patches/themes.ts b/src/patches/themes.ts index 087f915c..7972d360 100644 --- a/src/patches/themes.ts +++ b/src/patches/themes.ts @@ -111,7 +111,7 @@ function getThemesLocation(oldFile: string): ThemesLocation | null { const assemblyMatch = oldFile.match(assemblyPat); if (!assemblyMatch || assemblyMatch.index == undefined) { console.error( - `patch: themes: failed to find assembly spread for variable "${varNameMatch[1]}"`, + `patch: themes: failed to find assembly spread for variable "${varNameMatch[1]}"` ); return null; } From 9b8a55d5462c0eefaa607152467ec9d2b35102c4 Mon Sep 17 00:00:00 2001 From: onempty0814 <3182459C@student.gla.ac.uk> Date: Fri, 24 Apr 2026 21:47:42 +0100 Subject: [PATCH 4/5] fix(themes): relax assemblyPat quantifier from {2,} to {1,} --- src/patches/themes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patches/themes.ts b/src/patches/themes.ts index 7972d360..02549b57 100644 --- a/src/patches/themes.ts +++ b/src/patches/themes.ts @@ -106,7 +106,7 @@ function getThemesLocation(oldFile: string): ThemesLocation | null { const autoVarName = varNameMatch[1].replace(/[$]/g, '\\$'); // Find assembly: [...autoVar, theme1, theme2, ..., ...customThemes.map( const assemblyPat = new RegExp( - `\\[\\.\\.\\.${autoVarName}(?:,[A-Za-z_$][\\w$]*){2,},\\.\\.\\.` + `\\[\\.\\.\\.${autoVarName}(?:,[A-Za-z_$][\\w$]*){1,},\\.\\.\\.` ); const assemblyMatch = oldFile.match(assemblyPat); if (!assemblyMatch || assemblyMatch.index == undefined) { From 4d507665a2095c2eadc0e2109a3cf8e21a63d326 Mon Sep 17 00:00:00 2001 From: onempty0814 <3182459C@student.gla.ac.uk> Date: Sun, 3 May 2026 13:53:40 +0100 Subject: [PATCH 5/5] docs(themes): add JSDoc to getThemesLocation and writeThemes --- src/patches/themes.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/patches/themes.ts b/src/patches/themes.ts index 02549b57..9be45ac1 100644 --- a/src/patches/themes.ts +++ b/src/patches/themes.ts @@ -9,6 +9,19 @@ type ThemesLocation = { obj: LocationResult & { prefix: string }; }; +/** + * Locates the three theme-related code sections in the CC bundle that need to + * be rewritten: the switch statement (colors per theme), the options array + * (dropdown items), and the name-mapping object (theme ID → display name). + * + * Handles two assembly formats: + * - CC <2.1.92: options array is a single inline `[{label,value},...]` literal. + * - CC >=2.1.92: "auto" option is its own variable; remaining built-ins are + * spread in an assembly expression `[...autoVar,t1,t2,...,...custom.map()]`. + * + * @param oldFile - The CC bundle source as a string + * @returns Locations of the three sections, or null if any section is not found + */ function getThemesLocation(oldFile: string): ThemesLocation | null { // === Switch Statement === // CC >=2.1.83: switch(A){case"light":return LX9;...default:return CX9} @@ -155,6 +168,17 @@ function getThemesLocation(oldFile: string): ThemesLocation | null { }; } +/** + * Rewrites the theme-related sections of the CC bundle to inject custom themes. + * + * Replaces the switch statement, options dropdown array, and name-mapping object + * so that all provided themes (including built-ins like dark/light) are available + * via `/theme`. Processes in reverse index order to avoid offset shifts. + * + * @param oldFile - The CC bundle source as a string + * @param themes - Full list of themes to inject (built-ins + custom) + * @returns The modified bundle, or null if any required section was not found + */ export const writeThemes = ( oldFile: string, themes: Theme[]