Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update mullvad extension #15981

Merged
merged 6 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions extensions/mullvad/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Mullvad Changelog

## [Added ability to select specific server, Added freceny sorting] - 2025-01-03

## [Support Reconnecting via the Mullvad CLI, Fixed HUD message display] - 2024-10-25

## [Support Connecting, Disconnecting and Selecting Target Locations via the Mullvad CLI] - 2024-01-20
Expand Down
103 changes: 77 additions & 26 deletions extensions/mullvad/src/selectLocation.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Action, ActionPanel, Detail, List, PopToRootType, showHUD } from "@raycast/api";
import { showFailureToast, useExec } from "@raycast/utils";
import { exec, execSync } from "child_process";
import { useState } from "react";
import { promisify } from "node:util";
import { showFailureToast, useExec, useFrecencySorting } from "@raycast/utils";
import { execSync } from "child_process";
import { mullvadNotInstalledHint } from "./utils";

type Location = {
Expand All @@ -11,79 +9,131 @@ type Location = {
city: string;
cityCode: string;
id: string;
servers: { id: string }[];
};

const countryRegex = /^(?<country>.+)\s\((?<countryCode>.+)\)/;
const cityRegex = /^(?<city>.+)\s\((?<cityCode>.+)\)/;
const serverRegex = /^(?<server>.+?)\s/;

function parseRelayList(rawRelayList: string): Location[] {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const locations: Location[] = [];
let currentCountry;
let currentCountryCode;
if (rawRelayList)
for (const line of rawRelayList.split("\n")) {
if (line.startsWith("\t\t")) continue;
let currentServerList: { id: string }[] = [];
if (rawRelayList) {
const lines = rawRelayList.split("\n");
let i = 0;
while (i < lines.length) {
const line = lines[i];

if (line.startsWith("\t")) {
const cityMatch = line.trim().match(cityRegex);
if (cityMatch) {
while (i + 1 < lines.length && lines[i + 1].startsWith("\t\t")) {
const serverMatch = lines[i + 1].trim().match(serverRegex);
if (serverMatch) {
const { server } = serverMatch.groups!;
currentServerList.push({ id: server });
}
i++;
}

const { city, cityCode } = cityMatch.groups!;
locations.push({
country: currentCountry!,
countryCode: currentCountryCode!,
city,
cityCode,
id: `${currentCountryCode!}/${cityCode}`,
servers: currentServerList,
});
currentServerList = [];
}
} else {
const countryMatch = line.match(countryRegex);
if (countryMatch) {
const { country, countryCode } = countryMatch.groups!;
currentCountry = country;
currentCountryCode = countryCode;
}
continue;
}

const countryMatch = line.match(countryRegex);
if (countryMatch) {
const { country, countryCode } = countryMatch.groups!;
currentCountry = country;
currentCountryCode = countryCode;
}
i++;
}
}
/* eslint-enable @typescript-eslint/no-non-null-assertion */

return locations;
}

function ServerList({
location,
visitLocation,
}: {
location: Location;
visitLocation: (item: Location) => Promise<void>;
}) {
const { data: sortedServers, visitItem: visitServer } = useFrecencySorting(location.servers);

async function setServer(server: { id: string }) {
visitLocation(location);
// If we call visitServer directly afterwards, it won't update both frequencies
setTimeout(() => visitServer(server), 10);

execSync(`mullvad relay set location ${server.id}`);

await showHUD("Location changed", { clearRootSearch: true, popToRootType: PopToRootType.Immediate });
}

return (
<List>
{sortedServers.map((server) => (
<List.Item
key={server.id}
id={server.id}
title={server.id}
actions={
<ActionPanel>
<Action title="Select Server" onAction={() => setServer(server).catch(showFailureToast)} />
</ActionPanel>
}
/>
))}
</List>
);
}

export default function Command() {
const isMullvadInstalled = useExec("mullvad", ["version"]);
const rawRelayList = useExec("mullvad", ["relay", "list"], { execute: !!isMullvadInstalled.data });
const [selectedLocation, setSelectedLocation] = useState<string | null>(null);

const locations = rawRelayList.data ? parseRelayList(rawRelayList.data) : [];
const { data: sortedLocations, visitItem: visitLocation } = useFrecencySorting(locations);

if (rawRelayList.isLoading || isMullvadInstalled.isLoading) return <List isLoading={true} />;
if (!isMullvadInstalled.data || isMullvadInstalled.error) return <Detail markdown={mullvadNotInstalledHint} />;
if (rawRelayList.error) return <Detail markdown={rawRelayList.error.message} />;
if (!rawRelayList.data) throw new Error("Couldn't fetch list of relays");

const locations = parseRelayList(rawRelayList.data);

async function setLocation() {
if (!selectedLocation) return;
const [countryCode, cityCode] = selectedLocation.split("/");
async function setLocation(location: Location) {
const [countryCode, cityCode] = location.id.split("/");
visitLocation(location);

execSync(`mullvad relay set location ${countryCode} ${cityCode}`);

await showHUD("Location changed", { clearRootSearch: true, popToRootType: PopToRootType.Immediate });
}

return (
<List onSelectionChange={setSelectedLocation}>
{locations.map((l) => (
<List>
{sortedLocations.map((l) => (
<List.Item
key={l.id}
id={l.id}
title={`${l.country} / ${l.city}`}
subtitle={`${l.countryCode}-${l.cityCode}`}
detail={
<List.Item.Detail
markdown="test"
metadata={
<List.Item.Detail.Metadata>
<List.Item.Detail.Metadata.Label title="Country Code" text={l.countryCode} />
Expand All @@ -94,7 +144,8 @@ export default function Command() {
}
actions={
<ActionPanel>
<Action title="Switch Location" onAction={() => setLocation().catch(showFailureToast)} />
<Action title="Select Location" onAction={() => setLocation(l).catch(showFailureToast)} />
<Action.Push title="Switch Server" target={<ServerList location={l} visitLocation={visitLocation} />} />
</ActionPanel>
}
/>
Expand Down