From ed10eac11cd8e657e7eb4b74ba67410329d1c429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lima?= Date: Thu, 1 Oct 2026 22:19:07 +0100 Subject: [PATCH] fix(desktop): support Linux distro protocol launchers Distribution packages run T3 Code on a shared Electron, so the URL handler's Exec pointed at that bare Electron instead of the package's launcher. T3CODE_DESKTOP_LAUNCHER_PATH names the launcher, and both the early portal entry and the handler registration prefer it over APPIMAGE and process.execPath. Assisted-By: Claude Opus 5.5 --- apps/desktop/src/app/DesktopConfig.ts | 1 + apps/desktop/src/app/DesktopEnvironment.ts | 2 ++ apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts | 13 +++++++++++++ apps/desktop/src/app/DesktopLinuxUrlHandler.ts | 11 ++++++++--- .../desktop/src/app/DesktopPreReadyPlatform.test.ts | 12 ++++++++++++ apps/desktop/src/app/DesktopPreReadyPlatform.ts | 5 ++++- 6 files changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/app/DesktopConfig.ts b/apps/desktop/src/app/DesktopConfig.ts index 773ea24509cf..11f975a6b86a 100644 --- a/apps/desktop/src/app/DesktopConfig.ts +++ b/apps/desktop/src/app/DesktopConfig.ts @@ -55,6 +55,7 @@ export const DesktopConfig = Config.all({ otlpProtocol: Config.schema(OtlpProtocol, "T3CODE_OTLP_PROTOCOL").pipe( Config.withDefault("http/json"), ), + desktopLauncherPath: trimmedString("T3CODE_DESKTOP_LAUNCHER_PATH"), appImagePath: trimmedString("APPIMAGE"), disableAutoUpdate: optionalBoolean("T3CODE_DISABLE_AUTO_UPDATE"), mockUpdates: optionalBoolean("T3CODE_DESKTOP_MOCK_UPDATES"), diff --git a/apps/desktop/src/app/DesktopEnvironment.ts b/apps/desktop/src/app/DesktopEnvironment.ts index beb09bd50a46..3fdb969f07b0 100644 --- a/apps/desktop/src/app/DesktopEnvironment.ts +++ b/apps/desktop/src/app/DesktopEnvironment.ts @@ -85,6 +85,7 @@ export class DesktopEnvironment extends Context.Service< readonly linuxDesktopEntryName: string; readonly linuxWmClass: string; readonly linuxApplicationsDir: string; + readonly desktopLauncherPath: Option.Option; readonly appImagePath: Option.Option; readonly userDataDirName: string; readonly legacyUserDataDirName: string; @@ -245,6 +246,7 @@ const make = Effect.fn("desktop.environment.make")(function* ( linuxDesktopEntryName: resolveLinuxDesktopEntryName(isDevelopment), linuxWmClass: isDevelopment ? "t3code-dev" : "t3code", linuxApplicationsDir, + desktopLauncherPath: config.desktopLauncherPath, appImagePath: config.appImagePath, userDataDirName, legacyUserDataDirName, diff --git a/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts b/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts index 893c1aaebe1d..e6f4ffdb9226 100644 --- a/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts +++ b/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts @@ -32,6 +32,7 @@ const makeEnvironment = (path: Path.Path, overrides: Record = { linuxDesktopEntryName: "com.t3tools.T3Code.desktop", linuxWmClass: "t3code", linuxApplicationsDir: "/home/alice/.local/share/applications", + desktopLauncherPath: Option.none(), appImagePath: Option.some("/home/alice/Applications/T3-Code.AppImage"), path, ...overrides, @@ -254,6 +255,18 @@ describe("DesktopLinuxUrlHandler", () => { }); }); + it.effect("launches a distribution package through its own launcher", () => { + const recorded = emptyRecording(); + + return Effect.gen(function* () { + yield* runRegister(recorded, { + environment: { desktopLauncherPath: Option.some("/usr/bin/t3code") }, + }); + + assert.include(recorded.files[0]?.content, 'Exec="/usr/bin/t3code" %U'); + }); + }); + it.effect("does not rewrite the pre-ready entry while the portal can be reading it", () => { const recorded = emptyRecording(); diff --git a/apps/desktop/src/app/DesktopLinuxUrlHandler.ts b/apps/desktop/src/app/DesktopLinuxUrlHandler.ts index f01e81d890bc..d1d79594e1e5 100644 --- a/apps/desktop/src/app/DesktopLinuxUrlHandler.ts +++ b/apps/desktop/src/app/DesktopLinuxUrlHandler.ts @@ -126,9 +126,14 @@ export const make = Effect.gen(function* () { const iconPath = environment.path.join(iconsDir, `${environment.linuxDesktopEntryName}.png`); const writeDesktopEntry = Effect.gen(function* () { - // Inside the mounted AppImage, process.execPath points at a transient - // /tmp/.mount_* path — the handler must launch the AppImage itself. - const execTarget = Option.getOrElse(environment.appImagePath, () => process.execPath); + // Distribution packages run the app on a shared Electron, so process.execPath + // is that Electron rather than their launcher. Inside the mounted AppImage, + // process.execPath points at a transient /tmp/.mount_* path — the handler + // must launch the AppImage itself. + const execTarget = environment.desktopLauncherPath.pipe( + Option.orElse(() => environment.appImagePath), + Option.getOrElse(() => process.execPath), + ); const content = renderUrlHandlerDesktopEntry({ displayName: environment.displayName, execTarget, diff --git a/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts b/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts index 7e859aaf981a..24142d5b312e 100644 --- a/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts +++ b/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts @@ -156,6 +156,18 @@ describe("DesktopPreReadyPlatform", () => { }).pipe(Effect.provideService(HostProcessPlatform, "linux")); }); + it.effect("points the early portal entry at a distribution launcher", () => { + vi.stubEnv("T3CODE_DESKTOP_LAUNCHER_PATH", " /usr/bin/t3code "); + getSwitchValueMock.mockReturnValue(""); + return Effect.gen(function* () { + yield* DesktopPreReadyPlatform.make; + assert.include(writeFileSyncMock.mock.calls[0]?.[1], 'Exec="/usr/bin/t3code" %U'); + }).pipe( + Effect.provideService(HostProcessPlatform, "linux"), + Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())), + ); + }); + it.effect( "acquires a synchronous pre-ready layer before an asynchronous Clerk-shaped layer", () => diff --git a/apps/desktop/src/app/DesktopPreReadyPlatform.ts b/apps/desktop/src/app/DesktopPreReadyPlatform.ts index 9b73a5d825bf..686c116825fd 100644 --- a/apps/desktop/src/app/DesktopPreReadyPlatform.ts +++ b/apps/desktop/src/app/DesktopPreReadyPlatform.ts @@ -97,7 +97,10 @@ export const make = Effect.gen(function* () { isDevelopment: linux.isDevelopment, appVersion: Electron.app.getVersion(), }).displayName, - execTarget: process.env.APPIMAGE?.trim() || process.execPath, + execTarget: + process.env.T3CODE_DESKTOP_LAUNCHER_PATH?.trim() || + process.env.APPIMAGE?.trim() || + process.execPath, scheme: ElectronProtocol.getDesktopScheme(linux.isDevelopment), ...(iconPath === undefined ? {} : { iconPath }), }),