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,
Heading,
Icon,
RadioDropdown,
SettingRow,
SettingRowBoolean,
PlainInput,
SettingRowSelect,
SettingSelectControl,
SettingSnippet,
SettingsList,
SettingsSection,
VStack,
} 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 { DecryptListEditor } 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 (
Proxy
Listening, decryption, traffic and terminals
name="httpVersion"
title="HTTP version"
description="For new connections"
value={proxyState?.httpVersion ?? "auto"}
disabled={elsewhere}
onChange={(httpVersion) => setHttpVersion.mutate({ httpVersion })}
options={[
{ label: "Auto", 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}
) : (
`${capitalize(row.title.replace(/^Helper: /, ""))}. ${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="Captured 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 (