1import {
2 type AdvertisedEndpoint,
3 type AuthGrantScope,
4 AuthTerminalOperateScope,
5 AuthTerminalReadScope,
6 type DesktopBridge,
7 type DesktopWslState,
8} from "@t3tools/contracts";
9
10/**
11 * Operating terminals without being able to list them leaves a client
12 * allocating a fresh shell for every script run it cannot see, so the pairing
13 * form keeps `terminal:read` alongside `terminal:operate`.
14 */
15export function togglePairingScopeSelection(
16 current: ReadonlyArray<AuthGrantScope>,
17 scope: AuthGrantScope,
18 checked: boolean,
19): ReadonlyArray<AuthGrantScope> {
20 const without = (scopes: ReadonlyArray<AuthGrantScope>) =>
21 scopes.filter((currentScope) => currentScope !== scope);
22 if (!checked) {
23 return scope === AuthTerminalReadScope
24 ? without(current).filter((currentScope) => currentScope !== AuthTerminalOperateScope)
25 : without(current);
26 }
27 const next = [...without(current), scope];
28 return scope === AuthTerminalOperateScope && !next.includes(AuthTerminalReadScope)
29 ? [...next, AuthTerminalReadScope]
30 : next;
31}
32
33/** A missing access list says nothing about whether other clients exist. */
34export function canRevokeOtherClients(
35 clientSessions: ReadonlyArray<{ readonly current: boolean }> | null,
36): boolean {
37 return clientSessions === null || clientSessions.some((session) => !session.current);
38}
39
40type WslEnableBridge = Pick<DesktopBridge, "setWslBackendEnabled" | "setWslDistro" | "setWslOnly">;
41
42/**
43 * A QR code encoding a loopback URL makes the scanning device dial itself, so
44 * loopback endpoints stay copyable from the endpoint menu but are never
45 * offered as QR targets.
46 */
47export function isQrShareableEndpoint(endpoint: AdvertisedEndpoint): boolean {
48 return endpoint.status !== "unavailable" && endpoint.reachability !== "loopback";
49}
50
51export function isWslSettingsRowVisible(input: {
52 readonly state: DesktopWslState | null;
53 readonly error: string | null;
54}): boolean {
55 const { state, error } = input;
56 return state ? state.available || state.enabled || state.wslOnly : error !== null;
57}
58
59export type QrEndpointOption = {
60 /** Unique per endpoint instance (AdvertisedEndpoint.id); safe as a React key. */
61 readonly id: string;
62 /**
63 * Stable per endpoint *type* (endpointDefaultPreferenceKey). Multiple
64 * endpoints can share one, so it is only used to match the saved default.
65 */
66 readonly preferenceKey: string;
67 /** False for endpoints that stay copyable but must never render as a QR. */
68 readonly qrShareable: boolean;
69};
70
71/**
72 * Resolves which endpoint the share panel shows: the user's explicit pick,
73 * else the saved default endpoint, else the first QR-shareable option (so the
74 * panel never opens on a loopback QR), else the first option. A stale
75 * selectedId (endpoint disappeared) falls back rather than blanking the panel.
76 */
77export function selectQrEndpointOption<T extends QrEndpointOption>(
78 options: ReadonlyArray<T>,
79 selectedId: string | null,
80 defaultPreferenceKey: string | null,
81): T | null {
82 return (
83 (selectedId !== null ? options.find((option) => option.id === selectedId) : undefined) ??
84 (defaultPreferenceKey !== null
85 ? options.find((option) => option.preferenceKey === defaultPreferenceKey)
86 : undefined) ??
87 options.find((option) => option.qrShareable) ??
88 options[0] ??
89 null
90 );
91}
92
93export async function applyWslEnableSelection(input: {
94 readonly bridge: WslEnableBridge;
95 readonly mode: "both" | "wsl-only";
96 readonly nextDistro: string | null;
97 readonly persistedDistro: string | null;
98}): Promise<DesktopWslState> {
99 const { bridge, mode, nextDistro, persistedDistro } = input;
100
101 // Stage every preference before enabling. The desktop only relaunches for
102 // mode/distro changes while WSL is active, so the final enable observes the
103 // complete selection and is the only call that may relaunch.
104 await bridge.setWslOnly(mode === "wsl-only");
105 if (persistedDistro !== nextDistro) {
106 await bridge.setWslDistro(nextDistro);
107 }
108 return await bridge.setWslBackendEnabled(true);
109}