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
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopServerExposure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ describe("DesktopServerExposure", () => {
get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
setLinuxNativeWindowFrame: () => Effect.die("unexpected Linux window frame change"),
setServerExposureMode: () => Effect.fail(settingsFailure),
setTailscaleServe: () => Effect.fail(settingsFailure),
setUpdateChannel: () => Effect.die("unexpected update channel change"),
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ import {
getAppBranding,
getLocalEnvironmentBootstraps,
getLocalEnvironmentBearerToken,
getLinuxNativeWindowFrame,
getWindowFullscreenState,
openExternal,
pickFolder,
setTheme,
setLinuxNativeWindowFrame,
showContextMenu,
} from "./methods/window.ts";
import * as PreviewIpc from "./methods/preview.ts";
Expand All @@ -52,6 +54,8 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handleSync(getWindowFullscreenState);
yield* ipc.handleSync(getLocalEnvironmentBootstraps);
yield* ipc.handle(getLocalEnvironmentBearerToken);
yield* ipc.handle(getLinuxNativeWindowFrame);
yield* ipc.handle(setLinuxNativeWindowFrame);

yield* ipc.handle(getClientSettings);
yield* ipc.handle(setClientSettings);
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
export const MENU_ACTION_CHANNEL = "desktop:menu-action";
export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state";
export const GET_LINUX_NATIVE_WINDOW_FRAME_CHANNEL = "desktop:get-linux-native-window-frame";
export const SET_LINUX_NATIVE_WINDOW_FRAME_CHANNEL = "desktop:set-linux-native-window-frame";
export const UPDATE_STATE_CHANNEL = "desktop:update-state";
export const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
export const UPDATE_SET_CHANNEL_CHANNEL = "desktop:update-set-channel";
Expand Down
45 changes: 44 additions & 1 deletion apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ import type * as Electron from "electron";

import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts";
import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts";
import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import { getLocalEnvironmentBootstraps, getWindowFullscreenState } from "./window.ts";
import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts";
import {
getLinuxNativeWindowFrame,
getLocalEnvironmentBootstraps,
getWindowFullscreenState,
setLinuxNativeWindowFrame,
} from "./window.ts";

const readyWslConfig: DesktopBackendManager.DesktopBackendStartConfig = {
executablePath: "wsl.exe",
Expand Down Expand Up @@ -146,3 +153,39 @@ describe("getWindowFullscreenState", () => {
);
});
});

describe("Linux native window frame settings", () => {
it.effect("reads and changes the preference on Linux", () => {
const layer = Layer.mergeAll(
DesktopAppSettings.layerTest(),
Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
platform: "linux",
} as DesktopEnvironment.DesktopEnvironment["Service"]),
);

return Effect.gen(function* () {
assert.isFalse(yield* getLinuxNativeWindowFrame.handler(undefined));
assert.isTrue(yield* setLinuxNativeWindowFrame.handler(true));
assert.isTrue(yield* getLinuxNativeWindowFrame.handler(undefined));
}).pipe(Effect.provide(layer));
});

it.effect("reports the preference as unsupported outside Linux", () => {
const layer = Layer.mergeAll(
DesktopAppSettings.layerTest({
...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
linuxNativeWindowFrame: true,
}),
Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
platform: "darwin",
} as DesktopEnvironment.DesktopEnvironment["Service"]),
);

return Effect.gen(function* () {
assert.isNull(yield* getLinuxNativeWindowFrame.handler(undefined));
assert.isFalse(yield* setLinuxNativeWindowFrame.handler(false));
const settings = yield* DesktopAppSettings.DesktopAppSettings;
assert.isTrue((yield* settings.get).linuxNativeWindowFrame);
}).pipe(Effect.provide(layer));
});
});
28 changes: 28 additions & 0 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,34 @@ export const getWindowFullscreenState = DesktopIpc.makeSyncIpcMethod({
}),
});

export const getLinuxNativeWindowFrame = DesktopIpc.makeIpcMethod({
channel: IpcChannels.GET_LINUX_NATIVE_WINDOW_FRAME_CHANNEL,
payload: Schema.Void,
result: Schema.NullOr(Schema.Boolean),
handler: Effect.fn("desktop.ipc.window.getLinuxNativeWindowFrame")(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
if (environment.platform !== "linux") {
return null;
}
const settings = yield* DesktopAppSettings.DesktopAppSettings;
return (yield* settings.get).linuxNativeWindowFrame;
}),
});

export const setLinuxNativeWindowFrame = DesktopIpc.makeIpcMethod({
channel: IpcChannels.SET_LINUX_NATIVE_WINDOW_FRAME_CHANNEL,
payload: Schema.Boolean,
result: Schema.Boolean,
handler: Effect.fn("desktop.ipc.window.setLinuxNativeWindowFrame")(function* (enabled) {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
if (environment.platform !== "linux") {
return false;
}
const settings = yield* DesktopAppSettings.DesktopAppSettings;
return (yield* settings.setLinuxNativeWindowFrame(enabled)).settings.linuxNativeWindowFrame;
}),
});

