1import { DesktopWslStateSchema, type DesktopWslState } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
4import * as Schema from "effect/Schema";
5
6import * as DesktopLifecycle from "../../app/DesktopLifecycle.ts";
7import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts";
8import * as DesktopWslBackend from "../../wsl/DesktopWslBackend.ts";
9import * as DesktopWslEnvironment from "../../wsl/DesktopWslEnvironment.ts";
10import * as IpcChannels from "../channels.ts";
11import { makeIpcMethod } from "../DesktopIpc.ts";
12
13const readWslState: Effect.Effect<
14 DesktopWslState,
15 never,
16 | DesktopAppSettings.DesktopAppSettings
17 | DesktopWslEnvironment.DesktopWslEnvironment
18 | DesktopWslBackend.DesktopWslBackend
19> = Effect.gen(function* () {
20 const appSettings = yield* DesktopAppSettings.DesktopAppSettings;
21 const wslEnvironment = yield* DesktopWslEnvironment.DesktopWslEnvironment;
22 const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;
23 const settings = yield* appSettings.get;
24 const available = settings.localEnvironmentEnabled && (yield* wslEnvironment.isAvailable);
25 // Only enumerate distros when WSL is actually available — listDistros on a
26 // non-WSL host would spawn wsl.exe and hit the timeout for nothing.
27 const distros = available ? yield* wslEnvironment.listDistros : [];
28 const preflightError = yield* wslBackend.lastPreflightError;
29 return {
30 enabled: settings.wslBackendEnabled,
31 distro: settings.wslDistro,
32 available,
33 wslOnly: settings.wslOnly,
34 distros,
35 // Only the dual-mode secondary records this; a wsl-only failure surfaces via
36 // a dialog + Windows fallback, so it stays null there.
37 preflightError: settings.wslOnly ? null : Option.getOrNull(preflightError),
38 };
39});
40
41export const getWslState = makeIpcMethod({
42 channel: IpcChannels.GET_WSL_STATE_CHANNEL,
43 payload: Schema.Void,
44 result: DesktopWslStateSchema,
45 handler: Effect.fn("desktop.ipc.wsl.getState")(function* () {
46 return yield* readWslState;
47 }),
48});
49
50export const setWslBackendEnabled = makeIpcMethod({
51 channel: IpcChannels.SET_WSL_BACKEND_ENABLED_CHANNEL,
52 payload: Schema.Boolean,
53 result: DesktopWslStateSchema,
54 handler: Effect.fn("desktop.ipc.wsl.setEnabled")(function* (enabled) {
55 const appSettings = yield* DesktopAppSettings.DesktopAppSettings;
56 const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;
57 const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
58 const previousSettings = yield* appSettings.get;
59 const updateSettings = enabled
60 ? appSettings.setWslBackendEnabled(true)
61 : appSettings.applyWslWindowsFallback;
62 const change = yield* updateSettings;
63 const settings = yield* appSettings.get;
64 const changedWslOnlyPrimary = enabled
65 ? settings.wslOnly
66 : previousSettings.wslBackendEnabled && previousSettings.wslOnly;
67 if (changedWslOnlyPrimary && change.changed) {
68 const state = yield* readWslState;
69 yield* lifecycle.relaunch(`wslBackendEnabled=${enabled}`);
70 return state;
71 }
72 // Reconcile is idempotent and never fails; no need for a swap-style
73 // rollback when the WSL side has trouble coming up. With both
74 // backends running side by side, "WSL didn't start" is a transient
75 // state on one instance — the primary stays up either way.
76 yield* wslBackend.reconcile;
77 return yield* readWslState;
78 }),
79});
80
81export const setWslDistro = makeIpcMethod({
82 channel: IpcChannels.SET_WSL_DISTRO_CHANNEL,
83 payload: Schema.NullOr(Schema.String),
84 result: DesktopWslStateSchema,
85 handler: Effect.fn("desktop.ipc.wsl.setDistro")(function* (distro) {
86 const appSettings = yield* DesktopAppSettings.DesktopAppSettings;
87 const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;
88 const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
89 const change = yield* appSettings.setWslDistro(distro);
90 const settings = yield* appSettings.get;
91 // In active wsl-only mode the pool's primary IS the WSL backend, and its
92 // distro is captured when that backend starts, so relaunch to replace it.
93 // When WSL is disabled, this only stages a preference for the next enable.
94 if (settings.wslBackendEnabled && settings.wslOnly && change.changed) {
95 const state = yield* readWslState;
96 yield* lifecycle.relaunch(`wslDistro=${distro ?? "default"}`);
97 return state;
98 }
99 yield* wslBackend.reconcile;
100 return yield* readWslState;
101 }),
102});
103
104export const setWslOnly = makeIpcMethod({
105 channel: IpcChannels.SET_WSL_ONLY_CHANNEL,
106 payload: Schema.Boolean,
107 result: DesktopWslStateSchema,
108 handler: Effect.fn("desktop.ipc.wsl.setOnly")(function* (enabled) {
109 // wsl-only decides which backend the pool spins up as "primary", and that
110 // decision is captured once at layer init. A disabled WSL backend always
111 // leaves Windows primary active, so mode changes can be staged without a
112 // relaunch and applied by the subsequent enable call.
113 const appSettings = yield* DesktopAppSettings.DesktopAppSettings;
114 const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
115 const change = yield* appSettings.setWslOnly(enabled);
116 const state = yield* readWslState;
117 if (state.enabled && change.changed) {
118 yield* lifecycle.relaunch(`wslOnly=${enabled}`);
119 }
120 return state;
121 }),
122});