diff --git a/.gitignore b/.gitignore index b89d66d..95633de 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ nani-app/ build/ dist/ +result .cache/ *.dmg *.part diff --git a/scripts/patches/deeplink-readiness.mjs b/scripts/patches/deeplink-readiness.mjs new file mode 100644 index 0000000..2282cda --- /dev/null +++ b/scripts/patches/deeplink-readiness.mjs @@ -0,0 +1,29 @@ +import { definePatch, PHASE_MAIN_BUNDLE } from "./descriptor.mjs"; +import { markerStatus, replaceExactly } from "./lib.mjs"; + +export const DEEPLINK_MARKER = "/* nani-linux:deeplink-readiness */"; +export const DEEPLINK_ANCHOR = + 'if(t&&e){o.info(`Handling initial deeplink: ${t}`);const n=t;e.webContents.once("did-finish-load",()=>{He(n)})}'; +const DEEPLINK_REPLACEMENT = + 'if(t&&e){o.info(`Handling initial deeplink: ${t}`);const n=t;e.webContents.isLoading()?e.webContents.once("did-finish-load",()=>{He(n)}):He(n)}' + + DEEPLINK_MARKER; + +export default definePatch({ + id: "deeplink-readiness", + phase: PHASE_MAIN_BUNDLE, + order: 12, + required: true, + apply({ source }) { + const status = markerStatus(source, DEEPLINK_MARKER); + if (status) return { source, status }; + return { + source: replaceExactly( + source, + DEEPLINK_ANCHOR, + DEEPLINK_REPLACEMENT, + "initial deep link readiness", + ), + status: "applied", + }; + }, +}); diff --git a/scripts/patches/index.mjs b/scripts/patches/index.mjs index c413e13..d4577b0 100644 --- a/scripts/patches/index.mjs +++ b/scripts/patches/index.mjs @@ -1,4 +1,6 @@ +import deeplinkReadiness from "./deeplink-readiness.mjs"; import disableUpdater from "./disable-updater.mjs"; +import linuxCloseToTray from "./linux-close-to-tray.mjs"; import linuxOnboarding from "./linux-onboarding.mjs"; import linuxTray from "./linux-tray.mjs"; import ocrStub from "./ocr-stub.mjs"; @@ -10,6 +12,8 @@ export const corePatchDescriptors = [ ocrStub, linuxOnboarding, linuxTray, + linuxCloseToTray, + deeplinkReadiness, disableUpdater, xdgAutostart, ]; diff --git a/scripts/patches/linux-close-to-tray.mjs b/scripts/patches/linux-close-to-tray.mjs new file mode 100644 index 0000000..f8d9f84 --- /dev/null +++ b/scripts/patches/linux-close-to-tray.mjs @@ -0,0 +1,28 @@ +import { definePatch, PHASE_MAIN_BUNDLE } from "./descriptor.mjs"; +import { markerStatus, replaceExactly } from "./lib.mjs"; + +export const CLOSE_TO_TRAY_MARKER = "/* nani-linux:linux-close-to-tray */"; +export const CLOSE_TO_TRAY_ANCHOR = 'process.platform==="darwin"&&Fs(d)'; +const CLOSE_TO_TRAY_REPLACEMENT = + '(process.platform==="darwin"||process.platform==="linux")&&Fs(d)' + + CLOSE_TO_TRAY_MARKER; + +export default definePatch({ + id: "linux-close-to-tray", + phase: PHASE_MAIN_BUNDLE, + order: 11, + required: true, + apply({ source }) { + const status = markerStatus(source, CLOSE_TO_TRAY_MARKER); + if (status) return { source, status }; + return { + source: replaceExactly( + source, + CLOSE_TO_TRAY_ANCHOR, + CLOSE_TO_TRAY_REPLACEMENT, + "Linux close-to-tray", + ), + status: "applied", + }; + }, +}); diff --git a/scripts/patches/xdg-autostart.mjs b/scripts/patches/xdg-autostart.mjs index d32ae00..afb143e 100644 --- a/scripts/patches/xdg-autostart.mjs +++ b/scripts/patches/xdg-autostart.mjs @@ -4,8 +4,9 @@ import { markerStatus, replaceExactly } from "./lib.mjs"; export const AUTOSTART_MARKER = "/* nani-linux:xdg-autostart */"; export const AUTOSTART_GET_ANCHOR = ',bi=(async()=>({autoLaunchEnabled:c.app.getLoginItemSettings().openAtLogin})),Ai="ms-settings:startupapps";'; export const AUTOSTART_SET_ANCHOR = 'const Mi=(async(e,t)=>{if(process.platform==="win32")return await vi();try{return c.app.setLoginItemSettings({openAtLogin:t,openAsHidden:!0}),{ok:!0}}catch(n){return{ok:!1,message:n instanceof Error?n.message:"Unknown error"}}})'; +export const AUTOSTART_SYNC_ANCHOR = 'Fi(),Zs(),c.app.on("browser-window-created"'; -const AUTOSTART_GET_REPLACEMENT = String.raw`,$naniAutostartPath=()=>S.join(process.env.XDG_CONFIG_HOME||S.join(c.app.getPath("home"),".config"),"autostart","nani.desktop"),$naniDesktopQuote=e=>'"'+e.replace(/([\\"\u0060$])/g,"\\$1")+'"',$naniGetAutostart=async()=>te.access($naniAutostartPath()).then(()=>!0,()=>!1),$naniSetAutostart=async e=>{const t=$naniAutostartPath();if(!e)return await te.rm(t,{force:!0}),{ok:!0};const n=process.env.NANI_LAUNCHER_PATH;if(!n||!S.isAbsolute(n)||/[\r\n]/.test(n))return{ok:!1,message:"NANI_LAUNCHER_PATH must be an absolute path"};await te.mkdir(S.dirname(t),{recursive:!0});const r=t+".tmp-"+process.pid+"-"+Date.now(),a="[Desktop Entry]\nType=Application\nName=Nani Translate\nExec="+$naniDesktopQuote(n)+" --autostart\nTerminal=false\nX-GNOME-Autostart-enabled=true\n";try{return await te.writeFile(r,a,{mode:420}),await te.rename(r,t),{ok:!0}}finally{await te.rm(r,{force:!0}).catch(()=>{})}},bi=(async()=>process.platform==="linux"?({autoLaunchEnabled:await $naniGetAutostart()}):({autoLaunchEnabled:c.app.getLoginItemSettings().openAtLogin}))` + AUTOSTART_MARKER + ',Ai="ms-settings:startupapps";'; +const AUTOSTART_GET_REPLACEMENT = String.raw`,$naniAutostartPath=()=>S.join(process.env.XDG_CONFIG_HOME||S.join(c.app.getPath("home"),".config"),"autostart","nani.desktop"),$naniDesktopQuote=e=>'"'+e.replace(/([\\"\u0060$])/g,"\\$1")+'"',$naniGetAutostart=async()=>te.access($naniAutostartPath()).then(()=>!0,()=>!1),$naniSetAutostart=async e=>{const t=$naniAutostartPath();if(!e)return await te.rm(t,{force:!0}),{ok:!0};const n=process.env.NANI_LAUNCHER_PATH;if(!n||!S.isAbsolute(n)||/[\r\n]/.test(n))return{ok:!1,message:"NANI_LAUNCHER_PATH must be an absolute path"};await te.mkdir(S.dirname(t),{recursive:!0});const r=t+".tmp-"+process.pid+"-"+Date.now(),a="[Desktop Entry]\nType=Application\nName=Nani Translate\nExec="+$naniDesktopQuote(n)+" --autostart\nTerminal=false\nX-GNOME-Autostart-enabled=true\n";try{return await te.writeFile(r,a,{mode:420}),await te.rename(r,t),{ok:!0}}finally{await te.rm(r,{force:!0}).catch(()=>{})}},$naniSyncAutostart=async()=>{await $naniGetAutostart()&&await $naniSetAutostart(!0)},bi=(async()=>process.platform==="linux"?({autoLaunchEnabled:await $naniGetAutostart()}):({autoLaunchEnabled:c.app.getLoginItemSettings().openAtLogin}))` + AUTOSTART_MARKER + ',Ai="ms-settings:startupapps";'; const AUTOSTART_SET_REPLACEMENT = 'const Mi=(async(e,t)=>{if(process.platform==="win32")return await vi();try{return process.platform==="linux"?await $naniSetAutostart(t):(c.app.setLoginItemSettings({openAtLogin:t,openAsHidden:!0}),{ok:!0})}catch(n){return{ok:!1,message:n instanceof Error?n.message:"Unknown error"}}})'; @@ -29,6 +30,12 @@ export default definePatch({ AUTOSTART_SET_REPLACEMENT, "XDG autostart setter", ); + patched = replaceExactly( + patched, + AUTOSTART_SYNC_ANCHOR, + 'Fi(),Zs(),process.platform==="linux"&&await $naniSyncAutostart(),c.app.on("browser-window-created"', + "XDG autostart launcher sync", + ); return { source: patched, status: "applied" }; }, }); diff --git a/tests/asar-patches.test.mjs b/tests/asar-patches.test.mjs index b21bcce..5e1b4e7 100644 --- a/tests/asar-patches.test.mjs +++ b/tests/asar-patches.test.mjs @@ -10,6 +10,10 @@ import { XSEL_RELATIVE_PATH, } from "../scripts/patch-asar.mjs"; +import deeplinkReadiness, { + DEEPLINK_ANCHOR, + DEEPLINK_MARKER, +} from "../scripts/patches/deeplink-readiness.mjs"; import { normalizeDescriptors, PHASE_MAIN_BUNDLE } from "../scripts/patches/descriptor.mjs"; import disableUpdater, { UPDATER_ENABLE_ANCHOR, @@ -17,6 +21,10 @@ import disableUpdater, { UPDATER_MARKER, } from "../scripts/patches/disable-updater.mjs"; import { runPatchDescriptors } from "../scripts/patches/engine.mjs"; +import linuxCloseToTray, { + CLOSE_TO_TRAY_ANCHOR, + CLOSE_TO_TRAY_MARKER, +} from "../scripts/patches/linux-close-to-tray.mjs"; import linuxOnboarding, { ONBOARDING_ANCHOR, verifyLinuxOnboarding, @@ -32,6 +40,7 @@ import xdgAutostart, { AUTOSTART_GET_ANCHOR, AUTOSTART_MARKER, AUTOSTART_SET_ANCHOR, + AUTOSTART_SYNC_ANCHOR, } from "../scripts/patches/xdg-autostart.mjs"; function temporaryDirectory(t) { @@ -45,6 +54,8 @@ test("descriptors have deterministic phase/order and reject duplicate ids", () = xdgAutostart, disableUpdater, linuxTray, + linuxCloseToTray, + deeplinkReadiness, ocrStub, pruneNonLinux, linuxOnboarding, @@ -54,6 +65,8 @@ test("descriptors have deterministic phase/order and reject duplicate ids", () = "ocr-stub", "linux-onboarding", "linux-tray", + "linux-close-to-tray", + "deeplink-readiness", "disable-updater", "xdg-autostart", ]); @@ -64,12 +77,21 @@ test("Nani 1.1.0 semantic anchors apply once and are idempotent", () => { let source = [ UPDATER_ENABLE_ANCHOR, TRAY_ANCHOR, + CLOSE_TO_TRAY_ANCHOR, + DEEPLINK_ANCHOR, AUTOSTART_GET_ANCHOR, AUTOSTART_SET_ANCHOR, + AUTOSTART_SYNC_ANCHOR, UPDATER_HANDLERS_ANCHOR, ].join(";"); - for (const descriptor of [linuxTray, disableUpdater, xdgAutostart]) { + for (const descriptor of [ + linuxTray, + linuxCloseToTray, + deeplinkReadiness, + disableUpdater, + xdgAutostart, + ]) { const result = descriptor.apply({ source }); assert.equal(result.status, "applied"); source = result.source; @@ -78,11 +100,16 @@ test("Nani 1.1.0 semantic anchors apply once and are idempotent", () => { assert.equal(again.source, source); } assert.match(source, new RegExp(TRAY_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + assert.match(source, new RegExp(CLOSE_TO_TRAY_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + assert.match(source, new RegExp(DEEPLINK_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.match(source, new RegExp(UPDATER_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.match(source, new RegExp(AUTOSTART_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.match(source, /process\.platform==="linux"\?async\(\)=>!1:xa/); assert.match(source, /nativeImage\.createFromPath/); assert.match(source, /NANI_LAUNCHER_PATH/); + assert.match(source, /await \$naniSyncAutostart\(\)/); + assert.match(source, /webContents\.isLoading\(\)/); + assert.match(source, /process\.platform==="darwin"\|\|process\.platform==="linux"/); }); test("required anchor drift is reported and main bundle remains unchanged", () => { diff --git a/tests/runtime-packaging.test.mjs b/tests/runtime-packaging.test.mjs index 88a439c..c775449 100644 --- a/tests/runtime-packaging.test.mjs +++ b/tests/runtime-packaging.test.mjs @@ -53,6 +53,14 @@ test("explicit ozone CLI argument wins and URL arguments pass through", () => { ]); }); +test("hidden autostart argument passes through to Electron", () => { + const setup = fixture(); + assert.deepEqual(runLauncher(setup, ["--autostart"]), [ + "--ozone-platform=x11", + "--autostart", + ]); +}); + test("launcher limits optional CDP to localhost", () => { const setup = fixture(); assert.deepEqual(runLauncher(setup, [], { NANI_CDP_PORT: "9222" }), [