Skip to content

Commit

Permalink
Update mullvad extension (raycast#15981)
Browse files Browse the repository at this point in the history
* Update mullvad extension

- Support Reconnecting via the Mullvad CLI, Fixed HUD message display
- Initial commit

* Fixed code style issues

* Update mullvad extension

- Cleanup
- Added frequency sorting location/server list
- Add option to select specific server
- Removed test code
- Initial commit

* updated changelog

* Update CHANGELOG.md and optimise images

---------

Co-authored-by: raycastbot <[email protected]>
  • Loading branch information
2 people authored and gogocharli committed Jan 6, 2025
1 parent 32ba811 commit b0bdea5
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 26 deletions.
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

0 comments on commit b0bdea5

Please sign in to comment.