Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
83 changes: 83 additions & 0 deletions apps/desktop/src/app/DesktopEarlyElectronStartup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as NodePath from "node:path";
import { assert, describe, it } from "@effect/vitest";

import {
restoreEarlyLinuxDeviceScaleFactor,
resolveEarlyLinuxElectronOptions,
resolveEarlyLinuxPasswordStorePreference,
resolveLinuxDisableFeaturesSwitch,
Expand Down Expand Up @@ -269,3 +270,85 @@ describe("DesktopEarlyElectronStartup", () => {
});
});
});

describe("Linux device scale across update relaunches", () => {
const input = {
env: { T3CODE_HOME: "/isolated" },
homeDirectory: "/home/user",
joinPath: NodePath.posix.join,
};

it("restores the exact explicit scale on a subsequent launch without arguments", () => {
let saved = "";
const applied: Array<[string, string]> = [];
const launch = (explicit: string | null) =>
restoreEarlyLinuxDeviceScaleFactor({
...input,
commandLine: {
hasSwitch: () => explicit !== null,
getSwitchValue: () => explicit ?? "",
appendSwitch: (name, value) => applied.push([name, value]),
},
readFileString: () => saved,
writeFileString: (path, value) => {
assert.equal(path, "/isolated/userdata/linux-device-scale-factor");
saved = value;
},
});
launch("1.75");
assert.deepEqual(applied, []);
launch(null);
assert.deepEqual(applied, [["force-device-scale-factor", "1.75"]]);
launch("1");
launch(null);
assert.deepEqual(applied.at(-1), ["force-device-scale-factor", "1"]);
});

it.each(["", "garbage", "0", "-1", "Infinity", "NaN"])(
"ignores invalid persisted scale %j",
(value) => {
const applied: Array<[string, string]> = [];
restoreEarlyLinuxDeviceScaleFactor({
...input,
commandLine: {
hasSwitch: () => false,
getSwitchValue: () => "",
appendSwitch: (name, scale) => applied.push([name, scale]),
},
readFileString: () => value,
writeFileString: () => assert.fail("unexpected write"),
});
assert.deepEqual(applied, []);
},
);

it("preserves explicit switches even when persistence fails", () => {
restoreEarlyLinuxDeviceScaleFactor({
...input,
commandLine: {
hasSwitch: () => true,
getSwitchValue: () => "2",
appendSwitch: () => assert.fail("explicit scale overridden"),
},
readFileString: () => assert.fail("explicit scale must take precedence"),
writeFileString: () => {
throw new Error("read-only");
},
});
});

it("keeps default scaling when state is missing", () => {
restoreEarlyLinuxDeviceScaleFactor({
...input,
commandLine: {
hasSwitch: () => false,
getSwitchValue: () => "",
appendSwitch: () => assert.fail("default scale overridden"),
},
readFileString: () => {
throw new Error("missing");
},
writeFileString: () => assert.fail("unexpected write"),
});
});
});
40 changes: 40 additions & 0 deletions apps/desktop/src/app/DesktopEarlyElectronStartup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,43 @@ export function resolveEarlyLinuxElectronOptions(
}),
};
}

/** Remember the explicit Chromium scale across AppImage updates, which drop launch arguments. */
export function restoreEarlyLinuxDeviceScaleFactor(
input: EarlyDesktopSettingsInput & {
readonly commandLine: {
readonly hasSwitch: (name: string) => boolean;
readonly getSwitchValue: (name: string) => string;
readonly appendSwitch: (name: string, value: string) => void;
};
readonly writeFileString: (path: string, value: string) => void;
},
): void {
// Keep this separate from desktop-settings.json: the settings service rewrites
// that document, and Chromium needs the value before that service is available.
const path = input.joinPath(
resolveEarlyDesktopSettingsPath(input),
"..",
"linux-device-scale-factor",
);
const switchName = "force-device-scale-factor";
const isValid = (value: string) =>
value.trim().length > 0 && Number.isFinite(Number(value)) && Number(value) > 0;
if (input.commandLine.hasSwitch(switchName)) {
const value = input.commandLine.getSwitchValue(switchName);
if (isValid(value)) {
try {
input.writeFileString(path, value);
} catch {
// A read-only state directory must not prevent startup with the explicit switch.
}
}
return;
}
try {
const value = input.readFileString(path).trim();
if (isValid(value)) input.commandLine.appendSwitch(switchName, value);
} catch {
// First launch, missing state, or unreadable state: keep Chromium's default.
}
}
17 changes: 16 additions & 1 deletion apps/desktop/src/app/DesktopPreReadyPlatform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {
writeFileSyncMock,
copyFileSyncMock,
execFileSyncMock,
readFileSyncMock,
} = vi.hoisted(() => ({
appendSwitchMock: vi.fn(),
getSwitchValueMock: vi.fn(),
Expand All @@ -25,6 +26,7 @@ const {
writeFileSyncMock: vi.fn(),
copyFileSyncMock: vi.fn(),
execFileSyncMock: vi.fn(),
readFileSyncMock: vi.fn(),
}));

vi.mock("electron", () => ({
Expand All @@ -45,7 +47,7 @@ vi.mock("electron", () => ({
}));

vi.mock("node:fs", () => ({
readFileSync: () => "{}",
readFileSync: readFileSyncMock,
mkdirSync: mkdirSyncMock,
writeFileSync: writeFileSyncMock,
copyFileSync: copyFileSyncMock,
Expand All @@ -68,6 +70,19 @@ describe("DesktopPreReadyPlatform", () => {
writeFileSyncMock.mockReset();
copyFileSyncMock.mockReset();
execFileSyncMock.mockReset();
readFileSyncMock.mockReset().mockReturnValue("{}");
});

it.effect("restores saved scaling before asynchronous startup can observe it", () => {
readFileSyncMock.mockImplementation((path: string) =>
path.endsWith("linux-device-scale-factor") ? "1.75" : "{}",
);
return Effect.gen(function* () {
const startup = Promise.resolve().then(() => appendSwitchMock.mock.calls.slice());
yield* DesktopPreReadyPlatform.make;
const switches = yield* Effect.promise(() => startup);
assert.deepInclude(switches, ["force-device-scale-factor", "1.75"]);
}).pipe(Effect.provideService(HostProcess.Platform, "linux"));
});

it.effect("preserves an explicit Linux password-store switch", () => {
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/app/DesktopPreReadyPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ export const make = Effect.gen(function* () {
platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess(homeDirectory) : null;

if (linux !== null) {
DesktopEarlyElectronStartup.restoreEarlyLinuxDeviceScaleFactor({
env: process.env,
homeDirectory,
joinPath: NodePath.posix.join,
commandLine: Electron.app.commandLine,
readFileString: (path) => NodeFS.readFileSync(path, "utf8"),
writeFileString: (path, value) => {
NodeFS.mkdirSync(NodePath.posix.dirname(path), { recursive: true });
NodeFS.writeFileSync(path, value, "utf8");
},
});
// The portal also requires a valid desktop entry. An AppImage update may
// have removed the executable referenced by the previous launch's entry.
try {
Expand Down
Loading