Skip to content

Commit dbe2459

Browse files
Merge branch 'main' into feat/file-working-thread
2 parents 49feac3 + 6524a8d commit dbe2459

128 files changed

Lines changed: 5206 additions & 663 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.coderabbit.config.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { defineConfig, type CodeRabbitContext } from "@coderabbitai/config";
22

3-
// Org members and collaborators merge their own pull requests. On anyone else's, CodeRabbit
4-
// requests changes until its comments are resolved, then approves.
3+
// Org members and collaborators can approve their own pull requests.
54
const UNGATED_AUTHORS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
65
const isGated = ({ pr }: CodeRabbitContext) => !UNGATED_AUTHORS.has(pr?.authorAssociation ?? "");
76

87
export default defineConfig((ctx) => ({
98
reviews: {
109
high_level_summary: false,
1110
review_status: false,
12-
request_changes_workflow: isGated(ctx),
11+
request_changes_workflow: false,
1312
allow_author_approval: !isGated(ctx),
1413
auto_review: {
1514
enabled: true,

‎apps/desktop/src/app/DesktopEarlyElectronStartup.test.ts‎

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { assert, describe, it } from "@effect/vitest";
55
import {
66
resolveEarlyLinuxElectronOptions,
77
resolveEarlyLinuxPasswordStorePreference,
8+
resolveLinuxDisableFeaturesSwitch,
89
} from "./DesktopEarlyElectronStartup.ts";
910

1011
describe("DesktopEarlyElectronStartup", () => {
@@ -120,4 +121,152 @@ describe("DesktopEarlyElectronStartup", () => {
120121

121122
assert.equal(preference, "gnome-libsecret");
122123
});
124+
125+
describe("resolveLinuxDisableFeaturesSwitch", () => {
126+
const hyprland = { XDG_CURRENT_DESKTOP: "Hyprland", XDG_SESSION_TYPE: "wayland" };
127+
const releaseCommits: Record<string, string> = {
128+
"0.48.0": "5ee35f914f921e5696030698e74fb5566a804768",
129+
"0.48.1": "29e2e59fdbab8ed2cc23a20e3c6043d5decb5cdc",
130+
"0.49.0": "9958d297641b5c84dcff93f9039d80a5ad37ab00",
131+
"0.50.0": "c4a4c341568944bd4fb9cd503558b2de602c0213",
132+
"0.50.1": "4e242d086e20b32951fdc0ebcbfb4d41b5be8dcc",
133+
};
134+
// Shape of `hyprctl version -j` from a release tarball, trimmed to the fields we read.
135+
const hyprctlVersion =
136+
(version: string, overrides: Record<string, unknown> = {}) =>
137+
() =>
138+
JSON.stringify({
139+
commit: releaseCommits[version] ?? "0123456789abcdef0123456789abcdef01234567",
140+
version,
141+
dirty: false,
142+
tag: `v${version}`,
143+
flags: [],
144+
...overrides,
145+
});
146+
const brokenHyprland = {
147+
env: hyprland,
148+
ozonePlatform: "wayland",
149+
enableFeatures: null,
150+
disableFeatures: null,
151+
readHyprctlVersionJson: hyprctlVersion("0.48.1"),
152+
};
153+
154+
it.each(["0.48.0", "0.48.1", "0.49.0", "0.50.0", "0.50.1"])(
155+
"disables the color manager on Hyprland %s",
156+
(version) => {
157+
assert.equal(
158+
resolveLinuxDisableFeaturesSwitch({
159+
...brokenHyprland,
160+
readHyprctlVersionJson: hyprctlVersion(version),
161+
}),
162+
"WaylandWpColorManagerV1",
163+
);
164+
},
165+
);
166+
167+
it.each([
168+
{ label: "case-insensitive desktop tokens", env: { XDG_CURRENT_DESKTOP: "foo:HYPRLAND" } },
169+
{
170+
label: "XDG_SESSION_DESKTOP when XDG_CURRENT_DESKTOP is unset",
171+
env: { XDG_SESSION_DESKTOP: "hyprland" },
172+
},
173+
])("detects Hyprland via $label", ({ env }) => {
174+
assert.equal(
175+
resolveLinuxDisableFeaturesSwitch({ ...brokenHyprland, env }),
176+
"WaylandWpColorManagerV1",
177+
);
178+
});
179+
180+
it("keeps existing disabled features", () => {
181+
assert.equal(
182+
resolveLinuxDisableFeaturesSwitch({
183+
...brokenHyprland,
184+
disableFeatures: "Vulkan,WaylandWpColorManagerV1Extra",
185+
}),
186+
"Vulkan,WaylandWpColorManagerV1Extra,WaylandWpColorManagerV1",
187+
);
188+
});
189+
190+
it.each([
191+
{ label: "Hyprland 0.47.2", readHyprctlVersionJson: hyprctlVersion("0.47.2") },
192+
{ label: "Hyprland 0.51.0", readHyprctlVersionJson: hyprctlVersion("0.51.0") },
193+
{ label: "Hyprland 0.56.2", readHyprctlVersionJson: hyprctlVersion("0.56.2") },
194+
{ label: "an unrecognized version", readHyprctlVersionJson: hyprctlVersion("1.0.0") },
195+
{
196+
label: "a malformed version",
197+
readHyprctlVersionJson: hyprctlVersion("0.48.not-a-version"),
198+
},
199+
{ label: "a two-part version", readHyprctlVersionJson: hyprctlVersion("0.48") },
200+
{ label: "a prerelease version", readHyprctlVersionJson: hyprctlVersion("0.49.0-rc1") },
201+
{
202+
label: "a development build after an affected release",
203+
readHyprctlVersionJson: hyprctlVersion("0.50.1", { tag: "v0.50.1-87-g4e8875b5" }),
204+
},
205+
{
206+
label: "a later Nix build reporting an affected release tag",
207+
readHyprctlVersionJson: hyprctlVersion("0.50.1", {
208+
commit: "4e8875b5e9700c81ca4e169dc7b85bb5b3c8cb7a",
209+
}),
210+
},
211+
{
212+
label: "an affected version with another release's commit",
213+
readHyprctlVersionJson: hyprctlVersion("0.48.0", { commit: releaseCommits["0.48.1"] }),
214+
},
215+
{ label: "an unknown patch release", readHyprctlVersionJson: hyprctlVersion("0.50.2") },
216+
{
217+
label: "missing commit metadata",
218+
readHyprctlVersionJson: () =>
219+
JSON.stringify({ version: "0.48.1", tag: "v0.48.1", dirty: false }),
220+
},
221+
{
222+
label: "a build without git metadata",
223+
readHyprctlVersionJson: hyprctlVersion("0.48.1", { tag: "" }),
224+
},
225+
{
226+
label: "a dirty build",
227+
readHyprctlVersionJson: hyprctlVersion("0.48.1", { dirty: true }),
228+
},
229+
{
230+
label: "output without build metadata",
231+
readHyprctlVersionJson: () => JSON.stringify({ version: "0.48.1" }),
232+
},
233+
{ label: "unparseable hyprctl output", readHyprctlVersionJson: () => "ok" },
234+
{
235+
label: "a missing hyprctl",
236+
readHyprctlVersionJson: () => {
237+
throw new Error("spawnSync hyprctl ENOENT");
238+
},
239+
},
240+
])("preserves color management for $label", (overrides) => {
241+
assert.isNull(resolveLinuxDisableFeaturesSwitch({ ...brokenHyprland, ...overrides }));
242+
});
243+
244+
it.each([
245+
{
246+
label: "a stale Hyprland signature in KDE",
247+
env: { XDG_CURRENT_DESKTOP: "KDE", HYPRLAND_INSTANCE_SIGNATURE: "abc_123" },
248+
},
249+
{
250+
label: "resolved X11 in a Wayland session",
251+
ozonePlatform: "x11",
252+
env: { ...hyprland, WAYLAND_DISPLAY: "wayland-1" },
253+
},
254+
{ label: "a missing platform", ozonePlatform: null },
255+
{ label: "an unrelated platform", ozonePlatform: "headless" },
256+
{ label: "already disabled features", disableFeatures: "Vulkan, WaylandWpColorManagerV1" },
257+
{ label: "an explicitly enabled feature", enableFeatures: "Vulkan,WaylandWpColorManagerV1" },
258+
{
259+
label: "a feature enabled with trial parameters",
260+
enableFeatures: "*WaylandWpColorManagerV1<Study.Group:mode/strict",
261+
},
262+
])("skips the Hyprland probe for $label", (overrides) => {
263+
assert.isNull(
264+
resolveLinuxDisableFeaturesSwitch({
265+
...brokenHyprland,
266+
...overrides,
267+
readHyprctlVersionJson: () => assert.fail("hyprctl should not run"),
268+
}),
269+
);
270+
});
271+
});
123272
});

‎apps/desktop/src/app/DesktopEarlyElectronStartup.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,78 @@ export function resolveEarlyLinuxPasswordStorePreference(
8181
}
8282
}
8383

84+
const WAYLAND_COLOR_MANAGER_FEATURE = "WaylandWpColorManagerV1";
85+
86+
// Chromium feature entries look like `*Name<Trial.Group:param/value`.
87+
const featureListNames = (value: string | null): ReadonlyArray<string> =>
88+
(value ?? "").split(",").flatMap((entry) => entry.trim().match(/^\*?([^<:]+)/)?.[1] ?? []);
89+
90+
// A stale HYPRLAND_INSTANCE_SIGNATURE is often inherited into other desktops, so only
91+
// trust the session's desktop names.
92+
const isHyprlandSession = (env: NodeJS.ProcessEnv): boolean =>
93+
(trimNonEmpty(env.XDG_CURRENT_DESKTOP) ?? trimNonEmpty(env.XDG_SESSION_DESKTOP) ?? "")
94+
.split(":")
95+
.some((name) => name.trim().toLowerCase() === "hyprland");
96+
97+
const HyprctlVersionJson = fromLenientJson(
98+
Schema.Struct({
99+
version: Schema.String,
100+
tag: Schema.String,
101+
commit: Schema.String,
102+
dirty: Schema.Boolean,
103+
}),
104+
);
105+
const decodeHyprctlVersionJson = Schema.decodeSync(HyprctlVersionJson);
106+
107+
// Hyprland 0.48.0 added wp-color-manager-v1 (hyprwm/Hyprland#9444) advertising
108+
// FEATURE_SET_PRIMARIES while rejecting set_primaries outside debug mode, which Chromium
109+
// treats as a fatal protocol error. 0.51.0 accepts them (hyprwm/Hyprland#11499).
110+
// Only stock builds of these releases count. Matching the commit as well as the tag
111+
// keeps later development builds that still report an old VERSION and tag (Nix
112+
// synthesizes `v${VERSION}`) on the default.
113+
const AFFECTED_HYPRLAND_RELEASE_COMMITS: ReadonlyMap<string, string> = new Map([
114+
["0.48.0", "5ee35f914f921e5696030698e74fb5566a804768"],
115+
["0.48.1", "29e2e59fdbab8ed2cc23a20e3c6043d5decb5cdc"],
116+
["0.49.0", "9958d297641b5c84dcff93f9039d80a5ad37ab00"],
117+
["0.50.0", "c4a4c341568944bd4fb9cd503558b2de602c0213"],
118+
["0.50.1", "4e242d086e20b32951fdc0ebcbfb4d41b5be8dcc"],
119+
]);
120+
121+
const hasBrokenColorManager = (readHyprctlVersionJson: () => string): boolean => {
122+
try {
123+
const { version, tag, commit, dirty } = decodeHyprctlVersionJson(readHyprctlVersionJson());
124+
return (
125+
AFFECTED_HYPRLAND_RELEASE_COMMITS.get(version) === commit && tag === `v${version}` && !dirty
126+
);
127+
} catch {
128+
return false;
129+
}
130+
};
131+
132+
/**
133+
* Returns the merged `--disable-features` value to apply before Electron is ready,
134+
* or null to leave the command line alone. `ozonePlatform` is the resolved
135+
* `--ozone-platform` value; Electron fills it in before app code runs.
136+
*
137+
* Color management stays on unless `hyprctl version -j` identifies a stock Hyprland
138+
* release known to kill the native Wayland connection right after the first window opens.
139+
*/
140+
export function resolveLinuxDisableFeaturesSwitch(input: {
141+
readonly env: NodeJS.ProcessEnv;
142+
readonly ozonePlatform: string | null;
143+
readonly enableFeatures: string | null;
144+
readonly disableFeatures: string | null;
145+
readonly readHyprctlVersionJson: () => string;
146+
}): string | null {
147+
if (input.ozonePlatform !== "wayland" || !isHyprlandSession(input.env)) return null;
148+
if (featureListNames(input.enableFeatures).includes(WAYLAND_COLOR_MANAGER_FEATURE)) return null;
149+
if (featureListNames(input.disableFeatures).includes(WAYLAND_COLOR_MANAGER_FEATURE)) return null;
150+
if (!hasBrokenColorManager(input.readHyprctlVersionJson)) return null;
151+
return input.disableFeatures === null
152+
? WAYLAND_COLOR_MANAGER_FEATURE
153+
: `${input.disableFeatures},${WAYLAND_COLOR_MANAGER_FEATURE}`;
154+
}
155+
84156
export function resolveEarlyLinuxElectronOptions(
85157
input: EarlyLinuxElectronOptionsInput,
86158
): EarlyLinuxElectronOptions {

‎apps/desktop/src/app/DesktopPreReadyPlatform.test.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
mkdirSyncMock,
1515
writeFileSyncMock,
1616
copyFileSyncMock,
17+
execFileSyncMock,
1718
} = vi.hoisted(() => ({
1819
appendSwitchMock: vi.fn(),
1920
getSwitchValueMock: vi.fn(),
@@ -23,6 +24,7 @@ const {
2324
mkdirSyncMock: vi.fn(),
2425
writeFileSyncMock: vi.fn(),
2526
copyFileSyncMock: vi.fn(),
27+
execFileSyncMock: vi.fn(),
2628
}));
2729

2830
vi.mock("electron", () => ({
@@ -49,6 +51,10 @@ vi.mock("node:fs", () => ({
4951
copyFileSync: copyFileSyncMock,
5052
}));
5153

54+
vi.mock("node:child_process", () => ({
55+
execFileSync: execFileSyncMock,
56+
}));
57+
5258
import * as DesktopPreReadyPlatform from "./DesktopPreReadyPlatform.ts";
5359

5460
describe("DesktopPreReadyPlatform", () => {
@@ -61,6 +67,7 @@ describe("DesktopPreReadyPlatform", () => {
6167
mkdirSyncMock.mockReset();
6268
writeFileSyncMock.mockReset();
6369
copyFileSyncMock.mockReset();
70+
execFileSyncMock.mockReset();
6471
});
6572

6673
it.effect("preserves an explicit Linux password-store switch", () => {
@@ -81,6 +88,57 @@ describe("DesktopPreReadyPlatform", () => {
8188
);
8289
});
8390

91+
it.effect.each([
92+
{
93+
label: "merges the color-manager fallback into disabled features on Hyprland 0.48.1",
94+
switches: { "ozone-platform": "wayland", "disable-features": "Vulkan" },
95+
hyprlandVersion: "0.48.1",
96+
hyprlandCommit: "29e2e59fdbab8ed2cc23a20e3c6043d5decb5cdc",
97+
expected: [["disable-features", "Vulkan,WaylandWpColorManagerV1"]],
98+
},
99+
{
100+
label: "preserves color management on Hyprland 0.51.0",
101+
switches: { "ozone-platform": "wayland", "disable-features": "Vulkan" },
102+
hyprlandVersion: "0.51.0",
103+
hyprlandCommit: "0123456789abcdef0123456789abcdef01234567",
104+
expected: [],
105+
},
106+
{
107+
label: "preserves color management when Electron resolved X11",
108+
switches: { "ozone-platform": "x11", "disable-features": "Vulkan" },
109+
hyprlandVersion: "0.48.1",
110+
hyprlandCommit: "29e2e59fdbab8ed2cc23a20e3c6043d5decb5cdc",
111+
expected: [],
112+
},
113+
])("$label", ({ switches, hyprlandVersion, hyprlandCommit, expected }) => {
114+
vi.stubEnv("XDG_CURRENT_DESKTOP", "Hyprland");
115+
vi.stubEnv("XDG_SESSION_TYPE", "wayland");
116+
const commandLine = new Map<string, string>(Object.entries(switches));
117+
hasSwitchMock.mockImplementation((switchName: string) => commandLine.has(switchName));
118+
getSwitchValueMock.mockImplementation(
119+
(switchName: string) => commandLine.get(switchName) ?? "",
120+
);
121+
execFileSyncMock.mockReturnValue(
122+
JSON.stringify({
123+
commit: hyprlandCommit,
124+
version: hyprlandVersion,
125+
dirty: false,
126+
tag: `v${hyprlandVersion}`,
127+
}),
128+
);
129+
130+
return DesktopPreReadyPlatform.make.pipe(
131+
Effect.provideService(HostProcess.Platform, "linux"),
132+
Effect.map(() => {
133+
const featureSwitches = appendSwitchMock.mock.calls.filter(([name]) =>
134+
["disable-features", "enable-features", "ozone-platform"].includes(name),
135+
);
136+
assert.deepEqual(featureSwitches, expected);
137+
}),
138+
Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())),
139+
);
140+
});
141+
84142
it.effect.each([
85143
{ previousEntry: undefined, label: "missing" },
86144
{ previousEntry: 'Exec="/Applications/deleted-previous.AppImage" %U', label: "stale" },

‎apps/desktop/src/app/DesktopPreReadyPlatform.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// @effect-diagnostics nodeBuiltinImport:off - pre-ready Electron setup reads settings and prepares the Linux desktop entry synchronously before app services are available.
1+
// @effect-diagnostics nodeBuiltinImport:off - pre-ready Electron setup reads settings, prepares the Linux desktop entry, and asks Hyprland for its version synchronously before app services are available.
2+
import * as NodeChildProcess from "node:child_process";
23
import * as NodeFS from "node:fs";
34
import * as NodePath from "node:path";
45
import * as Context from "effect/Context";
@@ -115,6 +116,24 @@ export const make = Effect.gen(function* () {
115116
if (linux.passwordStore !== null && linuxPasswordStoreCommandLine === null) {
116117
Electron.app.commandLine.appendSwitch("password-store", linux.passwordStore);
117118
}
119+
// Chromium keeps only the last --disable-features value, so pass the merged list.
120+
const disableFeatures = DesktopEarlyElectronStartup.resolveLinuxDisableFeaturesSwitch({
121+
env: process.env,
122+
ozonePlatform: readCommandLineSwitchValue(Electron.app.commandLine, "ozone-platform"),
123+
enableFeatures: readCommandLineSwitchValue(Electron.app.commandLine, "enable-features"),
124+
disableFeatures: readCommandLineSwitchValue(Electron.app.commandLine, "disable-features"),
125+
// Runs only on Hyprland Wayland sessions without an explicit feature override.
126+
readHyprctlVersionJson: () =>
127+
NodeChildProcess.execFileSync("hyprctl", ["version", "-j"], {
128+
encoding: "utf8",
129+
stdio: ["ignore", "pipe", "ignore"],
130+
timeout: 1000,
131+
maxBuffer: 64 * 1024,
132+
}),
133+
});
134+
if (disableFeatures !== null) {
135+
Electron.app.commandLine.appendSwitch("disable-features", disableFeatures);
136+
}
118137
}
119138

120139
return { linux, linuxPasswordStoreCommandLine };

0 commit comments

Comments
 (0)