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
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
pickFolder,
pickProjectFavicon,
pickThemeFiles,
restartApp,
setTheme,
showContextMenu,
} from "./methods/window.ts";
Expand Down Expand Up @@ -123,6 +124,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(openExternal);
yield* ipc.handle(openSystemSettings);
yield* ipc.handle(probeRemoteEditors);
yield* ipc.handle(restartApp);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const SNAP_SHOT_EVENT_CHANNEL = "desktop:snap-shot-event";
export const QUIT_SHORTCUT_CHANNEL = "desktop:quit-shortcut";
export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state";
export const RESTART_APP_CHANNEL = "desktop:restart-app";
export const DESKTOP_APP_ACTIVATION_READY_CHANNEL = "desktop:app-activation-ready";
export const DESKTOP_APP_ACTIVATION_COMPLETE_CHANNEL = "desktop:app-activation-complete";
export const DESKTOP_APP_ACTIVATION_REQUEST_CHANNEL = "desktop:app-activation-request";
Expand Down
51 changes: 51 additions & 0 deletions apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ import { vi } from "vite-plus/test";

import type * as Electron from "electron";

import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";
import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts";
import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts";
import * as DesktopLifecycle from "../../app/DesktopLifecycle.ts";
import * as DesktopShutdown from "../../app/DesktopShutdown.ts";
import * as DesktopState from "../../app/DesktopState.ts";
import * as ElectronApp from "../../electron/ElectronApp.ts";
import * as ElectronDialog from "../../electron/ElectronDialog.ts";
import * as ElectronTheme from "../../electron/ElectronTheme.ts";
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import * as DesktopWindow from "../../window/DesktopWindow.ts";
import {
getLocalEnvironmentBootstraps,
getWindowFullscreenState,
pickProjectFavicon,
restartApp,
} from "./window.ts";

const readyWslConfig: DesktopBackendManager.DesktopBackendStartConfig = {
Expand Down Expand Up @@ -153,6 +161,49 @@ describe("getWindowFullscreenState", () => {
});
});

describe("restartApp", () => {
it.effect("requests a graceful command-palette relaunch", () => {
const relaunchReasons: string[] = [];
const lifecycleLayer = Layer.succeed(
DesktopLifecycle.DesktopLifecycle,
DesktopLifecycle.DesktopLifecycle.of({
relaunch: (reason) =>
Effect.sync(() => {
relaunchReasons.push(reason);
}),
register: Effect.void,
}),
);
const unusedRuntimeLayer = Layer.mergeAll(
DesktopShutdown.layer,
DesktopState.layer,
Layer.succeed(
DesktopEnvironment.DesktopEnvironment,
DesktopEnvironment.DesktopEnvironment.of(
{} as DesktopEnvironment.DesktopEnvironment["Service"],
),
),
Layer.succeed(
DesktopWindow.DesktopWindow,
DesktopWindow.DesktopWindow.of({} as DesktopWindow.DesktopWindow["Service"]),
),
Layer.succeed(
ElectronApp.ElectronApp,
ElectronApp.ElectronApp.of({} as ElectronApp.ElectronApp["Service"]),
),
Layer.succeed(
ElectronTheme.ElectronTheme,
ElectronTheme.ElectronTheme.of({} as ElectronTheme.ElectronTheme["Service"]),
),
);

return Effect.gen(function* () {
yield* restartApp.handler(undefined);
assert.deepEqual(relaunchReasons, ["command-palette"]);
}).pipe(Effect.provide(Layer.merge(lifecycleLayer, unusedRuntimeLayer)));
});
});

