From ef6ffa133073a83726b54c20f39229be1e9d4388 Mon Sep 17 00:00:00 2001 From: Nicholas Oh Date: Mon, 25 May 2026 21:30:52 +0800 Subject: [PATCH 1/2] Add Linux editor open destinations --- scripts/patches/main-process.js | 261 +++++++++++++++++++++++++++++++- 1 file changed, 258 insertions(+), 3 deletions(-) diff --git a/scripts/patches/main-process.js b/scripts/patches/main-process.js index cc05ef88..89c5eca7 100644 --- a/scripts/patches/main-process.js +++ b/scripts/patches/main-process.js @@ -10,7 +10,7 @@ const { } = require("./shared.js"); // Main-process patches adapt Electron shell behavior: windows, tray, menu, -// single-instance handling, file manager integration, and packaged runtime glue. +// single-instance handling, open destinations, and packaged runtime glue. function applyLinuxFileManagerPatch(currentSource) { const block = findCallBlock(currentSource, "id:`fileManager`"); if (block == null) { @@ -19,7 +19,7 @@ function applyLinuxFileManagerPatch(currentSource) { } if (block.text.includes("linux:{")) { - return currentSource; + return applyLinuxEditorOpenTargetsPatch(currentSource); } const electronVar = requireName(currentSource, "electron"); @@ -43,7 +43,7 @@ function applyLinuxFileManagerPatch(currentSource) { block.text.slice(0, insertionPoint + 1) + linuxFileManager + block.text.slice(insertionPoint + 1); - const patchedSource = + let patchedSource = currentSource.slice(0, block.start) + patchedBlock + currentSource.slice(block.end); const patchedBlockCheck = patchedSource.slice(block.start, block.start + patchedBlock.length); @@ -56,9 +56,264 @@ function applyLinuxFileManagerPatch(currentSource) { return currentSource; } + patchedSource = applyLinuxEditorOpenTargetsPatch(patchedSource); + return patchedSource; } +function applyLinuxEditorOpenTargetsPatch(currentSource) { + const fsVar = requireName(currentSource, "node:fs"); + const pathVar = requireName(currentSource, "node:path"); + if (fsVar == null || pathVar == null) { + return currentSource; + } + + const editorHelperNeedle = + "function vT({id:e,label:t,icon:n,darwinDetect:r,win32Detect:i,darwinEnv:a,darwinArgs:o,hidden:s}){return{id:e,platforms:{darwin:r?{label:t,icon:n,kind:`editor`,hidden:s,detect:r,env:a,args:o??yT,supportsSsh:!0}:void 0,win32:i?{label:t,icon:n,kind:`editor`,hidden:s,detect:i,args:yT,supportsSsh:!0}:void 0}}}"; + const editorHelperReplacement = + "function vT({id:e,label:t,icon:n,darwinDetect:r,win32Detect:i,linuxDetect:c,darwinEnv:a,darwinArgs:o,hidden:s}){return{id:e,platforms:{darwin:r?{label:t,icon:n,kind:`editor`,hidden:s,detect:r,env:a,args:o??yT,supportsSsh:!0}:void 0,win32:i?{label:t,icon:n,kind:`editor`,hidden:s,detect:i,args:yT,supportsSsh:!0}:void 0,linux:c?{label:t,icon:n,kind:`editor`,hidden:s,detect:c,args:(...e)=>__codexLinuxOpenArgs(c?.(),...e,yT),open:async({command:e,path:t,location:n})=>{await __codexLinuxOpenEditor(e,t,n,yT)},supportsSsh:!0}:void 0}}}"; + const oldEditorHelperReplacement = + "function vT({id:e,label:t,icon:n,darwinDetect:r,win32Detect:i,linuxDetect:c,darwinEnv:a,darwinArgs:o,hidden:s}){return{id:e,platforms:{darwin:r?{label:t,icon:n,kind:`editor`,hidden:s,detect:r,env:a,args:o??yT,supportsSsh:!0}:void 0,win32:i?{label:t,icon:n,kind:`editor`,hidden:s,detect:i,args:yT,supportsSsh:!0}:void 0,linux:c?{label:t,icon:n,kind:`editor`,hidden:s,detect:c,args:yT,supportsSsh:!0}:void 0}}}"; + const linuxCommandHelper = + `function __codexLinuxOpenCommand(__codexCommands,__codexDesktopEntries=[],__codexNames=[]){for(let __codexCommand of __codexCommands){let __codexFound=__codexLinuxExecutable(__codexCommand);if(__codexFound)return __codexFound}let __codexNeedles=new Set([...__codexCommands,...__codexDesktopEntries.map(__codexEntry=>__codexEntry.replace(/\\.desktop$/u,\`\`)),...__codexNames].map(__codexValue=>__codexValue.toLowerCase()));for(let __codexDir of __codexLinuxDesktopDirs())for(let __codexEntry of __codexDesktopEntries){let __codexFound=__codexLinuxDesktopCommand((0,${pathVar}.join)(__codexDir,__codexEntry),__codexNeedles);if(__codexFound)return __codexFound}for(let __codexDir of __codexLinuxDesktopDirs())for(let __codexFile of __codexLinuxDesktopFiles(__codexDir)){let __codexFound=__codexLinuxDesktopCommand(__codexFile,__codexNeedles);if(__codexFound)return __codexFound}return null}function __codexLinuxExecutable(__codexCommand){if(!__codexCommand)return null;if((0,${pathVar}.isAbsolute)(__codexCommand))return __codexLinuxExecutableFile(__codexCommand);for(let __codexDir of (process.env.PATH&&process.env.PATH.length>0?process.env.PATH:\`/usr/local/bin:/usr/bin:/bin\`).split(\`:\`)){if(!__codexDir||(0,${pathVar}.isAbsolute)(__codexDir)===!1)continue;let __codexFound=__codexLinuxExecutableFile((0,${pathVar}.join)(__codexDir,__codexCommand));if(__codexFound)return __codexFound}return null}function __codexLinuxExecutableFile(__codexFile){try{if((0,${fsVar}.existsSync)(__codexFile)&&(0,${fsVar}.statSync)(__codexFile).isFile()){(0,${fsVar}.accessSync)(__codexFile,${fsVar}.constants.X_OK);return __codexFile}}catch{}return null}function __codexLinuxOpenArgs(__codexCommand,__codexPath,__codexLocation,__codexArgs){return typeof __codexCommand===\`string\`&&__codexCommand.startsWith(\`codex-linux-desktop:\`)?[]:__codexArgs(__codexPath,__codexLocation)}async function __codexLinuxOpenEditor(__codexCommand,__codexPath,__codexLocation,__codexArgs){if(typeof __codexCommand===\`string\`&&__codexCommand.startsWith(\`codex-linux-desktop:\`)){await __codexLinuxLaunchDesktopEntry(__codexCommand.slice(20),__codexPath);return}await new Promise((__codexResolve,__codexReject)=>{let __codexDone=!1,__codexTimer,__codexChild;try{__codexChild=require(\`node:child_process\`).spawn(__codexCommand,__codexArgs(__codexPath,__codexLocation),{detached:!0,stdio:\`ignore\`,windowsHide:!0});__codexTimer=setTimeout(()=>{__codexDone=!0;__codexChild.unref?.();__codexResolve()},400),__codexTimer.unref?.(),__codexChild.on(\`error\`,__codexError=>{__codexDone||(clearTimeout(__codexTimer),__codexReject(__codexError))}),__codexChild.on(\`close\`,__codexCode=>{__codexDone||(clearTimeout(__codexTimer),__codexCode===0?__codexResolve():__codexReject(Error(\`Linux editor launch failed\`)))})}catch(__codexError){clearTimeout(__codexTimer),__codexReject(__codexError)}})}async function __codexLinuxLaunchDesktopEntry(__codexFile,__codexPath){let __codexGio=__codexLinuxExecutable(\`gio\`);if(__codexGio)try{await __codexLinuxOpenEditor(__codexGio,\`launch\`,null,()=>[\`launch\`,__codexFile,__codexPath]);return}catch{}let __codexGtk=__codexLinuxExecutable(\`gtk-launch\`);if(__codexGtk)try{await __codexLinuxOpenEditor(__codexGtk,__codexPath,null,__codexPath=>[__codexLinuxDesktopLaunchId(__codexFile),__codexLinuxPathToFileUri(__codexPath)]);return}catch{}throw Error(\`Linux desktop entry launch failed\`)}function __codexLinuxDesktopLaunchId(__codexFile){return(0,${pathVar}.basename)(__codexFile).replace(/\.desktop$/u,\`\`)}function __codexLinuxPathToFileUri(__codexPath){try{return require(\`node:url\`).pathToFileURL(__codexPath).toString()}catch{return __codexPath}}function __codexLinuxHomeDir(){let __codexHome=process.env.HOME;return typeof __codexHome===\`string\`&&__codexHome.length>0?__codexHome:\`/nonexistent\`}function __codexLinuxDesktopDirs(){let __codexHome=__codexLinuxHomeDir(),__codexDataHome=process.env.XDG_DATA_HOME&&(0,${pathVar}.isAbsolute)(process.env.XDG_DATA_HOME)?[process.env.XDG_DATA_HOME]:[(0,${pathVar}.join)(__codexHome,\`.local/share\`)],__codexDataDirs=(process.env.XDG_DATA_DIRS&&process.env.XDG_DATA_DIRS.length>0?process.env.XDG_DATA_DIRS:\`/usr/local/share:/usr/share\`).split(\`:\`).filter(Boolean),__codexRoots=[...__codexDataHome,...__codexDataDirs,(0,${pathVar}.join)(__codexHome,\`.local/share/flatpak/exports/share\`),\`/var/lib/flatpak/exports/share\`,\`/var/lib/snapd/desktop\`],__codexSeen=new Set;return __codexRoots.map(__codexRoot=>(0,${pathVar}.join)(__codexRoot,\`applications\`)).filter(__codexDir=>__codexDir&&(0,${pathVar}.isAbsolute)(__codexDir)&&!__codexSeen.has(__codexDir)&&(__codexSeen.add(__codexDir),!0))}function __codexLinuxDesktopFiles(__codexDir){let __codexCache=globalThis.__codexLinuxDesktopFilesCache??=new Map,__codexSignature=__codexLinuxFileSignature(__codexDir),__codexCached=__codexCache.get(__codexDir);if(__codexCached&&__codexCached.signature===__codexSignature)return __codexCached.files;let __codexFiles=[];try{for(let __codexEntry of (0,${fsVar}.readdirSync)(__codexDir,{withFileTypes:!0})){let __codexFile=(0,${pathVar}.join)(__codexDir,__codexEntry.name);(__codexEntry.isFile()||__codexEntry.isSymbolicLink())&&__codexEntry.name.endsWith(\`.desktop\`)&&__codexFiles.push(__codexFile)}}catch{}return __codexCache.set(__codexDir,{signature:__codexSignature,files:__codexFiles}),__codexFiles}function __codexLinuxFileSignature(__codexFile){try{let __codexStat=(0,${fsVar}.statSync)(__codexFile);return __codexStat.mtimeMs+":"+__codexStat.size}catch{return null}}function __codexLinuxDesktopCommand(__codexFile,__codexNeedles){try{if(!(0,${fsVar}.existsSync)(__codexFile))return null;let __codexEntry=__codexLinuxDesktopEntry(__codexFile);if(!__codexEntry||!__codexLinuxDesktopMatches(__codexEntry,__codexNeedles))return null;if(__codexEntry.TryExec&&!__codexLinuxDesktopTryExecAvailable(__codexEntry.TryExec))return null;return __codexLinuxDesktopExecCommand(__codexEntry,__codexNeedles)}catch{return null}}function __codexLinuxDesktopEntry(__codexFile){let __codexCache=globalThis.__codexLinuxDesktopEntryCache??=new Map,__codexSignature=__codexLinuxFileSignature(__codexFile),__codexCached=__codexCache.get(__codexFile);if(__codexCached&&__codexCached.signature===__codexSignature)return __codexCached.entry;let __codexEntry=__codexLinuxParseDesktopEntry(__codexFile);if(__codexEntry)__codexEntry.DesktopFile=__codexFile;return __codexCache.set(__codexFile,{signature:__codexSignature,entry:__codexEntry}),__codexEntry}function __codexLinuxParseDesktopEntry(__codexFile){let __codexEntry={Id:(0,${pathVar}.basename)(__codexFile).replace(/\\.desktop$/u,\`\`)},__codexSection=\`\`;for(let __codexLine of (0,${fsVar}.readFileSync)(__codexFile,\`utf8\`).split(/\\r?\\n/u)){let __codexTrimmed=__codexLine.trim();if(!__codexTrimmed||__codexTrimmed.startsWith(\`#\`))continue;if(__codexTrimmed.startsWith(\`[\`)&&__codexTrimmed.endsWith(\`]\`)){__codexSection=__codexTrimmed.slice(1,-1);continue}if(__codexSection&&__codexSection!==\`Desktop Entry\`)continue;let __codexEquals=__codexTrimmed.indexOf(\`=\`);if(__codexEquals<1)continue;let __codexKey=__codexTrimmed.slice(0,__codexEquals).replace(/\\[.*\\]$/u,\`\`),__codexValue=__codexTrimmed.slice(__codexEquals+1);__codexEntry[__codexKey]??=__codexValue}let __codexBool=__codexValue=>(__codexValue||\`\`).trim().toLowerCase()===\`true\`;return(__codexEntry.Type&&__codexEntry.Type!==\`Application\`)||__codexBool(__codexEntry.NoDisplay)||__codexBool(__codexEntry.Hidden)||!__codexEntry.Exec?null:__codexEntry}function __codexLinuxDesktopMatches(__codexEntry,__codexNeedles){let __codexValues=[__codexEntry.Id,__codexEntry.Name,__codexEntry.GenericName,__codexEntry.StartupWMClass,__codexEntry[\`X-AppImage-Name\`],...__codexLinuxDesktopExecTokens(__codexEntry.Exec)].filter(Boolean).map(__codexLinuxDesktopTokenName);return __codexValues.some(__codexValue=>__codexNeedles.has(__codexValue))}function __codexLinuxDesktopExecCommand(__codexEntry,__codexNeedles){let __codexTokens=__codexLinuxDesktopExecTokens(__codexEntry.Exec);for(let __codexToken of __codexTokens){if(__codexToken.includes(\`=\`)&&!__codexToken.startsWith(\`/\`))continue;if(!__codexNeedles.has(__codexLinuxDesktopTokenName(__codexToken)))continue;let __codexFound=__codexLinuxExecutable(__codexToken);if(__codexFound)return __codexFound}for(let __codexIndex=0;__codexIndex<__codexTokens.length-2;__codexIndex++)if((0,${pathVar}.basename)(__codexTokens[__codexIndex])===\`flatpak\`&&__codexTokens[__codexIndex+1]===\`run\`&&__codexEntry.DesktopFile)return \`codex-linux-desktop:\${__codexEntry.DesktopFile}\`;for(let __codexIndex=0;__codexIndex<__codexTokens.length-1;__codexIndex++)if((0,${pathVar}.basename)(__codexTokens[__codexIndex])===\`sh\`&&__codexTokens[__codexIndex+1]===\`-c\`){let __codexFound=__codexLinuxDesktopExecCommand({...__codexEntry,Exec:__codexTokens[__codexIndex+2]??\`\`},__codexNeedles);if(__codexFound)return __codexFound}return null}function __codexLinuxDesktopTryExecAvailable(__codexTryExec){let __codexTokens=__codexLinuxDesktopExecTokens(__codexTryExec),__codexHasCandidate=!1;for(let __codexIndex=0;__codexIndex<__codexTokens.length-1;__codexIndex++)if((0,${pathVar}.basename)(__codexTokens[__codexIndex])===\`sh\`&&__codexTokens[__codexIndex+1]===\`-c\`)return __codexLinuxDesktopTryExecAvailable(__codexTokens[__codexIndex+2]??\`\`);for(let __codexToken of __codexTokens){let __codexBase=(0,${pathVar}.basename)(__codexToken);if(__codexToken.includes(\`=\`)&&!__codexToken.startsWith(\`/\`))continue;if(__codexToken.startsWith(\`-\`)||__codexBase===\`env\`||__codexBase===\`sh\`)continue;__codexHasCandidate=!0;if(__codexLinuxExecutable(__codexToken))return !0}return !__codexHasCandidate}function __codexLinuxDesktopTokenName(__codexValue){return (0,${pathVar}.basename)(__codexValue).replace(/\\.desktop$/u,\`\`).replace(/\\.(appimage|sh|bin)$/iu,\`\`).toLowerCase()}function __codexLinuxDesktopExecTokens(__codexExec){if(!__codexExec)return[];let __codexTokens=[],__codexToken=\`\`,__codexQuote=null,__codexEscaped=!1;for(let __codexIndex=0;__codexIndex<__codexExec.length;__codexIndex++){let __codexChar=__codexExec[__codexIndex];if(__codexEscaped){__codexToken+=__codexChar;__codexEscaped=!1;continue}if(__codexChar===\`\\\\\`){__codexEscaped=!0;continue}if(__codexQuote){__codexChar===__codexQuote?__codexQuote=null:__codexToken+=__codexChar;continue}if(__codexChar===\`"\`||__codexChar===\`'\`){__codexQuote=__codexChar;continue}if(/\\s/u.test(__codexChar)){__codexToken&&(__codexTokens.push(__codexToken),__codexToken=\`\`);continue}__codexToken+=__codexChar}return __codexToken&&__codexTokens.push(__codexToken),__codexTokens.filter(__codexToken=>!/^%[fFuUdDnNickvm]$/u.test(__codexToken))}`; + + let patchedSource = replaceNeedleOnce( + currentSource, + editorHelperNeedle, + editorHelperReplacement + linuxCommandHelper, + ); + patchedSource = patchedSource.replace(oldEditorHelperReplacement, editorHelperReplacement); + if (!patchedSource.includes(editorHelperReplacement)) { + return currentSource; + } + patchedSource = replaceLinuxCommandHelper(patchedSource, linuxCommandHelper); + if (!patchedSource.includes(linuxCommandHelper)) { + patchedSource = patchedSource.replace(editorHelperReplacement, editorHelperReplacement + linuxCommandHelper); + } + + const helperTargets = [ + ["antigravity", ["antigravity"], ["antigravity.desktop"], ["Antigravity"]], + ["cursor", ["cursor"], ["cursor.desktop"], ["Cursor"]], + ["vscode", ["code"], ["code.desktop", "visual-studio-code.desktop", "com.visualstudio.code.desktop"], ["Code", "Visual Studio Code", "VS Code"]], + [ + "vscodeInsiders", + ["code-insiders"], + [ + "code-insiders.desktop", + "visual-studio-code-insiders.desktop", + "com.visualstudio.code.insiders.desktop", + ], + ["Code - Insiders", "Visual Studio Code - Insiders", "VS Code Insiders"], + ], + ["windsurf", ["windsurf"], ["windsurf.desktop", "com.exafunction.windsurf.desktop"], ["Windsurf"]], + ]; + + for (const [id, commands, desktopEntries, desktopNames] of helperTargets) { + patchedSource = addLinuxDetectToHelperOpenTarget(patchedSource, id, commands, desktopEntries, desktopNames); + } + + return addLinuxZedOpenTarget(patchedSource); +} + +function replaceNeedleOnce(currentSource, needle, replacement) { + if (currentSource.includes(replacement) || !currentSource.includes(needle)) { + return currentSource; + } + + return currentSource.replace(needle, replacement); +} + +function replaceLinuxCommandHelper(currentSource, replacement) { + const start = currentSource.indexOf("function __codexLinuxOpenCommand("); + if (start === -1) { + return currentSource; + } + + const helperNames = [ + "__codexLinuxOpenCommand", + "__codexLinuxExecutable", + "__codexLinuxExecutableFile", + "__codexLinuxHomeDir", + "__codexLinuxOpenArgs", + "__codexLinuxOpenEditor", + "__codexLinuxLaunchDesktopEntry", + "__codexLinuxDesktopLaunchId", + "__codexLinuxPathToFileUri", + "__codexLinuxDesktopDirs", + "__codexLinuxDesktopFiles", + "__codexLinuxFileSignature", + "__codexLinuxDesktopCommand", + "__codexLinuxDesktopEntry", + "__codexLinuxParseDesktopEntry", + "__codexLinuxDesktopMatches", + "__codexLinuxDesktopExecCommand", + "__codexLinuxDesktopTryExecAvailable", + "__codexLinuxDesktopTokenName", + "__codexLinuxDesktopExecTokens", + ]; + let end = start; + for (;;) { + const nextStart = skipHelperSeparator(currentSource, end); + const nextName = helperNames.find( + (name) => + currentSource.startsWith(`function ${name}(`, nextStart) || + currentSource.startsWith(`async function ${name}(`, nextStart), + ); + if (nextName == null) { + break; + } + const braceStart = currentSource.indexOf("{", nextStart); + const braceEnd = braceStart === -1 ? -1 : findMatchingBrace(currentSource, braceStart); + if (braceEnd === -1) { + return currentSource; + } + end = braceEnd + 1; + } + + return currentSource.slice(0, start) + replacement + currentSource.slice(end); +} + +function skipHelperSeparator(source, index) { + let cursor = index; + while (cursor < source.length && /[\s;]/u.test(source[cursor])) { + cursor += 1; + } + return cursor; +} + +function addLinuxDetectToHelperOpenTarget(currentSource, id, commands, desktopEntries, desktopNames) { + const block = findHelperOpenTargetOptionsBlock(currentSource, id); + if (block == null) { + return currentSource; + } + + const linuxDetect = `linuxDetect:()=>__codexLinuxOpenCommand(${javascriptArray(commands)},${javascriptArray(desktopEntries)},${javascriptArray(desktopNames)})`; + if (block.text.includes("linuxDetect:")) { + const patchedBlock = block.text.replace( + /linuxDetect:\(\)=>__codexLinuxOpenCommand\([^)]*\)/, + linuxDetect, + ); + if (patchedBlock === block.text) { + return currentSource; + } + return currentSource.slice(0, block.start) + patchedBlock + currentSource.slice(block.end); + } + + const insertionPoint = block.text.lastIndexOf("}"); + if (insertionPoint === -1) { + return currentSource; + } + + const patchedBlock = + block.text.slice(0, insertionPoint) + `,${linuxDetect}` + block.text.slice(insertionPoint); + return currentSource.slice(0, block.start) + patchedBlock + currentSource.slice(block.end); +} + +function findHelperOpenTargetOptionsBlock(source, id) { + const markerStart = source.indexOf(`id:\`${id}\``); + if (markerStart === -1) { + return null; + } + + const helperCallStart = source.lastIndexOf("vT(", markerStart); + const objectStart = helperCallStart === -1 ? -1 : source.indexOf("{", helperCallStart); + const objectEnd = objectStart === -1 ? -1 : findMatchingBrace(source, objectStart); + if ( + helperCallStart === -1 || + objectStart === -1 || + objectEnd === -1 || + objectStart > markerStart || + objectEnd < markerStart + ) { + return null; + } + + return { + start: objectStart, + end: objectEnd + 1, + text: source.slice(objectStart, objectEnd + 1), + }; +} + +function addLinuxZedOpenTarget(currentSource) { + const block = findOpenTargetDeclarationBlock(currentSource, "zed"); + if (block == null) { + return currentSource; + } + + const argsFn = block.text.match(/\bargs:([A-Za-z_$][\w$]*)/)?.[1]; + if (block.text.includes("linux:{")) { + if (argsFn == null || block.text.includes("__codexLinuxOpenEditor")) { + return currentSource; + } + + const linuxStart = block.text.indexOf("linux:{"); + const linuxObjectStart = block.text.indexOf("{", linuxStart); + const linuxObjectEnd = + linuxObjectStart === -1 ? -1 : findMatchingBrace(block.text, linuxObjectStart); + if (linuxObjectEnd === -1) { + return currentSource; + } + + const patchedBlock = + block.text.slice(0, linuxStart) + + zedLinuxOpenTargetBlock(argsFn) + + block.text.slice(linuxObjectEnd + 1); + return currentSource.slice(0, block.start) + patchedBlock + currentSource.slice(block.end); + } + + const insertionPoint = block.text.lastIndexOf("}}};"); + if (argsFn == null || insertionPoint === -1) { + return currentSource; + } + + const patchedBlock = + block.text.slice(0, insertionPoint + 1) + + `,${zedLinuxOpenTargetBlock(argsFn)}` + + block.text.slice(insertionPoint + 1); + return currentSource.slice(0, block.start) + patchedBlock + currentSource.slice(block.end); +} + +function zedLinuxOpenTargetBlock(argsFn) { + return ( + `linux:{label:\`Zed\`,icon:\`apps/zed.png\`,kind:\`editor\`,detect:()=>__codexLinuxOpenCommand([` + + "`zed`,`zeditor`,`zedit`,`zed-cli`" + + `],[\`dev.zed.Zed.desktop\`,\`zed.desktop\`],[\`Zed\`]),args:(...e)=>__codexLinuxOpenArgs(__codexLinuxOpenCommand([` + + "`zed`,`zeditor`,`zedit`,`zed-cli`" + + `],[\`dev.zed.Zed.desktop\`,\`zed.desktop\`],[\`Zed\`]),...e,${argsFn}),open:async({command:e,path:t,location:n})=>{await __codexLinuxOpenEditor(e,t,n,${argsFn})}}` + ); +} + +function findOpenTargetDeclarationBlock(source, id) { + const markerStart = source.indexOf(`id:\`${id}\``); + if (markerStart === -1) { + return null; + } + + const blockStart = Math.max( + source.lastIndexOf("var ", markerStart), + source.lastIndexOf("let ", markerStart), + source.lastIndexOf("const ", markerStart), + ); + const objectStart = blockStart === -1 ? -1 : source.indexOf("{", blockStart); + const objectEnd = objectStart === -1 ? -1 : findMatchingBrace(source, objectStart); + if (blockStart === -1 || objectStart === -1 || objectEnd === -1) { + return null; + } + + const semicolon = source.indexOf(";", objectEnd); + const blockEnd = semicolon === -1 ? objectEnd + 1 : semicolon + 1; + return { + start: blockStart, + end: blockEnd, + text: source.slice(blockStart, blockEnd), + }; +} + +function javascriptArray(values) { + return `[${values.map((value) => `\`${value}\``).join(",")}]`; +} + function applyLinuxWindowOptionsPatch(currentSource, iconAsset) { if (iconAsset == null) { return currentSource; From 80527f5224ff9fb0b42a20ebc904c6ee43a39f47 Mon Sep 17 00:00:00 2001 From: Nicholas Oh Date: Mon, 25 May 2026 21:30:52 +0800 Subject: [PATCH 2/2] Add Linux editor destination regressions --- scripts/patch-linux-window-ui.test.js | 200 +++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 2 deletions(-) diff --git a/scripts/patch-linux-window-ui.test.js b/scripts/patch-linux-window-ui.test.js index 5de91127..267cfcac 100644 --- a/scripts/patch-linux-window-ui.test.js +++ b/scripts/patch-linux-window-ui.test.js @@ -83,6 +83,15 @@ const mainBundlePrefix = "let n=require(`electron`),i=require(`node:path`),o=require(`node:fs`);"; const fileManagerBundle = "var lu=jl({id:`fileManager`,label:`Finder`,icon:`apps/finder.png`,kind:`fileManager`,darwin:{detect:()=>`open`,args:e=>il(e)},win32:{label:`File Explorer`,icon:`apps/file-explorer.png`,detect:uu,args:e=>il(e),open:async({path:e})=>du(e)}});function uu(){}"; +const openTargetEditorsBundle = [ + "function vT({id:e,label:t,icon:n,darwinDetect:r,win32Detect:i,darwinEnv:a,darwinArgs:o,hidden:s}){return{id:e,platforms:{darwin:r?{label:t,icon:n,kind:`editor`,hidden:s,detect:r,env:a,args:o??yT,supportsSsh:!0}:void 0,win32:i?{label:t,icon:n,kind:`editor`,hidden:s,detect:i,args:yT,supportsSsh:!0}:void 0}}}", + "var bT=vT({id:`antigravity`,label:`Antigravity`,icon:`apps/antigravity.png`,darwinDetect:()=>aT([`/Applications/Antigravity.app/Contents/Resources/app/bin/antigravity`]),win32Detect:xT});function xT(){}", + "var GT=vT({id:`cursor`,label:`Cursor`,icon:`apps/cursor.png`,darwinDetect:()=>qT()?.electronBin??null,win32Detect:JT,darwinEnv:()=>{let e={...process.env};return e.VSCODE_NODE_OPTIONS=e.NODE_OPTIONS,e}});function JT(){}", + "var HE=vT({id:`vscode`,label:`VS Code`,icon:`apps/vscode.png`,darwinDetect:()=>aT([`/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code`,`/Applications/Code.app/Contents/Resources/app/bin/code`]),win32Detect:UE});function UE(){}", + "var WE=vT({id:`vscodeInsiders`,label:`VS Code Insiders`,icon:`apps/vscode-insiders.png`,darwinDetect:()=>aT([`/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code`,`/Applications/Code - Insiders.app/Contents/Resources/app/bin/code`]),win32Detect:GE});function GE(){}", + "var KE=TT({id:`warp`,label:`Warp`,icon:`apps/warp.png`,appPaths:[`/Applications/Warp.app`],appName:`Warp`}),qE=vT({id:`windsurf`,label:`Windsurf`,icon:`apps/windsurf.png`,darwinDetect:()=>aT([`/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf`])}),JE=`wsl.exe`;", + "var iD={id:`zed`,platforms:{darwin:{label:`Zed`,icon:`apps/zed.png`,kind:`editor`,detect:aD,args:kE,open:async({command:e,path:t,location:n})=>{await lD(e,t,n)}},win32:{label:`Zed`,icon:`apps/zed.png`,kind:`editor`,detect:oD,args:kE}}};", +].join(""); const alreadyOpaqueBackgroundBundle = "process.platform===`linux`?{backgroundColor:e?t:n,backgroundMaterial:null}:{backgroundColor:r,backgroundMaterial:null}"; const opaqueBackgroundBundleWithDriftingGw = @@ -767,14 +776,201 @@ function avatarOverlayBundleFixture() { ].join(""); } -test("adds Linux file manager support without relying on exact minified variable names", () => { - const source = `${mainBundlePrefix}${fileManagerBundle}`; +test("adds Linux open destinations for current upstream registry shapes", () => { + const source = `${mainBundlePrefix}${openTargetEditorsBundle}${fileManagerBundle}`; const patched = applyPatchTwice(applyLinuxFileManagerPatch, source); assert.match(patched, /linux:\{label:`File Manager`/); assert.match(patched, /detect:\(\)=>`linux-file-manager`/); assert.match(patched, /n\.shell\.openPath\(__codexOpenTarget\)/); + assert.match(patched, /linux:c\?\{label:t,icon:n,kind:`editor`/); + assert.match( + patched, + /function __codexLinuxOpenCommand\(__codexCommands,__codexDesktopEntries=\[\],__codexNames=\[\]\)/, + ); + assert.match(patched, /function __codexLinuxExecutable\(__codexCommand\)/); + assert.match(patched, /function __codexLinuxDesktopMatches\(__codexEntry,__codexNeedles\)/); + assert.match(patched, /id:`cursor`[^]*linuxDetect:\(\)=>__codexLinuxOpenCommand\(\[`cursor`\],\[`cursor\.desktop`\],\[`Cursor`\]\)/); + assert.match(patched, /id:`vscode`[^]*linuxDetect:\(\)=>__codexLinuxOpenCommand\(\[`code`\]/); + assert.match(patched, /id:`vscodeInsiders`[^]*linuxDetect:\(\)=>__codexLinuxOpenCommand\(\[`code-insiders`\]/); + assert.match(patched, /id:`windsurf`[^]*linuxDetect:\(\)=>__codexLinuxOpenCommand\(\[`windsurf`\]/); + assert.match(patched, /id:`antigravity`[^]*linuxDetect:\(\)=>__codexLinuxOpenCommand\(\[`antigravity`\]/); + assert.match( + patched, + /linux:\{label:`Zed`,icon:`apps\/zed\.png`,kind:`editor`,detect:\(\)=>__codexLinuxOpenCommand\(\[`zed`,`zeditor`,`zedit`,`zed-cli`\]/, + ); +}); + +const editorHelperOpenTargetBundle = + "function vT({id:e,label:t,icon:n,darwinDetect:r,win32Detect:i,darwinEnv:a,darwinArgs:o,hidden:s}){return{id:e,platforms:{darwin:r?{label:t,icon:n,kind:`editor`,hidden:s,detect:r,env:a,args:o??yT,supportsSsh:!0}:void 0,win32:i?{label:t,icon:n,kind:`editor`,hidden:s,detect:i,args:yT,supportsSsh:!0}:void 0}}}"; + +function patchedEditorFixture(targetDeclaration) { + return applyPatchTwice( + applyLinuxFileManagerPatch, + [ + mainBundlePrefix, + "var yT=e=>[e];", + editorHelperOpenTargetBundle, + targetDeclaration, + "function jl(e){return e}function il(e){return [e]}", + fileManagerBundle, + ].join(""), + ); +} + +function runPatchedExpression(patched, expression, env, requireOverrides = {}) { + return new Function( + "require", + "process", + `delete globalThis.__codexLinuxDesktopFilesCache;delete globalThis.__codexLinuxDesktopEntryCache;${patched};return ${expression};`, + )( + (name) => { + if (name === "electron") { + return {}; + } + return requireOverrides[name] ?? require(name); + }, + { platform: "linux", env }, + ); +} + +function writeDesktopEntry(file, lines) { + fs.writeFileSync(file, ["[Desktop Entry]", "Type=Application", ...lines, ""].join("\n")); +} + +test("detects AppImage-installed Cursor through desktop Exec wrappers", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-cursor-wrapper-")); + try { + const appImage = path.join(tempDir, "Cursor.AppImage"); + const applicationsDir = path.join(tempDir, "share", "applications"); + fs.mkdirSync(applicationsDir, { recursive: true }); + fs.writeFileSync(appImage, "#!/bin/sh\nexit 0\n"); + fs.chmodSync(appImage, 0o755); + + const patched = patchedEditorFixture( + "var GT=vT({id:`cursor`,label:`Cursor`,icon:`apps/cursor.png`,darwinDetect:()=>null,win32Detect:()=>null});", + ); + const env = { + PATH: path.join(tempDir, "bin"), + XDG_DATA_HOME: path.join(tempDir, "share"), + XDG_DATA_DIRS: "", + }; + + for (const exec of [ + `Exec=${appImage} %F`, + `Exec=/usr/bin/env APPIMAGELAUNCHER_DISABLE=1 ${appImage} %U`, + `Exec=/bin/sh -c "${appImage} %U"`, + ]) { + writeDesktopEntry(path.join(applicationsDir, "appimagekit-random-Cursor.desktop"), [ + "Name=Cursor", + "X-AppImage-Name=Cursor", + exec, + ]); + assert.equal(runPatchedExpression(patched, "GT.platforms.linux.detect()", env), appImage); + } + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + +test("rejects wrapper-only TryExec and refreshes desktop entry caches", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-desktop-tryexec-")); + try { + const appImage = path.join(tempDir, "Cursor.AppImage"); + const applicationsDir = path.join(tempDir, "share", "applications"); + const desktopFile = path.join(applicationsDir, "cursor.desktop"); + fs.mkdirSync(applicationsDir, { recursive: true }); + fs.writeFileSync(appImage, "#!/bin/sh\nexit 0\n"); + fs.chmodSync(appImage, 0o755); + + const patched = patchedEditorFixture( + "var GT=vT({id:`cursor`,label:`Cursor`,icon:`apps/cursor.png`,darwinDetect:()=>null,win32Detect:()=>null});", + ); + const detectCursor = new Function( + "require", + "process", + `delete globalThis.__codexLinuxDesktopFilesCache;delete globalThis.__codexLinuxDesktopEntryCache;${patched};return()=>GT.platforms.linux.detect();`, + )( + (name) => { + if (name === "electron") { + return {}; + } + return require(name); + }, + { + platform: "linux", + env: { + PATH: path.join(tempDir, "bin"), + XDG_DATA_HOME: path.join(tempDir, "share"), + XDG_DATA_DIRS: "", + }, + }, + ); + + assert.equal(detectCursor(), null); + writeDesktopEntry(desktopFile, [ + "Name=Cursor", + "TryExec=env /missing/Cursor.AppImage", + `Exec=env ${appImage} %U`, + ]); + fs.utimesSync(applicationsDir, new Date(), new Date(Date.now() + 1000)); + assert.equal(detectCursor(), null); + + writeDesktopEntry(desktopFile, [ + "Name=Cursor", + `TryExec=env ${appImage}`, + `Exec=env ${appImage} %U`, + ]); + fs.utimesSync(desktopFile, new Date(), new Date(Date.now() + 2000)); + assert.equal(detectCursor(), appImage); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + +test("upgrades already-patched Zed targets for Flatpak desktop launches", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-zed-flatpak-upgrade-")); + try { + const binDir = path.join(tempDir, "bin"); + const applicationsDir = path.join(tempDir, "share", "applications"); + const gioLog = path.join(tempDir, "gio.log"); + fs.mkdirSync(binDir, { recursive: true }); + fs.mkdirSync(applicationsDir, { recursive: true }); + fs.writeFileSync(path.join(binDir, "gio"), `#!/bin/sh\nprintf '%s\\n' "$@" > ${JSON.stringify(gioLog)}\n`); + fs.chmodSync(path.join(binDir, "gio"), 0o755); + writeDesktopEntry(path.join(applicationsDir, "dev.zed.Zed.desktop"), [ + "Name=Zed", + "Exec=/usr/bin/flatpak run --command=zed dev.zed.Zed @@u %U @@", + ]); + + const source = [ + mainBundlePrefix, + "var yT=e=>[e];", + "function vT({id:e,label:t,icon:n,darwinDetect:r,win32Detect:i,linuxDetect:c,darwinEnv:a,darwinArgs:o,hidden:s}){return{id:e,platforms:{darwin:r?{label:t,icon:n,kind:`editor`,hidden:s,detect:r,env:a,args:o??yT,supportsSsh:!0}:void 0,win32:i?{label:t,icon:n,kind:`editor`,hidden:s,detect:i,args:yT,supportsSsh:!0}:void 0,linux:c?{label:t,icon:n,kind:`editor`,hidden:s,detect:c,args:yT,supportsSsh:!0}:void 0}}}", + "function __codexLinuxOpenCommand(e,t=[]){return e[0]??t[0]??null}", + "var iD={id:`zed`,platforms:{darwin:{label:`Zed`,icon:`apps/zed.png`,kind:`editor`,detect:()=>null,args:yT},win32:{label:`Zed`,icon:`apps/zed.png`,kind:`editor`,detect:()=>null,args:yT},linux:{label:`Zed`,icon:`apps/zed.png`,kind:`editor`,detect:()=>__codexLinuxOpenCommand([`zed`,`zeditor`,`zedit`,`zed-cli`],[`dev.zed.Zed.desktop`,`zed.desktop`]),args:yT}}};", + "function jl(e){return e}function il(e){return [e]}", + "var lu=jl({id:`fileManager`,label:`Finder`,icon:`apps/finder.png`,kind:`fileManager`,darwin:{detect:()=>`open`,args:e=>il(e)},win32:{label:`File Explorer`,icon:`apps/file-explorer.png`,detect:uu,args:e=>il(e)},linux:{label:`File Manager`,icon:`apps/file-explorer.png`,detect:()=>`linux-file-manager`,args:e=>[e]}});function uu(){}", + ].join(""); + const patched = applyLinuxFileManagerPatch(source); + const platform = runPatchedExpression(patched, "iD.platforms.linux", { + PATH: binDir, + XDG_DATA_HOME: path.join(tempDir, "share"), + XDG_DATA_DIRS: "", + }); + const command = platform.detect(); + + assert.equal(command, `codex-linux-desktop:${path.join(applicationsDir, "dev.zed.Zed.desktop")}`); + assert.deepEqual(platform.args("/tmp/project"), []); + await platform.open({ command, path: "/tmp/project" }); + assert.equal( + fs.readFileSync(gioLog, "utf8"), + `launch\n${path.join(applicationsDir, "dev.zed.Zed.desktop")}\n/tmp/project\n`, + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } }); test("preserves user-enabled remote_control config on Linux", () => {