import { useQueryClient } from "@tanstack/react-query"; import { formatSize } from "@yaakapp-internal/lib/formatSize"; import type { HttpVersion, SystemProxyStatus, TrafficRetention } from "@yaakapp-internal/proxy-lib"; import { Button, Dialog, DialogFooter, Icon, IconTooltip, RadioDropdown, SettingRow, SettingRowBoolean, PlainInput, SettingRowSelect, SettingSelectControl, SettingSnippet, SettingsList, SettingsPage, SettingsSection, } from "@yaakapp-internal/ui"; import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai"; import { useState } from "react"; import { useRpcMutation } from "../hooks/useRpcMutation"; import { useRpcQuery } from "../hooks/useRpcQuery"; import { helperDialogOpenAtom } from "../lib/atoms"; import { confirmClearFlowsAtom } from "../lib/clearFlows"; import { defaultShell, environmentBlock, environmentVars, TERMINAL_SHELLS, type TerminalShell, } from "../lib/env"; import { KEEP_TRAFFIC_OPTIONS, sizeFromOptionValue, sizeOptionsFor, sizeOptionValue, } from "../lib/settings"; import { helperRow } from "../lib/systemProxy"; import { proxyStateAtom, refreshProxyState } from "../lib/store"; import { getOsType } from "../lib/tauri"; import { useShowToast } from "../lib/toast"; import { DecryptionSettings } from "./DecryptListEditor"; /** * The proxy's own settings tab: how it listens and the system proxy, what it decrypts, what * happens to captured traffic, and the environment for one terminal. Everything set once and * left alone, which is what left the state pill's menu. */ export function SettingsProxy() { const proxyState = useAtomValue(proxyStateAtom); const store = useStore(); const refresh = { onSettled: () => void refreshProxyState(store) }; const setHttpVersion = useRpcMutation("set_http_version", refresh); const setAtLaunch = useRpcMutation("set_system_proxy_at_launch", refresh); const systemProxy = useRpcQuery("system_proxy_status", {}); // Another process owning the engine wouldn't see changes until it restarts const elsewhere = proxyState?.otherOwner != null; return ( name="httpVersion" title="HTTP version" description="The change applies to new connections." value={proxyState?.httpVersion ?? "auto"} disabled={elsewhere} onChange={(httpVersion) => setHttpVersion.mutate({ httpVersion })} options={[ { label: "Automatic", value: "auto" }, { label: "HTTP/1.1 only", value: "http1Only" }, ]} /> {systemProxy.data?.supported === true && ( <> setAtLaunch.mutate({ enabled })} /> )} {proxyState?.traffic === "keep" && } ); } /** A port from 1 to 65535, or null */ function parsePort(text: string): number | null { if (!/^\d+$/.test(text.trim())) return null; const port = Number(text.trim()); return port > 0 && port < 65536 ? port : null; } /** * The port, saved when the field is left or Enter is pressed rather than on each keystroke: * every save restarts a running proxy, so typing 8080 must not restart it on 8, 80 and 808. */ function PortRow({ disabled }: { disabled: boolean }) { const proxyState = useAtomValue(proxyStateAtom); const store = useStore(); const showToast = useShowToast(); const saved = proxyState?.configuredPort ?? 9090; const [draft, setDraft] = useState(null); // Puts the saved port back in the field after a rejected or failed change const [resets, setResets] = useState(0); const reset = () => { setDraft(null); setResets((n) => n + 1); }; const setPort = useRpcMutation("set_port", { onError: (err) => showToast({ message: err.message, color: "danger" }), onSettled: () => { reset(); void refreshProxyState(store); }, }); const commit = () => { if (draft == null || setPort.isPending) return; const port = parsePort(draft); if (port == null || port === saved) { reset(); return; } setPort.mutate({ port }); }; return ( parsePort(v) != null} onChange={setDraft} onBlur={commit} onKeyDownCapture={(e) => { if (e.key === "Enter") commit(); }} /> ); } function HelperRow({ status }: { status: SystemProxyStatus }) { const queryClient = useQueryClient(); const openDialog = useSetAtom(helperDialogOpenAtom); const unregister = useRpcMutation("helper_unregister", { onSuccess: (status) => queryClient.setQueryData(["system_proxy_status", {}], status), }); const row = helperRow(status.helper, status.backend); return ( {unregister.error.message} ) : ( {row.description} ) } > {row.action != null && ( )} ); } /** Whether captured traffic is cleared as the app launches or kept between launches */ function TrafficRow() { const proxyState = useAtomValue(proxyStateAtom); const store = useStore(); const setTraffic = useRpcMutation("set_traffic", { onSettled: () => void refreshProxyState(store), }); return ( name="traffic" title="Recorded traffic" selectClassName="w-56!" value={proxyState?.traffic ?? "clearOnLaunch"} disabled={proxyState?.otherOwner != null} onChange={(traffic) => setTraffic.mutate({ traffic })} options={[ { label: "Clear on launch", value: "clearOnLaunch" }, { label: "Keep between launches", value: "keep" }, ]} /> ); } /** The most traffic kept between launches, beside what is stored now */ function KeepTrafficRow() { const proxyState = useAtomValue(proxyStateAtom); const store = useStore(); const size = useRpcQuery("get_storage_size", {}, { refetchInterval: 10_000 }); const setKeep = useRpcMutation("set_keep_traffic", { onSettled: () => void refreshProxyState(store), }); const keep = proxyState?.keepTrafficBytes ?? null; return (
{size.data != null && ( {formatSize(size.data.bytes)} used )} setKeep.mutate({ bytes: sizeFromOptionValue(v) })} options={sizeOptionsFor(KEEP_TRAFFIC_OPTIONS, keep).map(({ label, value }) => ({ label, value, }))} />
); } /** Asking before Clear, which its dialog's "Don't ask again" turns off */ function ConfirmClearRow() { const [confirm, setConfirm] = useAtom(confirmClearFlowsAtom); return ( ); } function ResetRow() { const [confirming, setConfirming] = useState(false); const store = useStore(); const queryClient = useQueryClient(); const showToast = useShowToast(); const reset = useRpcMutation("reset_captured_data", { onSuccess: () => { setConfirming(false); void queryClient.invalidateQueries(); showToast({ message: "Recorded data reset" }); }, onSettled: () => void refreshProxyState(store), }); return ( setConfirming(false)} size="sm" title="Reset Recorded Data" description="Permanently delete all traffic and its encryption keys?" > {reset.error != null &&

{reset.error.message}

} setConfirming(false) }, { label: "Reset", color: "danger", isLoading: reset.isPending, onClick: () => reset.mutate({}), }, ]} />
); } function TerminalRow() { const proxyState = useAtomValue(proxyStateAtom); const caStatus = useRpcQuery("ca_status", {}); const showToast = useShowToast(); const os = getOsType(); const [shell, setShell] = useState(() => defaultShell(os)); const openTerminal = useRpcMutation("open_terminal", { onError: (err) => showToast({ message: err.message, color: "danger" }), }); const port = proxyState?.port ?? proxyState?.otherOwner?.proxyPort ?? proxyState?.configuredPort ?? 9090; const caPath = caStatus.data?.certPath; const shellLabel = TERMINAL_SHELLS.find((s) => s.value === shell)?.label ?? shell; return (
Environment setShell(v as TerminalShell)} items={TERMINAL_SHELLS.map((s) => ({ label: s.label, value: s.value }))} >

Traffic from one terminal will be recorded.

{caPath != null && ( )}
); }