describe("pickProjectFavicon", () => {
it.effect("opens a single-image picker from the project directory", () =>
Effect.gen(function* () {
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import * as Schema from "effect/Schema";
import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts";
import * as DesktopLocalEnvironmentAuth from "../../backend/DesktopLocalEnvironmentAuth.ts";
import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";
import * as DesktopLifecycle from "../../app/DesktopLifecycle.ts";
import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts";
import * as DesktopWslBackend from "../../wsl/DesktopWslBackend.ts";
import * as DesktopWslEnvironment from "../../wsl/DesktopWslEnvironment.ts";
Expand Down Expand Up @@ -86,6 +87,16 @@ export const getWindowFullscreenState = DesktopIpc.makeSyncIpcMethod({
}),
});

export const restartApp = DesktopIpc.makeIpcMethod({
channel: IpcChannels.RESTART_APP_CHANNEL,
payload: Schema.Void,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.window.restartApp")(function* () {
const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
yield* lifecycle.relaunch("command-palette");
}),
});

export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({
channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL,
result: Schema.Array(DesktopEnvironmentBootstrapSchema),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {
ipcRenderer.removeListener(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener);
};
},
restartApp: () => ipcRenderer.invoke(IpcChannels.RESTART_APP_CHANNEL),
getUpdateState: () => ipcRenderer.invoke(IpcChannels.UPDATE_GET_STATE_CHANNEL),
setUpdateChannel: (channel) =>
ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, channel),
Expand Down
17 changes: 16 additions & 1 deletion apps/desktop/src/settings/DesktopAppSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const DesktopSettingsPatch = Schema.Struct({
}),
),
),
mainWindowFullscreen: Schema.optionalKey(Schema.Boolean),
mainWindowMaximized: Schema.optionalKey(Schema.Boolean),
serverExposureMode: Schema.optionalKey(Schema.Literals(["local-only", "network-accessible"])),
tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean),
Expand Down Expand Up @@ -107,6 +108,7 @@ describe("DesktopSettings", () => {
{
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowFullscreen: false,
mainWindowMaximized: false,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
Expand Down Expand Up @@ -136,6 +138,7 @@ describe("DesktopSettings", () => {
assert.deepEqual(yield* settings.load, {
linuxPasswordStore: "gnome-libsecret",
mainWindowBounds: null,
mainWindowFullscreen: false,
mainWindowMaximized: false,
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
Expand Down Expand Up @@ -243,6 +246,7 @@ describe("DesktopSettings", () => {
assert.deepEqual(yield* settings.load, {
linuxPasswordStore: "auto",
mainWindowBounds: { x: 120, y: 80, width: 1280, height: 900 },
mainWindowFullscreen: false,
mainWindowMaximized: false,
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
Expand All @@ -263,12 +267,14 @@ describe("DesktopSettings", () => {
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* writeSettingsPatch({
mainWindowBounds: { x: 10.5, y: 20, width: 839, height: 620 },
mainWindowFullscreen: true,
mainWindowMaximized: true,
serverExposureMode: "network-accessible",
});

const loaded = yield* settings.load;
assert.isNull(loaded.mainWindowBounds);
assert.isFalse(loaded.mainWindowFullscreen);
assert.isFalse(loaded.mainWindowMaximized);
assert.equal(loaded.serverExposureMode, "network-accessible");
}),
Expand Down Expand Up @@ -299,6 +305,7 @@ describe("DesktopSettings", () => {
assert.deepEqual(yield* settings.load, {
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowFullscreen: false,
mainWindowMaximized: false,
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
Expand All @@ -320,14 +327,19 @@ describe("DesktopSettings", () => {
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopAppSettings.DesktopAppSettings;

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

const persisted = yield* decodeDesktopSettingsPatch(
yield* fileSystem.readFileString(environment.desktopSettingsPath),
);
assert.deepEqual(persisted, {
mainWindowBounds: { x: -1200, y: 40, width: 1440, height: 960 },
mainWindowFullscreen: true,
mainWindowMaximized: true,
serverExposureMode: "network-accessible",
} satisfies typeof DesktopSettingsPatch.Type);
Expand All @@ -347,6 +359,7 @@ describe("DesktopSettings", () => {
assert.deepEqual(yield* settings.load, {
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowFullscreen: false,
mainWindowMaximized: false,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
Expand Down Expand Up @@ -375,6 +388,7 @@ describe("DesktopSettings", () => {
assert.deepEqual(yield* settings.load, {
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowFullscreen: false,
mainWindowMaximized: false,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
Expand Down Expand Up @@ -402,6 +416,7 @@ describe("DesktopSettings", () => {
assert.deepEqual(yield* settings.load, {
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowFullscreen: false,
mainWindowMaximized: false,
serverExposureMode: "local-only",
tailscaleServeEnabled: true,
Expand Down
22 changes: 17 additions & 5 deletions apps/desktop/src/settings/DesktopAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { isValidDistroName } from "../wsl/wslPathParsing.ts";
export interface DesktopSettings {
readonly linuxPasswordStore: LinuxPasswordStorePreference;
readonly mainWindowBounds: DesktopWindowBounds | null;
readonly mainWindowFullscreen: boolean;
readonly mainWindowMaximized: boolean;
readonly serverExposureMode: DesktopServerExposureMode;
readonly tailscaleServeEnabled: boolean;
Expand Down Expand Up @@ -75,6 +76,7 @@ export const DEFAULT_MAIN_WINDOW_SIZE = {
export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
linuxPasswordStore: DEFAULT_LINUX_PASSWORD_STORE,
mainWindowBounds: null,
mainWindowFullscreen: false,
mainWindowMaximized: false,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
Expand All @@ -96,6 +98,7 @@ const DesktopWindowBoundsDocument = Schema.Struct({
const DesktopSettingsDocument = Schema.Struct({
linuxPasswordStore: Schema.optionalKey(Schema.Unknown),
mainWindowBounds: Schema.optionalKey(Schema.NullOr(DesktopWindowBoundsDocument)),
mainWindowFullscreen: Schema.optionalKey(Schema.Boolean),
mainWindowMaximized: Schema.optionalKey(Schema.Boolean),
serverExposureMode: Schema.optionalKey(DesktopServerExposureModeSchema),
tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean),
Expand Down Expand Up @@ -155,6 +158,7 @@ export class DesktopAppSettings extends Context.Service<
readonly setMainWindowBounds: (
bounds: DesktopWindowBounds,
isMaximized: boolean,
isFullscreen: boolean,
) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;
readonly setServerExposureMode: (
mode: DesktopServerExposureMode,
Expand Down Expand Up @@ -226,6 +230,7 @@ function normalizeDesktopSettingsDocument(
return {
linuxPasswordStore: normalizeLinuxPasswordStorePreference(parsed.linuxPasswordStore),
mainWindowBounds,
mainWindowFullscreen: mainWindowBounds !== null && parsed.mainWindowFullscreen === true,
mainWindowMaximized: mainWindowBounds !== null && parsed.mainWindowMaximized === true,
serverExposureMode:
parsed.serverExposureMode === "network-accessible" ? "network-accessible" : "local-only",
Expand Down Expand Up @@ -253,6 +258,9 @@ function toDesktopSettingsDocument(
if (settings.mainWindowBounds !== null) {
document.mainWindowBounds = settings.mainWindowBounds;
}
if (settings.mainWindowFullscreen) {
document.mainWindowFullscreen = true;
}
if (settings.mainWindowMaximized) {
document.mainWindowMaximized = true;
}
Expand Down Expand Up @@ -300,14 +308,17 @@ function setMainWindowBounds(
settings: DesktopSettings,
bounds: DesktopWindowBounds,
isMaximized: boolean,
isFullscreen: boolean,
): DesktopSettings {
return settings.mainWindowBounds !== null &&
desktopWindowBoundsEquivalence(settings.mainWindowBounds, bounds) &&
settings.mainWindowMaximized === isMaximized
settings.mainWindowMaximized === isMaximized &&
settings.mainWindowFullscreen === isFullscreen
? settings
: {
...settings,
mainWindowBounds: bounds,
mainWindowFullscreen: isFullscreen,
mainWindowMaximized: isMaximized,
};
}
Expand Down Expand Up @@ -507,14 +518,15 @@ export const make = Effect.gen(function* () {
);
return yield* SynchronizedRef.setAndGet(settingsRef, settings);
}).pipe(Effect.withSpan("desktop.settings.load")),
setMainWindowBounds: (bounds, isMaximized) =>
persist((settings) => setMainWindowBounds(settings, bounds, isMaximized)).pipe(
setMainWindowBounds: (bounds, isMaximized, isFullscreen) =>
persist((settings) => setMainWindowBounds(settings, bounds, isMaximized, isFullscreen)).pipe(
Effect.withSpan("desktop.settings.setMainWindowBounds", {
attributes: {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
isFullscreen,
isMaximized,
},
}),
Expand Down Expand Up @@ -576,8 +588,8 @@ export const layerTest = (initialSettings: DesktopSettings = DEFAULT_DESKTOP_SET
return DesktopAppSettings.of({
get: SynchronizedRef.get(settingsRef),
load: SynchronizedRef.get(settingsRef),
setMainWindowBounds: (bounds, isMaximized) =>
update((settings) => setMainWindowBounds(settings, bounds, isMaximized)),
setMainWindowBounds: (bounds, isMaximized, isFullscreen) =>
update((settings) => setMainWindowBounds(settings, bounds, isMaximized, isFullscreen)),
setServerExposureMode: (mode) =>
update((settings) => setServerExposureMode(settings, mode)),
setTailscaleServe: (input) => update((settings) => setTailscaleServe(settings, input)),
Expand Down
Loading
Loading