export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({
channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL,
result: Schema.Array(DesktopEnvironmentBootstrapSchema),
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ contextBridge.exposeInMainWorld("desktopBridge", {
ipcRenderer.removeListener(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener);
};
},
getLinuxNativeWindowFrame: () =>
ipcRenderer.invoke(IpcChannels.GET_LINUX_NATIVE_WINDOW_FRAME_CHANNEL),
setLinuxNativeWindowFrame: (enabled: boolean) =>
ipcRenderer.invoke(IpcChannels.SET_LINUX_NATIVE_WINDOW_FRAME_CHANNEL, enabled),
getUpdateState: () => ipcRenderer.invoke(IpcChannels.UPDATE_GET_STATE_CHANNEL),
setUpdateChannel: (channel) =>
ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, channel),
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/settings/DesktopAppSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
import * as DesktopAppSettings from "./DesktopAppSettings.ts";

const DesktopSettingsPatch = Schema.Struct({
linuxNativeWindowFrame: Schema.optionalKey(Schema.Boolean),
linuxPasswordStore: Schema.optionalKey(
Schema.Literals(["auto", "gnome-libsecret", "kwallet", "kwallet5", "kwallet6"]),
),
Expand Down Expand Up @@ -105,6 +106,7 @@ describe("DesktopSettings", () => {
assert.deepEqual(
DesktopAppSettings.resolveDefaultDesktopSettings("0.0.17-nightly.20260415.1"),
{
linuxNativeWindowFrame: false,
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowMaximized: false,
Expand All @@ -125,6 +127,7 @@ describe("DesktopSettings", () => {
Effect.gen(function* () {
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* writeSettingsPatch({
linuxNativeWindowFrame: true,
linuxPasswordStore: "gnome-libsecret",
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
Expand All @@ -134,6 +137,7 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
linuxNativeWindowFrame: true,
linuxPasswordStore: "gnome-libsecret",
mainWindowBounds: null,
mainWindowMaximized: false,
Expand Down Expand Up @@ -192,6 +196,9 @@ describe("DesktopSettings", () => {
Effect.gen(function* () {
const settings = yield* DesktopAppSettings.DesktopAppSettings;

const nativeFrame = yield* settings.setLinuxNativeWindowFrame(false);
assert.isFalse(nativeFrame.changed);

const exposure = yield* settings.setServerExposureMode("local-only");
assert.isFalse(exposure.changed);

Expand Down Expand Up @@ -241,6 +248,7 @@ describe("DesktopSettings", () => {
);

assert.deepEqual(yield* settings.load, {
linuxNativeWindowFrame: false,
linuxPasswordStore: "auto",
mainWindowBounds: { x: 120, y: 80, width: 1280, height: 900 },
mainWindowMaximized: false,
Expand Down Expand Up @@ -297,6 +305,7 @@ describe("DesktopSettings", () => {
);

assert.deepEqual(yield* settings.load, {
linuxNativeWindowFrame: false,
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowMaximized: false,
Expand All @@ -320,13 +329,15 @@ describe("DesktopSettings", () => {
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopAppSettings.DesktopAppSettings;

yield* settings.setLinuxNativeWindowFrame(true);
yield* settings.setMainWindowBounds({ x: -1200, y: 40, width: 1440, height: 960 }, true);
yield* settings.setServerExposureMode("network-accessible");

const persisted = yield* decodeDesktopSettingsPatch(
yield* fileSystem.readFileString(environment.desktopSettingsPath),
);
assert.deepEqual(persisted, {
linuxNativeWindowFrame: true,
mainWindowBounds: { x: -1200, y: 40, width: 1440, height: 960 },
mainWindowMaximized: true,
serverExposureMode: "network-accessible",
Expand All @@ -345,6 +356,7 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
linuxNativeWindowFrame: false,
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowMaximized: false,
Expand Down Expand Up @@ -373,6 +385,7 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
linuxNativeWindowFrame: false,
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowMaximized: false,
Expand Down Expand Up @@ -400,6 +413,7 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
linuxNativeWindowFrame: false,
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowMaximized: false,
Expand Down
27 changes: 27 additions & 0 deletions apps/desktop/src/settings/DesktopAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { resolveDefaultDesktopUpdateChannel } from "../updates/updateChannels.ts
import { isValidDistroName } from "../wsl/wslPathParsing.ts";

export interface DesktopSettings {
readonly linuxNativeWindowFrame: boolean;
readonly linuxPasswordStore: LinuxPasswordStorePreference;
readonly mainWindowBounds: DesktopWindowBounds | null;
readonly mainWindowMaximized: boolean;
Expand Down Expand Up @@ -73,6 +74,7 @@ export const DEFAULT_MAIN_WINDOW_SIZE = {
} as const;

export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
linuxNativeWindowFrame: false,
linuxPasswordStore: DEFAULT_LINUX_PASSWORD_STORE,
mainWindowBounds: null,
mainWindowMaximized: false,
Expand All @@ -94,6 +96,7 @@ const DesktopWindowBoundsDocument = Schema.Struct({
});

const DesktopSettingsDocument = Schema.Struct({
linuxNativeWindowFrame: Schema.optionalKey(Schema.Boolean),
linuxPasswordStore: Schema.optionalKey(Schema.Unknown),
mainWindowBounds: Schema.optionalKey(Schema.NullOr(DesktopWindowBoundsDocument)),
mainWindowMaximized: Schema.optionalKey(Schema.Boolean),
Expand Down Expand Up @@ -156,6 +159,9 @@ export class DesktopAppSettings extends Context.Service<
bounds: DesktopWindowBounds,
isMaximized: boolean,
) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;
readonly setLinuxNativeWindowFrame: (
enabled: boolean,
) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;
readonly setServerExposureMode: (
mode: DesktopServerExposureMode,
) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;
Expand Down Expand Up @@ -224,6 +230,7 @@ function normalizeDesktopSettingsDocument(
(parsed.wslBackendEnabled === undefined && parsed.wslMode === "wsl");

return {
linuxNativeWindowFrame: parsed.linuxNativeWindowFrame === true,
linuxPasswordStore: normalizeLinuxPasswordStorePreference(parsed.linuxPasswordStore),
mainWindowBounds,
mainWindowMaximized: mainWindowBounds !== null && parsed.mainWindowMaximized === true,
Expand All @@ -247,6 +254,9 @@ function toDesktopSettingsDocument(
): DesktopSettingsDocument {
const document: Mutable<DesktopSettingsDocument> = {};

if (settings.linuxNativeWindowFrame !== defaults.linuxNativeWindowFrame) {
document.linuxNativeWindowFrame = settings.linuxNativeWindowFrame;
}
if (settings.linuxPasswordStore !== defaults.linuxPasswordStore) {
document.linuxPasswordStore = settings.linuxPasswordStore;
}
Expand Down Expand Up @@ -312,6 +322,15 @@ function setMainWindowBounds(
};
}

function setLinuxNativeWindowFrame(settings: DesktopSettings, enabled: boolean): DesktopSettings {
return settings.linuxNativeWindowFrame === enabled
? settings
: {
...settings,
linuxNativeWindowFrame: enabled,
};
}

function setTailscaleServe(
settings: DesktopSettings,
input: { readonly enabled: boolean; readonly port: Option.Option<number> },
Expand Down Expand Up @@ -518,6 +537,12 @@ export const make = Effect.gen(function* () {
},
}),
),
setLinuxNativeWindowFrame: (enabled) =>
persist((settings) => setLinuxNativeWindowFrame(settings, enabled)).pipe(
Effect.withSpan("desktop.settings.setLinuxNativeWindowFrame", {
attributes: { enabled },
}),
),
setServerExposureMode: (mode) =>
persist((settings) => setServerExposureMode(settings, mode)).pipe(
Effect.withSpan("desktop.settings.setServerExposureMode", { attributes: { mode } }),
Expand Down Expand Up @@ -577,6 +602,8 @@ export const layerTest = (initialSettings: DesktopSettings = DEFAULT_DESKTOP_SET
load: SynchronizedRef.get(settingsRef),
setMainWindowBounds: (bounds, isMaximized) =>
update((settings) => setMainWindowBounds(settings, bounds, isMaximized)),
setLinuxNativeWindowFrame: (enabled) =>
update((settings) => setLinuxNativeWindowFrame(settings, enabled)),
setServerExposureMode: (mode) =>
update((settings) => setServerExposureMode(settings, mode)),
setTailscaleServe: (input) => update((settings) => setTailscaleServe(settings, input)),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/updates/DesktopUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
setLinuxNativeWindowFrame: () => Effect.die("unexpected Linux window frame change"),
setServerExposureMode: () => Effect.die("unexpected server exposure update"),
setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"),
setUpdateChannel: () => Effect.fail(setUpdateChannelError),
Expand Down
23 changes: 23 additions & 0 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ function makeTestLayer(input: {
}
return { settings: desktopSettings, changed };
}),
setLinuxNativeWindowFrame: () => Effect.die("unexpected Linux window frame change"),
setServerExposureMode: () => Effect.die("unexpected server exposure update"),
setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"),
setUpdateChannel: () => Effect.die("unexpected update channel change"),
Expand Down Expand Up @@ -367,6 +368,28 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n
});

describe("DesktopWindow", () => {
it("uses native window decorations only when the Linux preference is enabled", () => {
assert.deepEqual(DesktopWindow.resolveWindowTitleBarOptions(false, "linux", true), {
frame: true,
});
assert.deepEqual(DesktopWindow.resolveWindowTitleBarOptions(false, "win32", true), {
titleBarStyle: "hidden",
titleBarOverlay: {
color: "#01000000",
height: 40,
symbolColor: "#1f2937",
},
});
assert.deepEqual(DesktopWindow.resolveWindowTitleBarOptions(false, "linux", false), {
titleBarStyle: "hidden",
titleBarOverlay: {
color: "#01000000",
height: 40,
symbolColor: "#1f2937",
},
});
});

it("restores bounds only when the window fits within a connected display", () => {
const persistedBounds = { x: 2040, y: 80, width: 1320, height: 880 };
const displays = [
Expand Down
Loading
Loading