| 1 | import { |
| 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 | */ |
| 15 | export 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. */ |
| 34 | export function canRevokeOtherClients( |
| 35 | clientSessions: ReadonlyArray<{ readonly current: boolean }> | null, |
| 36 | ): boolean { |
| 37 | return clientSessions === null || clientSessions.some((session) => !session.current); |
| 38 | } |
| 39 | |
| 40 | type 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 | */ |
| 47 | export function isQrShareableEndpoint(endpoint: AdvertisedEndpoint): boolean { |
| 48 | return endpoint.status !== "unavailable" && endpoint.reachability !== "loopback"; |
| 49 | } |
| 50 | |
| 51 | export 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 | |
| 59 | export 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 | */ |
| 77 | export 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 | |
| 93 | export 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 | } |