Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 198 additions & 2 deletions scripts/patch-linux-window-ui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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", () => {
Expand Down
Loading
Loading